From e8faa3d5b59f77a098506d42482d0e8505cc6e2d Mon Sep 17 00:00:00 2001 From: Sergey Date: Mon, 1 Jun 2026 22:39:30 +0300 Subject: [PATCH] Add Dynamic Tiler extension --- dynamic-tiler@bnku/README.md | 224 + .../files/dynamic-tiler@bnku/extension.js | 4427 +++++++++++++++++ .../files/dynamic-tiler@bnku/icon.png | Bin 0 -> 1782 bytes .../files/dynamic-tiler@bnku/metadata.json | 7 + .../dynamic-tiler@bnku/settings-schema.json | 276 + dynamic-tiler@bnku/info.json | 6 + dynamic-tiler@bnku/screenshot.png | Bin 0 -> 62366 bytes 7 files changed, 4940 insertions(+) create mode 100644 dynamic-tiler@bnku/README.md create mode 100644 dynamic-tiler@bnku/files/dynamic-tiler@bnku/extension.js create mode 100644 dynamic-tiler@bnku/files/dynamic-tiler@bnku/icon.png create mode 100644 dynamic-tiler@bnku/files/dynamic-tiler@bnku/metadata.json create mode 100644 dynamic-tiler@bnku/files/dynamic-tiler@bnku/settings-schema.json create mode 100644 dynamic-tiler@bnku/info.json create mode 100644 dynamic-tiler@bnku/screenshot.png diff --git a/dynamic-tiler@bnku/README.md b/dynamic-tiler@bnku/README.md new file mode 100644 index 00000000..ea091c79 --- /dev/null +++ b/dynamic-tiler@bnku/README.md @@ -0,0 +1,224 @@ +# Dynamic Tiler 🚀 + +English | [Русский](README.ru.md) + +**Dynamic Tiler** is a configurable, intent-aware tiling extension for the Cinnamon Desktop Environment. It blends the flexibility of floating desktops with the productivity of tiling window managers. By combining a decoupled, test-backed TypeScript engine with a native GJS (Gnome JavaScript) Cinnamon extension, it provides predictable keyboard layout shifts, fluid mouse dragging with real-time visual previews, and a transaction history to easily revert layout changes. + +Instead of forcing your windows into a rigid, static layout that breaks under normal desktop use, Dynamic Tiler is designed to adapt to your daily workflow. It shifts, squeezes, and restores layouts dynamically, offering the efficiency of tiling while maintaining the forgiving feel of a traditional desktop. + +--- + +## 📌 Table of Contents +* [📖 The Story & Motivation](#-the-story--motivation) +* [🚀 Getting Started](#-getting-started) +* [⌨️ User Guide: Hotkeys & Workflows](#-user-guide-hotkeys--workflows) +* [🖱️ User Guide: Mouse Interactions](#-user-guide-mouse-interactions) +* [⚙️ Configuration & Customization](#-configuration--customization) +* [🏗️ Under the Hood: Modular Engine (For Developers)](#-under-the-hood-modular-engine-for-developers) +* [⚡ Under the Hood: Advanced Mathematical Solvers (For Developers)](#-under-the-hood-advanced-mathematical-solvers-for-developers) +* [🧑‍💻 QA & Code Testing (For Developers)](#-qa--code-testing-for-developers) +* [📈 Project Status & How to Help](#-project-status--how-to-help) + +--- + +## 📖 The Story & Motivation + +Most developers and power users love the core idea of tiling window managers: zero wasted screen space, hands-on-keyboard efficiency, and clean window alignment. However, in practice, full tiling window managers (such as i3, dwm, or bspwm) often bring frustrating drawbacks: +* Simple floating windows—like quick dialog boxes, color pickers, or popups—get stretched into giant columns, ruining the layout. +* Mouse interaction is either ignored or feels clunky. +* A single accidental window drag or resize can permanently ruin a carefully arranged workspace. +* Configuring and porting workflows between different desktop environments is often a tedious manual process. + +Dynamic Tiler was built to address these frustrations directly. The project started as a crude, CLI-driven X11 experiment that triggered layout shifts using external command-line utilities (`wmctrl`, `xdotool`). While the math worked, spawning multiple external processes on every keypress caused a noticeable lag (~150ms+). + +To make the interaction feel instantaneous and smooth, we restructured the codebase: +1. **Engine Decoupling:** We isolated the tiling mathematics and layout solvers into a pure TypeScript engine, completely free of any operating system or window-manager APIs. +2. **Native Cinnamon Integration:** We integrated this engine inside a native Cinnamon Spice extension, executing directly within Mutter/Muffin’s rendering thread. This eliminated the process-spawning overhead, dropping response times to around **8–12ms**. +3. **UX-Driven Focus:** Over multiple development iterations, we added defensive safeguards such as transactional history rollback, elastic resizes, pointer hysteresis to prevent preview flickering, and axis-carving heuristics. + +Today, Dynamic Tiler is a practical window-management tool focused on providing a fluid, predictable everyday experience. + +--- + +## 🚀 Getting Started + +### 📋 Requirements +* Linux Mint / Cinnamon Desktop Environment (running on X11). +* Node.js and npm. +* For the legacy CLI only: `wmctrl`, `xdotool`, `x11-utils`, and `x11-xserver-utils`. + +Install package requirements on Ubuntu / Linux Mint: +```bash +sudo apt update +sudo apt install nodejs npm wmctrl xdotool x11-utils x11-xserver-utils +``` + +### 📦 Installation +1. Clone this repository. +2. Install dependencies and verify that the engine is ready: + ```bash + npm install + npm test + ``` +3. Compile and install the extension directly into your local directory: + ```bash + npm run build:extension + ``` + *(This bundles the source code using `esbuild` and installs the extension directly into `~/.local/share/cinnamon/extensions/dynamic-tiler@bnku/`)* +4. Restart Cinnamon (`Alt + F2`, type `r`, press `Enter`), or open the **Cinnamon Extensions** application and toggle **Dynamic Tiler** on. + +--- + +## ⌨️ User Guide: Hotkeys & Workflows + +Dynamic Tiler's keyboard shortcuts are designed to feel intuitive and responsive. Below is an explanation of what each keybinding option in the settings does and how it behaves in practice. + +### Keyboard & Mouse Shortcuts (Configurable Settings) + +| Action | Default Keybinding | Interface Setting Key | Engine Result / UX Feel | +| :--- | :--- | :--- | :--- | +| **Tile Left** | `Super + Left` | `keybinding-tile-left` | Expand or snap window leftwards, sliding neighbors. | +| **Tile Right** | `Super + Right` | `keybinding-tile-right` | Expand or snap window rightwards, sliding neighbors. | +| **Tile Up** | `Super + Up` | `keybinding-tile-up` | Expand or snap window upwards, sliding neighbors. | +| **Tile Down** | `Super + Down` | `keybinding-tile-down` | Expand or snap window downwards, sliding neighbors. | +| **Shift Left** | `Ctrl + Super + Left` | `keybinding-shift-left` | Immediately shift window to the leftmost edge. | +| **Shift Right** | `Ctrl + Super + Right` | `keybinding-shift-right` | Immediately shift window to the rightmost edge. | +| **Shift Up** | `Ctrl + Super + Up` | `keybinding-shift-up` | Immediately shift window to the topmost edge. | +| **Shift Down** | `Ctrl + Super + Down` | `keybinding-shift-down` | Immediately shift window to the bottommost edge. | +| **Restore Window** | `Super + BackSpace` | `keybinding-restore` | Exit the grid and restore the original floating bounds. | +| **Drag & Snap Modifier** | `Ctrl` (holds during drag) | `dnd-modifier-key` | Snaps a dragged window into the grid with visual overlays. | +| **Window Swap Modifier** | `Ctrl + Shift` (holds during drag) | `dnd-swap-modifier-key` | Swaps two windows' physical coordinates (green overlay). | + +### How the Hotkeys Feel in Practice + +* **Tile Left / Right / Up / Down:** + - If the active window is floating, pressing these keys instantly snaps it to that half of the screen. + - If the window is already tiled, pressing the same direction key will grow its boundary by grid steps, pushing its neighbors to the side. + - If the window hits the screen edge, it will compress its neighbors down to their minimum size (`minColumnSpan`/`minRowSpan`). If they cannot compress any further, the active window will start shrinking from the *opposite* side, letting the neighboring window expand. This prevents layout locks and feels like the window is sliding across a tracks system. +* **Shift Left / Right / Up / Down:** + - This acts as a rapid relocation gesture. Instead of resizing step-by-step, it instantly sends the focused window to the outermost half of the grid in that direction. It is perfect for quickly throwing a terminal or chat app to a peripheral screen edge. +* **Restore Window:** + - This is your quick escape hatch. If you need to temporarily use a window in a floating state, pressing this key instantly pulls it out of the grid. Simultaneously, the engine detects the empty space and collapses the vacant slot, causing the surrounding windows to expand and fill the void seamlessly. + +--- + +## 🖱️ User Guide: Mouse Interactions + +Dynamic Tiler offers an organic mouse-dragging layout workflow that snaps into the grid. + +* **Grid Snapping:** Hold your configured DnD modifier (default is `Ctrl` key while dragging, configured as `d` in settings) and drag a window. You'll see transparent blue preview overlays guiding you to snap zones. +* **Stack Insertion:** Drag a window between two tiled windows in a vertical or horizontal column. The visual guide will indicate that a new middle row/column is opening. +* **Corridor Slide:** Drag a window to a boundary side. Adjacent windows will shift to provide an insertion corridor. +* **Explicit Swap:** Hold `Control + Shift` (default, configured as `d`) while dragging a window over another. The target window will highlight with a green frame. Drop the window to trade their physical coordinates. +* **Floating Extraction:** Drag a tiled window out of its grid slot. The gap left behind will visually outline itself, collapsing automatically once your cursor moves more than `80px` from the starting point. + +--- + +## ⚙️ Configuration & Customization + +The native configuration panel in Cinnamon's Extensions settings is split into four pages: + +1. **Layout:** + * **Gaps:** Adjust window spacing in pixels (e.g., `8px` for a clean gaps aesthetic). + * **Enable Previews:** Toggle real-time overlay previews. + * **Grid Columns / Rows:** Customize the default grid density (e.g., `12 x 6` or `8 x 6`). + * **Monitor Profiles & Overrides:** Set different grid counts for horizontal, vertical, or ultrawide displays, or write explicit manual overrides (e.g., `0:24x12, 1:6x12`). + * **Minimum Column / Row Spans:** Set the smallest grid size a window can shrink to. +2. **Drag & Drop:** + * Toggle mouse snapping and bind the trigger modifier (e.g., `d`). + * Enable/disable window swapping and configure its modifier (e.g., `d`). +3. **Keyboard:** + * Bind hotkeys for directional tiling, edge shifting, and layout restoration. +4. **Diagnostics:** + * Toggle debug logging to write detailed trace data to `~/.xsession-errors`. + +--- + +## 🏗️ Under the Hood: Modular Engine (For Developers) + +The codebase is organized around a small, platform-agnostic TypeScript engine. The layout math lives separately from the Cinnamon/X11 integration, while adapters handle window-manager APIs, configuration, caching, and shell-specific details. + +Because the core layout math is entirely platform-agnostic, the engine itself can be ported to other environments (such as GNOME Shell, KDE KWin, or wlroots-based Wayland compositors) by implementing new infrastructure adapters. + +### Module Responsibilities: +* **`GridSpans.ts`**: Handles the logical coordinate scales and index ranges for the active grid. +* **`GeometryConverter.ts`**: Converts physical pixel boundaries into grid index spans, adjusting for gaps, title bars, and monitor-specific workareas. +* **`InitialLayout.ts`**: Locates vacant screen zones when a new window is opened, placing it neatly at an edge or adjacent to an active window. +* **`ChainBlockDetector.ts`**: Evaluates layout boundaries using the **Chain Block Detection** algorithm to see if a group of touching windows has room to slide or compress. +* **`ChainTransitions.ts`**: Solves proportional shifts. When a window expands, adjacent windows are gently pushed; they only compress when the layout hits a screen boundary, maintaining their relative proportions. +* **`DragTiling.ts`**: Handles mouse interactions, including Drag-and-Drop layouts, swap gestures, and the transaction journal. + +--- + +## ⚡ Under the Hood: Advanced Mathematical Solvers (For Developers) + +### 1. Elastic Resizing & Accordion Spring +When you attempt to grow a window with keyboard shortcuts, Dynamic Tiler uses an elastic spring model: +* **Accordion Push:** Growing a window pushes its adjacent neighbors in that direction. +* **Symmetrical Memory Fallbacks:** If a window is trapped (e.g., its left neighbors are already pushed against the screen edge and compressed to their minimum allowed span), pressing the expand shortcut (`Super + Left`) will softly shrink the window from the *right* side instead, allowing the right neighbor to expand. This prevents the layout from locking up and keeps the keyboard interaction predictable. + +### 2. Temporal Correction Memory (Keyboard Undo) +If you resize a window in the wrong direction and immediately press the opposite direction shortcut, the engine detects this as an **Undo request** and restores the window’s exact previous geometry. +* If you pause for about **2.5 seconds**, the shortcut resumes its normal behavior (modifying the opposite boundary of the window). + +### 3. Transaction-Backed Mouse Snapping & Safe Extraction +Drag-and-Drop is often unpredictable in traditional tiling layouts. Dynamic Tiler addresses this through transaction tracking: +* **Safe Extraction & Reversion:** If you drag a tiled window out of the grid to float it, the engine checks its transaction journal (`DragTransactionSnapshot` containing `beforeStates`, `afterStates`, and affected windows). Instead of leaving an awkward gap or stretching adjacent windows arbitrarily, it tries to **revert affected neighbors** to their exact pre-tiled geometry. If the surrounding layout was changed in the meantime, it falls back to a clean vacancy collapse (`collapseVacancy`). + +### 4. Advanced Mouse Mechanics +* **Cursor-Centered Snapping:** The drag target is calculated relative to where your mouse cursor is pointing and the preferred size of the dragged window, rather than the window's top-left corner. We use jitter-filtering session memory to prevent layout previews from jumping wildly due to minor hand movements. +* **Axis Carving Scoring:** When you drag a window over another, the solver scores both horizontal and vertical candidates by calculating **minimal area loss**. Dropping near the top/bottom edges carves a vertical slot; dropping on the sides carves a horizontal slot. This prevents wide windows (like a browser) from being crushed into narrow strips when you drop a utility window nearby. +* **Corridor Edge Sliding:** If you drag a window to the outer edge of a tight row, the solver slides the entire row inward, compressing a wide neighbor if possible, to open up a clean slot at the screen edge. +* **Explicit Swap Mode:** Holding the configured Swap modifier (default `d`) while dragging enables Swap Mode. Instead of pushing windows aside, the dragged window and target window trade positions. A **green bounding border** clearly indicates that the windows will swap. +* **Accidental Release Shield:** If you let go of the modifier within 80 pixels of where you started dragging, the engine cancels the drag instead of extracting the window, preventing accidental floating states. + +--- + +## 🧑‍💻 QA & Code Testing (For Developers) + +Dynamic Tiler ensures reliability through a comprehensive test harness that evaluates layout edge cases: + +* **UX Logic Tests (`tests/usecase.test.ts`)**: Validates keyboard behaviors, including correction memory, restore states, and physical dimension detection. +* **Engine Integration Tests (`tests/engine.test.ts`)**: Houses a suite of specs (approx. 100 KB) verifying: + * Proportional accordion compressions and corridor edge sliding. + * Pointer-centered DND target calculations. + * Transaction history journaling and multi-window restoration. + * Property-style fuzzed permutation tests that check layout boundaries across hundreds of random layout rearrangements to prevent overlaps or engine crashes. + +### Useful Developer Commands: +```bash +# Run the test suite +npm test + +# Build the Cinnamon Extension local bundle +npm run build:extension + +# Build the legacy command-line tool (dist/cli.js) +npm run build:cli +``` + +--- + +## 📈 Project Status & How to Help + +Dynamic Tiler is currently in a **very early stage of development**. However, it is actively used and tested by the developer on a daily basis as a primary window-management workflow. This ensures that the user experience (UX) will continue to evolve, stabilize, and improve in the future. + +You can directly support the project's development by **submitting bug reports and layout suggestions**. Your real-world feedback on edge cases, multi-monitor setups, and daily usage is what helps make this tool more robust and reliable for everyone. + +To learn how to enable diagnostics, capture debug logs, and submit a high-quality, reproducible bug report, please read our [Contributing Guidelines](CONTRIBUTING.md). + +### 🏆 Completed in Recent Iterations: +* Decoupled layout math into a pure TypeScript engine. +* Implemented axis-carving scoring to resolve wide/narrow window collisions logically. +* Added transaction journaling to rollback neighbor geometries when a window is floated. +* Created cursor-centered mouse snapping and hysteresis filters to eliminate preview stutter. +* Added modifier-driven explicit window swaps with visual green previews. + +### 🗺️ Next Milestones: +* **Global Layout Scoring:** Evaluating the density and total "cost" of the entire layout to make better decisions during complex multi-window relocations. +* **Wayland Portability Research:** Studying how to take our decoupled TypeScript core and run it natively inside Wayland environments (such as Mutter Wayland or KDE KWin). +* **Non-Intrusive Status Indicators:** Displaying subtle, temporary hints on preview borders (like `Minimum Width`) to make engine layout decisions transparent. + +--- + +*Dynamic Tiler is built for users who want the speed of a keyboard layout manager without losing the fluid usability of a mouse-driven desktop. Try it, customize it, and let us know what you think!* 🌟 diff --git a/dynamic-tiler@bnku/files/dynamic-tiler@bnku/extension.js b/dynamic-tiler@bnku/files/dynamic-tiler@bnku/extension.js new file mode 100644 index 00000000..90d47767 --- /dev/null +++ b/dynamic-tiler@bnku/files/dynamic-tiler@bnku/extension.js @@ -0,0 +1,4427 @@ +"use strict"; +var DynamicTiler = (() => { + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // src/extension.ts + var extension_exports = {}; + __export(extension_exports, { + disable: () => disable, + enable: () => enable, + init: () => init + }); + + // src/core/types.ts + function getGridColumns(config) { + return config.gridColumns || config.gridSize; + } + function getGridRows(config) { + return config.gridRows || config.gridSize; + } + function getMinColumnSpan(config) { + return config.minColumnSpan || config.minSpan; + } + function getMinRowSpan(config) { + return config.minRowSpan || config.minSpan; + } + + // src/core/engine/GridSpans.ts + var HORIZONTAL_SPANS = [ + [0, 2], + // 0 + [0, 4], + // 1 + [0, 6], + // 2 (left 1/2) + [0, 8], + // 3 + [0, 10], + // 4 + [0, 12], + // 5 (full width) + [2, 12], + // 6 + [4, 12], + // 7 + [6, 12], + // 8 (right 1/2) + [8, 12], + // 9 + [10, 12] + // 10 + ]; + var VERTICAL_SPANS = [ + [0, 2], + // 0 + [0, 4], + // 1 + [0, 6], + // 2 (top 1/2) + [0, 8], + // 3 + [0, 10], + // 4 + [0, 12], + // 5 (full height) + [2, 12], + // 6 + [4, 12], + // 7 + [6, 12], + // 8 (bottom 1/2) + [8, 12], + // 9 + [10, 12] + // 10 + ]; + function spanToHIndex(span) { + for (let i = 0; i < HORIZONTAL_SPANS.length; i++) { + if (HORIZONTAL_SPANS[i][0] === span[0] && HORIZONTAL_SPANS[i][1] === span[1]) { + return i; + } + } + return 5; + } + function spanToVIndex(span) { + for (let i = 0; i < VERTICAL_SPANS.length; i++) { + if (VERTICAL_SPANS[i][0] === span[0] && VERTICAL_SPANS[i][1] === span[1]) { + return i; + } + } + return 3; + } + + // src/core/engine/GeometryConverter.ts + var GeometryConverter = class { + /** + * Преобразует физическую геометрию окна в логические колонки (hSpan) на указанном мониторе + */ + static geometryToHSpan(geom, monitor, config = { gridSize: 12, gridColumns: 12, gridRows: 12, minSpan: 2, minColumnSpan: 2, minRowSpan: 2, step: 2, gaps: 0 }) { + const { workarea } = monitor; + const gridColumns = getGridColumns(config); + const colWidth = workarea.width / gridColumns; + const relLeft = geom.x - workarea.x; + const relRight = geom.x + geom.width - workarea.x; + let startCol = Math.floor(relLeft / colWidth); + const startRemainder = relLeft / colWidth - startCol; + if (startRemainder > 0.2) { + startCol = Math.min(gridColumns - 1, startCol + 1); + } + startCol = Math.max(0, startCol); + let endCol = Math.ceil(relRight / colWidth); + const endRemainder = endCol - relRight / colWidth; + if (endRemainder > 0.2) { + endCol = Math.max(1, endCol - 1); + } + endCol = Math.min(gridColumns, endCol); + return [startCol, endCol]; + } + /** + * Преобразует физическую геометрию окна в логические строки (vSpan) на указанном мониторе + */ + static geometryToVSpan(geom, monitor, config = { gridSize: 12, gridColumns: 12, gridRows: 12, minSpan: 2, minColumnSpan: 2, minRowSpan: 2, step: 2, gaps: 0 }) { + const { workarea } = monitor; + const gridRows = getGridRows(config); + const rowHeight = workarea.height / gridRows; + const relTop = geom.y - workarea.y; + const relBottom = geom.y + geom.height - workarea.y; + let startRow = Math.floor(relTop / rowHeight); + const startRemainder = relTop / rowHeight - startRow; + if (startRemainder > 0.2) { + startRow = Math.min(gridRows - 1, startRow + 1); + } + startRow = Math.max(0, startRow); + let endRow = Math.ceil(relBottom / rowHeight); + const endRemainder = endRow - relBottom / rowHeight; + if (endRemainder > 0.2) { + endRow = Math.max(1, endRow - 1); + } + endRow = Math.min(gridRows, endRow); + return [startRow, endRow]; + } + /** + * Преобразует абстрактные доли WindowState в реальные координаты Geometry с учетом отступов (gaps) + */ + static stateToGeometry(state, screen, config) { + const { workarea } = screen; + const gaps = config.gaps || 0; + const gridColumns = getGridColumns(config); + const gridRows = getGridRows(config); + const hSpan = state.hSpan || HORIZONTAL_SPANS[state.hIndex] || [0, gridColumns]; + const vSpan = state.vSpan || VERTICAL_SPANS[state.vIndex] || [0, gridRows]; + const colWidth = workarea.width / gridColumns; + const rowHeight = workarea.height / gridRows; + const xStart = workarea.x + Math.round(hSpan[0] * colWidth); + const xEnd = workarea.x + Math.round(hSpan[1] * colWidth); + let width = xEnd - xStart; + let x = xStart; + const yStart = workarea.y + Math.round(vSpan[0] * rowHeight); + const yEnd = workarea.y + Math.round(vSpan[1] * rowHeight); + let height = yEnd - yStart; + let y = yStart; + if (gaps > 0) { + const minDimension = 100; + const maxGapW = Math.max(0, (width - minDimension) / 2); + const maxGapH = Math.max(0, (height - minDimension) / 2); + const gapW = Math.min(gaps, maxGapW); + const gapH = Math.min(gaps, maxGapH); + x += gapW; + y += gapH; + width -= 2 * gapW; + height -= 2 * gapH; + } + return { x, y, width, height }; + } + }; + + // src/core/engine/InitialLayout.ts + var InitialLayout = class { + /** + * Находит наиболее подходящий горизонтальный спан для первого тайлинга в зависимости от направления и соседей + */ + static getInitialHSpan(direction, siblingSpans, config, fixedVSpan) { + const gridColumns = getGridColumns(config); + const halfColumns = Math.round(gridColumns / 2); + if (direction === "shift-left") return [0, halfColumns]; + if (direction === "shift-right") return [halfColumns, gridColumns]; + const spans = this.getInitialSpans(direction, siblingSpans, config, { + fixedVSpan: fixedVSpan || [0, getGridRows(config)] + }); + return spans.hSpan; + } + /** + * Находит наиболее подходящий вертикальный спан для первого тайлинга в зависимости от направления и соседей + */ + static getInitialVSpan(direction, siblingSpans, config, fixedHSpan) { + const gridRows = getGridRows(config); + const halfRows = Math.round(gridRows / 2); + if (direction === "shift-up") return [0, halfRows]; + if (direction === "shift-down") return [halfRows, gridRows]; + const spans = this.getInitialSpans(direction, siblingSpans, config, { + fixedHSpan: fixedHSpan || [0, getGridColumns(config)] + }); + return spans.vSpan; + } + /** + * Находит наиболее подходящий двумерный макет для первого тайлинга + */ + static getInitialSpans(direction, siblingSpans, config, options = {}) { + const gridColumns = getGridColumns(config); + const gridRows = getGridRows(config); + const minColumnSpan = getMinColumnSpan(config); + const minRowSpan = getMinRowSpan(config); + const halfColumns = Math.round(gridColumns / 2); + const halfRows = Math.round(gridRows / 2); + const occupied = Array.from({ length: gridRows }, () => new Array(gridColumns).fill(false)); + for (const sibling of siblingSpans) { + const [hStart, hEnd] = sibling.hSpan; + const [vStart, vEnd] = sibling.vSpan; + for (let r = vStart; r < vEnd; r++) { + for (let c = hStart; c < hEnd; c++) { + if (r >= 0 && r < gridRows && c >= 0 && c < gridColumns) { + occupied[r][c] = true; + } + } + } + } + if (direction === "shift-left") { + return { hSpan: [0, halfColumns], vSpan: options.fixedVSpan || [0, gridRows] }; + } + if (direction === "shift-right") { + return { hSpan: [halfColumns, gridColumns], vSpan: options.fixedVSpan || [0, gridRows] }; + } + if (direction === "shift-up") { + return { hSpan: options.fixedHSpan || [0, gridColumns], vSpan: [0, halfRows] }; + } + if (direction === "shift-down") { + return { hSpan: options.fixedHSpan || [0, gridColumns], vSpan: [halfRows, gridRows] }; + } + if (direction === "left") { + let bestHSpan = [0, halfColumns]; + let bestVSpan = options.fixedVSpan || [0, gridRows]; + for (let hStart = 0; hStart <= gridColumns - minColumnSpan; hStart++) { + let maxArea = 0; + let localBestHSpan = null; + let localBestVSpan = null; + const vIntervals = options.fixedVSpan ? [options.fixedVSpan] : []; + if (!options.fixedVSpan) { + for (let vStart = 0; vStart <= gridRows - minRowSpan; vStart += config.step) { + for (let vEnd = vStart + minRowSpan; vEnd <= gridRows; vEnd += config.step) { + vIntervals.push([vStart, vEnd]); + } + if (gridRows - vStart >= minRowSpan && (gridRows - vStart) % config.step !== 0) { + vIntervals.push([vStart, gridRows]); + } + } + } + for (const [vStart, vEnd] of vIntervals) { + let w = 0; + while (hStart + w < gridColumns && w < halfColumns) { + let colFree = true; + for (let r = vStart; r < vEnd; r++) { + if (occupied[r][hStart + w]) { + colFree = false; + break; + } + } + if (colFree) { + w++; + } else { + break; + } + } + if (w >= minColumnSpan) { + const area = w * (vEnd - vStart); + if (area > maxArea || area === maxArea && w > (localBestHSpan ? localBestHSpan[1] - localBestHSpan[0] : 0)) { + maxArea = area; + localBestHSpan = [hStart, hStart + w]; + localBestVSpan = [vStart, vEnd]; + } + } + } + if (localBestHSpan && localBestVSpan) { + bestHSpan = localBestHSpan; + bestVSpan = localBestVSpan; + break; + } + } + return { hSpan: bestHSpan, vSpan: bestVSpan }; + } + if (direction === "right") { + let bestHSpan = [halfColumns, gridColumns]; + let bestVSpan = options.fixedVSpan || [0, gridRows]; + for (let hEnd = gridColumns; hEnd >= minColumnSpan; hEnd--) { + let maxArea = 0; + let localBestHSpan = null; + let localBestVSpan = null; + const vIntervals = options.fixedVSpan ? [options.fixedVSpan] : []; + if (!options.fixedVSpan) { + for (let vStart = 0; vStart <= gridRows - minRowSpan; vStart += config.step) { + for (let vEnd = vStart + minRowSpan; vEnd <= gridRows; vEnd += config.step) { + vIntervals.push([vStart, vEnd]); + } + if (gridRows - vStart >= minRowSpan && (gridRows - vStart) % config.step !== 0) { + vIntervals.push([vStart, gridRows]); + } + } + } + for (const [vStart, vEnd] of vIntervals) { + let w = 0; + while (hEnd - 1 - w >= 0 && w < halfColumns) { + let colFree = true; + for (let r = vStart; r < vEnd; r++) { + if (occupied[r][hEnd - 1 - w]) { + colFree = false; + break; + } + } + if (colFree) { + w++; + } else { + break; + } + } + if (w >= minColumnSpan) { + const area = w * (vEnd - vStart); + if (area > maxArea || area === maxArea && w > (localBestHSpan ? localBestHSpan[1] - localBestHSpan[0] : 0)) { + maxArea = area; + localBestHSpan = [hEnd - w, hEnd]; + localBestVSpan = [vStart, vEnd]; + } + } + } + if (localBestHSpan && localBestVSpan) { + bestHSpan = localBestHSpan; + bestVSpan = localBestVSpan; + break; + } + } + return { hSpan: bestHSpan, vSpan: bestVSpan }; + } + if (direction === "up") { + let bestHSpan = options.fixedHSpan || [0, gridColumns]; + let bestVSpan = [0, halfRows]; + for (let vStart = 0; vStart <= gridRows - minRowSpan; vStart++) { + let maxArea = 0; + let localBestHSpan = null; + let localBestVSpan = null; + const hIntervals = options.fixedHSpan ? [options.fixedHSpan] : []; + if (!options.fixedHSpan) { + for (let hStart = 0; hStart <= gridColumns - minColumnSpan; hStart += config.step) { + for (let hEnd = hStart + minColumnSpan; hEnd <= gridColumns; hEnd += config.step) { + hIntervals.push([hStart, hEnd]); + } + if (gridColumns - hStart >= minColumnSpan && (gridColumns - hStart) % config.step !== 0) { + hIntervals.push([hStart, gridColumns]); + } + } + } + for (const [hStart, hEnd] of hIntervals) { + let h = 0; + while (vStart + h < gridRows && h < halfRows) { + let rowFree = true; + for (let c = hStart; c < hEnd; c++) { + if (occupied[vStart + h][c]) { + rowFree = false; + break; + } + } + if (rowFree) { + h++; + } else { + break; + } + } + if (h >= minRowSpan) { + const area = h * (hEnd - hStart); + if (area > maxArea || area === maxArea && h > (localBestVSpan ? localBestVSpan[1] - localBestVSpan[0] : 0)) { + maxArea = area; + localBestHSpan = [hStart, hEnd]; + localBestVSpan = [vStart, vStart + h]; + } + } + } + if (localBestHSpan && localBestVSpan) { + bestHSpan = localBestHSpan; + bestVSpan = localBestVSpan; + break; + } + } + return { hSpan: bestHSpan, vSpan: bestVSpan }; + } + if (direction === "down") { + let bestHSpan = options.fixedHSpan || [0, gridColumns]; + let bestVSpan = [halfRows, gridRows]; + for (let vEnd = gridRows; vEnd >= minRowSpan; vEnd--) { + let maxArea = 0; + let localBestHSpan = null; + let localBestVSpan = null; + const hIntervals = options.fixedHSpan ? [options.fixedHSpan] : []; + if (!options.fixedHSpan) { + for (let hStart = 0; hStart <= gridColumns - minColumnSpan; hStart += config.step) { + for (let hEnd = hStart + minColumnSpan; hEnd <= gridColumns; hEnd += config.step) { + hIntervals.push([hStart, hEnd]); + } + if (gridColumns - hStart >= minColumnSpan && (gridColumns - hStart) % config.step !== 0) { + hIntervals.push([hStart, gridColumns]); + } + } + } + for (const [hStart, hEnd] of hIntervals) { + let h = 0; + while (vEnd - 1 - h >= 0 && h < halfRows) { + let rowFree = true; + for (let c = hStart; c < hEnd; c++) { + if (occupied[vEnd - 1 - h][c]) { + rowFree = false; + break; + } + } + if (rowFree) { + h++; + } else { + break; + } + } + if (h >= minRowSpan) { + const area = h * (hEnd - hStart); + if (area > maxArea || area === maxArea && h > (localBestVSpan ? localBestVSpan[1] - localBestVSpan[0] : 0)) { + maxArea = area; + localBestHSpan = [hStart, hEnd]; + localBestVSpan = [vEnd - h, vEnd]; + } + } + } + if (localBestHSpan && localBestVSpan) { + bestHSpan = localBestHSpan; + bestVSpan = localBestVSpan; + break; + } + } + return { hSpan: bestHSpan, vSpan: bestVSpan }; + } + return { hSpan: [0, gridColumns], vSpan: [0, gridRows] }; + } + }; + + // src/core/engine/ChainBlockDetector.ts + var ChainBlockDetector = class { + /** + * Проверяет, заблокирована ли цепочка соприкасающихся окон слева от текущей границы + */ + static isLeftChainBlocked(startCol, siblingSpans, config, activeVSpan) { + let currentStart = startCol; + let chainLength = 0; + let currentChainWidth = 0; + let currentVSpan = activeVSpan; + while (true) { + const neighbor = siblingSpans.find( + (s) => s.hSpan[1] === currentStart && Math.max(s.vSpan[0], currentVSpan[0]) < Math.min(s.vSpan[1], currentVSpan[1]) + ); + if (!neighbor) break; + chainLength++; + const width = neighbor.hSpan[1] - neighbor.hSpan[0]; + currentChainWidth += width; + currentStart = neighbor.hSpan[0]; + currentVSpan = neighbor.vSpan; + } + if (chainLength === 0) return false; + const freeSpace = currentStart; + const minChainWidth = chainLength * getMinColumnSpan(config); + const compressionReserve = currentChainWidth - minChainWidth; + const movementReserve = freeSpace + compressionReserve; + return movementReserve <= 0; + } + /** + * Проверяет, заблокирована ли цепочка соприкасающихся окон справа от текущей границы + */ + static isRightChainBlocked(endCol, siblingSpans, config, activeVSpan) { + let currentEnd = endCol; + let chainLength = 0; + let currentChainWidth = 0; + let currentVSpan = activeVSpan; + while (true) { + const neighbor = siblingSpans.find( + (s) => s.hSpan[0] === currentEnd && Math.max(s.vSpan[0], currentVSpan[0]) < Math.min(s.vSpan[1], currentVSpan[1]) + ); + if (!neighbor) break; + chainLength++; + const width = neighbor.hSpan[1] - neighbor.hSpan[0]; + currentChainWidth += width; + currentEnd = neighbor.hSpan[1]; + currentVSpan = neighbor.vSpan; + } + if (chainLength === 0) return false; + const freeSpace = getGridColumns(config) - currentEnd; + const minChainWidth = chainLength * getMinColumnSpan(config); + const compressionReserve = currentChainWidth - minChainWidth; + const movementReserve = freeSpace + compressionReserve; + return movementReserve <= 0; + } + /** + * Проверяет, заблокирована ли цепочка соприкасающихся окон сверху от текущей границы + */ + static isTopChainBlocked(startRow, siblingSpans, config, activeHSpan) { + let currentStart = startRow; + let chainLength = 0; + let currentChainHeight = 0; + let currentHSpan = activeHSpan; + while (true) { + const neighbor = siblingSpans.find( + (s) => s.vSpan[1] === currentStart && Math.max(s.hSpan[0], currentHSpan[0]) < Math.min(s.hSpan[1], currentHSpan[1]) + ); + if (!neighbor) break; + chainLength++; + const height = neighbor.vSpan[1] - neighbor.vSpan[0]; + currentChainHeight += height; + currentStart = neighbor.vSpan[0]; + currentHSpan = neighbor.hSpan; + } + if (chainLength === 0) return false; + const freeSpace = currentStart; + const minChainHeight = chainLength * getMinRowSpan(config); + const compressionReserve = currentChainHeight - minChainHeight; + const movementReserve = freeSpace + compressionReserve; + return movementReserve <= 0; + } + /** + * Проверяет, заблокирована ли цепочка соприкасающихся окон снизу от текущей границы + */ + static isBottomChainBlocked(endRow, siblingSpans, config, activeHSpan) { + let currentEnd = endRow; + let chainLength = 0; + let currentChainHeight = 0; + let currentHSpan = activeHSpan; + while (true) { + const neighbor = siblingSpans.find( + (s) => s.vSpan[0] === currentEnd && Math.max(s.hSpan[0], currentHSpan[0]) < Math.min(s.hSpan[1], currentHSpan[1]) + ); + if (!neighbor) break; + chainLength++; + const height = neighbor.vSpan[1] - neighbor.vSpan[0]; + currentChainHeight += height; + currentEnd = neighbor.vSpan[1]; + currentHSpan = neighbor.hSpan; + } + if (chainLength === 0) return false; + const freeSpace = getGridRows(config) - currentEnd; + const minChainHeight = chainLength * getMinRowSpan(config); + const compressionReserve = currentChainHeight - minChainHeight; + const movementReserve = freeSpace + compressionReserve; + return movementReserve <= 0; + } + }; + + // src/core/engine/ChainTransitions.ts + var ChainTransitions = class { + /** + * Рассчитывает новые состояния для всей цепочки соприкасающихся окон на основе направления + */ + static calculateChainTransitions(activeId, direction, config, activeWindows, allVisibleSpans = [], calculateNextStateFn, getDefaultStateFn) { + const result = {}; + const gridColumns = getGridColumns(config); + const gridRows = getGridRows(config); + const minColumnSpan = getMinColumnSpan(config); + const minRowSpan = getMinRowSpan(config); + const activeWin = activeWindows.find((w) => w.windowId === activeId); + if (!activeWin) { + const siblingSpans = allVisibleSpans.length > 0 ? allVisibleSpans : activeWindows.map((w) => ({ + hSpan: w.state.hSpan || HORIZONTAL_SPANS[w.state.hIndex] || [0, gridColumns], + vSpan: w.state.vSpan || VERTICAL_SPANS[w.state.vIndex] || [0, gridRows] + })); + const defaultState = getDefaultStateFn(); + const nextActiveState2 = calculateNextStateFn(defaultState, direction, config, siblingSpans); + result[activeId] = nextActiveState2; + return result; + } + const currentActiveState = { ...activeWin.state }; + if (!currentActiveState.hSpan) { + currentActiveState.hSpan = HORIZONTAL_SPANS[currentActiveState.hIndex] || [0, gridColumns]; + } + if (!currentActiveState.vSpan) { + currentActiveState.vSpan = VERTICAL_SPANS[currentActiveState.vIndex] || [0, gridRows]; + } + const siblings = activeWindows.filter((w) => w.windowId !== activeId).map((w) => ({ + hSpan: w.state.hSpan || HORIZONTAL_SPANS[w.state.hIndex] || [0, gridColumns], + vSpan: w.state.vSpan || VERTICAL_SPANS[w.state.vIndex] || [0, gridRows] + })); + const nextActiveState = calculateNextStateFn(currentActiveState, direction, config, siblings); + result[activeId] = nextActiveState; + if (direction === "shift-left" || direction === "shift-right" || direction === "shift-up" || direction === "shift-down") { + return result; + } + const normalizedWindows = activeWindows.map((w) => { + const state = { ...w.state }; + if (!state.hSpan) { + state.hSpan = HORIZONTAL_SPANS[state.hIndex] || [0, gridColumns]; + } + if (!state.vSpan) { + state.vSpan = VERTICAL_SPANS[state.vIndex] || [0, gridRows]; + } + return { windowId: w.windowId, state }; + }); + if (direction === "left" || direction === "right") { + const sortedWins = [...normalizedWindows].sort((a, b) => { + const aSpan = a.windowId === activeId ? nextActiveState.hSpan : a.state.hSpan; + const bSpan = b.windowId === activeId ? nextActiveState.hSpan : b.state.hSpan; + return aSpan[0] - bSpan[0]; + }); + const N = sortedWins.length; + const k = sortedWins.findIndex((w) => w.windowId === activeId); + const newSpans = sortedWins.map((w) => { + return w.windowId === activeId ? [...nextActiveState.hSpan] : [...w.state.hSpan]; + }); + const MIN_WIDTH = minColumnSpan; + for (let i = k + 1; i < N; i++) { + const currWin = sortedWins[i]; + const currOriginalStart = currWin.state.hSpan[0]; + const touchBoundaries = []; + for (let j = 0; j < i; j++) { + const prevWin = sortedWins[j]; + const hasVSpanOverlap = Math.max(prevWin.state.vSpan[0], currWin.state.vSpan[0]) < Math.min(prevWin.state.vSpan[1], currWin.state.vSpan[1]); + const touching = prevWin.state.hSpan[1] === currOriginalStart && hasVSpanOverlap; + if (touching) { + touchBoundaries.push(newSpans[j][1]); + } + } + if (touchBoundaries.length > 0) { + const origStart = currWin.state.hSpan[0]; + const origEnd = currWin.state.hSpan[1]; + newSpans[i][0] = Math.max(...touchBoundaries); + const shift = newSpans[i][0] - origStart; + if (shift > 0) { + let limit = gridColumns; + for (const other of sortedWins) { + if (other.windowId === currWin.windowId) continue; + const hasVSpanOverlap = Math.max(other.state.vSpan[0], currWin.state.vSpan[0]) < Math.min(other.state.vSpan[1], currWin.state.vSpan[1]); + if (other.state.hSpan[0] >= origEnd && hasVSpanOverlap) { + if (other.state.hSpan[0] < limit) { + limit = other.state.hSpan[0]; + } + } + } + const freeSpace = limit - origEnd; + const allowedShift = Math.max(0, Math.min(shift, freeSpace)); + newSpans[i][1] = origEnd + allowedShift; + } else if (shift < 0) { + const hasRightAnchor = sortedWins.some((other) => { + if (other.windowId === currWin.windowId) return false; + const hasVSpanOverlap = Math.max(other.state.vSpan[0], currWin.state.vSpan[0]) < Math.min(other.state.vSpan[1], currWin.state.vSpan[1]); + return other.state.hSpan[0] === origEnd && hasVSpanOverlap; + }); + const isAnchored = origEnd === gridColumns || hasRightAnchor; + newSpans[i][1] = isAnchored ? origEnd : origEnd + shift; + } + const width = newSpans[i][1] - newSpans[i][0]; + if (width < MIN_WIDTH) { + newSpans[i][1] = newSpans[i][0] + MIN_WIDTH; + if (newSpans[i][1] > gridColumns) { + newSpans[i][1] = gridColumns; + newSpans[i][0] = gridColumns - MIN_WIDTH; + } + } + } + } + for (let i = k - 1; i >= 0; i--) { + const currWin = sortedWins[i]; + const currOriginalEnd = currWin.state.hSpan[1]; + const touchBoundaries = []; + for (let j = i + 1; j < N; j++) { + const nextWin = sortedWins[j]; + const hasVSpanOverlap = Math.max(nextWin.state.vSpan[0], currWin.state.vSpan[0]) < Math.min(nextWin.state.vSpan[1], currWin.state.vSpan[1]); + const touching = nextWin.state.hSpan[0] === currOriginalEnd && hasVSpanOverlap; + if (touching) { + touchBoundaries.push(newSpans[j][0]); + } + } + if (touchBoundaries.length > 0) { + const origStart = currWin.state.hSpan[0]; + const origEnd = currWin.state.hSpan[1]; + newSpans[i][1] = Math.min(...touchBoundaries); + const shift = origEnd - newSpans[i][1]; + if (shift > 0) { + let limit = 0; + for (const other of sortedWins) { + if (other.windowId === currWin.windowId) continue; + const hasVSpanOverlap = Math.max(other.state.vSpan[0], currWin.state.vSpan[0]) < Math.min(other.state.vSpan[1], currWin.state.vSpan[1]); + if (other.state.hSpan[1] <= origStart && hasVSpanOverlap) { + if (other.state.hSpan[1] > limit) { + limit = other.state.hSpan[1]; + } + } + } + const freeSpace = origStart - limit; + const allowedShift = Math.max(0, Math.min(shift, freeSpace)); + newSpans[i][0] = origStart - allowedShift; + } else if (shift < 0) { + const hasLeftAnchor = sortedWins.some((other) => { + if (other.windowId === currWin.windowId) return false; + const hasVSpanOverlap = Math.max(other.state.vSpan[0], currWin.state.vSpan[0]) < Math.min(other.state.vSpan[1], currWin.state.vSpan[1]); + return other.state.hSpan[1] === origStart && hasVSpanOverlap; + }); + const isAnchored = origStart === 0 || hasLeftAnchor; + newSpans[i][0] = isAnchored ? origStart : origStart - shift; + } + const width = newSpans[i][1] - newSpans[i][0]; + if (width < MIN_WIDTH) { + newSpans[i][0] = newSpans[i][1] - MIN_WIDTH; + if (newSpans[i][0] < 0) { + newSpans[i][0] = 0; + newSpans[i][1] = MIN_WIDTH; + } + } + } + } + for (let i = 0; i < N; i++) { + const w = sortedWins[i]; + if (w.windowId === activeId) { + nextActiveState.hSpan = newSpans[i]; + nextActiveState.hIndex = spanToHIndex(newSpans[i]); + } else { + const nextState = { + ...w.state, + hSpan: newSpans[i], + hIndex: spanToHIndex(newSpans[i]), + lastDirection: direction + }; + result[w.windowId] = nextState; + } + } + } + if (direction === "up" || direction === "down") { + const sortedWins = [...normalizedWindows].sort((a, b) => { + const aSpan = a.windowId === activeId ? nextActiveState.vSpan : a.state.vSpan; + const bSpan = b.windowId === activeId ? nextActiveState.vSpan : b.state.vSpan; + return aSpan[0] - bSpan[0]; + }); + const N = sortedWins.length; + const k = sortedWins.findIndex((w) => w.windowId === activeId); + const newSpans = sortedWins.map((w) => { + return w.windowId === activeId ? [...nextActiveState.vSpan] : [...w.state.vSpan]; + }); + const MIN_HEIGHT = minRowSpan; + for (let i = k + 1; i < N; i++) { + const currWin = sortedWins[i]; + const currOriginalStart = currWin.state.vSpan[0]; + const touchBoundaries = []; + for (let j = 0; j < i; j++) { + const prevWin = sortedWins[j]; + const hasHSpanOverlap = Math.max(prevWin.state.hSpan[0], currWin.state.hSpan[0]) < Math.min(prevWin.state.hSpan[1], currWin.state.hSpan[1]); + const touching = prevWin.state.vSpan[1] === currOriginalStart && hasHSpanOverlap; + if (touching) { + touchBoundaries.push(newSpans[j][1]); + } + } + if (touchBoundaries.length > 0) { + const origStart = currWin.state.vSpan[0]; + const origEnd = currWin.state.vSpan[1]; + newSpans[i][0] = Math.max(...touchBoundaries); + const shift = newSpans[i][0] - origStart; + if (shift > 0) { + let limit = gridRows; + for (const other of sortedWins) { + if (other.windowId === currWin.windowId) continue; + const hasHSpanOverlap = Math.max(other.state.hSpan[0], currWin.state.hSpan[0]) < Math.min(other.state.hSpan[1], currWin.state.hSpan[1]); + if (other.state.vSpan[0] >= origEnd && hasHSpanOverlap) { + if (other.state.vSpan[0] < limit) { + limit = other.state.vSpan[0]; + } + } + } + const freeSpace = limit - origEnd; + const allowedShift = Math.max(0, Math.min(shift, freeSpace)); + newSpans[i][1] = origEnd + allowedShift; + } else if (shift < 0) { + const hasBottomAnchor = sortedWins.some((other) => { + if (other.windowId === currWin.windowId) return false; + const hasHSpanOverlap = Math.max(other.state.hSpan[0], currWin.state.hSpan[0]) < Math.min(other.state.hSpan[1], currWin.state.hSpan[1]); + return other.state.vSpan[0] === origEnd && hasHSpanOverlap; + }); + const isAnchored = origEnd === gridRows || hasBottomAnchor; + newSpans[i][1] = isAnchored ? origEnd : origEnd + shift; + } + const height = newSpans[i][1] - newSpans[i][0]; + if (height < MIN_HEIGHT) { + newSpans[i][1] = newSpans[i][0] + MIN_HEIGHT; + if (newSpans[i][1] > gridRows) { + newSpans[i][1] = gridRows; + newSpans[i][0] = gridRows - MIN_HEIGHT; + } + } + } + } + for (let i = k - 1; i >= 0; i--) { + const currWin = sortedWins[i]; + const currOriginalEnd = currWin.state.vSpan[1]; + const touchBoundaries = []; + for (let j = i + 1; j < N; j++) { + const nextWin = sortedWins[j]; + const hasHSpanOverlap = Math.max(nextWin.state.hSpan[0], currWin.state.hSpan[0]) < Math.min(nextWin.state.hSpan[1], currWin.state.hSpan[1]); + const touching = nextWin.state.vSpan[0] === currOriginalEnd && hasHSpanOverlap; + if (touching) { + touchBoundaries.push(newSpans[j][0]); + } + } + if (touchBoundaries.length > 0) { + const origStart = currWin.state.vSpan[0]; + const origEnd = currWin.state.vSpan[1]; + newSpans[i][1] = Math.min(...touchBoundaries); + const shift = origEnd - newSpans[i][1]; + if (shift > 0) { + let limit = 0; + for (const other of sortedWins) { + if (other.windowId === currWin.windowId) continue; + const hasHSpanOverlap = Math.max(other.state.hSpan[0], currWin.state.hSpan[0]) < Math.min(other.state.hSpan[1], currWin.state.hSpan[1]); + if (other.state.vSpan[1] <= origStart && hasHSpanOverlap) { + if (other.state.vSpan[1] > limit) { + limit = other.state.vSpan[1]; + } + } + } + const freeSpace = origStart - limit; + const allowedShift = Math.max(0, Math.min(shift, freeSpace)); + newSpans[i][0] = origStart - allowedShift; + } else if (shift < 0) { + const hasTopAnchor = sortedWins.some((other) => { + if (other.windowId === currWin.windowId) return false; + const hasHSpanOverlap = Math.max(other.state.hSpan[0], currWin.state.hSpan[0]) < Math.min(other.state.hSpan[1], currWin.state.hSpan[1]); + return other.state.vSpan[1] === origStart && hasHSpanOverlap; + }); + const isAnchored = origStart === 0 || hasTopAnchor; + newSpans[i][0] = isAnchored ? origStart : origStart - shift; + } + const height = newSpans[i][1] - newSpans[i][0]; + if (height < MIN_HEIGHT) { + newSpans[i][0] = newSpans[i][1] - MIN_HEIGHT; + if (newSpans[i][0] < 0) { + newSpans[i][0] = 0; + newSpans[i][1] = MIN_HEIGHT; + } + } + } + } + for (let i = 0; i < N; i++) { + const w = sortedWins[i]; + if (w.windowId === activeId) { + nextActiveState.vSpan = newSpans[i]; + nextActiveState.vIndex = spanToVIndex(newSpans[i]); + } else { + const nextState = { + ...w.state, + vSpan: newSpans[i], + vIndex: spanToVIndex(newSpans[i]), + lastDirection: direction + }; + result[w.windowId] = nextState; + } + } + } + return result; + } + }; + + // src/core/TilingEngine.ts + var TilingEngine = class { + /** + * Возвращает дефолтное пустое состояние окна (до тайлинга) + */ + static getDefaultState() { + return { + hIndex: 5, + // [0, 12] (полная ширина) + vIndex: 5, + // [0, 12] (полная высота) + hSpan: [0, 12], + vSpan: [0, 12], + lastDirection: null + }; + } + /** + * Находит наиболее подходящий горизонтальный спан для первого тайлинга в зависимости от направления и соседей + */ + static getInitialHSpan(direction, siblingSpans, config, fixedVSpan) { + return InitialLayout.getInitialHSpan(direction, siblingSpans, config, fixedVSpan); + } + /** + * Находит наиболее подходящий вертикальный спан для первого тайлинга в зависимости от направления и соседей + */ + static getInitialVSpan(direction, siblingSpans, config, fixedHSpan) { + return InitialLayout.getInitialVSpan(direction, siblingSpans, config, fixedHSpan); + } + /** + * Рассчитывает следующее состояние окна на основе текущего состояния, направления и конфигурации + */ + static calculateNextState(currentState, direction, config, siblingSpans = []) { + const nextState = { ...currentState }; + const gridColumns = getGridColumns(config); + const gridRows = getGridRows(config); + const minColumnSpan = getMinColumnSpan(config); + const minRowSpan = getMinRowSpan(config); + if (!nextState.hSpan) { + nextState.hSpan = HORIZONTAL_SPANS[nextState.hIndex] || [0, gridColumns]; + } + if (!nextState.vSpan) { + nextState.vSpan = VERTICAL_SPANS[nextState.vIndex] || [0, gridRows]; + } + const halfColumns = Math.round(gridColumns / 2); + const halfRows = Math.round(gridRows / 2); + if (currentState.lastDirection === null) { + switch (direction) { + case "left": { + const spans = InitialLayout.getInitialSpans("left", siblingSpans, config); + nextState.hSpan = spans.hSpan; + nextState.vSpan = spans.vSpan; + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = nextState.hSpan[0] > 0 ? "right" : "left"; + break; + } + case "right": { + const spans = InitialLayout.getInitialSpans("right", siblingSpans, config); + nextState.hSpan = spans.hSpan; + nextState.vSpan = spans.vSpan; + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = nextState.hSpan[1] < gridColumns ? "left" : "right"; + break; + } + case "up": { + const spans = InitialLayout.getInitialSpans("up", siblingSpans, config); + nextState.hSpan = spans.hSpan; + nextState.vSpan = spans.vSpan; + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = nextState.vSpan[0] > 0 ? "down" : "up"; + break; + } + case "down": { + const spans = InitialLayout.getInitialSpans("down", siblingSpans, config); + nextState.hSpan = spans.hSpan; + nextState.vSpan = spans.vSpan; + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = nextState.vSpan[1] < gridRows ? "up" : "down"; + break; + } + case "shift-left": + nextState.hSpan = [0, halfColumns]; + nextState.vSpan = [0, gridRows]; + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = "shift-left"; + break; + case "shift-right": + nextState.hSpan = [halfColumns, gridColumns]; + nextState.vSpan = [0, gridRows]; + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = "shift-right"; + break; + case "shift-up": + nextState.hSpan = [0, gridColumns]; + nextState.vSpan = [0, halfRows]; + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = "shift-up"; + break; + case "shift-down": + nextState.hSpan = [0, gridColumns]; + nextState.vSpan = [halfRows, gridRows]; + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = "shift-down"; + break; + } + } else { + const isHorizontalOld = currentState.lastDirection === "left" || currentState.lastDirection === "right" || currentState.lastDirection === "shift-left" || currentState.lastDirection === "shift-right"; + const isVerticalOld = currentState.lastDirection === "up" || currentState.lastDirection === "down" || currentState.lastDirection === "shift-up" || currentState.lastDirection === "shift-down"; + const isHorizontalNew = direction === "left" || direction === "right" || direction === "shift-left" || direction === "shift-right"; + const isVerticalNew = direction === "up" || direction === "down" || direction === "shift-up" || direction === "shift-down"; + const isBothSpansCompressed = currentState.hSpan[1] - currentState.hSpan[0] < gridColumns && currentState.vSpan[1] - currentState.vSpan[0] < gridRows; + if (!isBothSpansCompressed) { + if (isHorizontalOld && isVerticalNew) { + nextState.hSpan = currentState.hSpan; + nextState.hIndex = currentState.hIndex; + nextState.vSpan = this.getInitialVSpan(direction, siblingSpans, config, currentState.hSpan); + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = direction; + return nextState; + } + if (isVerticalOld && isHorizontalNew) { + nextState.vSpan = currentState.vSpan; + nextState.vIndex = currentState.vIndex; + nextState.hSpan = this.getInitialHSpan(direction, siblingSpans, config, currentState.vSpan); + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.lastDirection = direction; + return nextState; + } + } + switch (direction) { + case "left": { + const [start, end] = nextState.hSpan; + let newStart = start; + let newEnd = end; + if (start > 0) { + newStart = Math.max(0, start - config.step); + } else { + newEnd = Math.max(minColumnSpan, end - config.step); + } + const targetSpan = [newStart, newEnd]; + const leftCollision = targetSpan[0] < currentState.hSpan[0] && ChainBlockDetector.isLeftChainBlocked(currentState.hSpan[0], siblingSpans, config, currentState.vSpan); + if (leftCollision) { + const currentStart = currentState.hSpan[0]; + const currentEnd = currentState.hSpan[1]; + nextState.hSpan = [ + currentStart, + Math.max(currentStart + minColumnSpan, currentEnd - config.step) + ]; + } else { + nextState.hSpan = targetSpan; + } + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.lastDirection = "left"; + break; + } + case "right": { + const [start, end] = nextState.hSpan; + let newStart = start; + let newEnd = end; + if (end < gridColumns) { + newEnd = Math.min(gridColumns, end + config.step); + } else { + newStart = Math.min(gridColumns - minColumnSpan, start + config.step); + } + const targetSpan = [newStart, newEnd]; + const rightCollision = targetSpan[1] > currentState.hSpan[1] && ChainBlockDetector.isRightChainBlocked(currentState.hSpan[1], siblingSpans, config, currentState.vSpan); + if (rightCollision) { + const currentStart = currentState.hSpan[0]; + const currentEnd = currentState.hSpan[1]; + nextState.hSpan = [ + Math.min(currentEnd - minColumnSpan, currentStart + config.step), + currentEnd + ]; + } else { + nextState.hSpan = targetSpan; + } + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.lastDirection = "right"; + break; + } + case "up": { + const [start, end] = nextState.vSpan; + let newStart = start; + let newEnd = end; + if (start > 0) { + newStart = Math.max(0, start - config.step); + } else { + newEnd = Math.max(minRowSpan, end - config.step); + } + const targetSpan = [newStart, newEnd]; + const topCollision = targetSpan[0] < currentState.vSpan[0] && ChainBlockDetector.isTopChainBlocked(currentState.vSpan[0], siblingSpans, config, currentState.hSpan); + if (topCollision) { + const currentStart = currentState.vSpan[0]; + const currentEnd = currentState.vSpan[1]; + nextState.vSpan = [ + currentStart, + Math.max(currentStart + minRowSpan, currentEnd - config.step) + ]; + } else { + nextState.vSpan = targetSpan; + } + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = "up"; + break; + } + case "down": { + const [start, end] = nextState.vSpan; + let newStart = start; + let newEnd = end; + if (end < gridRows) { + newEnd = Math.min(gridRows, end + config.step); + } else { + newStart = Math.min(gridRows - minRowSpan, start + config.step); + } + const targetSpan = [newStart, newEnd]; + const bottomCollision = targetSpan[1] > currentState.vSpan[1] && ChainBlockDetector.isBottomChainBlocked(currentState.vSpan[1], siblingSpans, config, currentState.hSpan); + if (bottomCollision) { + const currentStart = currentState.vSpan[0]; + const currentEnd = currentState.vSpan[1]; + nextState.vSpan = [ + Math.min(currentEnd - minRowSpan, currentStart + config.step), + currentEnd + ]; + } else { + nextState.vSpan = targetSpan; + } + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = "down"; + break; + } + case "shift-left": + nextState.hSpan = [0, halfColumns]; + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.lastDirection = "shift-left"; + break; + case "shift-right": + nextState.hSpan = [halfColumns, gridColumns]; + nextState.hIndex = this.spanToHIndex(nextState.hSpan); + nextState.lastDirection = "shift-right"; + break; + case "shift-up": + nextState.vSpan = [0, halfRows]; + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = "shift-up"; + break; + case "shift-down": + nextState.vSpan = [halfRows, gridRows]; + nextState.vIndex = this.spanToVIndex(nextState.vSpan); + nextState.lastDirection = "shift-down"; + break; + } + } + return nextState; + } + /** + * Рассчитывает новые состояния для всей цепочки соприкасающихся окон на основе направления + */ + static calculateChainTransitions(activeId, direction, config, activeWindows, allVisibleSpans = []) { + return ChainTransitions.calculateChainTransitions( + activeId, + direction, + config, + activeWindows, + allVisibleSpans, + this.calculateNextState.bind(this), + this.getDefaultState.bind(this) + ); + } + static spanToHIndex(span) { + return spanToHIndex(span); + } + static spanToVIndex(span) { + return spanToVIndex(span); + } + static geometryToHSpan(geom, monitor, config) { + return GeometryConverter.geometryToHSpan(geom, monitor, config); + } + static geometryToVSpan(geom, monitor, config) { + return GeometryConverter.geometryToVSpan(geom, monitor, config); + } + static stateToGeometry(state, screen, config) { + return GeometryConverter.stateToGeometry(state, screen, config); + } + }; + + // src/core/usecases/TilingUseCase.ts + var _TilingUseCase = class _TilingUseCase { + constructor(shell, cache, configProvider) { + __publicField(this, "shell", shell); + __publicField(this, "cache", cache); + __publicField(this, "configProvider", configProvider); + __publicField(this, "resizeTransactions", []); + __publicField(this, "cacheWriteGeneration", 0); + } + tile(direction) { + const config = this.configProvider.getConfig(); + const configForMonitor = (monitor) => this.configProvider.getConfigForMonitor ? this.configProvider.getConfigForMonitor(monitor) : config; + const windowId = this.shell.getActiveWindowId(); + if (!windowId) { + throw new Error("Could not retrieve active window ID."); + } + const windowGeom = this.shell.getWindowGeometry(windowId); + const extents = this.shell.getFrameExtents(windowId); + const monitors = this.shell.getActiveMonitors(); + const activeMonitor = this.shell.findMonitorForWindow(windowGeom, monitors); + const activeConfig = configForMonitor(activeMonitor); + const visibleWindowIds = this.shell.getVisibleWindowIds(); + const allCached = this.cache.getAllCachedWindows(); + const activeWindowsOnMonitor = []; + let activeWindowIsResized = false; + let activeWindowPhysicalState = null; + const activeCached = allCached[windowId]; + if (activeCached) { + try { + const currentGeom = this.shell.getWindowGeometry(windowId); + const ext = this.shell.getFrameExtents(windowId); + const currentVisible = { + x: currentGeom.x + ext.left, + y: currentGeom.y + ext.top, + width: currentGeom.width - ext.left - ext.right, + height: currentGeom.height - ext.top - ext.bottom + }; + const diffX = Math.abs(currentVisible.x - activeCached.tiledGeometry.x); + const diffY = Math.abs(currentVisible.y - activeCached.tiledGeometry.y); + const diffW = Math.abs(currentVisible.width - activeCached.tiledGeometry.width); + const diffH = Math.abs(currentVisible.height - activeCached.tiledGeometry.height); + const currentMonitor = this.shell.findMonitorForWindow(currentVisible, monitors); + const currentConfig = configForMonitor(currentMonitor); + const hSpan = TilingEngine.geometryToHSpan(currentVisible, currentMonitor, currentConfig); + const vSpan = TilingEngine.geometryToVSpan(currentVisible, currentMonitor, currentConfig); + const spanChanged = !this.spansEqual(hSpan, activeCached.state.hSpan) || !this.spansEqual(vSpan, activeCached.state.vSpan); + const THRESHOLD = 80; + if (spanChanged || diffX > THRESHOLD || diffY > THRESHOLD || diffW > THRESHOLD || diffH > THRESHOLD) { + activeWindowIsResized = true; + activeWindowPhysicalState = { + state: { + ...activeCached.state, + hSpan, + vSpan, + hIndex: TilingEngine.spanToHIndex(hSpan), + vIndex: TilingEngine.spanToVIndex(vSpan) + }, + visibleGeometry: currentVisible, + frameGeometry: currentGeom, + monitor: currentMonitor + }; + } + } catch { + } + } + for (const id of visibleWindowIds) { + let cachedWin = allCached[id]; + if (!cachedWin) { + try { + const currentGeom2 = this.shell.getWindowGeometry(id); + const ext = this.shell.getFrameExtents(id); + const currentVisible = { + x: currentGeom2.x + ext.left, + y: currentGeom2.y + ext.top, + width: currentGeom2.width - ext.left - ext.right, + height: currentGeom2.height - ext.top - ext.bottom + }; + const currentMonitor2 = this.shell.findMonitorForWindow(currentVisible, monitors); + const currentConfig = configForMonitor(currentMonitor2); + const hSpan = TilingEngine.geometryToHSpan(currentVisible, currentMonitor2, currentConfig); + const vSpan = TilingEngine.geometryToVSpan(currentVisible, currentMonitor2, currentConfig); + const testState = { + hIndex: TilingEngine.spanToHIndex(hSpan), + vIndex: TilingEngine.spanToVIndex(vSpan), + hSpan, + vSpan, + lastDirection: null + }; + const idealGeom = TilingEngine.stateToGeometry(testState, currentMonitor2, currentConfig); + const diffX = Math.abs(currentVisible.x - idealGeom.x); + const diffY = Math.abs(currentVisible.y - idealGeom.y); + const diffW = Math.abs(currentVisible.width - idealGeom.width); + const diffH = Math.abs(currentVisible.height - idealGeom.height); + const SNAP_THRESHOLD = 80; + if (diffX <= SNAP_THRESHOLD && diffY <= SNAP_THRESHOLD && diffW <= SNAP_THRESHOLD && diffH <= SNAP_THRESHOLD) { + let hasOverlap = false; + for (const [cachedId, cachedW] of Object.entries(allCached)) { + if (cachedId === id) continue; + let cachedMonitor = currentMonitor2; + try { + const g = this.shell.getWindowGeometry(cachedId); + cachedMonitor = this.shell.findMonitorForWindow(g, monitors); + } catch { + cachedMonitor = this.shell.findMonitorForWindow(cachedW.tiledGeometry, monitors); + } + if (cachedMonitor.id === currentMonitor2.id) { + const hasH = Math.max(hSpan[0], cachedW.state.hSpan[0]) < Math.min(hSpan[1], cachedW.state.hSpan[1]); + const hasV = Math.max(vSpan[0], cachedW.state.vSpan[0]) < Math.min(vSpan[1], cachedW.state.vSpan[1]); + if (hasH && hasV) { + hasOverlap = true; + break; + } + } + } + if (!hasOverlap) { + const restoredState = { + hIndex: TilingEngine.spanToHIndex(hSpan), + vIndex: TilingEngine.spanToVIndex(vSpan), + hSpan, + vSpan, + lastDirection: null + }; + this.cache.saveState(id, restoredState, currentVisible, currentGeom2); + cachedWin = this.cache.getCachedWindow(id); + } + } + } catch { + } + } + if (!cachedWin) continue; + let windowState = { ...cachedWin.state }; + let currentMonitor = activeMonitor; + let currentGeom = cachedWin.tiledGeometry; + if (id === windowId && activeWindowIsResized && activeWindowPhysicalState) { + windowState = activeWindowPhysicalState.state; + currentMonitor = activeWindowPhysicalState.monitor; + currentGeom = activeWindowPhysicalState.frameGeometry; + this.cache.saveState( + id, + windowState, + activeWindowPhysicalState.visibleGeometry, + cachedWin.originalGeometry || currentGeom + ); + } else { + try { + currentGeom = this.shell.getWindowGeometry(id); + const ext = this.shell.getFrameExtents(id); + const currentVisible = { + x: currentGeom.x + ext.left, + y: currentGeom.y + ext.top, + width: currentGeom.width - ext.left - ext.right, + height: currentGeom.height - ext.top - ext.bottom + }; + currentMonitor = this.shell.findMonitorForWindow(currentVisible, monitors); + if (id !== windowId) { + if (currentMonitor.id !== activeMonitor.id) { + continue; + } + const diffX = Math.abs(currentVisible.x - cachedWin.tiledGeometry.x); + const diffY = Math.abs(currentVisible.y - cachedWin.tiledGeometry.y); + const diffW = Math.abs(currentVisible.width - cachedWin.tiledGeometry.width); + const diffH = Math.abs(currentVisible.height - cachedWin.tiledGeometry.height); + const currentConfig = configForMonitor(currentMonitor); + const hSpan = TilingEngine.geometryToHSpan(currentVisible, currentMonitor, currentConfig); + const vSpan = TilingEngine.geometryToVSpan(currentVisible, currentMonitor, currentConfig); + const spanChanged = !this.spansEqual(hSpan, cachedWin.state.hSpan) || !this.spansEqual(vSpan, cachedWin.state.vSpan); + const THRESHOLD = 80; + if (spanChanged || diffX > THRESHOLD || diffY > THRESHOLD || diffW > THRESHOLD || diffH > THRESHOLD) { + windowState = { + ...cachedWin.state, + hSpan, + vSpan, + hIndex: TilingEngine.spanToHIndex(hSpan), + vIndex: TilingEngine.spanToVIndex(vSpan) + }; + this.cache.saveState(id, windowState, currentVisible, cachedWin.originalGeometry || currentGeom); + } + } + } catch { + currentMonitor = this.shell.findMonitorForWindow(cachedWin.tiledGeometry, monitors); + if (id !== windowId && currentMonitor.id !== activeMonitor.id) { + continue; + } + } + } + if (id === windowId) { + if (currentMonitor.id !== activeMonitor.id) continue; + } + const isOldStateSchema = typeof windowState.hIndex !== "number" || typeof windowState.vIndex !== "number"; + if (isOldStateSchema) continue; + activeWindowsOnMonitor.push({ + windowId: id, + state: windowState + }); + } + const undoTransaction = this.getUndoResizeTransaction( + windowId, + direction, + activeMonitor.id, + activeWindowsOnMonitor, + activeMonitor, + activeConfig + ); + if (undoTransaction) { + const operationId2 = this.nextCacheWriteGeneration(); + const axis = this.resizeAxis(direction); + this.applyStates( + undoTransaction.before, + windowId, + activeMonitor, + activeConfig, + operationId2 + ); + if (axis) { + this.refreshUndoStack(windowId, activeMonitor.id, axis); + } + return; + } + const beforeStates = this.captureStates(activeWindowsOnMonitor); + const chainStates = TilingEngine.calculateChainTransitions( + windowId, + direction, + activeConfig, + activeWindowsOnMonitor + ); + const operationId = this.nextCacheWriteGeneration(); + for (const [id, nextState] of Object.entries(chainStates)) { + if (id === windowId) continue; + try { + const cachedWin = allCached[id]; + const currentGeom = this.shell.getWindowGeometry(id); + const originalGeom = cachedWin ? cachedWin.originalGeometry || currentGeom : currentGeom; + const nextGeom = TilingEngine.stateToGeometry(nextState, activeMonitor, activeConfig); + this.shell.unmaximizeWindow(id); + this.shell.applyGeometry(id, nextGeom); + setTimeout(() => { + if (operationId !== this.cacheWriteGeneration) return; + try { + const realGeom = this.shell.getWindowGeometry(id); + this.cache.saveState(id, nextState, realGeom, originalGeom); + } catch { + this.cache.saveState(id, nextState, nextGeom, originalGeom); + } + }, 100); + } catch { + } + } + const activeNextState = chainStates[windowId]; + if (activeNextState) { + const cachedWin = allCached[windowId]; + const originalGeom = cachedWin ? cachedWin.originalGeometry || windowGeom : windowGeom; + const nextGeom = TilingEngine.stateToGeometry(activeNextState, activeMonitor, activeConfig); + try { + this.shell.unmaximizeWindow(windowId); + this.shell.applyGeometry(windowId, nextGeom); + setTimeout(() => { + if (operationId !== this.cacheWriteGeneration) return; + try { + const realGeom = this.shell.getWindowGeometry(windowId); + this.cache.saveState(windowId, activeNextState, realGeom, originalGeom); + } catch { + this.cache.saveState(windowId, activeNextState, nextGeom, originalGeom); + } + }, 100); + this.shell.raiseWindow(windowId); + } catch { + } + } + this.rememberResizeTransaction(windowId, direction, activeMonitor.id, beforeStates, chainStates); + } + restore() { + const windowId = this.shell.getActiveWindowId(); + if (!windowId) { + throw new Error("Could not retrieve active window ID."); + } + const cached = this.cache.getCachedWindow(windowId); + if (cached && cached.originalGeometry) { + this.shell.unmaximizeWindow(windowId); + this.shell.applyGeometry(windowId, cached.originalGeometry); + this.cache.clearState(windowId); + } else { + throw new Error("No original geometry saved for this window."); + } + } + clearCache() { + const windowId = this.shell.getActiveWindowId(); + if (windowId) { + this.cache.clearState(windowId); + } else { + throw new Error("Could not get active window ID for clearing cache."); + } + } + nextCacheWriteGeneration() { + this.cacheWriteGeneration += 1; + return this.cacheWriteGeneration; + } + captureStates(windows) { + const states = {}; + for (const win of windows) { + states[win.windowId] = this.cloneState(win.state); + } + return states; + } + cloneState(state) { + return { + ...state, + hSpan: [...state.hSpan], + vSpan: [...state.vSpan] + }; + } + rememberResizeTransaction(windowId, direction, monitorId, before, after) { + const axis = this.resizeAxis(direction); + if (!axis || !this.hasMeaningfulResize(before, after, axis)) { + this.pruneResizeTransactions(); + return; + } + const plainDirection = direction; + const clonedAfter = {}; + for (const [id, state] of Object.entries(after)) { + clonedAfter[id] = this.cloneState(state); + } + this.pruneResizeTransactions(); + this.resizeTransactions.push({ + windowId, + monitorId, + axis, + direction: plainDirection, + expiresAt: Date.now() + _TilingUseCase.INVERSE_RESIZE_TTL_MS, + before, + after: clonedAfter + }); + if (this.resizeTransactions.length > _TilingUseCase.MAX_RESIZE_UNDO_DEPTH) { + this.resizeTransactions = this.resizeTransactions.slice(-_TilingUseCase.MAX_RESIZE_UNDO_DEPTH); + } + } + getUndoResizeTransaction(windowId, direction, monitorId, activeWindows, monitor, config) { + const axis = this.resizeAxis(direction); + if (!axis) return null; + this.pruneResizeTransactions(); + for (let i = this.resizeTransactions.length - 1; i >= 0; i -= 1) { + const transaction = this.resizeTransactions[i]; + if (transaction.windowId !== windowId || transaction.monitorId !== monitorId || transaction.axis !== axis || this.oppositeDirection(transaction.direction) !== direction) { + continue; + } + const currentStates = this.capturePhysicalStates(activeWindows, Object.keys(transaction.after), monitor, config); + let matches = true; + for (const [id, expectedAfter] of Object.entries(transaction.after)) { + const current = currentStates[id]; + if (!current || !this.statesHaveSameSpans(current, expectedAfter)) { + matches = false; + break; + } + } + if (!matches) return null; + this.resizeTransactions.splice(i, 1); + return transaction; + } + return null; + } + pruneResizeTransactions() { + const now = Date.now(); + this.resizeTransactions = this.resizeTransactions.filter((transaction) => transaction.expiresAt >= now); + } + refreshUndoStack(windowId, monitorId, axis) { + const expiresAt = Date.now() + _TilingUseCase.INVERSE_RESIZE_TTL_MS; + for (const transaction of this.resizeTransactions) { + if (transaction.windowId === windowId && transaction.monitorId === monitorId && transaction.axis === axis) { + transaction.expiresAt = expiresAt; + } + } + } + capturePhysicalStates(fallbackWindows, windowIds, monitor, config) { + const fallbackStates = this.captureStates(fallbackWindows); + const states = {}; + for (const id of windowIds) { + try { + const frame = this.shell.getWindowGeometry(id); + const ext = this.shell.getFrameExtents(id); + const visible = { + x: frame.x + ext.left, + y: frame.y + ext.top, + width: frame.width - ext.left - ext.right, + height: frame.height - ext.top - ext.bottom + }; + const hSpan = TilingEngine.geometryToHSpan(visible, monitor, config); + const vSpan = TilingEngine.geometryToVSpan(visible, monitor, config); + states[id] = { + ...fallbackStates[id] || TilingEngine.getDefaultState(), + hSpan, + vSpan, + hIndex: TilingEngine.spanToHIndex(hSpan), + vIndex: TilingEngine.spanToVIndex(vSpan) + }; + } catch { + if (fallbackStates[id]) { + states[id] = fallbackStates[id]; + } + } + } + return states; + } + applyStates(states, activeId, monitor, config, operationId) { + for (const [id, state] of Object.entries(states)) { + if (id === activeId) continue; + this.applySingleState(id, state, monitor, config, operationId, false); + } + const activeState = states[activeId]; + if (activeState) { + this.applySingleState(activeId, activeState, monitor, config, operationId, true); + } + } + applySingleState(id, state, monitor, config, operationId, raise) { + try { + const currentGeom = this.shell.getWindowGeometry(id); + const cachedWin = this.cache.getCachedWindow(id); + const originalGeom = cachedWin ? cachedWin.originalGeometry || currentGeom : currentGeom; + const nextGeom = TilingEngine.stateToGeometry(state, monitor, config); + this.shell.unmaximizeWindow(id); + this.shell.applyGeometry(id, nextGeom); + setTimeout(() => { + if (operationId !== this.cacheWriteGeneration) return; + try { + const realGeom = this.shell.getWindowGeometry(id); + this.cache.saveState(id, state, realGeom, originalGeom); + } catch { + this.cache.saveState(id, state, nextGeom, originalGeom); + } + }, 100); + if (raise) { + this.shell.raiseWindow(id); + } + } catch { + } + } + resizeAxis(direction) { + if (direction === "left" || direction === "right") return "horizontal"; + if (direction === "up" || direction === "down") return "vertical"; + return null; + } + oppositeDirection(direction) { + switch (direction) { + case "left": + return "right"; + case "right": + return "left"; + case "up": + return "down"; + case "down": + return "up"; + } + } + hasMeaningfulResize(before, after, axis) { + for (const [id, afterState] of Object.entries(after)) { + const beforeState = before[id]; + if (!beforeState) continue; + if (axis === "horizontal" && !this.spansEqual(beforeState.hSpan, afterState.hSpan)) { + return true; + } + if (axis === "vertical" && !this.spansEqual(beforeState.vSpan, afterState.vSpan)) { + return true; + } + } + return false; + } + statesHaveSameSpans(a, b) { + return this.spansEqual(a.hSpan, b.hSpan) && this.spansEqual(a.vSpan, b.vSpan); + } + spansEqual(a, b) { + return a[0] === b[0] && a[1] === b[1]; + } + }; + __publicField(_TilingUseCase, "INVERSE_RESIZE_TTL_MS", 2500); + __publicField(_TilingUseCase, "MAX_RESIZE_UNDO_DEPTH", 16); + var TilingUseCase = _TilingUseCase; + + // src/TilePreview.ts + var St = imports.gi.St; + var Clutter = imports.gi.Clutter; + var Meta = imports.gi.Meta; + var Main = imports.ui.main; + var TilePreview = class { + constructor() { + __publicField(this, "actor"); + __publicField(this, "_showing", false); + __publicField(this, "_rect", null); + __publicField(this, "_monitorIndex", -1); + __publicField(this, "anim_time", 150); + this.actor = new St.Bin({ style_class: "tile-preview", important: true }); + this.actor.set_style("background-color: rgba(52, 152, 219, 0.32); border: 2.5px solid #3498db; border-radius: 8px;"); + global.window_group.add_actor(this.actor); + this._reset(); + } + show(window, tileRect, monitorIndex, animate, animTime, customOpacity, isSecondary, variant, startFromWindow = true) { + this.anim_time = animTime || 150; + if (variant === "blocked" || variant === "blocked-overlap") { + this.actor.set_style("background-color: rgba(231, 76, 60, 0.16); border: 2.5px dashed rgba(231, 76, 60, 0.92); border-radius: 8px;"); + } else if (variant === "blocked-too-small") { + this.actor.set_style("background-color: rgba(243, 156, 18, 0.18); border: 2.5px dashed rgba(243, 156, 18, 0.94); border-radius: 8px;"); + } else if (variant === "blocked-out-of-bounds") { + this.actor.set_style("background-color: rgba(155, 89, 182, 0.16); border: 2.5px dashed rgba(155, 89, 182, 0.94); border-radius: 8px;"); + } else if (variant === "swap-primary") { + this.actor.set_style("background-color: rgba(46, 204, 113, 0.24); border: 3px solid rgba(46, 204, 113, 0.98); border-radius: 8px;"); + } else if (variant === "swap-secondary") { + this.actor.set_style("background-color: rgba(46, 204, 113, 0.12); border: 2.5px dashed rgba(46, 204, 113, 0.86); border-radius: 8px;"); + } else if (isSecondary) { + this.actor.set_style("background-color: rgba(52, 152, 219, 0.08); border: 1.5px dashed rgba(52, 152, 219, 0.5); border-radius: 6px;"); + } else { + this.actor.set_style("background-color: rgba(52, 152, 219, 0.32); border: 2.5px solid #3498db; border-radius: 8px;"); + } + if (this._rect && this._rect.x === tileRect.x && this._rect.y === tileRect.y && this._rect.width === tileRect.width && this._rect.height === tileRect.height) { + return; + } + const changeMonitor = this._monitorIndex === -1 || this._monitorIndex !== monitorIndex; + this._monitorIndex = monitorIndex; + this._rect = tileRect; + const { x, y, width, height } = tileRect; + if (!this._showing || changeMonitor) { + try { + const monitor = Main.layoutManager.monitors[monitorIndex]; + const monitorRect = new Meta.Rectangle({ + x: monitor.x, + y: monitor.y, + width: monitor.width, + height: monitor.height + }); + const [intersected, rect] = startFromWindow ? window.get_buffer_rect().intersect(monitorRect) : [false, null]; + if (intersected) { + this.actor.set_size(rect.width, rect.height); + this.actor.set_position(rect.x, rect.y); + } else { + this.actor.set_size(width, height); + this.actor.set_position(x, y); + } + } catch (e) { + this.actor.set_size(width, height); + this.actor.set_position(x, y); + } + this.actor.opacity = 0; + } + this._showing = true; + this.actor.show(); + const targetOpacity = customOpacity !== void 0 ? customOpacity : isSecondary ? 120 : 180; + const props = { + x, + y, + width, + height, + opacity: targetOpacity + }; + if (animate && Main.animations_enabled) { + this.actor.remove_all_transitions(); + Object.assign(props, { + duration: this.anim_time, + mode: Clutter.AnimationMode.EASE_OUT_QUAD + }); + this.actor.ease(props); + return; + } + this.actor.x = x; + this.actor.y = y; + this.actor.width = width; + this.actor.height = height; + this.actor.opacity = targetOpacity; + } + hide() { + if (!this._showing) return; + this._showing = false; + this.actor.remove_all_transitions(); + if (Main.animations_enabled) { + this.actor.ease({ + opacity: 0, + duration: this.anim_time, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => this._reset() + }); + } else { + this._reset(); + } + } + _reset() { + this.actor.hide(); + this._rect = null; + this._monitorIndex = -1; + } + destroy() { + this.actor.destroy(); + } + }; + + // src/CinnamonAdapters.ts + var Meta2 = imports.gi.Meta; + var CinnamonCache = class { + constructor() { + __publicField(this, "cache", {}); + } + saveState(windowId, state, tiledGeom, originalGeom) { + this.cache[windowId] = { + windowId, + state, + tiledGeometry: tiledGeom, + originalGeometry: originalGeom, + lastUpdated: Date.now() + }; + } + getCachedWindow(windowId) { + return this.cache[windowId] || null; + } + getAllCachedWindows() { + return this.cache; + } + clearState(windowId) { + delete this.cache[windowId]; + } + }; + var CinnamonConfigProvider = class { + constructor(ext) { + __publicField(this, "ext", ext); + } + getConfig() { + return this.ext.getConfigForMonitor ? this.ext.getConfigForMonitor(null) : this.buildConfig(); + } + getConfigForMonitor(monitor) { + return this.ext.getConfigForMonitor ? this.ext.getConfigForMonitor(monitor) : this.buildConfig(); + } + buildConfig() { + return { + gridSize: this.ext.gridSize !== void 0 ? this.ext.gridSize : 12, + gridColumns: this.ext.gridColumns !== void 0 ? this.ext.gridColumns : this.ext.gridSize !== void 0 ? this.ext.gridSize : 12, + gridRows: this.ext.gridRows !== void 0 ? this.ext.gridRows : 6, + minSpan: this.ext.minSpan !== void 0 ? this.ext.minSpan : 2, + minColumnSpan: this.ext.minColumnSpan !== void 0 ? this.ext.minColumnSpan : this.ext.minSpan !== void 0 ? this.ext.minSpan : 2, + minRowSpan: this.ext.minRowSpan !== void 0 ? this.ext.minRowSpan : this.ext.minSpan !== void 0 ? this.ext.minSpan : 2, + step: this.ext.step !== void 0 ? this.ext.step : 1, + gaps: this.ext.gaps !== void 0 ? this.ext.gaps : 8 + }; + } + }; + var CinnamonShellAdapter = class { + constructor(ext) { + __publicField(this, "ext", ext); + } + getActiveWindowId() { + const win = global.display.focus_window; + return win ? win.get_stable_sequence().toString() : ""; + } + getWindowGeometry(id) { + const win = this._findMetaWindow(id); + if (!win) { + throw new Error(`Window ${id} not found.`); + } + const rect = win.get_frame_rect(); + return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; + } + getFrameExtents(id) { + return { left: 0, right: 0, top: 0, bottom: 0 }; + } + getVisibleWindowIds() { + const workspace = global.workspace_manager.get_active_workspace(); + const actors = global.get_window_actors() || []; + return actors.map((a) => a.meta_window).filter((win) => { + if (!win) return false; + if (win.get_window_type() !== Meta2.WindowType.NORMAL) return false; + if (win.minimized) return false; + if (!win.is_on_all_workspaces() && win.get_workspace() !== workspace) return false; + return true; + }).map((win) => win.get_stable_sequence().toString()); + } + getActiveMonitors() { + const monitors = []; + const nMonitors = global.display.get_n_monitors(); + const activeWorkspace = global.workspace_manager.get_active_workspace(); + for (let i = 0; i < nMonitors; i++) { + const rect = global.display.get_monitor_geometry(i); + const workArea = activeWorkspace.get_work_area_for_monitor(i); + monitors.push({ + id: i.toString(), + width: rect.width, + height: rect.height, + x: rect.x, + y: rect.y, + workarea: { + x: workArea.x, + y: workArea.y, + width: workArea.width, + height: workArea.height + } + }); + } + return monitors; + } + findMonitorForWindow(geom, monitors) { + let maxArea = -1; + let bestMonitor = monitors[0]; + for (const m of monitors) { + const ix = Math.max(geom.x, m.workarea.x); + const iy = Math.max(geom.y, m.workarea.y); + const iw = Math.min(geom.x + geom.width, m.workarea.x + m.workarea.width) - ix; + const ih = Math.min(geom.y + geom.height, m.workarea.y + m.workarea.height) - iy; + if (iw > 0 && ih > 0) { + const area = iw * ih; + if (area > maxArea) { + maxArea = area; + bestMonitor = m; + } + } + } + return bestMonitor; + } + applyGeometry(id, geom) { + const win = this._findMetaWindow(id); + if (!win) return; + if (win.maximized_horz || win.maximized_vert) { + win.unmaximize(Meta2.MaximizeFlags.BOTH); + } + const enablePreview = this.ext.enablePreview !== void 0 ? this.ext.enablePreview : true; + if (enablePreview) { + const monitorIndex = win.get_monitor(); + const preview = new TilePreview(); + preview.show(win, geom, monitorIndex, true, 150); + imports.mainloop.timeout_add(60, () => { + try { + win.move_resize_frame(true, geom.x, geom.y, geom.width, geom.height); + } catch (e) { + } + return false; + }); + imports.mainloop.timeout_add(220, () => { + try { + preview.hide(); + preview.destroy(); + } catch (e) { + } + return false; + }); + } else { + win.move_resize_frame(true, geom.x, geom.y, geom.width, geom.height); + } + } + unmaximizeWindow(id) { + const win = this._findMetaWindow(id); + if (win && (win.maximized_horz || win.maximized_vert)) { + win.unmaximize(Meta2.MaximizeFlags.BOTH); + } + } + raiseWindow(id) { + const win = this._findMetaWindow(id); + if (win) { + win.activate(global.get_current_time()); + } + } + _findMetaWindow(stableSequence) { + const actors = global.get_window_actors() || []; + const actor = actors.find((a) => { + return a.meta_window && a.meta_window.get_stable_sequence().toString() === stableSequence; + }); + return actor ? actor.meta_window : null; + } + }; + + // src/DragTiling.ts + function hasSpanOverlap(spanA, spanB) { + return Math.max(spanA[0], spanB[0]) < Math.min(spanA[1], spanB[1]); + } + function spansEqual(spanA, spanB) { + return Boolean(spanA && spanB && spanA[0] === spanB[0] && spanA[1] === spanB[1]); + } + function shouldFloatAfterModifierRelease(input) { + const distance = Math.hypot(input.pointerX - input.startPointerX, input.pointerY - input.startPointerY); + const movementThreshold = input.thresholdPixels ?? 80; + return distance >= movementThreshold; + } + function shouldCancelSourceReturn(sourceState, targetHSpan, targetVSpan, intentPoint) { + if (!sourceState) return false; + if (intentPoint && intentPoint.h >= sourceState.hSpan[0] && intentPoint.h <= sourceState.hSpan[1] && intentPoint.v >= sourceState.vSpan[0] && intentPoint.v <= sourceState.vSpan[1]) { + return true; + } + return spansEqual(sourceState.hSpan, targetHSpan) && spansEqual(sourceState.vSpan, targetVSpan); + } + function hasLayoutOverlaps(states) { + const entries = Object.entries(states); + for (let i = 0; i < entries.length; i++) { + for (let j = i + 1; j < entries.length; j++) { + const a = entries[i][1]; + const b = entries[j][1]; + if (hasSpanOverlap(a.hSpan, b.hSpan) && hasSpanOverlap(a.vSpan, b.vSpan)) { + return true; + } + } + } + return false; + } + function solveDragTransitions(draggedId, targetHSpan, targetVSpan, config, activeWindows, options = {}) { + const states = calculateDragTransitions( + draggedId, + targetHSpan, + targetVSpan, + config, + activeWindows, + options + ); + const reason = getDragBlockReason(states, config); + return { + status: reason ? "blocked" : "valid", + states, + affected: getAffectedWindowIds(states, activeWindows), + reason + }; + } + function getDragBlockReason(states, config) { + const gridColumns = getGridColumns(config); + const gridRows = getGridRows(config); + const minColumnSpan = getMinColumnSpan(config); + const minRowSpan = getMinRowSpan(config); + for (const state of Object.values(states)) { + if (state.hSpan[0] < 0 || state.vSpan[0] < 0 || state.hSpan[1] > gridColumns || state.vSpan[1] > gridRows || state.hSpan[0] >= state.hSpan[1] || state.vSpan[0] >= state.vSpan[1]) { + return "outOfBounds"; + } + if (state.hSpan[1] - state.hSpan[0] < minColumnSpan || state.vSpan[1] - state.vSpan[0] < minRowSpan) { + return "tooSmall"; + } + } + if (hasLayoutOverlaps(states)) { + return "wouldOverlap"; + } + return void 0; + } + function cloneWindowState(state) { + return { + hIndex: state.hIndex, + vIndex: state.vIndex, + hSpan: [...state.hSpan], + vSpan: [...state.vSpan], + lastDirection: state.lastDirection + }; + } + function statesEqual(a, b) { + if (!a || !b) return false; + return a.hSpan[0] === b.hSpan[0] && a.hSpan[1] === b.hSpan[1] && a.vSpan[0] === b.vSpan[0] && a.vSpan[1] === b.vSpan[1]; + } + function restoreDragTransaction(snapshot, draggedId, config, activeWindows) { + if (!snapshot || snapshot.draggedId !== draggedId) return null; + const currentStates = new Map(activeWindows.map((w) => [w.windowId, w.state])); + if (!statesEqual(currentStates.get(draggedId), snapshot.afterStates[draggedId])) { + return null; + } + const idsToRestore = snapshot.affected.filter((id) => id !== draggedId && snapshot.beforeStates[id] && snapshot.afterStates[id]).sort((a, b) => a.localeCompare(b)); + if (idsToRestore.length === 0) return null; + for (const id of idsToRestore) { + if (!statesEqual(currentStates.get(id), snapshot.afterStates[id])) { + return null; + } + } + const restoredStates = {}; + for (const w of activeWindows) { + if (w.windowId === draggedId) continue; + restoredStates[w.windowId] = cloneWindowState(w.state); + } + for (const id of idsToRestore) { + restoredStates[id] = cloneWindowState(snapshot.beforeStates[id]); + } + if (getDragBlockReason(restoredStates, config)) { + return null; + } + return restoredStates; + } + function restoreDragTransactionHistory(snapshots, draggedId, monitorId, config, activeWindows) { + for (let i = snapshots.length - 1; i >= 0; i--) { + const snapshot = snapshots[i]; + if (snapshot.draggedId !== draggedId || snapshot.monitorId !== monitorId) { + continue; + } + const states = restoreDragTransaction(snapshot, draggedId, config, activeWindows); + if (states) { + return { + states, + snapshotIndex: i + }; + } + } + return null; + } + function getAffectedWindowIds(states, activeWindows) { + const originalStates = new Map(activeWindows.map((w) => [w.windowId, w.state])); + return Object.keys(states).filter((id) => { + const previous = originalStates.get(id); + if (!previous) return true; + const next = states[id]; + return previous.hSpan[0] !== next.hSpan[0] || previous.hSpan[1] !== next.hSpan[1] || previous.vSpan[0] !== next.vSpan[0] || previous.vSpan[1] !== next.vSpan[1]; + }).sort((a, b) => a.localeCompare(b)); + } + function spanSize(span) { + return span[1] - span[0]; + } + function stateArea(state) { + return spanSize(state.hSpan) * spanSize(state.vSpan); + } + function stateDelta(previous, next) { + return Math.abs(previous.hSpan[0] - next.hSpan[0]) + Math.abs(previous.hSpan[1] - next.hSpan[1]) + Math.abs(previous.vSpan[0] - next.vSpan[0]) + Math.abs(previous.vSpan[1] - next.vSpan[1]); + } + function scoreDragLayoutCandidate(states, config, activeWindows, draggedId, order = 0) { + const reason = getDragBlockReason(states, config); + const invalidPenalty = reason === "outOfBounds" ? 1e9 : reason === "tooSmall" ? 9e8 : reason === "wouldOverlap" ? 8e8 : 0; + const originalStates = new Map(activeWindows.map((w) => [w.windowId, w.state])); + const affected = getAffectedWindowIds(states, activeWindows); + let score = invalidPenalty + affected.length * 1e4 + order / 1e3; + for (const id of affected) { + const previous = originalStates.get(id); + const next = states[id]; + if (!next) continue; + if (!previous) { + score += id === draggedId ? 0 : 5e4; + continue; + } + const isDragged = id === draggedId; + const movement = stateDelta(previous, next); + const areaDelta = Math.abs(stateArea(previous) - stateArea(next)); + score += (isDragged ? 0 : 1e3) + movement * (isDragged ? 5 : 100) + areaDelta * (isDragged ? 2 : 25); + } + return score; + } + function computeDragTarget(input) { + const { draggedId, mx, my, monitor, config, activeWindows } = input; + const { workarea } = monitor; + const gridColumns = getGridColumns(config); + const gridRows = getGridRows(config); + const minColumnSpan = getMinColumnSpan(config); + const minRowSpan = getMinRowSpan(config); + const colWidth = workarea.width / gridColumns; + const rowHeight = workarea.height / gridRows; + const intentPoint = { + h: (mx - workarea.x) / colWidth, + v: (my - workarea.y) / rowHeight + }; + const targetWidth = Math.max(minColumnSpan, Math.min(gridColumns, input.preferredWidth)); + let startCol = Math.round(intentPoint.h - targetWidth / 2); + if (startCol + targetWidth > gridColumns) { + startCol = gridColumns - targetWidth; + } + if (startCol < 0) startCol = 0; + let targetHSpan = [startCol, startCol + targetWidth]; + const initialHSpan = [...targetHSpan]; + const ratioY = (my - workarea.y) / workarea.height; + const midRows = Math.round(gridRows / 2); + let targetVSpan; + const previousVSpan = input.previousTarget?.targetVSpan; + const shouldKeepTopHalf = spansEqual(previousVSpan, [0, midRows]) && ratioY < 0.32; + const shouldKeepBottomHalf = spansEqual(previousVSpan, [midRows, gridRows]) && ratioY > 0.68; + const shouldKeepFullHeight = spansEqual(previousVSpan, [0, gridRows]) && ratioY >= 0.24 && ratioY <= 0.76; + if (shouldKeepTopHalf || !shouldKeepBottomHalf && !shouldKeepFullHeight && ratioY < 0.28) { + targetVSpan = [0, midRows]; + } else if (shouldKeepBottomHalf || !shouldKeepTopHalf && !shouldKeepFullHeight && ratioY > 0.72) { + targetVSpan = [midRows, gridRows]; + } else { + targetVSpan = [0, gridRows]; + } + const initialVSpan = [...targetVSpan]; + const allStackWindowCandidates = activeWindows.filter((w) => w.windowId !== draggedId).map((w) => ({ + ...w, + hOverlap: Math.max(0, Math.min(targetHSpan[1], w.state.hSpan[1]) - Math.max(targetHSpan[0], w.state.hSpan[0])) + })); + const stackWindows = allStackWindowCandidates.filter((w) => w.hOverlap > 0).sort( + (a, b) => b.hOverlap - a.hOverlap || a.state.vSpan[0] - b.state.vSpan[0] || a.windowId.localeCompare(b.windowId) + ); + const stackGroups = /* @__PURE__ */ new Map(); + for (const w of allStackWindowCandidates) { + const key = `${w.state.hSpan[0]}:${w.state.hSpan[1]}`; + const existing = stackGroups.get(key); + if (existing) { + existing.windows.push(w); + } else { + stackGroups.set(key, { + hSpan: [...w.state.hSpan], + windows: [w] + }); + } + } + const stackGroupCandidates = Array.from(stackGroups.values()).map((group) => { + const containsCursor = intentPoint.h >= group.hSpan[0] && intentPoint.h <= group.hSpan[1]; + const hDistance = containsCursor ? 0 : Math.min(Math.abs(intentPoint.h - group.hSpan[0]), Math.abs(intentPoint.h - group.hSpan[1])); + return { + ...group, + containsCursor, + hDistance, + width: group.hSpan[1] - group.hSpan[0] + }; + }).filter((group) => group.containsCursor || group.hDistance <= 0.5).sort( + (a, b) => Number(b.containsCursor) - Number(a.containsCursor) || a.hDistance - b.hDistance || b.windows.length - a.windows.length || a.width - b.width || a.hSpan[0] - b.hSpan[0] + ); + const cursorVerticalGroup = stackGroupCandidates[0]; + const shouldPreferVerticalStack = (() => { + if (!cursorVerticalGroup) return false; + const cursorRow = intentPoint.v; + const boundaries = [0, gridRows]; + for (const w of cursorVerticalGroup.windows) { + boundaries.push(w.state.vSpan[0], w.state.vSpan[1]); + } + const uniqueBoundaries = Array.from(new Set(boundaries)).filter((v) => v >= 0 && v <= gridRows).sort((a, b) => a - b); + let nearestDistance = Infinity; + for (const boundary of uniqueBoundaries) { + nearestDistance = Math.min(nearestDistance, Math.abs(cursorRow - boundary)); + } + const stackTargetHeight = Math.max( + minRowSpan, + (cursorVerticalGroup.windows.length + 1) * minRowSpan <= gridRows ? Math.round(gridRows / (cursorVerticalGroup.windows.length + 1)) : minRowSpan + ); + const canFitStackVertically = (cursorVerticalGroup.windows.length + 1) * minRowSpan <= gridRows; + const canUseHorizontalRelief = cursorVerticalGroup.width < gridColumns; + return nearestDistance <= Math.max(1, stackTargetHeight / 2) && (canFitStackVertically || canUseHorizontalRelief); + })(); + const horizontalGroups = /* @__PURE__ */ new Map(); + for (const w of allStackWindowCandidates) { + const key = `${w.state.vSpan[0]}:${w.state.vSpan[1]}`; + const existing = horizontalGroups.get(key); + if (existing) { + existing.windows.push(w); + } else { + horizontalGroups.set(key, { + vSpan: [...w.state.vSpan], + windows: [w] + }); + } + } + const nearHorizontalScreenEdge = intentPoint.h <= 0.65 || intentPoint.h >= gridColumns - 0.65; + const targetOverlapsLeftEdgeWindow = allStackWindowCandidates.some( + (w) => w.state.hSpan[0] <= 0 && w.state.hSpan[1] - w.state.hSpan[0] <= minColumnSpan && targetHSpan[0] <= w.state.hSpan[1] + 1 && hasSpanOverlap(targetVSpan, w.state.vSpan) && intentPoint.h <= w.state.hSpan[1] + 1 + ); + const targetOverlapsRightEdgeWindow = allStackWindowCandidates.some( + (w) => w.state.hSpan[1] >= gridColumns && w.state.hSpan[1] - w.state.hSpan[0] <= minColumnSpan && targetHSpan[1] >= w.state.hSpan[0] - 1 && hasSpanOverlap(targetVSpan, w.state.vSpan) && intentPoint.h >= w.state.hSpan[0] - 1 + ); + const targetTouchesHorizontalScreenEdge = targetHSpan[0] <= 0 || targetHSpan[1] >= gridColumns || targetOverlapsLeftEdgeWindow || targetOverlapsRightEdgeWindow; + const horizontalGroupCandidates = Array.from(horizontalGroups.values()).filter((group) => group.windows.length >= 2 || nearHorizontalScreenEdge || targetTouchesHorizontalScreenEdge).map((group) => { + const containsCursor = intentPoint.v >= group.vSpan[0] && intentPoint.v <= group.vSpan[1]; + const vDistance = containsCursor ? 0 : Math.min(Math.abs(intentPoint.v - group.vSpan[0]), Math.abs(intentPoint.v - group.vSpan[1])); + return { + ...group, + containsCursor, + vDistance, + height: group.vSpan[1] - group.vSpan[0] + }; + }).filter((group) => group.containsCursor || group.vDistance <= 0.5).sort( + (a, b) => Number(b.containsCursor) - Number(a.containsCursor) || a.vDistance - b.vDistance || b.windows.length - a.windows.length || a.height - b.height || a.vSpan[0] - b.vSpan[0] + ); + let usedHorizontalStackTarget = false; + const cursorHorizontalGroup = horizontalGroupCandidates[0]; + const debug = { + mode: "base", + preferredWidth: input.preferredWidth, + preferredHeight: input.preferredHeight, + targetWidth, + initialHSpan, + initialVSpan, + verticalCandidates: stackGroupCandidates.length, + horizontalCandidates: horizontalGroupCandidates.length, + shouldPreferVerticalStack, + verticalGroup: cursorVerticalGroup ? { + hSpan: [...cursorVerticalGroup.hSpan], + windows: cursorVerticalGroup.windows.length, + containsCursor: cursorVerticalGroup.containsCursor, + hDistance: cursorVerticalGroup.hDistance + } : void 0, + horizontalGroup: cursorHorizontalGroup ? { + vSpan: [...cursorHorizontalGroup.vSpan], + windows: cursorHorizontalGroup.windows.length, + containsCursor: cursorHorizontalGroup.containsCursor, + vDistance: cursorHorizontalGroup.vDistance + } : void 0 + }; + if (cursorHorizontalGroup && !shouldPreferVerticalStack) { + const cursorCol = intentPoint.h; + const boundaries = [0, gridColumns]; + for (const w of cursorHorizontalGroup.windows) { + boundaries.push(w.state.hSpan[0], w.state.hSpan[1]); + } + const uniqueBoundaries = Array.from(new Set(boundaries)).filter((h) => h >= 0 && h <= gridColumns).sort((a, b) => a - b); + let nearestBoundary = uniqueBoundaries[0]; + let nearestDistance = Math.abs(cursorCol - nearestBoundary); + if (targetHSpan[0] <= 0 || targetOverlapsLeftEdgeWindow) { + nearestBoundary = 0; + nearestDistance = Math.abs(cursorCol); + } else if (targetHSpan[1] >= gridColumns || targetOverlapsRightEdgeWindow) { + nearestBoundary = gridColumns; + nearestDistance = Math.abs(cursorCol - gridColumns); + } else { + for (const boundary of uniqueBoundaries) { + const distance = Math.abs(cursorCol - boundary); + if (distance < nearestDistance) { + nearestBoundary = boundary; + nearestDistance = distance; + } + } + } + let usesStickyHorizontalBoundary = false; + const previousHorizontalBoundary = input.previousTarget?.debug.mode === "horizontal-stack" && spansEqual(input.previousTarget.targetVSpan, cursorHorizontalGroup.vSpan) ? input.previousTarget.debug.nearestBoundary : void 0; + if (previousHorizontalBoundary !== void 0 && uniqueBoundaries.includes(previousHorizontalBoundary)) { + const previousDistance = Math.abs(cursorCol - previousHorizontalBoundary); + const previousExitThreshold = Math.max(1, input.previousTarget?.debug.horizontalThreshold ?? 1) + 0.35; + if (previousDistance <= previousExitThreshold) { + nearestBoundary = previousHorizontalBoundary; + nearestDistance = previousDistance; + usesStickyHorizontalBoundary = true; + } + } + const adjacentWindows = cursorHorizontalGroup.windows.filter( + (w) => Math.abs(w.state.hSpan[0] - nearestBoundary) <= 1 || Math.abs(w.state.hSpan[1] - nearestBoundary) <= 1 + ); + const adjacentWidths = adjacentWindows.map((w) => w.state.hSpan[1] - w.state.hSpan[0]).filter((width) => width >= minColumnSpan); + const requestedSlotWidth = Math.min( + targetWidth, + adjacentWidths.length > 0 ? Math.max(minColumnSpan, Math.min(...adjacentWidths)) : targetWidth + ); + const isScreenEdgeBoundary = nearestBoundary <= 0 || nearestBoundary >= gridColumns; + let slotWidth = requestedSlotWidth; + if (!isScreenEdgeBoundary) { + const canCarveSlot = (width) => { + let start = Math.round(nearestBoundary - width / 2); + if (start < 0) start = 0; + if (start + width > gridColumns) { + start = gridColumns - width; + } + const candidateHSpan = [start, start + width]; + for (const w of adjacentWindows) { + const overlap = Math.max( + 0, + Math.min(candidateHSpan[1], w.state.hSpan[1]) - Math.max(candidateHSpan[0], w.state.hSpan[0]) + ); + if (overlap > 0 && w.state.hSpan[1] - w.state.hSpan[0] - overlap < minColumnSpan) { + return false; + } + } + return true; + }; + for (let width = requestedSlotWidth; width >= minColumnSpan; width--) { + if (canCarveSlot(width)) { + slotWidth = width; + break; + } + } + } + const isWideTargetClampedToEdge = isScreenEdgeBoundary && targetWidth > slotWidth && (nearestBoundary <= 0 && targetHSpan[0] <= 0 || nearestBoundary <= 0 && targetOverlapsLeftEdgeWindow || nearestBoundary >= gridColumns && targetHSpan[1] >= gridColumns || nearestBoundary >= gridColumns && targetOverlapsRightEdgeWindow); + const isMinimumEdgeNeighborInsertion = isScreenEdgeBoundary && (targetOverlapsLeftEdgeWindow || targetOverlapsRightEdgeWindow); + const horizontalThreshold = isScreenEdgeBoundary ? isWideTargetClampedToEdge || isMinimumEdgeNeighborInsertion ? Math.max(0.65, targetWidth / 2 + slotWidth / 2, minColumnSpan + 1) : Math.min(0.65, Math.max(0.35, slotWidth / 3)) : Math.max(1, requestedSlotWidth / 2); + debug.nearestBoundary = nearestBoundary; + debug.nearestDistance = nearestDistance; + debug.slotWidth = slotWidth; + debug.horizontalThreshold = horizontalThreshold; + if (nearestDistance <= horizontalThreshold || usesStickyHorizontalBoundary) { + targetVSpan = [...cursorHorizontalGroup.vSpan]; + if (nearestBoundary <= 0) { + targetHSpan = [0, slotWidth]; + } else if (nearestBoundary >= gridColumns) { + targetHSpan = [gridColumns - slotWidth, gridColumns]; + } else { + let boundaryStartCol = Math.round(nearestBoundary - slotWidth / 2); + if (boundaryStartCol < 0) boundaryStartCol = 0; + if (boundaryStartCol + slotWidth > gridColumns) { + boundaryStartCol = gridColumns - slotWidth; + } + targetHSpan = [boundaryStartCol, boundaryStartCol + slotWidth]; + } + usedHorizontalStackTarget = true; + debug.mode = "horizontal-stack"; + } + } + if (!usedHorizontalStackTarget && stackWindows.length > 0) { + const targetSpanWidth = targetHSpan[1] - targetHSpan[0]; + const overlapBasedStackWindows = (() => { + const maxOverlap = stackWindows[0].hOverlap; + return stackWindows.filter((w) => w.hOverlap === maxOverlap && w.hOverlap / targetSpanWidth >= 0.5).sort((a, b) => a.state.vSpan[0] - b.state.vSpan[0] || a.windowId.localeCompare(b.windowId)); + })(); + const cursorStackGroup = cursorVerticalGroup; + const columnStackWindows = cursorStackGroup ? cursorStackGroup.windows.sort((a, b) => a.state.vSpan[0] - b.state.vSpan[0] || a.windowId.localeCompare(b.windowId)) : overlapBasedStackWindows; + if (columnStackWindows.length > 0) { + const cursorRow = intentPoint.v; + const boundaries = [0, gridRows]; + for (const w of columnStackWindows) { + boundaries.push(w.state.vSpan[0], w.state.vSpan[1]); + } + const uniqueBoundaries = Array.from(new Set(boundaries)).filter((v) => v >= 0 && v <= gridRows).sort((a, b) => a - b); + let nearestBoundary = uniqueBoundaries[0]; + let nearestDistance = Math.abs(cursorRow - nearestBoundary); + for (const boundary of uniqueBoundaries) { + const distance = Math.abs(cursorRow - boundary); + if (distance < nearestDistance) { + nearestBoundary = boundary; + nearestDistance = distance; + } + } + const stackTargetHeight = Math.max( + minRowSpan, + (columnStackWindows.length + 1) * minRowSpan <= gridRows ? Math.round(gridRows / (columnStackWindows.length + 1)) : minRowSpan + ); + const boundaryThreshold = Math.max(1, stackTargetHeight / 2); + const canFitStackVertically = (columnStackWindows.length + 1) * minRowSpan <= gridRows; + const canUseHorizontalRelief = Boolean(cursorStackGroup && cursorStackGroup.width < gridColumns); + let usesStickyVerticalBoundary = false; + const previousVerticalBoundary = input.previousTarget?.debug.mode === "vertical-stack" && cursorStackGroup && spansEqual(input.previousTarget.targetHSpan, cursorStackGroup.hSpan) ? input.previousTarget.debug.nearestBoundary : void 0; + if (previousVerticalBoundary !== void 0 && uniqueBoundaries.includes(previousVerticalBoundary)) { + const previousDistance = Math.abs(cursorRow - previousVerticalBoundary); + if (previousDistance <= boundaryThreshold + 0.35) { + nearestBoundary = previousVerticalBoundary; + nearestDistance = previousDistance; + usesStickyVerticalBoundary = true; + } + } + if ((nearestDistance <= boundaryThreshold || usesStickyVerticalBoundary) && (canFitStackVertically || canUseHorizontalRelief)) { + debug.nearestBoundary = nearestBoundary; + debug.nearestDistance = nearestDistance; + debug.stackTargetHeight = stackTargetHeight; + debug.boundaryThreshold = boundaryThreshold; + debug.mode = "vertical-stack"; + if (cursorStackGroup) { + targetHSpan = [...cursorStackGroup.hSpan]; + } + if (nearestBoundary <= 0) { + targetVSpan = [0, stackTargetHeight]; + } else if (nearestBoundary >= gridRows) { + targetVSpan = [gridRows - stackTargetHeight, gridRows]; + } else { + let startRow = Math.round(nearestBoundary - stackTargetHeight / 2); + if (startRow < 0) startRow = 0; + if (startRow + stackTargetHeight > gridRows) { + startRow = gridRows - stackTargetHeight; + } + targetVSpan = [startRow, startRow + stackTargetHeight]; + } + } + } + } + return { + targetHSpan, + targetVSpan, + intentPoint, + debug + }; + } + function calculateDragTransitions(draggedId, targetHSpan, targetVSpan, config, activeWindows, options = {}) { + const states = {}; + const visited = /* @__PURE__ */ new Set(); + const touched = /* @__PURE__ */ new Set(); + const gridColumns = getGridColumns(config); + const gridRows = getGridRows(config); + const minColumnSpan = getMinColumnSpan(config); + const minRowSpan = getMinRowSpan(config); + const otherWindows = activeWindows.filter((w) => w.windowId !== draggedId).sort(compareWindowsByGridPosition); + for (const w of otherWindows) { + states[w.windowId] = { + hIndex: w.state.hIndex, + vIndex: w.state.vIndex, + hSpan: [...w.state.hSpan || [0, gridColumns]], + vSpan: [...w.state.vSpan || [0, gridRows]], + lastDirection: w.state.lastDirection + }; + } + const hasVerticalOverlap = (spanA, spanB) => { + return Math.max(spanA[0], spanB[0]) < Math.min(spanA[1], spanB[1]); + }; + const hasHorizontalOverlap = (spanA, spanB) => { + return Math.max(spanA[0], spanB[0]) < Math.min(spanA[1], spanB[1]); + }; + const spansEqual2 = (spanA, spanB) => { + return spanA[0] === spanB[0] && spanA[1] === spanB[1]; + }; + const spanSize2 = (span) => span[1] - span[0]; + const spanCenter = (span) => (span[0] + span[1]) / 2; + const cloneState = (state) => ({ + hIndex: state.hIndex, + vIndex: state.vIndex, + hSpan: [...state.hSpan], + vSpan: [...state.vSpan], + lastDirection: state.lastDirection + }); + const updateIndexes = (state) => { + state.hIndex = TilingEngine.spanToHIndex(state.hSpan); + state.vIndex = TilingEngine.spanToVIndex(state.vSpan); + }; + const setHSpan = (id, hSpan) => { + const state = states[id]; + state.hSpan = hSpan; + state.hIndex = TilingEngine.spanToHIndex(hSpan); + touched.add(id); + }; + const setVSpan = (id, vSpan) => { + const state = states[id]; + state.vSpan = vSpan; + state.vIndex = TilingEngine.spanToVIndex(vSpan); + touched.add(id); + }; + const cloneStates = (source) => { + const cloned = {}; + for (const [id, state] of Object.entries(source)) { + cloned[id] = cloneState(state); + } + return cloned; + }; + const scoreCandidateStates = (candidateStates, order = 0) => scoreDragLayoutCandidate(candidateStates, config, activeWindows, draggedId, order); + const applyCandidateStates = (candidateStates) => { + for (const [id, nextState] of Object.entries(candidateStates)) { + if (!states[id] || !statesEqual(states[id], nextState)) { + states[id] = cloneState(nextState); + touched.add(id); + } + } + }; + const rectsOverlap = (a, b) => { + return hasHorizontalOverlap(a.hSpan, b.hSpan) && hasVerticalOverlap(a.vSpan, b.vSpan); + }; + const maybeSwapWindows = () => { + if (!options.swapWindows || !options.intentPoint) return null; + const draggedWin2 = activeWindows.find((w) => w.windowId === draggedId); + if (!draggedWin2) return null; + const draggedState = draggedWin2.state; + const targetWin = otherWindows.find((w) => { + const state = w.state; + return options.intentPoint.h >= state.hSpan[0] && options.intentPoint.h <= state.hSpan[1] && options.intentPoint.v >= state.vSpan[0] && options.intentPoint.v <= state.vSpan[1]; + }); + if (!targetWin) return null; + const targetState = targetWin.state; + const swappedStates = {}; + for (const w of activeWindows) { + if (w.windowId === draggedId) { + swappedStates[w.windowId] = cloneState(targetState); + swappedStates[w.windowId].lastDirection = null; + } else if (w.windowId === targetWin.windowId) { + swappedStates[w.windowId] = cloneState(draggedState); + swappedStates[w.windowId].lastDirection = null; + } else { + swappedStates[w.windowId] = cloneState(w.state); + } + } + return swappedStates; + }; + const swapStates = maybeSwapWindows(); + if (swapStates) return swapStates; + const getCarveCandidatesAwayFromTarget = (id) => { + const state = states[id]; + const stateWidth = spanSize2(state.hSpan); + const stateHeight = spanSize2(state.vSpan); + const originalArea = stateWidth * stateHeight; + const hOverlap = Math.max(0, Math.min(targetHSpan[1], state.hSpan[1]) - Math.max(targetHSpan[0], state.hSpan[0])); + const vOverlap = Math.max(0, Math.min(targetVSpan[1], state.vSpan[1]) - Math.max(targetVSpan[0], state.vSpan[0])); + const hCoverage = stateWidth > 0 ? hOverlap / stateWidth : 0; + const vCoverage = stateHeight > 0 ? vOverlap / stateHeight : 0; + const touchesHorizontalEdge = Math.min(Math.abs(targetHSpan[0] - state.hSpan[0]), Math.abs(targetHSpan[1] - state.hSpan[1])) <= 0.5; + const touchesVerticalEdge = Math.min(Math.abs(targetVSpan[0] - state.vSpan[0]), Math.abs(targetVSpan[1] - state.vSpan[1])) <= 0.5; + const horizontalSpanCandidates = spanCenter(targetHSpan) >= spanCenter(state.hSpan) ? [[state.hSpan[0], targetHSpan[0]], [targetHSpan[1], state.hSpan[1]]] : [[targetHSpan[1], state.hSpan[1]], [state.hSpan[0], targetHSpan[0]]]; + const verticalSpanCandidates = spanCenter(targetVSpan) >= spanCenter(state.vSpan) ? [[state.vSpan[0], targetVSpan[0]], [targetVSpan[1], state.vSpan[1]]] : [[targetVSpan[1], state.vSpan[1]], [state.vSpan[0], targetVSpan[0]]]; + const candidates = []; + let order = 0; + for (const span of horizontalSpanCandidates) { + if (span[0] >= 0 && span[1] <= gridColumns && spanSize2(span) >= minColumnSpan) { + const remainingArea = spanSize2(span) * stateHeight; + const axisPreference = (vCoverage - hCoverage) * 2 + (touchesHorizontalEdge ? 1 : 0) - (touchesVerticalEdge ? 0.5 : 0) + 0.05; + candidates.push({ + axis: "horizontal", + span, + score: (originalArea - remainingArea) * 10 - axisPreference, + order: order++ + }); + } + } + for (const span of verticalSpanCandidates) { + if (span[0] >= 0 && span[1] <= gridRows && spanSize2(span) >= minRowSpan) { + const remainingArea = stateWidth * spanSize2(span); + const axisPreference = (hCoverage - vCoverage) * 2 + (touchesVerticalEdge ? 1 : 0) - (touchesHorizontalEdge ? 0.5 : 0); + candidates.push({ + axis: "vertical", + span, + score: (originalArea - remainingArea) * 10 - axisPreference, + order: order++ + }); + } + } + return candidates.sort( + (a, b) => a.score - b.score || a.order - b.order + ); + }; + const carveAwayFromTarget = (id) => { + const candidates = getCarveCandidatesAwayFromTarget(id); + let bestCandidate = null; + let bestScore = Number.POSITIVE_INFINITY; + for (const [index, candidate] of candidates.entries()) { + const candidateStates = cloneStates(states); + if (candidate.axis === "horizontal") { + candidateStates[id].hSpan = [...candidate.span]; + candidateStates[id].hIndex = TilingEngine.spanToHIndex(candidate.span); + } else { + candidateStates[id].vSpan = [...candidate.span]; + candidateStates[id].vIndex = TilingEngine.spanToVIndex(candidate.span); + } + const score = scoreCandidateStates(candidateStates, index) + candidate.score / 1e3; + if (score < bestScore) { + bestScore = score; + bestCandidate = candidate; + } + } + if (!bestCandidate) return false; + if (bestCandidate.axis === "horizontal") { + setHSpan(id, bestCandidate.span); + } else { + setVSpan(id, bestCandidate.span); + } + return true; + }; + const separateHorizontally = (movableId, anchorId) => { + const movable = states[movableId]; + const anchor = states[anchorId]; + const movableCenter = spanCenter(movable.hSpan); + const anchorCenter = spanCenter(anchor.hSpan); + const candidates = movableCenter >= anchorCenter ? [[anchor.hSpan[1], movable.hSpan[1]], [movable.hSpan[0], anchor.hSpan[0]]] : [[movable.hSpan[0], anchor.hSpan[0]], [anchor.hSpan[1], movable.hSpan[1]]]; + for (const candidate of candidates) { + if (candidate[0] >= 0 && candidate[1] <= gridColumns && spanSize2(candidate) >= minColumnSpan) { + setHSpan(movableId, candidate); + return true; + } + } + return false; + }; + const separateVertically = (movableId, anchorId) => { + const movable = states[movableId]; + const anchor = states[anchorId]; + const movableCenter = spanCenter(movable.vSpan); + const anchorCenter = spanCenter(anchor.vSpan); + const candidates = movableCenter >= anchorCenter ? [[anchor.vSpan[1], movable.vSpan[1]], [movable.vSpan[0], anchor.vSpan[0]]] : [[movable.vSpan[0], anchor.vSpan[0]], [anchor.vSpan[1], movable.vSpan[1]]]; + for (const candidate of candidates) { + if (candidate[0] >= 0 && candidate[1] <= gridRows && spanSize2(candidate) >= minRowSpan) { + setVSpan(movableId, candidate); + return true; + } + } + return false; + }; + const separateFromAnchor = (movableId, anchorId) => { + const movable = states[movableId]; + const anchor = states[anchorId]; + const hOverlap = Math.min(movable.hSpan[1], anchor.hSpan[1]) - Math.max(movable.hSpan[0], anchor.hSpan[0]); + const vOverlap = Math.min(movable.vSpan[1], anchor.vSpan[1]) - Math.max(movable.vSpan[0], anchor.vSpan[0]); + if (hOverlap <= vOverlap) { + return separateHorizontally(movableId, anchorId) || separateVertically(movableId, anchorId); + } + return separateVertically(movableId, anchorId) || separateHorizontally(movableId, anchorId); + }; + const sanitizeTouchedOverlaps = () => { + const ids = Object.keys(states); + for (let pass = 0; pass < ids.length * 2; pass++) { + let changed = false; + for (let i = 0; i < ids.length; i++) { + for (let j = i + 1; j < ids.length; j++) { + const aId = ids[i]; + const bId = ids[j]; + const a = states[aId]; + const b = states[bId]; + if (!rectsOverlap(a, b)) continue; + let movableId = null; + let anchorId = null; + if (aId === draggedId) { + movableId = bId; + anchorId = aId; + } else if (bId === draggedId) { + movableId = aId; + anchorId = bId; + } else if (touched.has(aId) && !touched.has(bId)) { + movableId = aId; + anchorId = bId; + } else if (touched.has(bId) && !touched.has(aId)) { + movableId = bId; + anchorId = aId; + } else if (touched.has(aId)) { + movableId = aId; + anchorId = bId; + } + if (movableId && anchorId && separateFromAnchor(movableId, anchorId)) { + updateIndexes(states[movableId]); + changed = true; + } + } + } + if (!changed) break; + } + }; + const draggedWin = activeWindows.find((w) => w.windowId === draggedId); + if (draggedWin && draggedWin.state && draggedWin.state.hSpan && draggedWin.state.vSpan) { + const vacantHSpan = draggedWin.state.hSpan; + const vacantVSpan = draggedWin.state.vSpan; + const isHSpanCropped = vacantHSpan[1] - vacantHSpan[0] < gridColumns; + const isVSpanCropped = vacantVSpan[1] - vacantVSpan[0] < gridRows; + const isSameAsTarget = spansEqual2(vacantHSpan, targetHSpan) && spansEqual2(vacantVSpan, targetVSpan); + if (!isSameAsTarget && (isHSpanCropped || isVSpanCropped)) { + const collapsedStates = collapseVacancy(draggedId, config, activeWindows); + for (const [id, collapsedState] of Object.entries(collapsedStates)) { + const previous = states[id]; + states[id] = cloneState(collapsedState); + if (!previous || previous.hSpan[0] !== collapsedState.hSpan[0] || previous.hSpan[1] !== collapsedState.hSpan[1] || previous.vSpan[0] !== collapsedState.vSpan[0] || previous.vSpan[1] !== collapsedState.vSpan[1]) { + touched.add(id); + } + } + } + } + states[draggedId] = { + hIndex: TilingEngine.spanToHIndex(targetHSpan), + vIndex: TilingEngine.spanToVIndex(targetVSpan), + hSpan: [...targetHSpan], + vSpan: [...targetVSpan], + lastDirection: null + }; + const tryRedistributeVerticalStackInsertion = () => { + if (spanSize2(targetVSpan) >= gridRows) return false; + const sameColumnWindows = otherWindows.filter((w) => spansEqual2(states[w.windowId].hSpan, targetHSpan)).sort((a, b) => states[a.windowId].vSpan[0] - states[b.windowId].vSpan[0] || a.windowId.localeCompare(b.windowId)); + const collidingStackWindows = sameColumnWindows.filter((w) => rectsOverlap(states[w.windowId], states[draggedId])); + if (sameColumnWindows.length === 0 || collidingStackWindows.length === 0) { + return false; + } + if ((sameColumnWindows.length + 1) * minRowSpan > gridRows) { + return false; + } + const stackTop = Math.min(...sameColumnWindows.map((w) => states[w.windowId].vSpan[0])); + const stackBottom = Math.max(...sameColumnWindows.map((w) => states[w.windowId].vSpan[1])); + if (stackTop > 0 || stackBottom < gridRows) { + return false; + } + const slotCount = sameColumnWindows.length + 1; + const targetCenter = spanCenter(targetVSpan); + const targetIndex = Math.max( + 0, + Math.min(slotCount - 1, Math.floor(targetCenter / (gridRows / slotCount))) + ); + const baseHeight = Math.floor(gridRows / slotCount); + const extraRows = gridRows - baseHeight * slotCount; + const slots = []; + let cursor = 0; + for (let i = 0; i < slotCount; i++) { + const height = baseHeight + (i < extraRows ? 1 : 0); + slots.push([cursor, cursor + height]); + cursor += height; + } + const candidateStates = {}; + for (const [id, state] of Object.entries(states)) { + candidateStates[id] = cloneState(state); + } + candidateStates[draggedId].hSpan = [...targetHSpan]; + candidateStates[draggedId].vSpan = [...slots[targetIndex]]; + updateIndexes(candidateStates[draggedId]); + let sourceIndex = 0; + for (let slotIndex = 0; slotIndex < slotCount; slotIndex++) { + if (slotIndex === targetIndex) continue; + const stackWindow = sameColumnWindows[sourceIndex++]; + const state = candidateStates[stackWindow.windowId]; + state.hSpan = [...targetHSpan]; + state.vSpan = [...slots[slotIndex]]; + updateIndexes(state); + } + if (getDragBlockReason(candidateStates, config)) { + return false; + } + for (const [id, nextState] of Object.entries(candidateStates)) { + if (!statesEqual(states[id], nextState)) { + states[id] = cloneState(nextState); + touched.add(id); + } + } + return true; + }; + const tryRelocateTightVerticalStack = () => { + const targetWidth = spanSize2(targetHSpan); + if (targetWidth < minColumnSpan) return false; + if (spanSize2(targetVSpan) >= gridRows) return false; + const sameColumnWindows = otherWindows.filter((w) => spansEqual2(states[w.windowId].hSpan, targetHSpan)).sort((a, b) => states[a.windowId].vSpan[0] - states[b.windowId].vSpan[0] || a.windowId.localeCompare(b.windowId)); + const collidingStackWindows = sameColumnWindows.filter((w) => rectsOverlap(states[w.windowId], states[draggedId])); + if (sameColumnWindows.length === 0 || collidingStackWindows.length === 0) { + return false; + } + const hasPinnedCollision = collidingStackWindows.some( + (w) => spanSize2(states[w.windowId].hSpan) <= minColumnSpan || spanSize2(states[w.windowId].vSpan) <= minRowSpan + ); + if (!hasPinnedCollision) return false; + const stackIds = new Set(sameColumnWindows.map((w) => w.windowId)); + const candidateHSpans = []; + const leftCandidate = [targetHSpan[0] - targetWidth, targetHSpan[0]]; + const rightCandidate = [targetHSpan[1], targetHSpan[1] + targetWidth]; + if (targetHSpan[1] >= gridColumns) { + candidateHSpans.push(leftCandidate, rightCandidate); + } else if (targetHSpan[0] <= 0) { + candidateHSpans.push(rightCandidate, leftCandidate); + } else { + candidateHSpans.push(leftCandidate, rightCandidate); + } + const overlapsMovedStack = (candidateStates, state) => { + for (const stackId of stackIds) { + if (hasHorizontalOverlap(state.hSpan, candidateStates[stackId].hSpan) && hasVerticalOverlap(state.vSpan, candidateStates[stackId].vSpan)) { + return true; + } + } + return false; + }; + const carveAwayFromHSpan = (state, avoidHSpan) => { + const avoidCenter = spanCenter(avoidHSpan); + const stateCenter = spanCenter(state.hSpan); + const candidates = avoidCenter >= stateCenter ? [[state.hSpan[0], avoidHSpan[0]], [avoidHSpan[1], state.hSpan[1]]] : [[avoidHSpan[1], state.hSpan[1]], [state.hSpan[0], avoidHSpan[0]]]; + for (const candidate of candidates) { + if (candidate[0] >= 0 && candidate[1] <= gridColumns && spanSize2(candidate) >= minColumnSpan) { + const nextState = cloneState(state); + nextState.hSpan = candidate; + updateIndexes(nextState); + return nextState; + } + } + return null; + }; + let bestStates = null; + let bestScore = Number.POSITIVE_INFINITY; + for (const [index, candidateHSpan] of candidateHSpans.entries()) { + if (candidateHSpan[0] < 0 || candidateHSpan[1] > gridColumns) continue; + const candidateStates = cloneStates(states); + for (const stackId of stackIds) { + candidateStates[stackId].hSpan = [...candidateHSpan]; + updateIndexes(candidateStates[stackId]); + } + let failed = false; + for (const [id, state] of Object.entries(candidateStates)) { + if (id === draggedId || stackIds.has(id)) continue; + if (!overlapsMovedStack(candidateStates, state)) continue; + const carved = carveAwayFromHSpan(state, candidateHSpan); + if (!carved) { + failed = true; + break; + } + candidateStates[id] = carved; + } + if (failed || getDragBlockReason(candidateStates, config)) { + continue; + } + const score = scoreCandidateStates(candidateStates, index); + if (score < bestScore) { + bestScore = score; + bestStates = candidateStates; + } + } + if (bestStates) { + applyCandidateStates(bestStates); + return true; + } + return false; + }; + const tryRelocateTightHorizontalStack = () => { + const targetWidth = spanSize2(targetHSpan); + if (targetWidth < minColumnSpan) return false; + if (spanSize2(targetHSpan) >= gridColumns) return false; + const sameRowWindows = otherWindows.filter((w) => spansEqual2(states[w.windowId].vSpan, targetVSpan)).sort((a, b) => states[a.windowId].hSpan[0] - states[b.windowId].hSpan[0] || a.windowId.localeCompare(b.windowId)); + const collidingRowWindows = sameRowWindows.filter((w) => rectsOverlap(states[w.windowId], states[draggedId])); + const intentNearLeftEdge = options.intentPoint ? options.intentPoint.h <= 0.65 : false; + const intentNearRightEdge = options.intentPoint ? options.intentPoint.h >= gridColumns - 0.65 : false; + const targetTouchesLeftEdge = targetHSpan[0] <= 0; + const targetTouchesRightEdge = targetHSpan[1] >= gridColumns; + const isExplicitScreenEdgeInsertion = targetTouchesLeftEdge && intentNearLeftEdge || targetTouchesRightEdge && intentNearRightEdge; + const isAutoNarrowedScreenEdgeInsertion = Boolean(options.preferredWidth && options.preferredWidth > targetWidth) && (targetTouchesLeftEdge || targetTouchesRightEdge); + if (sameRowWindows.length < 2 && !isExplicitScreenEdgeInsertion && !isAutoNarrowedScreenEdgeInsertion || collidingRowWindows.length === 0) { + return false; + } + const hasPinnedCollision = collidingRowWindows.some( + (w) => spanSize2(states[w.windowId].hSpan) <= minColumnSpan || spanSize2(states[w.windowId].vSpan) <= minRowSpan + ); + if (!hasPinnedCollision) return false; + const rowIds = new Set(sameRowWindows.map((w) => w.windowId)); + const shifts = []; + if (targetHSpan[1] >= gridColumns) { + shifts.push(-targetWidth); + } else if (targetHSpan[0] <= 0) { + shifts.push(targetWidth); + } else { + const targetCenter = spanCenter(targetHSpan); + const rowCenter = spanCenter([ + Math.min(...sameRowWindows.map((w) => states[w.windowId].hSpan[0])), + Math.max(...sameRowWindows.map((w) => states[w.windowId].hSpan[1])) + ]); + shifts.push(targetCenter >= rowCenter ? -targetWidth : targetWidth); + shifts.push(targetCenter >= rowCenter ? targetWidth : -targetWidth); + } + let bestStates = null; + let bestScore = Number.POSITIVE_INFINITY; + for (const [index, shift] of shifts.entries()) { + const candidateStates = cloneStates(states); + let failed = false; + for (const rowId of rowIds) { + const state = candidateStates[rowId]; + const nextHSpan = [state.hSpan[0] + shift, state.hSpan[1] + shift]; + if (nextHSpan[0] < 0 || nextHSpan[1] > gridColumns) { + failed = true; + break; + } + state.hSpan = nextHSpan; + updateIndexes(state); + } + if (failed || getDragBlockReason(candidateStates, config)) { + continue; + } + const score = scoreCandidateStates(candidateStates, index); + if (score < bestScore) { + bestScore = score; + bestStates = candidateStates; + } + } + if (bestStates) { + applyCandidateStates(bestStates); + return true; + } + return false; + }; + const tryCarveEdgeInsertionCorridor = () => { + if (!options.intentPoint) return false; + if (spanSize2(targetVSpan) < minRowSpan) return false; + const targetTouchesLeftEdge = targetHSpan[0] <= 0; + const targetTouchesRightEdge = targetHSpan[1] >= gridColumns; + const intentNearLeftEdge = options.intentPoint.h <= 0.65; + const intentNearRightEdge = options.intentPoint.h >= gridColumns - 0.65; + const isExplicitScreenEdgeInsertion = targetTouchesLeftEdge && intentNearLeftEdge || targetTouchesRightEdge && intentNearRightEdge; + const isAutoNarrowedScreenEdgeInsertion = Boolean(options.preferredWidth && options.preferredWidth > spanSize2(targetHSpan)) && (targetTouchesLeftEdge || targetTouchesRightEdge); + if (!isExplicitScreenEdgeInsertion && !isAutoNarrowedScreenEdgeInsertion) { + return false; + } + if (!targetTouchesLeftEdge && !targetTouchesRightEdge) return false; + const corridorWidth = spanSize2(targetHSpan); + const direction = targetTouchesLeftEdge ? 1 : -1; + let cursorStart = targetTouchesLeftEdge ? targetHSpan[0] : targetHSpan[1] - corridorWidth; + let cursorEnd = targetTouchesLeftEdge ? targetHSpan[1] : targetHSpan[1]; + const chainIds = /* @__PURE__ */ new Set(); + let donorId = null; + for (let guard = 0; guard < gridColumns; guard++) { + const columnWindows = otherWindows.filter((w) => { + const state = states[w.windowId]; + return state.hSpan[0] === cursorStart && state.hSpan[1] === cursorEnd && hasVerticalOverlap(state.vSpan, targetVSpan); + }).sort((a, b) => a.windowId.localeCompare(b.windowId)); + if (columnWindows.length === 0) { + const donorWindow = otherWindows.filter((w) => { + const state = states[w.windowId]; + if (!hasVerticalOverlap(state.vSpan, targetVSpan)) return false; + if (spanSize2(state.hSpan) - corridorWidth < minColumnSpan) return false; + return targetTouchesLeftEdge ? state.hSpan[0] === cursorStart : state.hSpan[1] === cursorEnd; + }).sort((a, b) => { + const aSize = spanSize2(states[a.windowId].hSpan); + const bSize = spanSize2(states[b.windowId].hSpan); + return bSize - aSize || a.windowId.localeCompare(b.windowId); + })[0]; + if (donorWindow) { + donorId = donorWindow.windowId; + } + break; + } + const columnWidth = cursorEnd - cursorStart; + if (columnWidth <= corridorWidth) { + for (const w of columnWindows) { + chainIds.add(w.windowId); + } + cursorStart += direction * corridorWidth; + cursorEnd += direction * corridorWidth; + if (cursorStart < 0 || cursorEnd > gridColumns) return false; + continue; + } + if (columnWidth - corridorWidth < minColumnSpan) { + return false; + } + donorId = columnWindows[0].windowId; + break; + } + if (chainIds.size === 0) return false; + const candidateStates = {}; + for (const [id, state] of Object.entries(states)) { + candidateStates[id] = cloneState(state); + } + for (const id of chainIds) { + const state = candidateStates[id]; + state.hSpan = [state.hSpan[0] + direction * corridorWidth, state.hSpan[1] + direction * corridorWidth]; + updateIndexes(state); + } + if (donorId) { + const donor = candidateStates[donorId]; + donor.hSpan = targetTouchesLeftEdge ? [donor.hSpan[0] + corridorWidth, donor.hSpan[1]] : [donor.hSpan[0], donor.hSpan[1] - corridorWidth]; + updateIndexes(donor); + } + if (getDragBlockReason(candidateStates, config)) { + return false; + } + for (const [id, nextState] of Object.entries(candidateStates)) { + if (!statesEqual(states[id], nextState)) { + states[id] = cloneState(nextState); + touched.add(id); + } + } + return true; + }; + if (!tryRedistributeVerticalStackInsertion()) { + tryRelocateTightVerticalStack(); + } + if (!tryCarveEdgeInsertionCorridor()) { + tryRelocateTightHorizontalStack(); + } + const windowsToProcess = otherWindows; + const pushLeft = (id, rightBoundary) => { + if (visited.has(id)) return; + visited.add(id); + const state = states[id]; + if (!state) return; + const width = state.hSpan[1] - state.hSpan[0]; + const newEnd = rightBoundary; + let newStart = Math.min(state.hSpan[0], newEnd - width); + newStart = Math.max(0, newStart); + const newEndClamped = Math.max(minColumnSpan, Math.min(newEnd, newStart + width)); + const newStartClamped = Math.max(0, newEndClamped - Math.max(minColumnSpan, width)); + state.hSpan = [newStartClamped, newEndClamped]; + state.hIndex = TilingEngine.spanToHIndex(state.hSpan); + touched.add(id); + for (const other of windowsToProcess) { + if (other.windowId === id) continue; + const otherState = states[other.windowId]; + if (hasVerticalOverlap(state.vSpan, otherState.vSpan) && otherState.hSpan[1] > state.hSpan[0] && otherState.hSpan[0] < state.hSpan[0]) { + pushLeft(other.windowId, state.hSpan[0]); + } + } + }; + const pushRight = (id, leftBoundary) => { + if (visited.has(id)) return; + visited.add(id); + const state = states[id]; + if (!state) return; + const width = state.hSpan[1] - state.hSpan[0]; + const newStart = leftBoundary; + let newEnd = Math.max(state.hSpan[1], newStart + width); + newEnd = Math.min(gridColumns, newEnd); + const newStartClamped = Math.max(0, Math.min(newStart, newEnd - minColumnSpan)); + const newEndClamped = Math.min(gridColumns, newStartClamped + Math.max(minColumnSpan, width)); + state.hSpan = [newStartClamped, newEndClamped]; + state.hIndex = TilingEngine.spanToHIndex(state.hSpan); + touched.add(id); + for (const other of windowsToProcess) { + if (other.windowId === id) continue; + const otherState = states[other.windowId]; + if (hasVerticalOverlap(state.vSpan, otherState.vSpan) && otherState.hSpan[0] < state.hSpan[1] && otherState.hSpan[1] > state.hSpan[1]) { + pushRight(other.windowId, state.hSpan[1]); + } + } + }; + const pushUp = (id, bottomBoundary) => { + if (visited.has(id)) return; + visited.add(id); + const state = states[id]; + if (!state) return; + const height = state.vSpan[1] - state.vSpan[0]; + const newEnd = bottomBoundary; + let newStart = Math.min(state.vSpan[0], newEnd - height); + newStart = Math.max(0, newStart); + const newEndClamped = Math.max(minRowSpan, Math.min(newEnd, newStart + height)); + const newStartClamped = Math.max(0, newEndClamped - Math.max(minRowSpan, height)); + state.vSpan = [newStartClamped, newEndClamped]; + state.vIndex = TilingEngine.spanToVIndex(state.vSpan); + touched.add(id); + for (const other of windowsToProcess) { + if (other.windowId === id) continue; + const otherState = states[other.windowId]; + if (hasHorizontalOverlap(state.hSpan, otherState.hSpan) && otherState.vSpan[1] > state.vSpan[0] && otherState.vSpan[0] < state.vSpan[0]) { + pushUp(other.windowId, state.vSpan[0]); + } + } + }; + const pushDown = (id, topBoundary) => { + if (visited.has(id)) return; + visited.add(id); + const state = states[id]; + if (!state) return; + const height = state.vSpan[1] - state.vSpan[0]; + const newStart = topBoundary; + let newEnd = Math.max(state.vSpan[1], newStart + height); + newEnd = Math.min(gridRows, newEnd); + const newStartClamped = Math.max(0, Math.min(newStart, newEnd - minRowSpan)); + const newEndClamped = Math.min(gridRows, newStartClamped + Math.max(minRowSpan, height)); + state.vSpan = [newStartClamped, newEndClamped]; + state.vIndex = TilingEngine.spanToVIndex(state.vSpan); + touched.add(id); + for (const other of windowsToProcess) { + if (other.windowId === id) continue; + const otherState = states[other.windowId]; + if (hasHorizontalOverlap(state.hSpan, otherState.hSpan) && otherState.vSpan[0] < state.vSpan[1] && otherState.vSpan[1] > state.vSpan[1]) { + pushDown(other.windowId, state.vSpan[1]); + } + } + }; + visited.add(draggedId); + for (const w of windowsToProcess) { + const state = states[w.windowId]; + if (hasVerticalOverlap(targetVSpan, state.vSpan) && hasHorizontalOverlap(targetHSpan, state.hSpan)) { + const centerTargetX = (targetHSpan[0] + targetHSpan[1]) / 2; + const centerTargetY = (targetVSpan[0] + targetVSpan[1]) / 2; + const centerWindowX = (state.hSpan[0] + state.hSpan[1]) / 2; + const centerWindowY = (state.vSpan[0] + state.vSpan[1]) / 2; + const dx = centerWindowX - centerTargetX; + const dy = centerWindowY - centerTargetY; + const carved = carveAwayFromTarget(w.windowId); + if (carved) { + continue; + } + if (Math.abs(dx) >= Math.abs(dy)) { + if (dx < 0) { + pushLeft(w.windowId, targetHSpan[0]); + } else { + pushRight(w.windowId, targetHSpan[1]); + } + } else { + if (dy < 0) { + pushUp(w.windowId, targetVSpan[0]); + } else { + pushDown(w.windowId, targetVSpan[1]); + } + } + } + } + sanitizeTouchedOverlaps(); + return states; + } + function compareWindowsByGridPosition(a, b) { + return a.state.vSpan[0] - b.state.vSpan[0] || a.state.hSpan[0] - b.state.hSpan[0] || a.state.vSpan[1] - b.state.vSpan[1] || a.state.hSpan[1] - b.state.hSpan[1] || a.windowId.localeCompare(b.windowId); + } + function collapseVacancy(vacantId, config, activeWindows) { + const states = {}; + const gridColumns = getGridColumns(config); + const gridRows = getGridRows(config); + const minColumnSpan = getMinColumnSpan(config); + const minRowSpan = getMinRowSpan(config); + const vacantWin = activeWindows.find((w) => w.windowId === vacantId); + if (!vacantWin) { + for (const w of activeWindows) { + states[w.windowId] = { + hIndex: w.state.hIndex, + vIndex: w.state.vIndex, + hSpan: [...w.state.hSpan || [0, gridColumns]], + vSpan: [...w.state.vSpan || [0, gridRows]], + lastDirection: w.state.lastDirection + }; + } + return states; + } + const vacantHSpan = vacantWin.state.hSpan; + const vacantVSpan = vacantWin.state.vSpan; + const otherWindows = activeWindows.filter((w) => w.windowId !== vacantId).sort(compareWindowsByGridPosition); + for (const w of otherWindows) { + states[w.windowId] = { + hIndex: w.state.hIndex, + vIndex: w.state.vIndex, + hSpan: [...w.state.hSpan || [0, gridColumns]], + vSpan: [...w.state.vSpan || [0, gridRows]], + lastDirection: w.state.lastDirection + }; + } + const hasVerticalOverlap = (spanA, spanB) => { + return Math.max(spanA[0], spanB[0]) < Math.min(spanA[1], spanB[1]); + }; + const hasHorizontalOverlap = (spanA, spanB) => { + return Math.max(spanA[0], spanB[0]) < Math.min(spanA[1], spanB[1]); + }; + const overlapSize = (spanA, spanB) => { + return Math.max(0, Math.min(spanA[1], spanB[1]) - Math.max(spanA[0], spanB[0])); + }; + const spanSize2 = (span) => span[1] - span[0]; + const spansEqual2 = (spanA, spanB) => { + return spanA[0] === spanB[0] && spanA[1] === spanB[1]; + }; + const covers = (container, inner) => { + return container[0] <= inner[0] && container[1] >= inner[1]; + }; + const wouldOverlap = (id, hSpan, vSpan) => { + for (const other of otherWindows) { + if (other.windowId === id) continue; + const s = states[other.windowId]; + if (hasHorizontalOverlap(hSpan, s.hSpan) && hasVerticalOverlap(vSpan, s.vSpan)) { + return true; + } + } + return false; + }; + const applyCandidate = (id, hSpan, vSpan) => { + const s = states[id]; + s.hSpan = hSpan; + s.vSpan = vSpan; + s.hIndex = TilingEngine.spanToHIndex(hSpan); + s.vIndex = TilingEngine.spanToVIndex(vSpan); + }; + const sameColumnWindows = otherWindows.filter((w) => spansEqual2(states[w.windowId].hSpan, vacantHSpan)); + const prefersVerticalStackCollapse = spanSize2(vacantHSpan) <= minColumnSpan || sameColumnWindows.length >= 2; + if (prefersVerticalStackCollapse && sameColumnWindows.length > 0) { + const stackEntries = [ + { id: vacantId, vSpan: vacantVSpan, vacant: true }, + ...sameColumnWindows.map((w) => ({ + id: w.windowId, + vSpan: states[w.windowId].vSpan, + vacant: false + })) + ].sort((a, b) => a.vSpan[0] - b.vSpan[0] || a.id.localeCompare(b.id)); + const vacantIndex = stackEntries.findIndex((entry) => entry.vacant); + let startIndex = vacantIndex; + let endIndex = vacantIndex; + while (startIndex > 0 && Math.abs(stackEntries[startIndex - 1].vSpan[1] - stackEntries[startIndex].vSpan[0]) <= 1) { + startIndex--; + } + while (endIndex < stackEntries.length - 1 && Math.abs(stackEntries[endIndex].vSpan[1] - stackEntries[endIndex + 1].vSpan[0]) <= 1) { + endIndex++; + } + const connectedStack = stackEntries.slice(startIndex, endIndex + 1); + const remainingStack = connectedStack.filter((entry) => !entry.vacant); + const stackStart = Math.min(...connectedStack.map((entry) => entry.vSpan[0])); + const stackEnd = Math.max(...connectedStack.map((entry) => entry.vSpan[1])); + const stackHeight = stackEnd - stackStart; + if (remainingStack.length > 0 && stackHeight >= remainingStack.length * minRowSpan) { + const previousStackStates = remainingStack.map((entry) => ({ + id: entry.id, + hSpan: [...states[entry.id].hSpan], + vSpan: [...states[entry.id].vSpan] + })); + for (let i = 0; i < remainingStack.length; i++) { + const nextStart = stackStart + Math.round(stackHeight * i / remainingStack.length); + const nextEnd = stackStart + Math.round(stackHeight * (i + 1) / remainingStack.length); + applyCandidate(remainingStack[i].id, [...vacantHSpan], [nextStart, nextEnd]); + } + if (!hasLayoutOverlaps(states)) { + return states; + } + for (const previous of previousStackStates) { + applyCandidate(previous.id, previous.hSpan, previous.vSpan); + } + } + } + const candidates = []; + for (const w of otherWindows) { + const s = states[w.windowId]; + if (hasVerticalOverlap(vacantVSpan, s.vSpan) && Math.abs(s.hSpan[1] - vacantHSpan[0]) <= 1) { + candidates.push({ + id: w.windowId, + priority: 100 + overlapSize(vacantVSpan, s.vSpan), + fullHSpan: [s.hSpan[0], vacantHSpan[1]], + fullVSpan: s.vSpan, + partialHSpan: covers(s.vSpan, vacantVSpan) ? [s.hSpan[0], vacantHSpan[1]] : void 0, + partialVSpan: covers(s.vSpan, vacantVSpan) ? [...vacantVSpan] : void 0 + }); + } + if (hasVerticalOverlap(vacantVSpan, s.vSpan) && Math.abs(s.hSpan[0] - vacantHSpan[1]) <= 1) { + candidates.push({ + id: w.windowId, + priority: 100 + overlapSize(vacantVSpan, s.vSpan), + fullHSpan: [vacantHSpan[0], s.hSpan[1]], + fullVSpan: s.vSpan, + partialHSpan: covers(s.vSpan, vacantVSpan) ? [vacantHSpan[0], s.hSpan[1]] : void 0, + partialVSpan: covers(s.vSpan, vacantVSpan) ? [...vacantVSpan] : void 0 + }); + } + if (hasHorizontalOverlap(vacantHSpan, s.hSpan) && Math.abs(s.vSpan[1] - vacantVSpan[0]) <= 1) { + const isSameColumnStackNeighbor = prefersVerticalStackCollapse && spansEqual2(s.hSpan, vacantHSpan); + candidates.push({ + id: w.windowId, + priority: (isSameColumnStackNeighbor ? 200 : 0) + overlapSize(vacantHSpan, s.hSpan), + fullHSpan: s.hSpan, + fullVSpan: [s.vSpan[0], vacantVSpan[1]] + }); + } + if (hasHorizontalOverlap(vacantHSpan, s.hSpan) && Math.abs(s.vSpan[0] - vacantVSpan[1]) <= 1) { + const isSameColumnStackNeighbor = prefersVerticalStackCollapse && spansEqual2(s.hSpan, vacantHSpan); + candidates.push({ + id: w.windowId, + priority: (isSameColumnStackNeighbor ? 200 : 0) + overlapSize(vacantHSpan, s.hSpan), + fullHSpan: s.hSpan, + fullVSpan: [vacantVSpan[0], s.vSpan[1]] + }); + } + } + candidates.sort((a, b) => b.priority - a.priority || a.id.localeCompare(b.id)); + for (const candidate of candidates) { + if (!wouldOverlap(candidate.id, candidate.fullHSpan, candidate.fullVSpan)) { + applyCandidate(candidate.id, candidate.fullHSpan, candidate.fullVSpan); + return states; + } + if (candidate.partialHSpan && candidate.partialVSpan && !wouldOverlap(candidate.id, candidate.partialHSpan, candidate.partialVSpan)) { + applyCandidate(candidate.id, candidate.partialHSpan, candidate.partialVSpan); + return states; + } + } + return states; + } + + // src/extension.ts + var Settings = imports.ui.settings; + var Main2 = imports.ui.main; + var Meta3 = imports.gi.Meta; + var Clutter2 = imports.gi.Clutter; + var DynamicTilerExtension = class { + constructor(metadata) { + __publicField(this, "metadata"); + __publicField(this, "settings"); + __publicField(this, "shell"); + __publicField(this, "cache"); + __publicField(this, "configProvider"); + __publicField(this, "useCase"); + __publicField(this, "bindings", {}); + // Settings values automatically bound by Cinnamon settings system + __publicField(this, "gaps"); + __publicField(this, "gridSize"); + __publicField(this, "gridColumns"); + __publicField(this, "gridRows"); + __publicField(this, "horizontalGridColumns"); + __publicField(this, "horizontalGridRows"); + __publicField(this, "verticalGridColumns"); + __publicField(this, "verticalGridRows"); + __publicField(this, "ultrawideGridColumns"); + __publicField(this, "ultrawideGridRows"); + __publicField(this, "monitorGridOverrides"); + __publicField(this, "minSpan"); + __publicField(this, "minColumnSpan"); + __publicField(this, "minRowSpan"); + __publicField(this, "step"); + __publicField(this, "enablePreview"); + __publicField(this, "enableDebugLogs"); + __publicField(this, "enable-dnd-tiling"); + __publicField(this, "dnd-modifier-key"); + __publicField(this, "dnd-swap-modifier-key"); + __publicField(this, "experimentalSwapSameShapeWindows"); + __publicField(this, "keybinding-tile-left"); + __publicField(this, "keybinding-tile-right"); + __publicField(this, "keybinding-tile-up"); + __publicField(this, "keybinding-tile-down"); + __publicField(this, "keybinding-shift-left"); + __publicField(this, "keybinding-shift-right"); + __publicField(this, "keybinding-shift-up"); + __publicField(this, "keybinding-shift-down"); + __publicField(this, "keybinding-restore"); + // DnD State tracking + __publicField(this, "grabBeginId", 0); + __publicField(this, "grabEndId", 0); + __publicField(this, "draggedWindow", null); + __publicField(this, "draggedWindowId", ""); + __publicField(this, "dragTimerId", 0); + __publicField(this, "previewsMap", {}); + __publicField(this, "lastDragStates", null); + __publicField(this, "lastDragMonitor", null); + __publicField(this, "dragOffsetX", 0); + __publicField(this, "dragOffsetY", 0); + __publicField(this, "dragSession", null); + __publicField(this, "vacancyPreview", null); + __publicField(this, "blockedPreview", null); + __publicField(this, "dndTransactions", []); + __publicField(this, "lastDndDebugSignature", ""); + __publicField(this, "lastDragTarget", null); + this.metadata = metadata; + this.shell = new CinnamonShellAdapter(this); + this.cache = new CinnamonCache(); + this.configProvider = new CinnamonConfigProvider(this); + this.useCase = new TilingUseCase(this.shell, this.cache, this.configProvider); + } + getConfigForMonitor(monitor) { + const fallbackGrid = this.gridSize !== void 0 ? this.gridSize : 12; + const baseColumns = this.gridColumns !== void 0 ? this.gridColumns : fallbackGrid; + const baseRows = this.gridRows !== void 0 ? this.gridRows : 6; + let gridColumns = baseColumns; + let gridRows = baseRows; + if (monitor) { + const ratio = monitor.workarea.width / Math.max(1, monitor.workarea.height); + if (ratio >= 2.1) { + gridColumns = this.ultrawideGridColumns !== void 0 ? this.ultrawideGridColumns : 12; + gridRows = this.ultrawideGridRows !== void 0 ? this.ultrawideGridRows : 6; + } else if (monitor.workarea.height > monitor.workarea.width) { + gridColumns = this.verticalGridColumns !== void 0 ? this.verticalGridColumns : gridColumns; + gridRows = this.verticalGridRows !== void 0 ? this.verticalGridRows : gridRows; + } else { + gridColumns = this.horizontalGridColumns !== void 0 ? this.horizontalGridColumns : 6; + gridRows = this.horizontalGridRows !== void 0 ? this.horizontalGridRows : 6; + } + const override = this.getMonitorGridOverride(String(monitor.id)); + if (override) { + gridColumns = override.columns; + gridRows = override.rows; + } + } + const minSpan = this.minSpan !== void 0 ? this.minSpan : 2; + return { + gridSize: Math.max(gridColumns, gridRows), + gridColumns, + gridRows, + minSpan, + minColumnSpan: this.minColumnSpan !== void 0 ? this.minColumnSpan : minSpan, + minRowSpan: this.minRowSpan !== void 0 ? this.minRowSpan : minSpan, + step: this.step !== void 0 ? this.step : 1, + gaps: this.gaps !== void 0 ? this.gaps : 8 + }; + } + getMonitorGridOverride(monitorId) { + const raw = (this.monitorGridOverrides || "").trim(); + if (!raw) return null; + const entries = raw.split(/[,\n;]/).map((part) => part.trim()).filter(Boolean); + for (const entry of entries) { + const match = entry.match(/^([^:=\s]+)\s*[:=]\s*(\d+)\s*x\s*(\d+)$/i); + if (!match || match[1] !== monitorId) continue; + const columns = parseInt(match[2], 10); + const rows = parseInt(match[3], 10); + if (columns >= 2 && rows >= 2) { + return { columns, rows }; + } + } + return null; + } + debugLog(message) { + if (this.enableDebugLogs === true) { + global.log(message); + } + } + enable() { + try { + this.settings = new Settings.ExtensionSettings(this, this.metadata.uuid, this.metadata.uuid); + this.settings.bindProperty(Settings.BindingDirection.IN, "gaps", "gaps", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "gridSize", "gridSize", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "gridColumns", "gridColumns", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "gridRows", "gridRows", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "horizontalGridColumns", "horizontalGridColumns", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "horizontalGridRows", "horizontalGridRows", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "verticalGridColumns", "verticalGridColumns", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "verticalGridRows", "verticalGridRows", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "ultrawideGridColumns", "ultrawideGridColumns", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "ultrawideGridRows", "ultrawideGridRows", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "monitorGridOverrides", "monitorGridOverrides", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "minSpan", "minSpan", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "minColumnSpan", "minColumnSpan", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "minRowSpan", "minRowSpan", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "step", "step", () => { + this.applyConfigurationChange(); + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "enablePreview", "enablePreview", () => { + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "enableDebugLogs", "enableDebugLogs", () => { + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "enable-dnd-tiling", "enable-dnd-tiling", () => { + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "dnd-modifier-key", "dnd-modifier-key", () => { + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "dnd-swap-modifier-key", "dnd-swap-modifier-key", () => { + }); + this.settings.bindProperty(Settings.BindingDirection.IN, "experimentalSwapSameShapeWindows", "experimentalSwapSameShapeWindows", () => { + }); + this.registerKeybinding("keybinding-tile-left", "left"); + this.registerKeybinding("keybinding-tile-right", "right"); + this.registerKeybinding("keybinding-tile-up", "up"); + this.registerKeybinding("keybinding-tile-down", "down"); + this.registerKeybinding("keybinding-shift-left", "shift-left"); + this.registerKeybinding("keybinding-shift-right", "shift-right"); + this.registerKeybinding("keybinding-shift-up", "shift-up"); + this.registerKeybinding("keybinding-shift-down", "shift-down"); + this.registerKeybinding("keybinding-restore", "restore"); + this.grabBeginId = global.display.connect("grab-op-begin", (...args) => { + let window = global.display.focus_window; + let op = null; + if (args.length === 4) { + window = args[2]; + op = args[3]; + } else if (args.length === 3) { + window = args[1]; + op = args[2]; + } else if (args.length === 2) { + op = args[1]; + } else { + op = args[args.length - 1]; + } + this.onGrabBegin(window, op); + }); + this.grabEndId = global.display.connect("grab-op-end", () => { + this.onGrabEnd(); + }); + try { + const monitors = this.shell.getActiveMonitors(); + const activeMonitor = monitors[0]; + const config = this.getConfigForMonitor(activeMonitor); + this.indexAllWindows(activeMonitor, config, monitors, true); + this.debugLog(`[Dynamic Tiler] Initial window indexing completed`); + } catch (err) { + global.logError(`[Dynamic Tiler] Initial indexing error: ${err.message}`); + } + this.debugLog(`[Dynamic Tiler] Extension enabled and hooks registered successfully`); + } catch (e) { + global.logError(`[Dynamic Tiler] Failed to enable extension: ${e}`); + } + } + disable() { + try { + if (this.grabBeginId) { + global.display.disconnect(this.grabBeginId); + this.grabBeginId = 0; + } + if (this.grabEndId) { + global.display.disconnect(this.grabEndId); + this.grabEndId = 0; + } + this.stopDragTimer(); + this.clearPreviews(); + for (const key of Object.keys(this.bindings)) { + Main2.keybindingManager.removeHotKey(key); + } + this.bindings = {}; + if (this.settings) { + this.settings.finalize(); + } + this.debugLog(`[Dynamic Tiler] Extension disabled successfully`); + } catch (e) { + global.logError(`[Dynamic Tiler] Failed to disable extension: ${e}`); + } + } + onGrabBegin(window, op) { + const enableDnd = this["enable-dnd-tiling"] !== false; + if (!enableDnd) return; + const isMoving = op === Meta3.GrabOp.MOVING || op === Meta3.GrabOp.KEYBOARD_MOVING || typeof op === "number" && op === 3; + if (!isMoving) { + return; + } + const win = window || global.display.focus_window; + if (win) { + this.draggedWindow = win; + this.draggedWindowId = win.get_stable_sequence().toString(); + this.lastDragStates = null; + this.lastDragMonitor = null; + this.lastDndDebugSignature = ""; + this.lastDragTarget = null; + const cached = this.cache.getCachedWindow(this.draggedWindowId); + const monitors = this.shell.getActiveMonitors(); + const geom = this.shell.getWindowGeometry(this.draggedWindowId); + const activeMonitor = this.shell.findMonitorForWindow(geom, monitors); + this.dragSession = { + draggedWindowId: this.draggedWindowId, + sourceMonitor: activeMonitor, + wasTiled: cached !== null, + sourceState: cached ? { ...cached.state } : null, + sourceGeometry: cached ? { ...cached.originalGeometry } : { ...geom }, + sourceTiledGeometry: cached ? { ...cached.tiledGeometry } : null, + startPointerX: 0, + startPointerY: 0, + lastDragStates: null, + lastDragBeforeStates: null, + lastDragAffected: [], + cancelled: false, + floated: false, + dndEngaged: false + }; + try { + const [mx, my] = global.get_pointer(); + this.dragSession.startPointerX = mx; + this.dragSession.startPointerY = my; + this.dragOffsetX = mx - geom.x; + this.dragOffsetY = my - geom.y; + } catch (err) { + this.dragSession.startPointerX = geom.x + Math.round(geom.width / 2); + this.dragSession.startPointerY = geom.y + Math.round(geom.height / 2); + this.dragOffsetX = 0; + this.dragOffsetY = 0; + } + this.debugLog(`[Dynamic Tiler] Window drag started: ID ${this.draggedWindowId}, Op: ${op}, Offset: ${this.dragOffsetX}, ${this.dragOffsetY}, WasTiled: ${this.dragSession.wasTiled}`); + try { + const config = this.getConfigForMonitor(activeMonitor); + this.indexAllWindows(activeMonitor, config, monitors); + } catch (err) { + global.logError(`[Dynamic Tiler] DnD Pre-indexing error: ${err.message}`); + } + this.stopDragTimer(); + this.dragTimerId = imports.mainloop.timeout_add(30, () => { + return this.onDragUpdate(); + }); + } + } + indexAllWindows(activeMonitor, config, monitors, forceAddNew = false) { + const visibleWindowIds = this.shell.getVisibleWindowIds(); + const visibleSet = new Set(visibleWindowIds); + const allCached = this.cache.getAllCachedWindows(); + for (const id of Object.keys(allCached)) { + if (!visibleSet.has(id)) { + this.cache.clearState(id); + } + } + const confirmedSpansMap = {}; + for (const id of visibleWindowIds) { + if (id === this.draggedWindowId) continue; + const cached = this.cache.getCachedWindow(id); + if (!cached && !forceAddNew) { + continue; + } + try { + const currentGeom = this.shell.getWindowGeometry(id); + const ext = this.shell.getFrameExtents(id); + const currentVisible = { + x: currentGeom.x + ext.left, + y: currentGeom.y + ext.top, + width: currentGeom.width - ext.left - ext.right, + height: currentGeom.height - ext.top - ext.bottom + }; + const currentMonitor = this.shell.findMonitorForWindow(currentVisible, monitors); + const currentConfig = this.getConfigForMonitor(currentMonitor); + const gridColumns = getGridColumns(currentConfig); + const gridRows = getGridRows(currentConfig); + const minColumnSpan = getMinColumnSpan(currentConfig); + const minRowSpan = getMinRowSpan(currentConfig); + let hSpan = [0, gridColumns]; + let vSpan = [0, gridRows]; + let isResting = false; + if (cached) { + const diffX = Math.abs(currentVisible.x - cached.tiledGeometry.x); + const diffY = Math.abs(currentVisible.y - cached.tiledGeometry.y); + const diffW = Math.abs(currentVisible.width - cached.tiledGeometry.width); + const diffH = Math.abs(currentVisible.height - cached.tiledGeometry.height); + const cachedFitsGrid = cached.state.hSpan[0] >= 0 && cached.state.hSpan[1] <= gridColumns && cached.state.vSpan[0] >= 0 && cached.state.vSpan[1] <= gridRows; + const MATCH_THRESHOLD = 40; + if (cachedFitsGrid && diffX <= MATCH_THRESHOLD && diffY <= MATCH_THRESHOLD && diffW <= MATCH_THRESHOLD && diffH <= MATCH_THRESHOLD) { + hSpan = cached.state.hSpan; + vSpan = cached.state.vSpan; + isResting = true; + } + } + if (!isResting) { + hSpan = TilingEngine.geometryToHSpan(currentVisible, currentMonitor, currentConfig); + vSpan = TilingEngine.geometryToVSpan(currentVisible, currentMonitor, currentConfig); + if (hSpan[1] - hSpan[0] < minColumnSpan) { + const center = Math.round((hSpan[0] + hSpan[1]) / 2); + hSpan[0] = Math.max(0, center - Math.floor(minColumnSpan / 2)); + hSpan[1] = Math.min(gridColumns, hSpan[0] + minColumnSpan); + hSpan[0] = Math.max(0, hSpan[1] - minColumnSpan); + } + if (vSpan[1] - vSpan[0] < minRowSpan) { + const center = Math.round((vSpan[0] + vSpan[1]) / 2); + vSpan[0] = Math.max(0, center - Math.floor(minRowSpan / 2)); + vSpan[1] = Math.min(gridRows, vSpan[0] + minRowSpan); + vSpan[0] = Math.max(0, vSpan[1] - minRowSpan); + } + } + if (!cached && forceAddNew) { + const candidateState = { + hIndex: TilingEngine.spanToHIndex(hSpan), + vIndex: TilingEngine.spanToVIndex(vSpan), + hSpan, + vSpan, + lastDirection: null + }; + const snappedGeom = TilingEngine.stateToGeometry(candidateState, currentMonitor, currentConfig); + const diffX = Math.abs(currentVisible.x - snappedGeom.x); + const diffY = Math.abs(currentVisible.y - snappedGeom.y); + const diffW = Math.abs(currentVisible.width - snappedGeom.width); + const diffH = Math.abs(currentVisible.height - snappedGeom.height); + const STARTUP_INDEX_THRESHOLD = 40; + if (diffX > STARTUP_INDEX_THRESHOLD || diffY > STARTUP_INDEX_THRESHOLD || diffW > STARTUP_INDEX_THRESHOLD || diffH > STARTUP_INDEX_THRESHOLD) { + continue; + } + } + let hasOverlap = false; + const confirmed = confirmedSpansMap[currentMonitor.id] || []; + for (const span of confirmed) { + const hasH = Math.max(hSpan[0], span.hSpan[0]) < Math.min(hSpan[1], span.hSpan[1]); + const hasV = Math.max(vSpan[0], span.vSpan[0]) < Math.min(vSpan[1], span.vSpan[1]); + if (hasH && hasV) { + hasOverlap = true; + break; + } + } + if (hasOverlap) { + if (cached) { + this.cache.clearState(id); + this.debugLog(`[Dynamic Tiler] Cache Sanitation: Stripped overlapping window ${id} from cache`); + } + continue; + } + if (!confirmedSpansMap[currentMonitor.id]) { + confirmedSpansMap[currentMonitor.id] = []; + } + confirmedSpansMap[currentMonitor.id].push({ hSpan, vSpan }); + if (isResting) { + continue; + } + const restoredState = { + hIndex: TilingEngine.spanToHIndex(hSpan), + vIndex: TilingEngine.spanToVIndex(vSpan), + hSpan, + vSpan, + lastDirection: null + }; + const originalGeom = cached ? cached.originalGeometry : currentGeom; + this.cache.saveState(id, restoredState, currentVisible, originalGeom); + this.debugLog(`[Dynamic Tiler] Auto-indexed window ${id} to cell [${hSpan.join(",")}] x [${vSpan.join(",")}]`); + } catch (e) { + } + } + } + onDragUpdate() { + if (!this.draggedWindowId) return false; + try { + const [mx, my, mods] = global.get_pointer(); + const isModifierPressed = this.isConfiguredModifierPressed(this["dnd-modifier-key"], "d", mods); + const isSwapModifierPressed = this.experimentalSwapSameShapeWindows === true && this.isConfiguredModifierPressed(this["dnd-swap-modifier-key"], "d", mods); + if (!isModifierPressed && !isSwapModifierPressed) { + if (this.dragSession && (this.dragSession.lastDragStates || this.dragSession.dndEngaged && !this.dragSession.floated)) { + const shouldExtract = this.dragSession.wasTiled === true && this.dragSession.sourceTiledGeometry && shouldFloatAfterModifierRelease({ + pointerX: mx, + pointerY: my, + startPointerX: this.dragSession.startPointerX, + startPointerY: this.dragSession.startPointerY + }); + this.clearPreviews(); + this.dragSession.lastDragStates = null; + this.dragSession.lastDragBeforeStates = null; + this.dragSession.lastDragAffected = []; + this.dragSession.floated = shouldExtract === true; + this.dragSession.cancelled = !this.dragSession.floated; + this.dragSession.dndEngaged = false; + this.lastDragStates = null; + try { + const win = this.shell._findMetaWindow(this.draggedWindowId); + if (win && this.dragSession.sourceGeometry) { + const orig = this.dragSession.sourceGeometry; + this.shell.unmaximizeWindow(this.draggedWindowId); + const nextX = mx - Math.round(orig.width / 2); + const nextY = my - 15; + win.move_resize_frame(true, nextX, nextY, orig.width, orig.height); + this.dragOffsetX = Math.round(orig.width / 2); + this.dragOffsetY = 15; + } + } catch (err) { + } + if (shouldExtract && this.dragSession.sourceTiledGeometry) { + try { + if (!this.vacancyPreview) { + this.vacancyPreview = new TilePreview(); + } + const win = this.shell._findMetaWindow(this.draggedWindowId); + if (win) { + const sourceMonitorIndex = parseInt(this.dragSession.sourceMonitor.id); + this.vacancyPreview.show(win, this.dragSession.sourceTiledGeometry, sourceMonitorIndex, true, 80, 60, true); + } + } catch (err) { + } + } + } else { + this.clearPreviews(); + } + this.lastDragStates = null; + this.lastDragMonitor = null; + this.lastDragTarget = null; + return true; + } + const monitors = this.shell.getActiveMonitors(); + const previousDragMonitorId = this.lastDragMonitor ? String(this.lastDragMonitor.id) : null; + let activeMonitor = monitors[0]; + for (const m of monitors) { + if (mx >= m.workarea.x && mx < m.workarea.x + m.workarea.width && my >= m.workarea.y && my < m.workarea.y + m.workarea.height) { + activeMonitor = m; + break; + } + } + if (previousDragMonitorId !== null && previousDragMonitorId !== String(activeMonitor.id)) { + this.lastDragTarget = null; + } + this.lastDragMonitor = activeMonitor; + if (this.dragSession) { + this.dragSession.targetMonitor = activeMonitor; + } + const config = this.getConfigForMonitor(activeMonitor); + const gridColumns = getGridColumns(config); + const gridRows = getGridRows(config); + const minColumnSpan = getMinColumnSpan(config); + const minRowSpan = getMinRowSpan(config); + let windowWidth = Math.round(gridColumns / 2); + let windowHeight = gridRows; + let windowSizeSource = "fallback"; + if (this.dragSession && this.dragSession.wasTiled && this.dragSession.sourceState) { + windowWidth = this.dragSession.sourceState.hSpan[1] - this.dragSession.sourceState.hSpan[0]; + windowHeight = this.dragSession.sourceState.vSpan[1] - this.dragSession.sourceState.vSpan[0]; + windowSizeSource = "source-state"; + } else { + try { + const geom = this.shell.getWindowGeometry(this.draggedWindowId); + const hSpan = TilingEngine.geometryToHSpan(geom, activeMonitor, config); + const vSpan = TilingEngine.geometryToVSpan(geom, activeMonitor, config); + windowWidth = Math.max(minColumnSpan, hSpan[1] - hSpan[0]); + windowHeight = Math.max(minRowSpan, vSpan[1] - vSpan[0]); + windowSizeSource = "current-geometry"; + } catch (e) { + try { + if (!this.dragSession || !this.dragSession.sourceGeometry) throw e; + const hSpan = TilingEngine.geometryToHSpan(this.dragSession.sourceGeometry, activeMonitor, config); + const vSpan = TilingEngine.geometryToVSpan(this.dragSession.sourceGeometry, activeMonitor, config); + windowWidth = Math.max(minColumnSpan, hSpan[1] - hSpan[0]); + windowHeight = Math.max(minRowSpan, vSpan[1] - vSpan[0]); + windowSizeSource = "source-geometry"; + } catch { + windowWidth = Math.round(gridColumns / 2); + windowHeight = Math.round(gridRows / 2); + windowSizeSource = "fallback"; + } + } + } + const visibleWindowIds = this.shell.getVisibleWindowIds(); + const activeWindowsOnMonitor = []; + for (const id of visibleWindowIds) { + let state = null; + let cachedWin = this.cache.getCachedWindow(id); + if (id === this.draggedWindowId) { + if (this.dragSession && this.dragSession.wasTiled && this.dragSession.sourceState) { + state = this.dragSession.sourceState; + } else { + continue; + } + } else { + if (cachedWin) { + state = cachedWin.state; + } + } + if (!state) continue; + let currentMonitor = activeMonitor; + if (id !== this.draggedWindowId) { + try { + const geom = this.shell.getWindowGeometry(id); + currentMonitor = this.shell.findMonitorForWindow(geom, monitors); + } catch { + if (cachedWin) { + currentMonitor = this.shell.findMonitorForWindow(cachedWin.tiledGeometry, monitors); + } + } + } + if (currentMonitor.id === activeMonitor.id) { + activeWindowsOnMonitor.push({ + windowId: id, + state + }); + } + } + const dragTarget = computeDragTarget({ + draggedId: this.draggedWindowId, + mx, + my, + monitor: activeMonitor, + config, + preferredWidth: windowWidth, + preferredHeight: windowHeight, + activeWindows: activeWindowsOnMonitor, + previousTarget: this.lastDragTarget + }); + if (this.dragSession && this.dragSession.wasTiled && this.dragSession.sourceMonitor && String(this.dragSession.sourceMonitor.id) === String(activeMonitor.id) && !isSwapModifierPressed && shouldCancelSourceReturn(this.dragSession.sourceState, dragTarget.targetHSpan, dragTarget.targetVSpan, dragTarget.intentPoint)) { + const sourceState = this.dragSession.sourceState; + const signature = `source-return:${this.draggedWindowId}:${sourceState.hSpan.join("-")}x${sourceState.vSpan.join("-")}:target=${dragTarget.targetHSpan.join("-")}x${dragTarget.targetVSpan.join("-")}:intent=${dragTarget.intentPoint.h.toFixed(2)},${dragTarget.intentPoint.v.toFixed(2)}`; + if (signature !== this.lastDndDebugSignature) { + this.lastDndDebugSignature = signature; + this.debugLog(`[Dynamic Tiler] DnD source-return cancel dragged=${this.draggedWindowId} source=${sourceState.hSpan.join("-")}x${sourceState.vSpan.join("-")} target=${dragTarget.targetHSpan.join("-")}x${dragTarget.targetVSpan.join("-")} intent=${dragTarget.intentPoint.h.toFixed(2)},${dragTarget.intentPoint.v.toFixed(2)}`); + } + this.clearBlockedPreview(); + this.clearPlacementPreviewsExcept(this.draggedWindowId); + try { + let preview = this.previewsMap[this.draggedWindowId]; + if (!preview) { + preview = new TilePreview(); + this.previewsMap[this.draggedWindowId] = preview; + } + const win = this.shell._findMetaWindow(this.draggedWindowId); + if (win) { + const frameGeom = TilingEngine.stateToGeometry(this.dragSession.sourceState, activeMonitor, config); + preview.show(win, frameGeom, parseInt(activeMonitor.id), false, 80, 220, false, "normal", false); + } + } catch (err) { + } + this.lastDragStates = null; + this.lastDragTarget = dragTarget; + this.dragSession.lastDragStates = null; + this.dragSession.lastDragBeforeStates = null; + this.dragSession.lastDragAffected = []; + this.dragSession.cancelled = true; + this.dragSession.floated = false; + this.dragSession.dndEngaged = false; + return true; + } + const dragResult = solveDragTransitions( + this.draggedWindowId, + dragTarget.targetHSpan, + dragTarget.targetVSpan, + config, + activeWindowsOnMonitor, + { + swapWindows: isSwapModifierPressed, + intentPoint: dragTarget.intentPoint, + preferredWidth: windowWidth + } + ); + const dragStates = dragResult.states; + this.logDndDecision( + dragTarget, + dragResult, + windowWidth, + windowHeight, + windowSizeSource, + activeWindowsOnMonitor + ); + if (dragResult.status === "blocked") { + this.clearPlacementPreviews(); + this.lastDragStates = null; + if (this.dragSession) { + this.dragSession.lastDragStates = null; + this.dragSession.lastDragBeforeStates = null; + this.dragSession.lastDragAffected = []; + this.dragSession.cancelled = true; + this.dragSession.floated = false; + this.dragSession.dndEngaged = true; + } + try { + if (!this.blockedPreview) { + this.blockedPreview = new TilePreview(); + } + const win = this.shell._findMetaWindow(this.draggedWindowId); + if (win) { + const blockedState = { + hIndex: TilingEngine.spanToHIndex(dragTarget.targetHSpan), + vIndex: TilingEngine.spanToVIndex(dragTarget.targetVSpan), + hSpan: [...dragTarget.targetHSpan], + vSpan: [...dragTarget.targetVSpan], + lastDirection: null + }; + const frameGeom = TilingEngine.stateToGeometry(blockedState, activeMonitor, config); + this.blockedPreview.show( + win, + frameGeom, + parseInt(activeMonitor.id), + true, + 80, + 140, + false, + this.getBlockedPreviewVariant(dragResult.reason) + ); + } + } catch (e) { + } + return true; + } + this.clearBlockedPreview(); + this.lastDragStates = dragStates; + this.lastDragTarget = dragTarget; + if (this.dragSession) { + const beforeStates = {}; + for (const activeWindow of activeWindowsOnMonitor) { + beforeStates[activeWindow.windowId] = { + hIndex: activeWindow.state.hIndex, + vIndex: activeWindow.state.vIndex, + hSpan: [...activeWindow.state.hSpan], + vSpan: [...activeWindow.state.vSpan], + lastDirection: activeWindow.state.lastDirection + }; + } + this.dragSession.lastDragStates = dragStates; + this.dragSession.lastDragBeforeStates = beforeStates; + this.dragSession.lastDragAffected = dragResult.affected; + this.dragSession.cancelled = false; + this.dragSession.floated = false; + this.dragSession.dndEngaged = true; + } + if (this.dragSession && this.dragSession.wasTiled && this.dragSession.sourceTiledGeometry) { + if (!this.vacancyPreview) { + this.vacancyPreview = new TilePreview(); + } + const sourceMonitorIndex = parseInt(this.dragSession.sourceMonitor.id); + const win = this.shell._findMetaWindow(this.draggedWindowId); + if (win) { + this.vacancyPreview.show(win, this.dragSession.sourceTiledGeometry, sourceMonitorIndex, true, 80, 50, true); + } + } + for (const [id, nextState] of Object.entries(dragStates)) { + const isDragged = id === this.draggedWindowId; + let hasChanged = true; + if (!isDragged) { + const cached = this.cache.getCachedWindow(id); + if (cached) { + const sameH = cached.state.hSpan[0] === nextState.hSpan[0] && cached.state.hSpan[1] === nextState.hSpan[1]; + const sameV = cached.state.vSpan[0] === nextState.vSpan[0] && cached.state.vSpan[1] === nextState.vSpan[1]; + if (sameH && sameV) { + hasChanged = false; + } + } + } + if (hasChanged) { + const frameGeom = TilingEngine.stateToGeometry(nextState, activeMonitor, config); + let preview = this.previewsMap[id]; + if (!preview) { + preview = new TilePreview(); + this.previewsMap[id] = preview; + } + const win = this.shell._findMetaWindow(id); + if (win) { + const monitorIndex = parseInt(activeMonitor.id); + if (isDragged) { + preview.show(win, frameGeom, monitorIndex, true, 80, 220, false, isSwapModifierPressed ? "swap-primary" : "normal"); + } else { + preview.show(win, frameGeom, monitorIndex, true, 80, isSwapModifierPressed ? 170 : 80, true, isSwapModifierPressed ? "swap-secondary" : "normal"); + } + } + } + } + const unusedIds = new Set(Object.keys(this.previewsMap)); + for (const id of Object.keys(dragStates)) { + const isDragged = id === this.draggedWindowId; + let hasChanged = true; + if (!isDragged) { + const cached = this.cache.getCachedWindow(id); + if (cached) { + const sameH = cached.state.hSpan[0] === dragStates[id].hSpan[0] && cached.state.hSpan[1] === dragStates[id].hSpan[1]; + const sameV = cached.state.vSpan[0] === dragStates[id].vSpan[0] && cached.state.vSpan[1] === dragStates[id].vSpan[1]; + if (sameH && sameV) { + hasChanged = false; + } + } + } + if (hasChanged) { + unusedIds.delete(id); + } + } + for (const id of unusedIds) { + this.previewsMap[id].destroy(); + delete this.previewsMap[id]; + } + } catch (e) { + global.logError(`[Dynamic Tiler] Drag update error: ${e.message} +${e.stack}`); + } + return true; + } + onGrabEnd() { + this.stopDragTimer(); + if (!this.draggedWindowId || !this.dragSession) { + this.draggedWindow = null; + this.draggedWindowId = ""; + this.lastDragStates = null; + this.lastDragMonitor = null; + this.dragSession = null; + this.lastDndDebugSignature = ""; + this.lastDragTarget = null; + return; + } + this.debugLog(`[Dynamic Tiler] Window drag ended. Active ID: ${this.draggedWindowId}`); + const session = this.dragSession; + this.clearPreviews(); + if (session && this.draggedWindowId) { + const monitors = this.shell.getActiveMonitors(); + const activeMonitor = this.lastDragMonitor || session.sourceMonitor; + const config = this.getConfigForMonitor(activeMonitor); + if (session.lastDragStates) { + this.debugLog(`[Dynamic Tiler] Committing DnD tiling session for ${Object.keys(session.lastDragStates).length} windows`); + if (session.lastDragBeforeStates && session.lastDragAffected && session.lastDragAffected.length > 0) { + const afterStates = {}; + for (const [id, state] of Object.entries(session.lastDragStates)) { + const nextState = state; + afterStates[id] = { + hIndex: nextState.hIndex, + vIndex: nextState.vIndex, + hSpan: [...nextState.hSpan], + vSpan: [...nextState.vSpan], + lastDirection: nextState.lastDirection + }; + } + this.recordDndTransaction({ + draggedId: this.draggedWindowId, + monitorId: String(activeMonitor.id), + beforeStates: session.lastDragBeforeStates, + afterStates, + affected: [...session.lastDragAffected] + }); + } + if (session.wasTiled && session.sourceMonitor && activeMonitor && session.sourceMonitor.id !== activeMonitor.id) { + this.debugLog(`[Dynamic Tiler] Cross-monitor drag detected. Collapsing vacancy on source monitor ${session.sourceMonitor.id}`); + this.collapseAndApplyVacancy(this.draggedWindowId, session.sourceMonitor, this.getConfigForMonitor(session.sourceMonitor), monitors); + } + for (const [id, nextState] of Object.entries(session.lastDragStates)) { + try { + const cached = this.cache.getCachedWindow(id); + const currentGeom = this.shell.getWindowGeometry(id); + const originalGeom = cached ? cached.originalGeometry : currentGeom; + this.shell.unmaximizeWindow(id); + const nextStateTyped = nextState; + const nextGeom = TilingEngine.stateToGeometry(nextStateTyped, activeMonitor, config); + this.shell.applyGeometry(id, nextGeom); + const hasPreview = this.enablePreview !== false; + const delay = hasPreview ? 100 : 20; + imports.mainloop.timeout_add(delay, () => { + try { + const realGeom = this.shell.getWindowGeometry(id); + this.cache.saveState(id, nextStateTyped, realGeom, originalGeom); + } catch (err) { + this.cache.saveState(id, nextStateTyped, nextGeom, originalGeom); + } + return false; + }); + } catch (e) { + global.logError(`[Dynamic Tiler] DnD Apply error for window ${id}: ${e.message}`); + } + } + } else if (session.floated && session.wasTiled && session.sourceMonitor) { + this.debugLog(`[Dynamic Tiler] DnD floated tiled window ${this.draggedWindowId}; collapsing source vacancy on monitor ${session.sourceMonitor.id}`); + this.collapseAndApplyVacancy(this.draggedWindowId, session.sourceMonitor, this.getConfigForMonitor(session.sourceMonitor), monitors); + this.cache.clearState(this.draggedWindowId); + } else if (session.cancelled) { + if (session.wasTiled && session.sourceTiledGeometry && session.sourceState) { + try { + this.shell.unmaximizeWindow(this.draggedWindowId); + this.shell.applyGeometry(this.draggedWindowId, session.sourceTiledGeometry); + this.cache.saveState( + this.draggedWindowId, + session.sourceState, + session.sourceTiledGeometry, + session.sourceGeometry || session.sourceTiledGeometry + ); + } catch (e) { + global.logError(`[Dynamic Tiler] DnD cancel restore error for window ${this.draggedWindowId}: ${e.message}`); + } + } + this.debugLog(`[Dynamic Tiler] DnD cancelled safely for window ${this.draggedWindowId}; layout cache left unchanged`); + } + } + this.draggedWindow = null; + this.draggedWindowId = ""; + this.lastDragStates = null; + this.lastDragMonitor = null; + this.dragSession = null; + this.lastDndDebugSignature = ""; + this.lastDragTarget = null; + } + collapseAndApplyVacancy(draggedId, monitor, config, monitors) { + try { + const visibleWindowIds = this.shell.getVisibleWindowIds(); + const activeWindowsOnMonitor = []; + for (const id of visibleWindowIds) { + const cached = this.cache.getCachedWindow(id); + if (cached) { + let currentMonitor = monitor; + try { + const geom = this.shell.getWindowGeometry(id); + currentMonitor = this.shell.findMonitorForWindow(geom, monitors); + } catch { + currentMonitor = this.shell.findMonitorForWindow(cached.tiledGeometry, monitors); + } + if (currentMonitor.id === monitor.id) { + activeWindowsOnMonitor.push({ + windowId: id, + state: cached.state + }); + } + } + } + let collapsedStates = null; + const restoredTransaction = restoreDragTransactionHistory( + this.dndTransactions, + draggedId, + String(monitor.id), + config, + activeWindowsOnMonitor + ); + if (restoredTransaction) { + collapsedStates = restoredTransaction.states; + this.dndTransactions.splice(restoredTransaction.snapshotIndex, 1); + this.debugLog(`[Dynamic Tiler] Restored DnD transaction neighbors for ${draggedId}`); + } else if (this.dndTransactions.some( + (transaction) => transaction.draggedId === draggedId && transaction.monitorId === String(monitor.id) + )) { + this.debugLog(`[Dynamic Tiler] DnD transaction restore skipped for ${draggedId}; falling back to vacancy collapse`); + } + if (!collapsedStates) { + collapsedStates = collapseVacancy(draggedId, config, activeWindowsOnMonitor); + } + for (const [id, nextState] of Object.entries(collapsedStates)) { + try { + const cached = this.cache.getCachedWindow(id); + const currentGeom = this.shell.getWindowGeometry(id); + const originalGeom = cached ? cached.originalGeometry : currentGeom; + const nextGeom = TilingEngine.stateToGeometry(nextState, monitor, config); + this.shell.applyGeometry(id, nextGeom); + const hasPreview = this.enablePreview !== false; + const delay = hasPreview ? 100 : 20; + imports.mainloop.timeout_add(delay, () => { + try { + const realGeom = this.shell.getWindowGeometry(id); + this.cache.saveState(id, nextState, realGeom, originalGeom); + } catch (err) { + this.cache.saveState(id, nextState, nextGeom, originalGeom); + } + return false; + }); + } catch (e) { + global.logError(`[Dynamic Tiler] Collapse apply error for window ${id}: ${e.message}`); + } + } + this.cache.clearState(draggedId); + this.debugLog(`[Dynamic Tiler] Successfully collapsed grid vacancy and cleared state for ${draggedId}`); + } catch (e) { + global.logError(`[Dynamic Tiler] Failed to collapse and apply vacancy: ${e.message}`); + } + } + recordDndTransaction(transaction) { + const maxTransactions = 8; + this.dndTransactions = this.dndTransactions.filter( + (existing) => !(existing.draggedId === transaction.draggedId && existing.monitorId === transaction.monitorId) + ); + this.dndTransactions.push(transaction); + if (this.dndTransactions.length > maxTransactions) { + this.dndTransactions.splice(0, this.dndTransactions.length - maxTransactions); + } + } + getBlockedPreviewVariant(reason) { + if (reason === "tooSmall") return "blocked-too-small"; + if (reason === "outOfBounds") return "blocked-out-of-bounds"; + return "blocked-overlap"; + } + isConfiguredModifierPressed(settingValue, fallback, mods) { + let hotkeySetting = settingValue || fallback; + if (Array.isArray(hotkeySetting)) { + hotkeySetting = hotkeySetting[0] || fallback; + } + const ctrl = hotkeySetting.includes("Control") || hotkeySetting.includes("Ctrl") || hotkeySetting.includes("Primary"); + const shift = hotkeySetting.includes("Shift"); + const alt = hotkeySetting.includes("Alt") || hotkeySetting.includes("Meta") || hotkeySetting.includes("Mod1"); + const superKey = hotkeySetting.includes("Super") || hotkeySetting.includes("Mod4"); + if (!ctrl && !shift && !alt && !superKey) { + return true; + } + const ctrlPressed = (mods & Clutter2.ModifierType.CONTROL_MASK) !== 0; + const shiftPressed = (mods & Clutter2.ModifierType.SHIFT_MASK) !== 0; + const altPressed = (mods & Clutter2.ModifierType.MOD1_MASK) !== 0; + const superPressed = (mods & Clutter2.ModifierType.SUPER_MASK) !== 0; + if (ctrl && !ctrlPressed) return false; + if (shift && !shiftPressed) return false; + if (alt && !altPressed) return false; + if (superKey && !superPressed) return false; + return true; + } + restoreAndCollapseActiveWindow() { + try { + const windowId = this.shell.getActiveWindowId(); + if (!windowId) return; + const cached = this.cache.getCachedWindow(windowId); + if (cached && cached.originalGeometry) { + const monitors = this.shell.getActiveMonitors(); + const geom = this.shell.getWindowGeometry(windowId); + const activeMonitor = this.shell.findMonitorForWindow(geom, monitors); + const config = this.getConfigForMonitor(activeMonitor); + this.collapseAndApplyVacancy(windowId, activeMonitor, config, monitors); + this.shell.unmaximizeWindow(windowId); + this.shell.applyGeometry(windowId, cached.originalGeometry); + this.cache.clearState(windowId); + this.debugLog(`[Dynamic Tiler] [Keyboard] Successfully restored and collapsed window ${windowId}`); + } + } catch (e) { + global.logError(`[Dynamic Tiler] Failed to restore and collapse window: ${e.message}`); + } + } + logDndDecision(dragTarget, dragResult, windowWidth, windowHeight, windowSizeSource, activeWindows) { + if (this.enableDebugLogs !== true) { + return; + } + const debug = dragTarget.debug; + const signature = [ + dragResult.status, + dragResult.reason || "ok", + debug.mode, + windowSizeSource, + `${windowWidth}x${windowHeight}`, + this.formatSpan(dragTarget.targetHSpan), + this.formatSpan(dragTarget.targetVSpan), + debug.nearestBoundary === void 0 ? "-" : debug.nearestBoundary, + debug.slotWidth === void 0 ? "-" : debug.slotWidth, + debug.nearestDistance === void 0 ? "-" : debug.nearestDistance.toFixed(2) + ].join("|"); + if (signature === this.lastDndDebugSignature) { + return; + } + this.lastDndDebugSignature = signature; + if (dragResult.status !== "blocked" && debug.mode === "base") { + return; + } + const horizontalGroup = debug.horizontalGroup ? `${this.formatSpan(debug.horizontalGroup.vSpan)} windows=${debug.horizontalGroup.windows} contains=${debug.horizontalGroup.containsCursor} dist=${debug.horizontalGroup.vDistance.toFixed(2)}` : "-"; + const verticalGroup = debug.verticalGroup ? `${this.formatSpan(debug.verticalGroup.hSpan)} windows=${debug.verticalGroup.windows} contains=${debug.verticalGroup.containsCursor} dist=${debug.verticalGroup.hDistance.toFixed(2)}` : "-"; + this.debugLog( + `[Dynamic Tiler] [DND] DnD trace status=${dragResult.status} reason=${dragResult.reason || "ok"} dragged=${this.draggedWindowId} size=${windowWidth}x${windowHeight} sizeSource=${windowSizeSource} intent=${debug.preferredWidth}x${debug.preferredHeight} pointer=${dragTarget.intentPoint.h.toFixed(2)},${dragTarget.intentPoint.v.toFixed(2)} mode=${debug.mode} initial=${this.formatSpan(debug.initialHSpan)}x${this.formatSpan(debug.initialVSpan)} target=${this.formatSpan(dragTarget.targetHSpan)}x${this.formatSpan(dragTarget.targetVSpan)} hCandidates=${debug.horizontalCandidates} vCandidates=${debug.verticalCandidates} preferV=${debug.shouldPreferVerticalStack} hGroup=${horizontalGroup} vGroup=${verticalGroup} boundary=${debug.nearestBoundary === void 0 ? "-" : debug.nearestBoundary} distance=${debug.nearestDistance === void 0 ? "-" : debug.nearestDistance.toFixed(2)} slot=${debug.slotWidth === void 0 ? "-" : debug.slotWidth} hThreshold=${debug.horizontalThreshold === void 0 ? "-" : debug.horizontalThreshold.toFixed(2)} stackHeight=${debug.stackTargetHeight === void 0 ? "-" : debug.stackTargetHeight} boundaryThreshold=${debug.boundaryThreshold === void 0 ? "-" : debug.boundaryThreshold.toFixed(2)} active=[${this.formatDndWindows(activeWindows)}]` + ); + } + formatSpan(span) { + return `${span[0]}-${span[1]}`; + } + formatDndWindows(windows) { + return windows.map((w) => `${w.windowId}:${this.formatSpan(w.state.hSpan)}x${this.formatSpan(w.state.vSpan)}`).join(","); + } + stopDragTimer() { + if (this.dragTimerId) { + imports.mainloop.source_remove(this.dragTimerId); + this.dragTimerId = 0; + } + } + clearPreviews() { + this.clearPlacementPreviews(); + this.clearBlockedPreview(); + } + clearPlacementPreviews() { + for (const [id, preview] of Object.entries(this.previewsMap)) { + try { + preview.hide(); + preview.destroy(); + } catch (e) { + } + } + this.previewsMap = {}; + if (this.vacancyPreview) { + try { + this.vacancyPreview.hide(); + this.vacancyPreview.destroy(); + } catch (e) { + } + this.vacancyPreview = null; + } + } + clearPlacementPreviewsExcept(keepId) { + for (const [id, preview] of Object.entries(this.previewsMap)) { + if (id === keepId) continue; + try { + preview.hide(); + preview.destroy(); + } catch (e) { + } + delete this.previewsMap[id]; + } + if (this.vacancyPreview) { + try { + this.vacancyPreview.hide(); + this.vacancyPreview.destroy(); + } catch (e) { + } + this.vacancyPreview = null; + } + } + clearBlockedPreview() { + if (this.blockedPreview) { + try { + this.blockedPreview.hide(); + this.blockedPreview.destroy(); + } catch (e) { + } + this.blockedPreview = null; + } + } + applyConfigurationChange() { + try { + const visibleWindowIds = this.shell.getVisibleWindowIds(); + const monitors = this.shell.getActiveMonitors(); + for (const id of visibleWindowIds) { + const cached = this.cache.getCachedWindow(id); + if (!cached) continue; + try { + const currentGeom = this.shell.getWindowGeometry(id); + const monitor = this.shell.findMonitorForWindow(currentGeom, monitors); + const config = this.getConfigForMonitor(monitor); + const hSpan = TilingEngine.geometryToHSpan(currentGeom, monitor, config); + const vSpan = TilingEngine.geometryToVSpan(currentGeom, monitor, config); + const nextState = { + ...cached.state, + hSpan, + vSpan, + hIndex: TilingEngine.spanToHIndex(hSpan), + vIndex: TilingEngine.spanToVIndex(vSpan) + }; + const nextGeom = TilingEngine.stateToGeometry(nextState, monitor, config); + const win = this.shell._findMetaWindow(id); + if (win) { + if (win.maximized_horz || win.maximized_vert) { + win.unmaximize(Meta3.MaximizeFlags.BOTH); + } + win.move_resize_frame(true, nextGeom.x, nextGeom.y, nextGeom.width, nextGeom.height); + this.cache.saveState(id, nextState, nextGeom, cached.originalGeometry); + } + } catch (e) { + } + } + } catch (e) { + global.logError(`[Dynamic Tiler] Failed to apply config change: ${e}`); + } + } + registerKeybinding(settingName, action) { + this.settings.bindProperty(Settings.BindingDirection.IN, settingName, settingName, () => { + this.updateHotKey(settingName, action); + }); + this.updateHotKey(settingName, action); + } + updateHotKey(settingName, action) { + const key = `dynamic-tiler-${settingName}`; + const value = this[settingName]; + if (this.bindings[key]) { + Main2.keybindingManager.removeHotKey(key); + delete this.bindings[key]; + } + if (!value || value === "") return; + try { + Main2.keybindingManager.addHotKey(key, value, () => { + try { + const activeId = this.shell.getActiveWindowId(); + this.debugLog(`[Dynamic Tiler] [Keyboard] Action triggered: key=${value}, action=${action}, activeWindowId=${activeId || "none"}`); + if (action === "restore") { + this.restoreAndCollapseActiveWindow(); + } else { + this.useCase.tile(action); + } + } catch (e) { + global.logError(`[Dynamic Tiler] Execution error for action ${action}: ${e.message}`); + } + }); + this.bindings[key] = value; + } catch (e) { + global.logError(`[Dynamic Tiler] Failed to register hotkey ${value}: ${e}`); + } + } + }; + var extensionInstance = null; + function init(metadata) { + extensionInstance = new DynamicTilerExtension(metadata); + } + function enable() { + if (extensionInstance) { + extensionInstance.enable(); + } + } + function disable() { + if (extensionInstance) { + extensionInstance.disable(); + } + } + return __toCommonJS(extension_exports); +})(); + +var init = DynamicTiler.init; +var enable = DynamicTiler.enable; +var disable = DynamicTiler.disable; diff --git a/dynamic-tiler@bnku/files/dynamic-tiler@bnku/icon.png b/dynamic-tiler@bnku/files/dynamic-tiler@bnku/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6903a4416c5dd49865501c110d80f37f94abfd0b GIT binary patch literal 1782 zcmeAS@N?(olHy`uVBq!ia0vp^4InJQ1SDn7Xx#@=3dtTpz6=aiYC!Q828LfC`4T;^6W_Rp>k zTFi=V51G4`2FY9Hnr;(&>r=h6^KQ3tF*B?AdrR}P?|=VfK4)q9WcHnVbC&$e*s^Wg z#*6G7Ay2+CG3X02xF|DpxG@OyGAKwfI+!puWUw$?Z4uOAnc5Mn!s!O>Ux_a@_#qSne*KYbf zXWzol$#=_aHvTV*e)l!w_>(EyUw^bC~1P@y+Rs5!GRQ`mZ|Y-1}DiX421_6Z4I3pMT@AuDI`RgwdtU zqPWeBEH(3gta37cJbQY*WZdPrywk^>ZLP1ZA=@!t=Y4Eg}bedv2oJ7ApME2lp?>!++zN) zTzk%mfG1zJ1;@O8yg#5`e${Q2Uum2zhnFtBxMClt*Q)*Z{!}h|u|322$Tf3ruFwU0 z6r*Mq3GL^%+p#RUruRKVE*Y)a(uCs z+cn_>2`>^pth#z})5>+;ulG50n5#3hImaGcvRgTuQz78ahZRLr8*3XiUggZ#ot(v~ zU~x4jX!GqO(;S3KbuQgC{K`_W@ug?!39x|D4WK{_(^s8>XJ4gn2zD&uJG|R^*|hh} zKhzK9UW;P3e)RdLoXw4`uU`Zj7yq^I?N#@CbJu$NwLc&G1KaoSXD>Q<{q?mOHOf3F z2?uA2B2PlszCUl$O!;SNXIs0b|0cWGxeG^UDCRE$*sw~n1=a-oKIvP2zeon5oy=7>*G`Z%j zd`oKCw|TyG%d5piR-HL_@7@!;+Q-@a3ER?)X8bhS{P#@w$z*l$+enFr2j zFJ>RT5XkZTM(?wU=Vod%pMN)H#!SKD>u07mezabCZlkofn^o!g)Gs;{fVt`2HT_o) zXU-8XZmsIv60UOY-mZi+H{J9`yOyo#PxrdoPP}~E@2K9)`LaLVVpUdavrhh8C3ax; z%gdsEDH=sZudm(UKX|zN_Otz8vQ!QH?>-FN?s?^Y*R$&n3K{;3Xsmj_T7w&$Io9+| z^DT{yThH+>SiXPN`Hm~SMfuk!yEFbd4azE63wAsVe7fpENU`VHcMpy(Q@^^<;XI`D zx|6o{YgK2(_M59_E-_x<$Fe$k;;Wn+vzd+7mX(`51D02(t}`$w|8SEaqPV;jZ~1R+ V`w|UVOJJ?Q;OXk;vd$@?2>@?l6IcKM literal 0 HcmV?d00001 diff --git a/dynamic-tiler@bnku/files/dynamic-tiler@bnku/metadata.json b/dynamic-tiler@bnku/files/dynamic-tiler@bnku/metadata.json new file mode 100644 index 00000000..285e699d --- /dev/null +++ b/dynamic-tiler@bnku/files/dynamic-tiler@bnku/metadata.json @@ -0,0 +1,7 @@ +{ + "uuid": "dynamic-tiler@bnku", + "name": "Dynamic Tiler", + "description": "Intent-aware keyboard and drag-and-drop window tiling for Cinnamon.", + "version": "1.0.0", + "cinnamon-version": ["6.0", "6.2", "6.4", "6.6"] +} diff --git a/dynamic-tiler@bnku/files/dynamic-tiler@bnku/settings-schema.json b/dynamic-tiler@bnku/files/dynamic-tiler@bnku/settings-schema.json new file mode 100644 index 00000000..1f3d6fe5 --- /dev/null +++ b/dynamic-tiler@bnku/files/dynamic-tiler@bnku/settings-schema.json @@ -0,0 +1,276 @@ +{ + "layout1": { + "type": "layout", + "pages": ["page_layout", "page_dnd", "page_keyboard", "page_diagnostics"], + "page_layout": { + "type": "page", + "title": "Layout", + "sections": ["section_spacing", "section_default_grid", "section_monitor_profiles", "section_monitor_overrides"] + }, + "page_dnd": { + "type": "page", + "title": "Drag & Drop", + "sections": ["section_dnd", "section_swap"] + }, + "page_keyboard": { + "type": "page", + "title": "Keyboard", + "sections": ["section_keybindings"] + }, + "page_diagnostics": { + "type": "page", + "title": "Diagnostics", + "sections": ["section_diagnostics"] + }, + "section_spacing": { + "type": "section", + "title": "Spacing & Preview", + "keys": ["gaps", "enablePreview"] + }, + "section_default_grid": { + "type": "section", + "title": "Default Grid", + "keys": ["gridColumns", "gridRows", "minColumnSpan", "minRowSpan", "step"] + }, + "section_monitor_profiles": { + "type": "section", + "title": "Monitor Profiles", + "keys": [ + "horizontalGridColumns", + "horizontalGridRows", + "verticalGridColumns", + "verticalGridRows", + "ultrawideGridColumns", + "ultrawideGridRows" + ] + }, + "section_monitor_overrides": { + "type": "section", + "title": "Per-Monitor Overrides", + "keys": ["monitorGridOverrides"] + }, + "section_dnd": { + "type": "section", + "title": "Drag-and-Drop Snapping", + "keys": ["enable-dnd-tiling", "dnd-modifier-key"] + }, + "section_swap": { + "type": "section", + "title": "Window Swap", + "keys": ["experimentalSwapSameShapeWindows", "dnd-swap-modifier-key"] + }, + "section_keybindings": { + "type": "section", + "title": "Keybindings", + "keys": [ + "keybinding-tile-left", + "keybinding-tile-right", + "keybinding-tile-up", + "keybinding-tile-down", + "keybinding-shift-left", + "keybinding-shift-right", + "keybinding-shift-up", + "keybinding-shift-down", + "keybinding-restore" + ] + }, + "section_diagnostics": { + "type": "section", + "title": "Debugging", + "keys": ["enableDebugLogs"] + } + }, + "gaps": { + "type": "spinbutton", + "default": 8, + "min": 0, + "max": 100, + "step": 1, + "units": "pixels", + "description": "Gap size between windows" + }, + "gridSize": { + "type": "spinbutton", + "default": 12, + "min": 4, + "max": 24, + "step": 2, + "description": "Legacy square grid size" + }, + "gridColumns": { + "type": "spinbutton", + "default": 12, + "min": 4, + "max": 32, + "step": 1, + "description": "Default grid columns" + }, + "gridRows": { + "type": "spinbutton", + "default": 6, + "min": 4, + "max": 32, + "step": 1, + "description": "Default grid rows" + }, + "horizontalGridColumns": { + "type": "spinbutton", + "default": 6, + "min": 4, + "max": 32, + "step": 1, + "description": "Columns for regular horizontal monitors" + }, + "horizontalGridRows": { + "type": "spinbutton", + "default": 6, + "min": 4, + "max": 32, + "step": 1, + "description": "Rows for regular horizontal monitors" + }, + "verticalGridColumns": { + "type": "spinbutton", + "default": 6, + "min": 4, + "max": 32, + "step": 1, + "description": "Columns for vertical monitors" + }, + "verticalGridRows": { + "type": "spinbutton", + "default": 12, + "min": 4, + "max": 32, + "step": 1, + "description": "Rows for vertical monitors" + }, + "ultrawideGridColumns": { + "type": "spinbutton", + "default": 12, + "min": 4, + "max": 48, + "step": 1, + "description": "Columns for ultrawide monitors" + }, + "ultrawideGridRows": { + "type": "spinbutton", + "default": 6, + "min": 4, + "max": 32, + "step": 1, + "description": "Rows for ultrawide monitors" + }, + "monitorGridOverrides": { + "type": "entry", + "default": "", + "description": "Optional monitor overrides, for example: 0:24x12, 1:6x12" + }, + "minSpan": { + "type": "spinbutton", + "default": 2, + "min": 1, + "max": 6, + "step": 1, + "description": "Legacy minimum window span" + }, + "minColumnSpan": { + "type": "spinbutton", + "default": 2, + "min": 1, + "max": 8, + "step": 1, + "description": "Minimum window width in columns" + }, + "minRowSpan": { + "type": "spinbutton", + "default": 2, + "min": 1, + "max": 8, + "step": 1, + "description": "Minimum window height in rows" + }, + "step": { + "type": "spinbutton", + "default": 1, + "min": 1, + "max": 4, + "step": 1, + "description": "Resize step size" + }, + "enablePreview": { + "type": "switch", + "default": true, + "description": "Enable tile preview animation" + }, + "enableDebugLogs": { + "type": "switch", + "default": false, + "description": "Write detailed Dynamic Tiler debug logs to ~/.xsession-errors" + }, + "enable-dnd-tiling": { + "type": "switch", + "default": true, + "description": "Enable drag-and-drop window tiling" + }, + "dnd-modifier-key": { + "type": "keybinding", + "default": "d", + "description": "Hold these modifiers while dragging to snap windows" + }, + "experimentalSwapSameShapeWindows": { + "type": "switch", + "default": true, + "description": "Enable modifier-driven window swap while dragging" + }, + "dnd-swap-modifier-key": { + "type": "keybinding", + "default": "d", + "description": "Hold these modifiers while dragging to swap windows" + }, + "keybinding-tile-left": { + "type": "keybinding", + "default": "Left", + "description": "Tile left" + }, + "keybinding-tile-right": { + "type": "keybinding", + "default": "Right", + "description": "Tile right" + }, + "keybinding-tile-up": { + "type": "keybinding", + "default": "Up", + "description": "Tile up" + }, + "keybinding-tile-down": { + "type": "keybinding", + "default": "Down", + "description": "Tile down" + }, + "keybinding-shift-left": { + "type": "keybinding", + "default": "Left", + "description": "Shift left" + }, + "keybinding-shift-right": { + "type": "keybinding", + "default": "Right", + "description": "Shift right" + }, + "keybinding-shift-up": { + "type": "keybinding", + "default": "Up", + "description": "Shift up" + }, + "keybinding-shift-down": { + "type": "keybinding", + "default": "Down", + "description": "Shift down" + }, + "keybinding-restore": { + "type": "keybinding", + "default": "BackSpace", + "description": "Restore window" + } +} diff --git a/dynamic-tiler@bnku/info.json b/dynamic-tiler@bnku/info.json new file mode 100644 index 00000000..50d132bb --- /dev/null +++ b/dynamic-tiler@bnku/info.json @@ -0,0 +1,6 @@ +{ + "uuid": "dynamic-tiler@bnku", + "name": "Dynamic Tiler", + "description": "Intent-aware keyboard and drag-and-drop window tiling for Cinnamon.", + "author": "bnku" +} diff --git a/dynamic-tiler@bnku/screenshot.png b/dynamic-tiler@bnku/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..e64376b7a8a7aa9ef0f54bb8d299d36e110b3f59 GIT binary patch literal 62366 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≺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 literal 0 HcmV?d00001