├── .gitignore ├── extension ├── enums.js ├── stylesheet.css ├── metadata.json ├── drawing.js ├── reordering.js ├── windowing.js ├── extension.js └── tiling.js ├── docs ├── basic design.odg ├── basic design.pdf ├── basic design.png └── .~lock.basic design.odg# ├── export-zip.sh ├── nest.sh ├── install.sh └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | old-tiling.js 2 | mosaic@mawitime.zip -------------------------------------------------------------------------------- /extension/enums.js: -------------------------------------------------------------------------------- 1 | export var window_spacing = 8; -------------------------------------------------------------------------------- /docs/basic design.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEM0NAssissan7/mosaic/HEAD/docs/basic design.odg -------------------------------------------------------------------------------- /docs/basic design.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEM0NAssissan7/mosaic/HEAD/docs/basic design.pdf -------------------------------------------------------------------------------- /docs/basic design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DEM0NAssissan7/mosaic/HEAD/docs/basic design.png -------------------------------------------------------------------------------- /docs/.~lock.basic design.odg#: -------------------------------------------------------------------------------- 1 | ,heikki,heikin-thinkpad-T14,21.04.2024 23:43,file:///home/heikki/.config/libreoffice/4; -------------------------------------------------------------------------------- /export-zip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | uuid="mosaic@mawitime" 3 | 4 | # Export directory to zip 5 | (cd extension && zip -r "../$uuid.zip" .) 6 | -------------------------------------------------------------------------------- /extension/stylesheet.css: -------------------------------------------------------------------------------- 1 | .feedforward { 2 | background-color: rgba(26, 95, 180, 0.4); 3 | border: 1px solid rgba(28, 113, 216, 1.0); 4 | z-index: -1; 5 | } -------------------------------------------------------------------------------- /nest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # export G_MESSAGES_DEBUG=all 4 | export MUTTER_DEBUG_DUMMY_MODE_SPECS=1920x1080 5 | 6 | dbus-run-session -- gnome-shell --nested --wayland -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | uuid="mosaic@mawitime" 3 | ./export-zip.sh # Export to zip 4 | gnome-extensions install --force "$uuid.zip" # Install using gnome-extensions 5 | rm "$uuid.zip" -------------------------------------------------------------------------------- /extension/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mosaic", 3 | "description": "Next-generation window management", 4 | "uuid": "mosaic@mawitime", 5 | "shell-version": [ 6 | "44", 7 | "45", 8 | "46", 9 | "47" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /extension/drawing.js: -------------------------------------------------------------------------------- 1 | import st from 'gi://St'; 2 | import * as main from 'resource:///org/gnome/shell/ui/main.js'; 3 | 4 | var boxes = []; 5 | 6 | export function rect(x, y, width, height) { 7 | const box = new st.BoxLayout({ style_class: "feedforward" }); 8 | box.x = x; 9 | box.y = y; 10 | box.width = width; 11 | box.height = height; 12 | boxes.push(box); 13 | main.uiGroup.add_child(box); 14 | } 15 | 16 | export function remove_boxes() { 17 | for(let box of boxes) 18 | main.uiGroup.remove_child(box); 19 | boxes = []; 20 | } 21 | 22 | export function clear_actors() { 23 | remove_boxes(); 24 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mosaic 2 | Next-generation window management 3 | 4 | # Disclaimer 5 | This extension is still very early software and is NOT ready for production use. It is still under active development. 6 | It will be made known when the extension will be ready for the first alpha release. As of right now, it is not 7 | recommended to be used in production, especially given the fast-changing nature of the codebase (as of right now). 8 | 9 | # Project Goals 10 | - Make the user do as little window management as possible 11 | - Increase productivity factor 12 | - Tile the windows in the most efficient way possible 13 | - Automatically manage workspaces 14 | - More powerful tiling features for GNOME (like corner tiling) 15 | 16 | # Installation instructions 17 | To install to your user extensions folder, do the following: 18 | 1. Download the source code by any means (and decompress archive if necessary) 19 | 2. Open a terminal in the directory of the project 20 | 3. Run `./install.sh` to install the extension 21 | 4. Log out and log back in 22 | 5. Enable the extension using either an extension manager or by running `gnome-extensions enable mosaic@mawitime` 23 | 24 | # Implemented features: 25 | - Mosaic tiling 26 | - New workspace on maximize 27 | - Automatic workspace creation 28 | - Dynamic window reording 29 | 30 | # Needs work: 31 | - Tiling algorithm 32 | - Event listeners 33 | 34 | # Missing features: 35 | - Corner tiling 36 | - Automatic snap-tiling 37 | - Overflow windows docked away 38 | 39 | # Needs review: 40 | - Event listeners 41 | - APIs 42 | -------------------------------------------------------------------------------- /extension/reordering.js: -------------------------------------------------------------------------------- 1 | import * as tiling from './tiling.js'; 2 | import * as windowing from './windowing.js'; 3 | 4 | var drag_start = false; 5 | var drag_timeout; 6 | 7 | export function cursor_distance(cursor, frame) { 8 | let x = cursor.x - (frame.x + frame.width / 2); 9 | let y = cursor.y - (frame.y + frame.height / 2); 10 | return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); 11 | } 12 | 13 | export function drag(meta_window, child_frame, id, windows) { 14 | let workspace = meta_window.get_workspace(); 15 | let monitor = meta_window.get_monitor(); 16 | 17 | let _cursor = global.get_pointer(); 18 | let cursor = { 19 | x: _cursor[0], 20 | y: _cursor[1] 21 | } 22 | 23 | let minimum_distance = Infinity; 24 | let target_id = null; 25 | for(let window of windows) { 26 | let distance = cursor_distance(cursor, window); 27 | if(distance < minimum_distance) 28 | { 29 | minimum_distance = distance; 30 | target_id = window.id; 31 | } 32 | } 33 | 34 | // Check intersection with original window position 35 | if(target_id === id || target_id === null) 36 | tiling.clear_tmp_swap(); 37 | else 38 | tiling.set_tmp_swap(id, target_id); 39 | 40 | if(tiling.tile_workspace_windows(workspace, null, monitor)) { 41 | tiling.clear_tmp_swap(); 42 | tiling.tile_workspace_windows(workspace, null, monitor) 43 | } 44 | 45 | if(drag_start) 46 | drag_timeout = setTimeout(() => { drag(meta_window, child_frame, id, windows); }, 50); 47 | } 48 | 49 | export function start_drag(meta_window) { 50 | let workspace = meta_window.get_workspace() 51 | let monitor = meta_window.get_monitor(); 52 | let meta_windows = windowing.get_monitor_workspace_windows(workspace, monitor); 53 | tiling.apply_swaps(workspace, meta_windows); 54 | let descriptors = tiling.windows_to_descriptors(meta_windows, monitor); 55 | 56 | tiling.create_mask(meta_window); 57 | tiling.clear_tmp_swap(); 58 | 59 | drag_start = true; 60 | drag(meta_window, meta_window.get_frame_rect(), meta_window.get_id(), JSON.parse(JSON.stringify(descriptors))); 61 | } 62 | 63 | export function stop_drag(meta_window, skip_apply) { 64 | let workspace = meta_window.get_workspace(); 65 | drag_start = false; 66 | clearTimeout(drag_timeout); 67 | 68 | tiling.destroy_masks(); 69 | if(!skip_apply) 70 | tiling.apply_tmp_swap(workspace); 71 | tiling.clear_tmp_swap(); 72 | tiling.tile_workspace_windows(workspace, null, meta_window.get_monitor()); 73 | } -------------------------------------------------------------------------------- /extension/windowing.js: -------------------------------------------------------------------------------- 1 | import * as tiling from './tiling.js'; 2 | 3 | function get_timestamp() { 4 | return global.get_current_time(); 5 | } 6 | 7 | function get_primary_monitor() { 8 | return global.display.get_primary_monitor(); 9 | } 10 | 11 | export function get_workspace() { 12 | return global.workspace_manager.get_active_workspace(); 13 | } 14 | 15 | function get_all_windows() { 16 | return global.display.list_all_windows(); 17 | } 18 | 19 | function get_focused_window() { 20 | let windows = get_all_windows(); 21 | for(let window of windows) { 22 | if(window.has_focus()) 23 | return window; 24 | } 25 | } 26 | 27 | function get_all_workspace_windows(monitor, allow_unrelated) { 28 | return get_monitor_workspace_windows(get_workspace(), monitor, allow_unrelated); 29 | } 30 | 31 | export function get_monitor_workspace_windows(workspace, monitor, allow_unrelated) { 32 | let _windows = []; 33 | let windows = workspace.list_windows(); 34 | for(let window of windows) 35 | if(window.get_monitor() === monitor && (is_related(window) || allow_unrelated)) 36 | _windows.push(window); 37 | return _windows; 38 | } 39 | 40 | function get_index(window) { 41 | let id = window.get_id(); 42 | let meta_windows = windowing.get_monitor_workspace_windows(window.get_workspace(), window.get_monitor()); 43 | for(let i = 0; i < meta_windows.length; i++) 44 | if(meta_windows[i].id === id) 45 | return i; 46 | return null; 47 | } 48 | 49 | export function move_back_window(window) { 50 | let workspace = window.get_workspace(); 51 | let active = workspace.active; 52 | let previous_workspace = workspace.get_neighbor(-3); 53 | if(!previous_workspace) { 54 | console.error("There is no workspace to the left."); 55 | return; 56 | } 57 | if(!tiling.window_fits(window, previous_workspace)) // Make sure there is space for the window in the previous workspace 58 | return workspace; 59 | window.change_workspace(previous_workspace); // Move window to previous workspace 60 | if(active) 61 | previous_workspace.activate(get_timestamp()); // Switch to it 62 | return previous_workspace; 63 | } 64 | 65 | export function move_oversized_window(window){ 66 | let previous_workspace = window.get_workspace(); 67 | let focus = previous_workspace.active; 68 | let new_workspace = global.workspace_manager.append_new_workspace(focus, get_timestamp()); 69 | let monitor = window.get_monitor(); 70 | 71 | window.change_workspace(new_workspace); 72 | global.workspace_manager.reorder_workspace(new_workspace, previous_workspace.index() + 1); 73 | 74 | setTimeout(() => { 75 | tiling.tile_workspace_windows(new_workspace, window, null, true); // Tile new workspace for window 76 | 77 | if(window.maximized_horizontally && window.maximized_vertically) { // Adjust the window positioning if it is maximized 78 | let offset = global.display.get_monitor_geometry(monitor).height - previous_workspace.get_work_area_for_monitor(monitor).height; // Get top bar offset (if applicable) 79 | let frame = window.get_frame_rect(); 80 | window.move_resize_frame(false, 0, offset, frame.width, frame.height - offset); // Move window to display properly 81 | } 82 | 83 | if(focus) 84 | window.focus(get_timestamp()); 85 | }, 50); 86 | 87 | return new_workspace; 88 | } 89 | 90 | export function is_primary(window) { 91 | if(window.get_monitor() === get_primary_monitor()) 92 | return true; 93 | return false; 94 | } 95 | 96 | export function is_excluded(meta_window) { 97 | if( !is_related(meta_window) || 98 | meta_window.is_hidden() 99 | ) 100 | return true; 101 | return false; 102 | } 103 | 104 | export function is_related(meta_window) { 105 | if( !meta_window.is_attached_dialog() && 106 | meta_window.window_type === 0 && 107 | !meta_window.is_on_all_workspaces() 108 | ) return true; 109 | return false; 110 | } 111 | 112 | export function renavigate(workspace, condition) { 113 | let previous_workspace = workspace.get_neighbor(-3); 114 | 115 | if(previous_workspace === 1 || previous_workspace.index() === workspace.index() || !previous_workspace) { 116 | previous_workspace = workspace.get_neighbor(-4); // The new workspace will be the one on the right instead. 117 | // Recheck to see if it is still a problematic workspace 118 | if( previous_workspace === 1 || 119 | previous_workspace.index() === workspace.index() || 120 | previous_workspace.index() === global.workspace_manager.get_n_workspaces() - 1) 121 | return; 122 | } 123 | 124 | if( condition && 125 | workspace.index() !== global.workspace_manager.get_n_workspaces() - 1) 126 | { 127 | previous_workspace.activate(get_timestamp()); 128 | } 129 | } -------------------------------------------------------------------------------- /extension/extension.js: -------------------------------------------------------------------------------- 1 | /* extension.js 2 | * 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 2 of the License, or 6 | * (at your option) any later version. 7 | * 8 | * This program is distributed in the hope that it will be useful, 9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | * GNU General Public License for more details. 12 | * 13 | * You should have received a copy of the GNU General Public License 14 | * along with this program. If not, see . 15 | * 16 | * SPDX-License-Identifier: GPL-2.0-or-later 17 | */ 18 | 19 | /* exported init */ 20 | import * as windowing from './windowing.js'; 21 | import * as tiling from './tiling.js'; 22 | import * as drawing from './drawing.js'; 23 | import * as reordering from './reordering.js'; 24 | 25 | let wm_eventids = []; 26 | let display_eventids = []; 27 | let workspace_man_eventids = []; 28 | let maximized_windows = []; 29 | 30 | let workspace_manager = global.workspace_manager; 31 | 32 | function tile_window_workspace(meta_window) { 33 | if(!meta_window) return; 34 | let workspace = meta_window.get_workspace(); 35 | if(!workspace) return; 36 | tiling.tile_workspace_windows(workspace, 37 | meta_window, 38 | null, 39 | false); 40 | } 41 | 42 | let size_changed = false; 43 | let event_timeout; 44 | let expanded_window_timeout; 45 | let tile_timeout; 46 | 47 | export default class Extension { 48 | constructor() { 49 | } 50 | 51 | tile_all_workspaces() { 52 | let n_workspaces = workspace_manager.get_n_workspaces(); 53 | for(let i = 0; i < n_workspaces; i++) { 54 | let workspace = workspace_manager.get_workspace_by_index(i); 55 | // Recurse all monitors 56 | let n_monitors = global.display.get_n_monitors(); 57 | for(let j = 0; j < n_monitors; j++) 58 | tiling.tile_workspace_windows(workspace, false, j, true); 59 | } 60 | } 61 | 62 | window_created_handler(_, window) { 63 | let timeout = setInterval(() => { 64 | let workspace = window.get_workspace(); 65 | let monitor = window.get_monitor(); 66 | // Ensure window is valid before performing any actions 67 | if( monitor !== null && 68 | window.wm_class !== null && 69 | window.get_compositor_private() && 70 | workspace.list_windows().length !== 0 && 71 | !window.is_hidden()) 72 | { 73 | clearTimeout(timeout); 74 | if(windowing.is_related(window)) { 75 | if((window.maximized_horizontally && 76 | window.maximized_vertically && 77 | windowing.get_monitor_workspace_windows(workspace, monitor).length > 1) || 78 | !tiling.window_fits(window, workspace, monitor)) 79 | windowing.move_oversized_window(window); 80 | else 81 | tiling.tile_workspace_windows(workspace, window, monitor, false); 82 | } 83 | } 84 | }, 10); 85 | } 86 | 87 | destroyed_handler(_, win) { 88 | let window = win.meta_window; 89 | let monitor = window.get_monitor(); 90 | if(monitor === global.display.get_primary_monitor()) { 91 | tiling.tile_workspace_windows(windowing.get_workspace(), 92 | global.display.get_focus_window(), 93 | null, 94 | true); 95 | let workspace = window.get_workspace() 96 | windowing.renavigate(workspace, windowing.get_monitor_workspace_windows(workspace, monitor).length === 0); 97 | } 98 | } 99 | 100 | switch_workspace_handler(_, win) { 101 | tile_window_workspace(win.meta_window); // Tile when switching to a workspace. Helps to create a more cohesive experience. 102 | } 103 | 104 | size_change_handler(_, win, mode) { 105 | let window = win.meta_window; 106 | if(windowing.is_related(window)) { 107 | let id = window.get_id(); 108 | let workspace = window.get_workspace(); 109 | let monitor = window.get_monitor(); 110 | 111 | if(mode === 2 || mode === 0) { // If the window was maximized 112 | if(window.maximized_horizontally === true && window.maximized_vertically === true && windowing.get_monitor_workspace_windows(workspace, monitor).length > 1) { 113 | // If maximized (and not alone), move to new workspace and activate it if it is on the active workspace 114 | let new_workspace = windowing.move_oversized_window(window); 115 | /* We mark the window as activated by using its id to index an array 116 | We put the value as the active workspace index so that if the workspace anatomy 117 | of the current workspace changes, it does not move the maximized window to an unrelated 118 | window. 119 | */ 120 | if(new_workspace) { 121 | maximized_windows[id] = { 122 | workspace: new_workspace.index(), 123 | monitor: monitor 124 | }; // Mark window as maximized 125 | tiling.tile_workspace_windows(workspace, false, monitor, false); // Sort the workspace where the window came from 126 | } 127 | } 128 | } else if(false && (mode === 3 || mode === 1)) { // If the window was unmaximized 129 | if( (window.maximized_horizontally === false || 130 | window.maximized_vertically === false) && // If window is not maximized 131 | maximized_windows[id] && 132 | windowing.get_monitor_workspace_windows(workspace, monitor).length === 1// If the workspace anatomy has not changed 133 | ) { 134 | if( maximized_windows[id].workspace === workspace.index() && 135 | maximized_windows[id].monitor === monitor 136 | ) { 137 | maximized_windows[id] = false; 138 | windowing.move_back_window(window); // Move the window back to its workspace 139 | tile_window_workspace(window); 140 | } 141 | } 142 | } 143 | } 144 | } 145 | 146 | size_changed_handler(_, win) { 147 | let window = win.meta_window; 148 | if(!size_changed && windowing.is_related(window)) { 149 | // Live resizing 150 | size_changed = true; 151 | tiling.tile_workspace_windows(window.get_workspace(), window, null, true); 152 | size_changed = false; 153 | } 154 | } 155 | 156 | grab_op_begin_handler(_, window, grabpo) { 157 | if( windowing.is_related(window) && 158 | (grabpo === 1 || grabpo === 1025) && // When a window has moved 159 | !(window.maximized_horizontally === true && window.maximized_vertically === true)) 160 | reordering.start_drag(window); 161 | // tile_window_workspace(window); 162 | } 163 | grab_op_end_handler(_, window, grabpo) { 164 | if(windowing.is_related(window)) { 165 | reordering.stop_drag(window); 166 | if( (grabpo === 1 || grabpo === 1025) && // When a window has moved 167 | !(window.maximized_horizontally === true && window.maximized_vertically === true)) 168 | { 169 | tiling.tile_workspace_windows(window.get_workspace(), window, null, false); 170 | } 171 | if(grabpo === 25601) // When released from resizing 172 | tile_window_workspace(window); 173 | } else 174 | reordering.stop_drag(window, true); 175 | } 176 | 177 | workspace_created_handler(_, index) { 178 | // tiling.append_workspace(index); 179 | } 180 | 181 | enable() { 182 | console.log("[MOSAIC]: Starting Mosaic layout manager."); 183 | 184 | wm_eventids.push(global.window_manager.connect('size-change', this.size_change_handler)); 185 | wm_eventids.push(global.window_manager.connect('size-changed', this.size_changed_handler)); 186 | display_eventids.push(global.display.connect('window-created', this.window_created_handler)); 187 | wm_eventids.push(global.window_manager.connect('destroy', this.destroyed_handler)); 188 | display_eventids.push(global.display.connect("grab-op-begin", this.grab_op_begin_handler)); 189 | display_eventids.push(global.display.connect("grab-op-end", this.grab_op_end_handler)); 190 | // workspace_man_eventids.push(global.workspace_manager.connect('workspace-added', this.workspace_created_handler)); 191 | // wm_eventids.push(global.window_manager.connect('switch-workspace', this.switch_workspace_handler)); 192 | 193 | // Sort all workspaces at startup 194 | setTimeout(this.tile_all_workspaces, 300); 195 | tile_timeout = setInterval(this.tile_all_workspaces, 60000 * 5); // Tile all windows every 5 minutes (in case the machine/display goes to sleep) 196 | } 197 | 198 | disable() { 199 | console.log("[MOSAIC]: Disabling Mosaic layout manager."); 200 | // Disconnect all events 201 | clearTimeout(tile_timeout); 202 | for(let eventid of wm_eventids) 203 | global.window_manager.disconnect(eventid); 204 | for(let eventid of display_eventids) 205 | global.display.disconnect(eventid); 206 | for(let eventid of workspace_man_eventids) 207 | global.workspace_manager.disconnect(eventid); 208 | drawing.clear_actors(); 209 | } 210 | } 211 | 212 | function init() { 213 | return new Extension(); 214 | } -------------------------------------------------------------------------------- /extension/tiling.js: -------------------------------------------------------------------------------- 1 | import * as enums from './enums.js'; 2 | import * as windowing from './windowing.js'; 3 | import * as reordering from './reordering.js'; 4 | import * as drawing from './drawing.js'; 5 | 6 | var masks = []; 7 | var working_windows = []; 8 | var tmp_swap = []; 9 | 10 | class window_descriptor{ 11 | constructor(meta_window, index) { 12 | let frame = meta_window.get_frame_rect(); 13 | 14 | this.index = index; 15 | this.x = frame.x; 16 | this.y = frame.y; 17 | this.width = frame.width; 18 | this.height = frame.height; 19 | this.id = meta_window.get_id(); 20 | } 21 | draw(meta_windows, x, y) { 22 | meta_windows[this.index].move_frame(false, 23 | x, 24 | y); 25 | } 26 | } 27 | 28 | function create_descriptor(meta_window, monitor, index, reference_window) { 29 | // If the input window is the same as the reference, make a descriptor for it anyways 30 | if(reference_window) 31 | if(meta_window.get_id() === reference_window.get_id()) 32 | return new window_descriptor(meta_window, index); 33 | 34 | if( windowing.is_excluded(meta_window) || 35 | meta_window.get_monitor() !== monitor || 36 | (meta_window.maximized_horizontally && meta_window.maximized_horizontally)) 37 | return false; 38 | return new window_descriptor(meta_window, index); 39 | } 40 | 41 | export function windows_to_descriptors(meta_windows, monitor, reference_window) { 42 | let descriptors = []; 43 | for(let i = 0; i < meta_windows.length; i++) { 44 | let descriptor = create_descriptor(meta_windows[i], monitor, i, reference_window); 45 | if(descriptor) 46 | descriptors.push(descriptor); 47 | } 48 | return descriptors; 49 | } 50 | 51 | function Level(work_area) { 52 | this.x = 0; 53 | this.y = 0; 54 | this.width = 0; 55 | this.height = 0; 56 | this.windows = []; 57 | this.work_area = work_area; 58 | } 59 | 60 | Level.prototype.draw_horizontal = function(meta_windows, work_area, y) { 61 | let x = this.x; 62 | for(let window of this.windows) { 63 | let center_offset = (work_area.height / 2 + work_area.y) - (y + window.height / 2); 64 | let y_offset = 0; 65 | if(center_offset > 0) 66 | y_offset = Math.min(center_offset, this.height - window.height); 67 | 68 | window.draw(meta_windows, x, y + y_offset); 69 | x += window.width + enums.window_spacing; 70 | } 71 | } 72 | 73 | function tile(windows, work_area) { 74 | let vertical = false; 75 | { 76 | let width = 0; 77 | let height = 0; 78 | for(let window of windows) { 79 | width = Math.max(window.width, width); 80 | height = Math.max(window.height, height); 81 | } 82 | // if(width < height) 83 | // vertical = true; 84 | } 85 | let levels = [new Level(work_area)]; 86 | let total_width = 0; 87 | let total_height = 0; 88 | let x, y; 89 | 90 | let overflow = false; 91 | 92 | if(!vertical) { // If the mode is going to be horizontal 93 | let window_widths = 0; 94 | windows.map(w => window_widths += w.width + enums.window_spacing) 95 | window_widths -= enums.window_spacing; 96 | 97 | let n_levels = Math.round(window_widths / work_area.width) + 1; 98 | let avg_level_width = window_widths / n_levels; 99 | let level = levels[0]; 100 | let level_index = 0; 101 | 102 | for(let window of windows) { // Add windows to levels 103 | if(level.width + enums.window_spacing + window.width > work_area.width) { // Create a new level 104 | total_width = Math.max(level.width, total_width); 105 | total_height += level.height + enums.window_spacing; 106 | level.x = (work_area.width - level.width) / 2 + work_area.x; 107 | levels.push(new Level(work_area)); 108 | level_index++; 109 | level = levels[level_index]; 110 | } 111 | if( Math.max(window.height, level.height) + total_height > work_area.height || 112 | window.width + level.width > work_area.width){ 113 | overflow = true; 114 | continue; 115 | } 116 | level.windows.push(window); 117 | if(level.width !== 0) 118 | level.width += enums.window_spacing; 119 | level.width += window.width; 120 | level.height = Math.max(window.height, level.height); 121 | } 122 | total_width = Math.max(level.width, total_width); 123 | total_height += level.height; 124 | level.x = (work_area.width - level.width) / 2 + work_area.x; 125 | 126 | y = (work_area.height - total_height) / 2 + work_area.y; 127 | } else { 128 | let window_heights = 0; 129 | windows.map(w => window_heights += w.height + enums.window_spacing) 130 | window_heights -= enums.window_spacing; 131 | 132 | let n_levels = Math.floor(window_heights / work_area.height) + 1; 133 | let avg_level_height = window_heights / n_levels; 134 | let level = levels[0]; 135 | let level_index = 0; 136 | 137 | for(let window of windows) { // Add windows to levels 138 | if(level.width > avg_level_height) { // Create a new level 139 | total_width = Math.max(level.width, total_width); 140 | total_height += level.height + enums.window_spacing; 141 | level.x = (work_area.width - level.width) / 2 + work_area.x; 142 | levels.push(new Level(work_area)); 143 | level_index++; 144 | level = levels[level_index]; 145 | } 146 | level.windows.push(window); 147 | if(level.width !== 0) 148 | level.width += enums.window_spacing; 149 | level.width += window.width; 150 | level.height = Math.max(window.height, level.height); 151 | } 152 | total_width = Math.max(level.width, total_width); 153 | total_height += level.height; 154 | level.x = (work_area.width - level.width) / 2 + work_area.x; 155 | 156 | y = (work_area.height - total_height) / 2 + work_area.y; 157 | } 158 | return { 159 | x: x, 160 | y: y, 161 | overflow: overflow, 162 | vertical: vertical, 163 | levels: levels 164 | } 165 | } 166 | 167 | function swap_elements (array, index1, index2) { 168 | if(!array[index1] || !array[index2]) 169 | return; // Prevent making swaps for elements that do not exist 170 | let tmp = array[index1]; 171 | array[index1] = array[index2]; 172 | array[index2] = tmp; 173 | } 174 | 175 | export function set_tmp_swap(id1, id2) { 176 | let index1 = null 177 | let index2 = null; 178 | 179 | for(let i = 0; i < working_windows.length; i++) { 180 | let window = working_windows[i]; 181 | if(window.id === id1 && index1 === null) 182 | index1 = i; 183 | if(window.id === id2 && index2 === null) 184 | index2 = i; 185 | } 186 | if(index1 !== null && index2 !== null) { 187 | if( index1 === index2 || 188 | (tmp_swap[0] === index2 && tmp_swap[1] === index1)) 189 | return; 190 | tmp_swap = [index1, index2]; 191 | } else 192 | console.error("Could not find both indexes for windows"); 193 | } 194 | 195 | export function clear_tmp_swap() { 196 | tmp_swap = []; 197 | } 198 | 199 | export function apply_tmp_swap(workspace) { 200 | if(!workspace.swaps) 201 | workspace.swaps = []; 202 | if(tmp_swap.length !== 0) 203 | workspace.swaps.push(tmp_swap); 204 | } 205 | 206 | export function apply_swaps(workspace, array) { 207 | if(workspace.swaps) 208 | for(let swap of workspace.swaps) 209 | swap_elements(array, swap[0], swap[1]); 210 | } 211 | 212 | export function apply_tmp(array) { 213 | if(tmp_swap.length !== 0) 214 | swap_elements(array, tmp_swap[0], tmp_swap[1]); 215 | } 216 | 217 | function get_working_info(workspace, window, monitor) { 218 | if(!workspace) // Failsafe for undefined workspace 219 | return false; 220 | 221 | let current_monitor = null; 222 | if(window) 223 | current_monitor = window.get_monitor(); 224 | else 225 | current_monitor = monitor; 226 | if(current_monitor === null || current_monitor === false) 227 | return false; 228 | 229 | let meta_windows = windowing.get_monitor_workspace_windows(workspace, current_monitor); 230 | 231 | // Put needed window info into an enum so it can be transferred between arrays 232 | let _windows = windows_to_descriptors(meta_windows, current_monitor, window); 233 | // Apply window layout swaps 234 | apply_swaps(workspace, _windows); 235 | working_windows = []; 236 | _windows.map(window => working_windows.push(window)); // Set working windows before tmp application 237 | apply_tmp(_windows); 238 | // Apply masks 239 | let windows = []; 240 | for(let window of _windows) 241 | windows.push(get_mask(window)); 242 | 243 | let work_area = workspace.get_work_area_for_monitor(current_monitor); // Get working area for current space 244 | if(!work_area) return false; 245 | 246 | return { 247 | monitor: current_monitor, 248 | meta_windows: meta_windows, 249 | windows: windows, 250 | work_area: work_area 251 | } 252 | } 253 | 254 | function draw_tile(tile_info, work_area, meta_windows) { 255 | let levels = tile_info.levels; 256 | let _x = tile_info.x; 257 | let _y = tile_info.y; 258 | if(!tile_info.vertical) { // Horizontal tiling 259 | let y = _y; 260 | for(let level of levels) { 261 | level.draw_horizontal(meta_windows, work_area, y); 262 | y += level.height + enums.window_spacing; 263 | } 264 | } else { // Vertical 265 | let x = _x; 266 | for(let level of levels) { 267 | level.draw_vertical(meta_windows, x); 268 | x += level.width + enums.window_spacing; 269 | } 270 | } 271 | } 272 | 273 | class Mask{ 274 | constructor(window) { 275 | this.x = window.x; 276 | this.y = window.y; 277 | this.width = window.width; 278 | this.height = window.height; 279 | } 280 | draw(_, x, y) { 281 | drawing.remove_boxes(); 282 | drawing.rect(x, y, this.width, this.height); 283 | } 284 | } 285 | 286 | export function create_mask(meta_window) { 287 | masks[meta_window.get_id()] = true; 288 | } 289 | 290 | export function destroy_masks() { 291 | drawing.remove_boxes(); 292 | masks = []; 293 | } 294 | 295 | export function get_mask(window) { 296 | if(masks[window.id]) 297 | return new Mask(window); 298 | return window; 299 | } 300 | 301 | export function tile_workspace_windows(workspace, reference_meta_window, _monitor, keep_oversized_windows) { 302 | let working_info = get_working_info(workspace, reference_meta_window, _monitor); 303 | if(!working_info) return; 304 | let meta_windows = working_info.meta_windows; 305 | let windows = working_info.windows; 306 | let work_area = working_info.work_area; 307 | let monitor = working_info.monitor; 308 | 309 | let tile_info = tile(windows, work_area); 310 | let overflow = tile_info.overflow; 311 | for(let window of windowing.get_monitor_workspace_windows(workspace, monitor)) 312 | if(window.maximized_horizontally && window.maximized_vertically) 313 | overflow = true; 314 | 315 | if(overflow && !keep_oversized_windows && reference_meta_window) { // Overflow clause 316 | let id = reference_meta_window.get_id(); 317 | let _windows = windows; 318 | for(let i = 0; i < _windows.length; i++) { 319 | if(meta_windows[_windows[i].index].get_id() === id) { 320 | _windows.splice(i, 1); 321 | break; 322 | } 323 | } 324 | windowing.move_oversized_window(reference_meta_window); 325 | tile_info = tile(_windows, work_area); 326 | } 327 | draw_tile(tile_info, work_area, meta_windows); 328 | return overflow; 329 | } 330 | 331 | export function window_fits(window, workspace, monitor) { 332 | let working_info = get_working_info(workspace, window, monitor); 333 | if(!working_info) return false; 334 | if(workspace.index() === window.get_workspace().index()) return true; 335 | 336 | let windows = working_info.windows; 337 | windows.push(new window_descriptor(window, windows.length)); 338 | 339 | for(let window of working_info.meta_windows) 340 | if(window.maximized_horizontally && window.maximized_vertically) 341 | return false; 342 | 343 | return !(tile(windows, working_info.work_area).overflow); 344 | } --------------------------------------------------------------------------------