├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── clippy.toml ├── examples └── main.rs ├── ipc ├── Cargo.toml └── src │ └── main.rs ├── service ├── Cargo.toml ├── examples │ └── glib.rs └── src │ └── lib.rs └── src ├── branch.rs ├── display.rs ├── events.rs ├── exceptions.rs ├── fork.rs ├── geom.rs ├── lib.rs ├── mod.rs ├── stack.rs ├── tiler.rs ├── window.rs └── workspace.rs /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target/ 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 System76 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | [package] 5 | name = "pop-tiler" 6 | description = "Generic tiling window manager library" 7 | license = "MPL-2.0" 8 | version = "0.1.0" 9 | edition = "2021" 10 | 11 | [workspace] 12 | members = [ "ipc", "service" ] 13 | 14 | [dependencies] 15 | derive_more = "0.99" 16 | either = "1.6" 17 | qcell = "0.5" 18 | serde = { version = "1.0", optional = true, features = ["derive"] } 19 | tracing = "0.1" 20 | tracing-subscriber = "0.2" 21 | ward = "2" 22 | 23 | [features] 24 | ipc = ["serde"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET = debug 2 | DEBUG ?= 0 3 | 4 | BASE_PATH = $(DESTDIR)/usr 5 | BIN_DIR = $(BASE_PATH)/bin 6 | BIN = $(BIN_DIR)/pop-tiler 7 | 8 | .PHONY = all clean install uninstall vendor 9 | 10 | ifeq ($(DEBUG),0) 11 | TARGET = release 12 | ARGS += --release 13 | endif 14 | 15 | VENDOR ?= 0 16 | ifneq ($(VENDOR),0) 17 | ARGS += --frozen --offline 18 | endif 19 | 20 | all: extract-vendor 21 | cargo build -p pop-tiler-ipc $(ARGS) 22 | 23 | clean: 24 | cargo clean 25 | 26 | distclean: 27 | rm -rf .cargo vendor vendor.tar target 28 | 29 | vendor: 30 | mkdir -p .cargo 31 | cargo vendor --sync plugins/Cargo.toml | head -n -1 > .cargo/config 32 | echo 'directory = "vendor"' >> .cargo/config 33 | tar pcf vendor.tar vendor 34 | rm -rf vendor 35 | 36 | extract-vendor: 37 | ifeq ($(VENDOR),1) 38 | rm -rf vendor; tar pxf vendor.tar 39 | endif 40 | 41 | install: 42 | install -Dm0755 target/$(TARGET)/pop-tiler-ipc $(BIN) 43 | 44 | uninstall: 45 | rm $(BIN) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pop Tiler 2 | 3 | Generic tiling window manager library for Rust, using an architecture based on [TCell](https://docs.rs/qcell/latest/qcell/struct.TCell.html). 4 | 5 | ## License 6 | 7 | Licensed under the [Mozilla Public License 2.0](https://choosealicense.com/licenses/mpl-2.0/). 8 | 9 | ### Contribution 10 | 11 | Any contribution intentionally submitted for inclusion in the work by you shall be licensed under the MPL-2.0. 12 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 System76 2 | # SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | msrv = "1.51" -------------------------------------------------------------------------------- /examples/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use pop_tiler::*; 5 | 6 | fn main() { 7 | tracing_subscriber::fmt() 8 | .with_writer(std::io::stderr) 9 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 10 | .init(); 11 | 12 | let mut owner = TCellOwner::<()>::new(); 13 | let t = &mut owner; 14 | 15 | let mut tiler = Tiler::default(); 16 | 17 | // Instruct about available displays. 18 | tiler.display_update(0, Rect::new(1, 1, 2560, 1440), t); // 2560x1440 display with ID 0 19 | 20 | // Assign workspaces to displays. 21 | tiler.workspace_update(0, 0, t); // Assign workspace 0 to display 0 22 | 23 | // Create some windows to assign. 24 | let win_a = tiler.window((0, 0)); 25 | let win_b = tiler.window((0, 1)); 26 | let win_c = tiler.window((0, 2)); 27 | let win_d = tiler.window((0, 3)); 28 | let win_e = tiler.window((0, 4)); 29 | let win_f = tiler.window((0, 5)); 30 | 31 | // Focus first workspace. 32 | tiler.workspace_switch(0, t); 33 | 34 | // Attach windows to active workspace. 35 | tiler.attach(&win_a, t); 36 | tiler.focus(&win_a, t); 37 | tiler.attach(&win_b, t); 38 | tiler.focus(&win_b, t); 39 | tiler.attach(&win_c, t); 40 | tiler.focus(&win_c, t); 41 | tiler.attach(&win_d, t); 42 | tiler.focus(&win_d, t); 43 | tiler.attach(&win_e, t); 44 | tiler.focus(&win_e, t); 45 | tiler.attach(&win_f, t); 46 | 47 | let mut first_fork = None; 48 | 49 | for event in tiler.events(t) { 50 | if first_fork.is_none() { 51 | if let Event::Fork(ref id, ForkUpdate { ref handle, .. }) = event { 52 | first_fork = Some((*id, *handle)); 53 | } 54 | } 55 | println!("Event: {:?}", event); 56 | } 57 | 58 | eprintln!("perform resize"); 59 | 60 | if let Some((fork, handle)) = first_fork { 61 | tiler.fork_resize(fork, handle / 2, t); 62 | } 63 | 64 | for event in tiler.events(t) { 65 | println!("Event: {:?}", event); 66 | } 67 | 68 | tiler.detach(&win_a, t); 69 | 70 | for event in tiler.events(t) { 71 | println!("Event: {:?}", event); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /ipc/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 System76 2 | # SPDX-License-Identifier: GPL-3.0-only 3 | 4 | [package] 5 | name = "pop-tiler-ipc" 6 | license = "GPL-3.0-only" 7 | version = "0.1.0" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | pop-tiler = { path = "../", features = ["ipc"] } 12 | pop-tiler-service = { path = "../service", features = ["ipc"]} 13 | serde_json = "1.0.68" -------------------------------------------------------------------------------- /ipc/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use pop_tiler::TCellOwner; 5 | use pop_tiler_service::Service; 6 | use std::io::{BufRead, BufReader, Write}; 7 | fn main() { 8 | let input = std::io::stdin(); 9 | let mut input = BufReader::new(input.lock()).lines(); 10 | 11 | let output = std::io::stdout(); 12 | let mut output = output.lock(); 13 | 14 | let mut t = TCellOwner::<()>::new(); 15 | let mut tiler = Service::default(); 16 | 17 | while let Some(Ok(line)) = input.next() { 18 | match serde_json::from_str(&line) { 19 | Ok(request) => { 20 | for event in tiler.handle(request, &mut t) { 21 | match serde_json::to_string(&event) { 22 | Ok(mut string) => { 23 | string.push('\n'); 24 | let _ = output.write_all(string.as_bytes()); 25 | } 26 | Err(why) => { 27 | eprintln!("pop-tiler-ipc: failed to serialize response: {}", why); 28 | } 29 | } 30 | } 31 | } 32 | Err(why) => { 33 | eprintln!("pop-tiler-ipc: failed to read from stdin: {}", why); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /service/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 System76 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | [package] 5 | name = "pop-tiler-service" 6 | description = "Simple drop-in microservice for integrating with pop-tiler" 7 | version = "0.1.0" 8 | license = "MPL-2.0" 9 | edition = "2018" 10 | 11 | [features] 12 | ipc = ["serde"] 13 | 14 | [dependencies] 15 | async-channel = "1" 16 | thiserror = "1" 17 | pop-tiler = { path = "../" } 18 | futures-lite = "1" 19 | async-io = "1" 20 | async-oneshot = "0.5" 21 | serde = { version = "1.0", optional = true, features = ["derive"] } 22 | 23 | [dev-dependencies] 24 | glib = "0.14" 25 | -------------------------------------------------------------------------------- /service/examples/glib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: LGPL-3.0-only 3 | 4 | use pop_tiler_service::TilerThread; 5 | 6 | fn main() { 7 | glib::MainContext::default().spawn(async move { 8 | let tiler = TilerThread::default(); 9 | let request = pop_tiler_service::Request::FocusLeft; 10 | eprintln!("Request: {:?}", request); 11 | let result = tiler.handle(request).await; 12 | eprintln!("Result: {:?}", result); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /service/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | #[cfg(feature = "ipc")] 5 | #[macro_use] 6 | extern crate serde; 7 | 8 | use async_channel::{Receiver, RecvError, SendError, Sender}; 9 | use pop_tiler::*; 10 | use std::thread; 11 | use thiserror::Error as ThisError; 12 | 13 | pub type Response = Vec; 14 | 15 | #[derive(Debug, ThisError)] 16 | pub enum Error { 17 | #[error("pop-tiler server-side request error")] 18 | ServerRequest(#[source] RecvError), 19 | 20 | #[error("pop-tiler client-side response error")] 21 | ClientResponse(#[source] RecvError), 22 | 23 | #[error("pop-tiler client-side request error")] 24 | ClientRequest(#[source] SendError), 25 | 26 | #[error("pop-tiler server-side response error")] 27 | ServerResponse(#[source] SendError), 28 | } 29 | 30 | /// An instruction to send to the pop-tiling service 31 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 32 | #[derive(Debug)] 33 | pub enum Request { 34 | /// Attach a window to the tiler. 35 | Attach(WindowID), 36 | /// Detach a window from the tiler. 37 | Detach(WindowID), 38 | /// Insert or update the dimensions of a display. 39 | DisplayUpdate { display: u32, dimensions: Rect }, 40 | /// Remove a display from the tree. 41 | DisplayDetach(u32), 42 | /// Make this window the actively-focused window. 43 | Focus(WindowID), 44 | /// Focus the window above the active window. 45 | FocusAbove, 46 | /// Focus the window below the active window. 47 | FocusBelow, 48 | /// Focus the display above the active one. 49 | FocusDisplayAbove, 50 | /// Focus the display below the active one. 51 | FocusDisplayBelow, 52 | /// Focus the display to the left of the active one. 53 | FocusDisplayLeft, 54 | /// Focus the display to the right of the active one. 55 | FocusDisplayRight, 56 | /// Focus the window to the left of the active one. 57 | FocusLeft, 58 | /// Focus the window to the right of the active one. 59 | FocusRight, 60 | /// Move the active window above. 61 | MoveAbove, 62 | /// Move the active window below. 63 | MoveBelow, 64 | /// Move the active window to the left. 65 | MoveLeft, 66 | /// Move the active window to the right. 67 | MoveRight, 68 | /// Toggle the orientation of the fork the window is attached to. 69 | ToggleOrientation, 70 | /// Toggle the stackability of the window. 71 | ToggleStack, 72 | /// Resize a fork with an updated split. 73 | Resize(usize, u32), 74 | /// Swap the positions of two windows. 75 | Swap(WindowID, WindowID), 76 | /// Switch to a different workspace. 77 | WorkspaceSwitch(u32), 78 | /// Associate a workspace with a display. 79 | WorkspaceUpdate { workspace: u32, display: u32 }, 80 | } 81 | 82 | pub struct Service { 83 | tiler: Tiler, 84 | } 85 | 86 | impl Default for Service { 87 | fn default() -> Self { 88 | Self { 89 | tiler: Tiler::default(), 90 | } 91 | } 92 | } 93 | 94 | impl Service { 95 | pub fn handle<'a>( 96 | &'a mut self, 97 | input: Request, 98 | t: &'a mut TCellOwner, 99 | ) -> impl Iterator + 'a { 100 | let tiler = &mut self.tiler; 101 | 102 | let window_from_id = |window: WindowID| tiler.windows.get(&window).cloned(); 103 | 104 | match input { 105 | Request::Attach(window) => { 106 | if let Some(window) = window_from_id(window) { 107 | tiler.attach(&window, t) 108 | } 109 | } 110 | 111 | Request::Detach(window) => { 112 | if let Some(window) = window_from_id(window) { 113 | tiler.detach(&window, t); 114 | } 115 | } 116 | 117 | Request::DisplayUpdate { 118 | display, 119 | dimensions, 120 | } => { 121 | tiler.display_update(display, dimensions, t); 122 | } 123 | 124 | Request::DisplayDetach(display_id) => tiler.display_detach(display_id, t), 125 | 126 | Request::Focus(window) => { 127 | if let Some(window) = window_from_id(window) { 128 | tiler.focus(&window, t); 129 | } 130 | } 131 | 132 | Request::FocusAbove => tiler.focus_above(t), 133 | Request::FocusBelow => tiler.focus_below(t), 134 | Request::FocusLeft => tiler.focus_left(t), 135 | Request::FocusRight => tiler.focus_right(t), 136 | Request::FocusDisplayAbove => tiler.focus_display_above(t), 137 | Request::FocusDisplayBelow => tiler.focus_display_below(t), 138 | Request::FocusDisplayLeft => tiler.focus_display_left(t), 139 | Request::FocusDisplayRight => tiler.focus_display_right(t), 140 | Request::MoveAbove => tiler.move_above(t), 141 | Request::MoveBelow => tiler.move_below(t), 142 | Request::MoveLeft => tiler.move_left(t), 143 | Request::MoveRight => tiler.move_right(t), 144 | 145 | Request::Resize(fork, handle) => tiler.fork_resize(fork, handle, t), 146 | 147 | Request::Swap(a, b) => { 148 | if let Some((a, b)) = window_from_id(a).zip(window_from_id(b)) { 149 | tiler.swap(&a, &b, t); 150 | } 151 | } 152 | 153 | Request::ToggleOrientation => tiler.toggle_orientation(t), 154 | 155 | Request::ToggleStack => tiler.stack_toggle(t), 156 | 157 | Request::WorkspaceSwitch(workspace) => { 158 | tiler.workspace_switch(workspace, t); 159 | } 160 | 161 | Request::WorkspaceUpdate { display, workspace } => { 162 | tiler.workspace_update(workspace, display, t); 163 | } 164 | } 165 | 166 | self.tiler.events(t) 167 | } 168 | } 169 | 170 | /// Handle for sending and receiving instructions to and from the pop-tiler. 171 | struct ClientThread { 172 | send: Sender, 173 | recv: Receiver, 174 | } 175 | 176 | impl ClientThread { 177 | pub fn new(send: Sender, recv: Receiver) -> Self { 178 | Self { send, recv } 179 | } 180 | /// Sends an instruction to pop-tiler, then waits for the response. 181 | pub async fn handle(&self, input: Request) -> Result { 182 | self.send.send(input).await.map_err(Error::ClientRequest)?; 183 | 184 | self.recv.recv().await.map_err(Error::ClientResponse) 185 | } 186 | } 187 | 188 | /// The pop-tiling service, which you can spawn in a separate thread / local async task 189 | struct ServiceThread { 190 | recv: Receiver, 191 | send: Sender, 192 | service: Service, 193 | t: TCellOwner, 194 | } 195 | 196 | impl ServiceThread { 197 | pub fn new(recv: Receiver, send: Sender, t: TCellOwner) -> Self { 198 | Self { 199 | recv, 200 | send, 201 | t, 202 | service: Service::default(), 203 | } 204 | } 205 | 206 | /// Starts an async event loop which will begin listening for instructions. 207 | pub async fn run(&mut self) -> Result<(), Error> { 208 | loop { 209 | let input = self.recv.recv().await.map_err(Error::ServerRequest)?; 210 | 211 | let output = self.service.handle(input, &mut self.t); 212 | 213 | self.send 214 | .send(output.collect()) 215 | .await 216 | .map_err(Error::ServerResponse)?; 217 | } 218 | } 219 | } 220 | 221 | /// Manages a thread running the pop-tiler service on it, and all communication to it. 222 | /// 223 | /// On drop of a value of this type, the background thread will be stopped. 224 | pub struct TilerThread { 225 | client: ClientThread, 226 | 227 | // On drop, a signal will be sent here to stop the background thread. 228 | drop_tx: async_oneshot::Sender<()>, 229 | } 230 | 231 | impl Default for TilerThread { 232 | fn default() -> Self { 233 | let (client_send, server_recv) = async_channel::unbounded(); 234 | let (server_send, client_recv) = async_channel::unbounded(); 235 | let (drop_tx, drop_rx) = async_oneshot::oneshot(); 236 | 237 | let client = ClientThread::new(client_send, client_recv); 238 | 239 | thread::spawn(move || { 240 | let t = TCellOwner::<()>::new(); 241 | 242 | // Tiling service as a future. 243 | let service = async move { 244 | if let Err(why) = ServiceThread::new(server_recv, server_send, t).run().await { 245 | eprintln!("pop-tiler service exited with error: {}", why); 246 | } 247 | }; 248 | 249 | // If the type is dropped, a message will be received that stops the service. 250 | let drop = async move { 251 | let _ = drop_rx.await; 252 | }; 253 | 254 | async_io::block_on(futures_lite::future::or(drop, service)); 255 | }); 256 | 257 | Self { client, drop_tx } 258 | } 259 | } 260 | 261 | impl TilerThread { 262 | /// Submits a request to the pop-tiling service managed by this type. 263 | pub async fn handle(&self, request: Request) -> Result { 264 | self.client.handle(request).await 265 | } 266 | } 267 | 268 | impl Drop for TilerThread { 269 | fn drop(&mut self) { 270 | let _ = self.drop_tx.send(()); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/branch.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::fork::ForkPtr; 5 | use crate::stack::StackPtr; 6 | use crate::window::WindowPtr; 7 | use crate::Rect; 8 | use crate::Tiler; 9 | use qcell::TCellOwner; 10 | use std::rc::Rc; 11 | 12 | pub(crate) enum Branch { 13 | Window(WindowPtr), 14 | Fork(ForkPtr), 15 | Stack(StackPtr), 16 | } 17 | impl Clone for Branch { 18 | fn clone(&self) -> Branch { 19 | match self { 20 | Branch::Window(w) => Branch::Window(w.clone()), 21 | Branch::Fork(f) => Branch::Fork(f.clone()), 22 | Branch::Stack(s) => Branch::Stack(s.clone()), 23 | } 24 | } 25 | } 26 | 27 | pub(crate) enum BranchRef<'a, T: 'static> { 28 | Window(&'a WindowPtr), 29 | Fork(&'a ForkPtr), 30 | Stack(&'a StackPtr), 31 | } 32 | impl<'a, T: 'static> Clone for BranchRef<'a, T> { 33 | fn clone(&self) -> BranchRef<'a, T> { 34 | match self { 35 | BranchRef::Window(w) => BranchRef::Window(w), 36 | BranchRef::Fork(f) => BranchRef::Fork(f), 37 | BranchRef::Stack(s) => BranchRef::Stack(s), 38 | } 39 | } 40 | } 41 | impl<'a, T: 'static> Copy for BranchRef<'a, T> {} 42 | 43 | impl Branch { 44 | pub fn work_area_update(&self, tiler: &mut Tiler, area: Rect, t: &mut TCellOwner) { 45 | match self { 46 | Branch::Fork(ptr) => ptr.work_area_update(tiler, area, t), 47 | Branch::Stack(ptr) => ptr.work_area_update(tiler, area, t), 48 | Branch::Window(ptr) => ptr.work_area_update(tiler, area, t), 49 | } 50 | } 51 | 52 | pub fn ref_eq<'a>(&self, other: BranchRef<'a, T>) -> bool { 53 | match (self, other) { 54 | (Branch::Window(a), BranchRef::Window(b)) => Rc::ptr_eq(a, b), 55 | (Branch::Fork(a), BranchRef::Fork(b)) => Rc::ptr_eq(a, b), 56 | (Branch::Stack(a), BranchRef::Stack(b)) => Rc::ptr_eq(a, b), 57 | _ => false, 58 | } 59 | } 60 | } 61 | 62 | impl PartialEq for Branch { 63 | fn eq(&self, other: &Self) -> bool { 64 | match (self, other) { 65 | (Branch::Window(a), Branch::Window(b)) => Rc::ptr_eq(a, b), 66 | (Branch::Fork(a), Branch::Fork(b)) => Rc::ptr_eq(a, b), 67 | (Branch::Stack(a), Branch::Stack(b)) => Rc::ptr_eq(a, b), 68 | _ => false, 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/display.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::workspace::WorkspacePtr; 5 | use crate::{Rect, Tiler}; 6 | use qcell::{TCell, TCellOwner}; 7 | use std::collections::HashMap; 8 | use std::fmt::{self, Debug}; 9 | use std::rc::Rc; 10 | 11 | /// A physical display, which has physical dimensions, and may have multiple workspaces associated with it. 12 | #[derive(Deref, DerefMut)] 13 | pub(crate) struct DisplayPtr(Rc>>); 14 | impl Clone for DisplayPtr { 15 | fn clone(&self) -> DisplayPtr { 16 | DisplayPtr(self.0.clone()) 17 | } 18 | } 19 | 20 | /// A physical display, which has physical dimensions, and may have multiple workspaces associated with it. 21 | #[derive(Default)] 22 | pub(crate) struct Display { 23 | pub area: Rect, 24 | pub active: Option, 25 | pub workspaces: HashMap>, 26 | } 27 | 28 | impl DisplayPtr { 29 | pub fn new(area: Rect) -> Self { 30 | Self(Rc::new(TCell::new(Display { 31 | area, 32 | active: None, 33 | workspaces: HashMap::new(), 34 | }))) 35 | } 36 | 37 | pub fn area(&self, t: &TCellOwner) -> Rect { 38 | self.ro(t).area 39 | } 40 | 41 | /// Assign a workspace to this display, removing the previous parent association of 42 | /// that workspace. 43 | pub fn assign_workspace(&self, workspace: WorkspacePtr, t: &mut TCellOwner) { 44 | // Assign workspace as a child of this display. 45 | { 46 | let id = workspace.ro(t).id; 47 | let this = self.rw(t); 48 | 49 | for ours in this.workspaces.values() { 50 | if Rc::ptr_eq(ours, &workspace) { 51 | return; 52 | } 53 | } 54 | 55 | this.workspaces.insert(id, workspace.clone()); 56 | } 57 | 58 | let previous_parent; 59 | 60 | // Define a new parent association for the workspace. 61 | { 62 | let workspace = workspace.rw(t); 63 | 64 | if Rc::ptr_eq(&workspace.parent, self) { 65 | return; 66 | } 67 | 68 | previous_parent = workspace.parent.clone(); 69 | workspace.parent = self.clone(); 70 | } 71 | 72 | // Remove the child association of the previous parent. 73 | previous_parent.remove_association(workspace, t); 74 | } 75 | 76 | /// Create a new workspace on this display. 77 | pub fn create_workspace(&self, id: u32, t: &mut TCellOwner) -> WorkspacePtr { 78 | // Create new workspace associated with this display. 79 | let workspace = WorkspacePtr::new(id, self.clone()); 80 | 81 | // Assign the workspace pointer to the display. 82 | let this = self.rw(t); 83 | this.workspaces.insert(id, workspace.clone()); 84 | 85 | // Set it as the active if one is not already set. 86 | if this.active.is_none() { 87 | this.active = Some(id); 88 | } 89 | 90 | workspace 91 | } 92 | 93 | pub fn remove_association(&self, workspace: WorkspacePtr, t: &mut TCellOwner) { 94 | let this = self.rw(t); 95 | 96 | if let Some(id) = this 97 | .workspaces 98 | .iter() 99 | .find(|(_, w)| Rc::ptr_eq(w, &workspace)) 100 | .map(|(id, _)| *id) 101 | { 102 | this.workspaces.remove(&id); 103 | } 104 | } 105 | 106 | /// Updates the work area of every workspace attached to this display. 107 | pub fn work_area_update(&self, tiler: &mut Tiler, area: Rect, t: &mut TCellOwner) { 108 | // Update the area of this display. 109 | self.rw(t).area = area; 110 | 111 | // Take ownership of this display's workspaces. 112 | let mut workspaces = HashMap::new(); 113 | std::mem::swap(&mut workspaces, &mut self.rw(t).workspaces); 114 | 115 | // Apply the update to all forks in each workspace. 116 | for workspace in workspaces.values() { 117 | if let Some(ref fork) = workspace.fork(t) { 118 | fork.work_area_update(tiler, area, t); 119 | } 120 | } 121 | 122 | // Give it back to the display. 123 | std::mem::swap(&mut workspaces, &mut self.rw(t).workspaces); 124 | } 125 | 126 | pub(crate) fn debug<'a>(&'a self, t: &'a TCellOwner) -> DisplayDebug<'a, T> { 127 | DisplayDebug::new(self, t) 128 | } 129 | } 130 | 131 | pub(crate) struct DisplayDebug<'a, T: 'static> { 132 | info: &'a DisplayPtr, 133 | t: &'a TCellOwner, 134 | } 135 | 136 | impl<'a, T> DisplayDebug<'a, T> { 137 | pub fn new(info: &'a DisplayPtr, t: &'a TCellOwner) -> Self { 138 | Self { info, t } 139 | } 140 | } 141 | 142 | impl<'a, T> Debug for DisplayDebug<'a, T> { 143 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 144 | let &Self { info, t } = self; 145 | let info = info.ro(t); 146 | 147 | let workspaces: Vec<_> = info 148 | .workspaces 149 | .iter() 150 | .map(|(_, w)| w.ro(t).debug(t)) 151 | .collect(); 152 | fmt.debug_struct("Display") 153 | .field("area", &info.area) 154 | .field("active", &info.active) 155 | .field("workspaces", &workspaces) 156 | .finish() 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::fork::ForkPtr; 5 | use crate::stack::{StackMovement, StackPtr}; 6 | use crate::window::WindowPtr; 7 | use crate::{Orientation, Rect, WindowID}; 8 | use qcell::TCellOwner; 9 | use std::collections::{BTreeMap, HashMap}; 10 | use std::rc::Rc; 11 | 12 | /// Instructs where to place a tiling component entity. 13 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 14 | #[derive(Debug)] 15 | pub struct Placement { 16 | pub area: Rect, 17 | pub workspace: u32, 18 | } 19 | 20 | /// An event for the window manager to act upon. 21 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 22 | #[derive(Debug)] 23 | pub enum Event { 24 | /// Focus this window. 25 | Focus(WindowID), 26 | 27 | /// Focus this workspace ID. 28 | FocusWorkspace(u32), 29 | 30 | /// Where to place a resize handle, in what orientation, and with what range limits. 31 | Fork(usize, ForkUpdate), 32 | 33 | // Destroy the fork associated with this ID. 34 | ForkDestroy(usize), 35 | 36 | /// A window was assigned to a stack 37 | StackAssign(usize, WindowID), 38 | 39 | /// A window was detached from a stack 40 | StackDetach(usize, WindowID), 41 | 42 | /// Destroy the stack associated with this ID. 43 | StackDestroy(usize), 44 | 45 | /// Alter the dimensions of an existing stack. 46 | StackPlace(usize, Placement), 47 | 48 | /// Raise this window of a stack to the top. 49 | /// Other windows in this stack should be hidden. 50 | StackRaise(usize, WindowID), 51 | 52 | /// Swap the position of these windows in a stack 53 | StackMovement(usize, StackMovement), 54 | 55 | // Change the visibility of a stack. 56 | StackVisibility(usize, bool), 57 | 58 | /// Alter the dimensions of a window actor. 59 | WindowPlace(WindowID, Placement), 60 | 61 | /// Change the visibility of a window. 62 | WindowVisibility(WindowID, bool), 63 | 64 | // Assign workspace to diplsay 65 | WorkspaceAssign { 66 | workspace: u32, 67 | display: u32, 68 | }, 69 | } 70 | 71 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 72 | #[derive(Debug)] 73 | pub struct ForkUpdate { 74 | /// On what workspace the fork resides. 75 | pub workspace: u32, 76 | /// The orientation of this fork. 77 | pub orientation: Orientation, 78 | /// The region this fork occupies. 79 | pub rect: Rect, 80 | /// Where to place the resize handle in that region. 81 | pub handle: u32, 82 | } 83 | 84 | #[derive(Default)] 85 | pub struct ForkEvents { 86 | pub destroy: bool, 87 | pub update: Option, 88 | } 89 | 90 | #[derive(Default)] 91 | pub struct WindowEvents { 92 | pub place: Option, 93 | pub visibility: Option, 94 | } 95 | 96 | #[derive(Default)] 97 | pub struct StackEvents { 98 | pub destroy: bool, 99 | pub assignments: BTreeMap, 100 | pub place: Option, 101 | pub visibility: Option, 102 | pub raise: Option, 103 | } 104 | 105 | pub(crate) struct EventQueue { 106 | pub(crate) forks: BTreeMap, 107 | pub(crate) windows: HashMap, 108 | pub(crate) stacks: BTreeMap, 109 | pub(crate) events: Vec, 110 | _marker: std::marker::PhantomData, 111 | } 112 | 113 | impl Default for EventQueue { 114 | fn default() -> EventQueue { 115 | EventQueue { 116 | forks: BTreeMap::new(), 117 | windows: HashMap::new(), 118 | stacks: BTreeMap::new(), 119 | events: Vec::new(), 120 | _marker: std::marker::PhantomData, 121 | } 122 | } 123 | } 124 | 125 | impl EventQueue { 126 | pub fn consume_events(&mut self) -> impl Iterator + '_ { 127 | let fork_events = { 128 | let mut forks = BTreeMap::new(); 129 | std::mem::swap(&mut self.forks, &mut forks); 130 | 131 | forks.into_iter().filter_map(|(id, event)| { 132 | if event.destroy { 133 | Some(Event::ForkDestroy(id)) 134 | } else { 135 | event.update.map(|update| Event::Fork(id, update)) 136 | } 137 | }) 138 | }; 139 | 140 | let stack_events = { 141 | let mut stacks = BTreeMap::new(); 142 | std::mem::swap(&mut self.stacks, &mut stacks); 143 | 144 | stacks.into_iter().flat_map(|(a, events)| { 145 | let attachments = events.assignments.into_iter().map(move |(id, attached)| { 146 | if attached { 147 | Event::StackAssign(a, id) 148 | } else { 149 | Event::StackDetach(a, id) 150 | } 151 | }); 152 | 153 | let iterator: Box> = if events.destroy { 154 | Box::new(std::iter::once(Event::StackDestroy(a))) 155 | } else { 156 | let placement = events 157 | .place 158 | .into_iter() 159 | .map(move |p| Event::StackPlace(a, p)); 160 | 161 | let visibility = events 162 | .visibility 163 | .into_iter() 164 | .map(move |v| Event::StackVisibility(a, v)); 165 | 166 | Box::new(placement.chain(visibility)) 167 | }; 168 | 169 | iterator.chain(attachments) 170 | }) 171 | }; 172 | 173 | let window_events = { 174 | let mut windows = HashMap::new(); 175 | std::mem::swap(&mut self.windows, &mut windows); 176 | 177 | windows.into_iter().flat_map(|(a, events)| { 178 | let placement = events 179 | .place 180 | .into_iter() 181 | .map(move |p| Event::WindowPlace(a, p)); 182 | 183 | let visibility = events 184 | .visibility 185 | .into_iter() 186 | .map(move |v| Event::WindowVisibility(a, v)); 187 | 188 | placement.chain(visibility) 189 | }) 190 | }; 191 | 192 | fork_events 193 | .chain(stack_events) 194 | .chain(window_events) 195 | .chain(self.events.drain(..)) 196 | } 197 | 198 | /// Instruct the window manager that this fork was destroyed. 199 | pub fn fork_destroy(&mut self, fork: &ForkPtr) { 200 | tracing::debug!("destroying Fork({:?})", Rc::as_ptr(fork)); 201 | self.forks 202 | .entry(Rc::as_ptr(fork) as usize) 203 | .or_default() 204 | .destroy = true; 205 | } 206 | 207 | /// Instruct the window manager about this fork's dimensions and split handle. 208 | pub fn fork_update(&mut self, fork: &ForkPtr, t: &TCellOwner) { 209 | self.forks 210 | .entry(Rc::as_ptr(fork) as usize) 211 | .or_default() 212 | .update = Some({ 213 | let fork = fork.ro(t); 214 | ForkUpdate { 215 | workspace: fork.workspace, 216 | orientation: fork.orientation, 217 | rect: fork.area, 218 | handle: fork.split_handle, 219 | } 220 | }); 221 | } 222 | 223 | /// Instruct the window manager that a window was assigned to a stack. 224 | pub fn stack_assign(&mut self, stack: &StackPtr, window: &WindowPtr, t: &TCellOwner) { 225 | *self 226 | .stacks 227 | .entry(Rc::as_ptr(stack) as usize) 228 | .or_default() 229 | .assignments 230 | .entry(window.id(t)) 231 | .or_default() = true; 232 | } 233 | 234 | /// Instruct the window manager that a window was detached from a stack. 235 | pub fn stack_detach(&mut self, stack: &StackPtr, window: &WindowPtr, t: &TCellOwner) { 236 | *self 237 | .stacks 238 | .entry(Rc::as_ptr(stack) as usize) 239 | .or_default() 240 | .assignments 241 | .entry(window.id(t)) 242 | .or_default() = false; 243 | } 244 | 245 | /// Instruct the window manager that this stack was destroyed. 246 | pub fn stack_destroy(&mut self, stack: &StackPtr) { 247 | self.stacks 248 | .entry(Rc::as_ptr(stack) as usize) 249 | .or_default() 250 | .destroy = true; 251 | } 252 | 253 | /// Instruct the window manager to ensure that this window should be the visible one in the stack. 254 | pub fn stack_raise_window( 255 | &mut self, 256 | stack: &StackPtr, 257 | window: &WindowPtr, 258 | t: &TCellOwner, 259 | ) { 260 | self.stacks 261 | .entry(Rc::as_ptr(stack) as usize) 262 | .or_default() 263 | .raise = Some(window.id(t)) 264 | } 265 | 266 | pub fn stack_movement(&mut self, stack: &StackPtr, movement: StackMovement) { 267 | self.events 268 | .push(Event::StackMovement(Rc::as_ptr(stack) as usize, movement)); 269 | } 270 | 271 | /// Instruct the window manager about a placement of a stack. 272 | pub fn stack_update(&mut self, stack: &StackPtr, t: &TCellOwner) { 273 | let stack_ = stack.ro(t); 274 | self.stacks 275 | .entry(Rc::as_ptr(stack) as usize) 276 | .or_default() 277 | .place = Some(Placement { 278 | area: stack_.area, 279 | workspace: stack_.workspace, 280 | }); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/exceptions.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pop-os/tiler/f1e618a5d0ef676440433c317f1fc98e80736376/src/exceptions.rs -------------------------------------------------------------------------------- /src/fork.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | pub(crate) use debug::ForkDebug; 5 | 6 | use super::branch::{Branch, BranchRef}; 7 | use super::window::WindowPtr; 8 | use crate::{Rect, Tiler}; 9 | use either::Either; 10 | use qcell::{TCell, TCellOwner}; 11 | use std::rc::Rc; 12 | 13 | /// The orientation of a fork. 14 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 15 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 16 | pub enum Orientation { 17 | Horizontal, 18 | Vertical, 19 | } 20 | 21 | #[derive(Deref, DerefMut, From)] 22 | pub(crate) struct ForkPtr(Rc>>); 23 | impl Clone for ForkPtr { 24 | fn clone(&self) -> ForkPtr { 25 | ForkPtr(self.0.clone()) 26 | } 27 | } 28 | 29 | impl ForkPtr { 30 | pub fn new(fork: Fork) -> Self { 31 | Self(Rc::new(TCell::new(fork))) 32 | } 33 | 34 | /// Locates the largest window in the fork, walking all of its branches. 35 | pub fn largest_window(&self, t: &TCellOwner) -> Option> { 36 | let mut largest_area = 0; 37 | let mut largest_window = None; 38 | 39 | let mut compare_window = |window: &WindowPtr| { 40 | let area = window.ro(t).rect.area(); 41 | 42 | if area > largest_area { 43 | largest_area = area; 44 | largest_window = Some(window.clone()); 45 | } 46 | }; 47 | 48 | for window in self.windows(t) { 49 | compare_window(&window); 50 | } 51 | 52 | largest_window 53 | } 54 | 55 | /// Change the orientation of the fork, if it differs. 56 | pub fn orientation_set( 57 | &self, 58 | tiler: &mut Tiler, 59 | orientation: Orientation, 60 | t: &mut TCellOwner, 61 | ) { 62 | if self.ro(t).orientation == orientation { 63 | return; 64 | } 65 | 66 | self.toggle_orientation(tiler, t); 67 | } 68 | 69 | /// Resets the orientation and split handle of this fork. 70 | pub fn reset_orientation(&self, tiler: &mut Tiler, t: &mut TCellOwner) { 71 | let this = self.rw(t); 72 | 73 | this.split_handle = match this.orientation { 74 | Orientation::Horizontal => this.area.width / 2, 75 | Orientation::Vertical => this.area.height / 2, 76 | }; 77 | 78 | let preferred = preferred_orientation(this.area); 79 | 80 | if this.orientation != preferred { 81 | self.toggle_orientation(tiler, t) 82 | } 83 | } 84 | 85 | /// Resize a fork with a new split 86 | pub fn resize(&self, tiler: &mut Tiler, split: u32, t: &mut TCellOwner) { 87 | let this = self.rw(t); 88 | 89 | let area = this.area; 90 | this.split_handle = match this.orientation { 91 | Orientation::Horizontal => split.min(area.width), 92 | Orientation::Vertical => split.min(area.height), 93 | }; 94 | 95 | self.work_area_refresh(tiler, t); 96 | } 97 | 98 | /// Toggle the orientation of the fork 99 | pub fn toggle_orientation(&self, tiler: &mut Tiler, t: &mut TCellOwner) { 100 | let this = self.rw(t); 101 | 102 | this.split_handle = match this.orientation { 103 | Orientation::Horizontal => { 104 | this.orientation = Orientation::Vertical; 105 | let ratio = (this.split_handle * 100) / this.area.width; 106 | (this.area.height * ratio) / 100 107 | } 108 | 109 | Orientation::Vertical => { 110 | this.orientation = Orientation::Horizontal; 111 | let ratio = (this.split_handle * 100) / this.area.height; 112 | (this.area.width * ratio) / 100 113 | } 114 | }; 115 | 116 | // Swap branches if a fork has had its orientation toggled twice. 117 | if this.orientation_toggled { 118 | if let Some(right) = this.right.as_mut() { 119 | std::mem::swap(&mut this.left, right); 120 | } 121 | } 122 | 123 | this.orientation_toggled = !this.orientation_toggled; 124 | 125 | self.work_area_refresh(tiler, t); 126 | } 127 | 128 | /// Swaps a window owned by this fork with a different window. 129 | pub fn swap(&self, our: &WindowPtr, their: &WindowPtr, t: &mut TCellOwner) { 130 | let this = self.rw(t); 131 | if let Branch::Window(ref mut window) = this.left { 132 | if Rc::ptr_eq(window, our) { 133 | this.left = Branch::Window(their.clone()); 134 | return; 135 | } 136 | } 137 | 138 | if let Some(Branch::Window(ref mut window)) = this.right { 139 | if Rc::ptr_eq(window, our) { 140 | this.right = Some(Branch::Window(their.clone())); 141 | } 142 | } 143 | } 144 | 145 | /// Generator which locates all windows in this fork, but does allocate. 146 | pub fn windows<'a>(&self, t: &'a TCellOwner) -> impl Iterator> + 'a { 147 | let mut forks: Vec> = vec![self.clone()]; 148 | let mut branches: Vec> = Vec::new(); 149 | let mut windows: Vec> = Vec::new(); 150 | 151 | std::iter::from_fn(move || { 152 | loop { 153 | if let Some(window) = windows.pop() { 154 | return Some(window); 155 | } 156 | 157 | if let Some(branch) = branches.pop() { 158 | match branch { 159 | Branch::Window(window) => { 160 | return Some(window); 161 | } 162 | 163 | Branch::Fork(fork) => { 164 | forks.push(fork.clone()); 165 | } 166 | 167 | Branch::Stack(stack) => { 168 | windows.extend_from_slice(&stack.ro(t).windows); 169 | return windows.pop(); 170 | } 171 | } 172 | } 173 | 174 | if let Some(fork) = forks.pop() { 175 | branches.push(fork.ro(t).left.clone()); 176 | 177 | if let Some(right) = fork.ro(t).right.clone() { 178 | branches.push(right); 179 | } 180 | 181 | continue; 182 | } 183 | 184 | break; 185 | } 186 | 187 | None 188 | }) 189 | } 190 | 191 | /// Recalculate the work areas of the fork's branches. 192 | pub fn work_area_refresh(&self, tiler: &mut Tiler, t: &mut TCellOwner) { 193 | self.work_area_update(tiler, self.ro(t).area, t) 194 | } 195 | 196 | /// Update the work area of the fork and its branches. 197 | #[allow(clippy::many_single_char_names)] 198 | pub fn work_area_update(&self, tiler: &mut Tiler, area: Rect, t: &mut TCellOwner) { 199 | tracing::debug!("assigning fork to {:?}", area); 200 | let mut left_rect = area; 201 | let left_branch: Branch; 202 | let mut right_branch: Option<(Branch, Rect)> = None; 203 | 204 | { 205 | let this = self.rw(t); 206 | 207 | // Update the location of the split in the fork 208 | this.split_handle = match this.orientation { 209 | Orientation::Horizontal => { 210 | let ratio = this.split_handle * 100 / this.area.width; 211 | area.width * ratio / 100 212 | } 213 | 214 | Orientation::Vertical => { 215 | let ratio = this.split_handle * 100 / this.area.height; 216 | area.height * ratio / 100 217 | } 218 | }; 219 | 220 | left_branch = this.left.clone(); 221 | 222 | if let Some(right) = this.right.clone() { 223 | let x = area.x; 224 | let y = area.y; 225 | let w = area.width; 226 | let h = area.height; 227 | let r = this.split_handle; 228 | 229 | match this.orientation { 230 | Orientation::Vertical => { 231 | left_rect = Rect::new(x, y, w, r); 232 | right_branch = Some((right, Rect::new(x, y + r, w, h - r))); 233 | } 234 | 235 | Orientation::Horizontal => { 236 | left_rect = Rect::new(x, y, r, h); 237 | right_branch = Some((right, Rect::new(x + r, y, w - r, h))); 238 | } 239 | } 240 | } 241 | 242 | this.area = area; 243 | }; 244 | 245 | // tracing::debug!("left branch = {:?}; right branch = {:?}", left_rect, right_branch.as_ref().map(|x| x.1)); 246 | 247 | left_branch.work_area_update(tiler, left_rect, t); 248 | 249 | if let Some((branch, rect)) = right_branch { 250 | branch.work_area_update(tiler, rect, t); 251 | } 252 | 253 | tiler.event_queue.fork_update(self, t); 254 | } 255 | 256 | pub fn debug<'a>(&'a self, t: &'a TCellOwner) -> ForkDebug<'a, T> { 257 | ForkDebug::new(self, t) 258 | } 259 | } 260 | 261 | /// Splits a tile into two branching paths. 262 | /// 263 | /// A branch may contain a window, a stack, or another fork. The dimensions of a fork are 264 | /// split between the two branches vertically or horizontally. 265 | pub(crate) struct Fork { 266 | /// The position and dimensions of this fork and its children. 267 | pub area: Rect, 268 | 269 | /// Pointer to the parent of this fork. 270 | pub parent: Option>, 271 | 272 | /// The left branch, which is only permitted to be a Fork if the right branch is also allocated. 273 | pub left: Branch, 274 | 275 | /// Right branch, which may be empty. 276 | pub right: Option>, 277 | 278 | /// The ID of the workspace that the fork is attached to. 279 | pub workspace: u32, 280 | 281 | /// How branches in this fork are aligned. 282 | pub orientation: Orientation, 283 | 284 | /// Location of the split in this fork. 285 | pub split_handle: u32, 286 | 287 | /// Tracks when we should flip branches. 288 | pub orientation_toggled: bool, 289 | } 290 | 291 | impl Fork { 292 | pub fn new(area: Rect, left: Branch, workspace: u32) -> Self { 293 | let orientation = preferred_orientation(area); 294 | 295 | let split_handle = match orientation { 296 | Orientation::Horizontal => area.x_center() - 1, 297 | Orientation::Vertical => area.y_center() - 1, 298 | }; 299 | 300 | Self { 301 | area, 302 | left, 303 | right: None, 304 | workspace, 305 | orientation, 306 | parent: None, 307 | split_handle, 308 | orientation_toggled: false, 309 | } 310 | } 311 | 312 | pub fn branch( 313 | &mut self, 314 | branch: BranchRef<'_, T>, 315 | ) -> Option, &mut Branch>> { 316 | if self.left_is(branch) { 317 | return Some(Either::Left(&mut self.left)); 318 | } 319 | 320 | if let Some(right) = &mut self.right { 321 | if right.ref_eq(branch) { 322 | return Some(Either::Right(right)); 323 | } 324 | } 325 | 326 | None 327 | } 328 | 329 | pub fn left_is(&self, branch: BranchRef<'_, T>) -> bool { 330 | self.left.ref_eq(branch) 331 | } 332 | 333 | pub fn right_is(&self, branch: BranchRef<'_, T>) -> bool { 334 | self.right.as_ref().map_or(false, |r| r.ref_eq(branch)) 335 | } 336 | } 337 | 338 | impl Drop for Fork { 339 | fn drop(&mut self) { 340 | tracing::debug!("dropped fork"); 341 | } 342 | } 343 | 344 | fn preferred_orientation(rect: Rect) -> Orientation { 345 | if rect.height > rect.width { 346 | Orientation::Vertical 347 | } else { 348 | Orientation::Horizontal 349 | } 350 | } 351 | 352 | mod debug { 353 | use super::{Branch, ForkPtr}; 354 | use qcell::TCellOwner; 355 | use std::fmt::{self, Debug}; 356 | use std::rc::Rc; 357 | 358 | pub(crate) struct ForkDebug<'a, T: 'static> { 359 | pub fork: &'a ForkPtr, 360 | pub t: &'a TCellOwner, 361 | } 362 | 363 | impl<'a, T> ForkDebug<'a, T> { 364 | pub fn new(fork: &'a ForkPtr, t: &'a TCellOwner) -> Self { 365 | Self { fork, t } 366 | } 367 | } 368 | 369 | impl<'a, T> Debug for ForkDebug<'a, T> { 370 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 371 | fn as_debug<'a, T>(branch: &'a Branch, t: &'a TCellOwner) -> Box { 372 | match branch { 373 | Branch::Window(window) => Box::new(window.id(t)), 374 | Branch::Stack(stack) => Box::new(stack.ro(t).debug(t)), 375 | Branch::Fork(fork) => Box::new(fork.debug(t)), 376 | } 377 | } 378 | 379 | let fork = self.fork.ro(self.t); 380 | 381 | let left = self.fork.ro(self.t).parent.as_ref().map(|p| Rc::as_ptr(&p)); 382 | 383 | let right = fork.right.as_ref().map(|branch| as_debug(branch, self.t)); 384 | 385 | fmt.debug_struct("Fork") 386 | .field("ptr", &Rc::as_ptr(self.fork)) 387 | .field("parent", &left) 388 | .field("orientation", &fork.orientation) 389 | .field("left", &as_debug(&fork.left, self.t)) 390 | .field("right", &right) 391 | .finish() 392 | } 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/geom.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 5 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] 6 | pub struct Point { 7 | x: u32, 8 | y: u32, 9 | } 10 | 11 | impl Point { 12 | pub fn distance(self, other: Point) -> f64 { 13 | (((other.x - self.x).pow(2) + (other.y - self.y).pow(2)) as f64).sqrt() 14 | } 15 | 16 | pub fn distance_from_rect(&self, rect: &Rect) -> f64 { 17 | self.distance(rect.north()) 18 | .min(self.distance(rect.south())) 19 | .min(self.distance(rect.east())) 20 | .min(self.distance(rect.west())) 21 | } 22 | } 23 | 24 | /// The positioning and dimensions of a rectangular object. 25 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 26 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] 27 | pub struct Rect { 28 | pub x: u32, 29 | pub y: u32, 30 | pub width: u32, 31 | pub height: u32, 32 | } 33 | 34 | impl Rect { 35 | pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self { 36 | Self { 37 | x, 38 | y, 39 | width, 40 | height, 41 | } 42 | } 43 | 44 | pub fn area(&self) -> u32 { 45 | self.width * self.height 46 | } 47 | 48 | pub fn distance_downward(&self, other: &Rect) -> f64 { 49 | self.south().distance(other.north()) 50 | } 51 | 52 | pub fn distance_eastward(&self, other: &Rect) -> f64 { 53 | self.west().distance(other.east()) 54 | } 55 | 56 | pub fn distance_upward(&self, other: &Rect) -> f64 { 57 | self.north().distance(other.south()) 58 | } 59 | 60 | pub fn distance_westward(&self, other: &Rect) -> f64 { 61 | self.east().distance(other.west()) 62 | } 63 | 64 | pub fn is_below(&self, other: &Rect) -> bool { 65 | self.y < other.y 66 | } 67 | 68 | pub fn is_above(&self, other: &Rect) -> bool { 69 | self.y > other.y 70 | } 71 | 72 | pub fn is_left(&self, other: &Rect) -> bool { 73 | self.x > other.x 74 | } 75 | 76 | pub fn is_right(&self, other: &Rect) -> bool { 77 | self.x < other.x 78 | } 79 | 80 | pub fn east(&self) -> Point { 81 | Point { 82 | x: self.x_end(), 83 | y: self.y_center(), 84 | } 85 | } 86 | 87 | pub fn north(&self) -> Point { 88 | Point { 89 | x: self.x_center(), 90 | y: self.y, 91 | } 92 | } 93 | 94 | pub fn south(&self) -> Point { 95 | Point { 96 | x: self.x_center(), 97 | y: self.y_end(), 98 | } 99 | } 100 | 101 | pub fn west(&self) -> Point { 102 | Point { 103 | x: self.x, 104 | y: self.y_center(), 105 | } 106 | } 107 | 108 | pub fn x_center(&self) -> u32 { 109 | self.x + self.width / 2 110 | } 111 | 112 | pub fn x_end(&self) -> u32 { 113 | self.x + self.width 114 | } 115 | 116 | pub fn y_center(&self) -> u32 { 117 | self.y + self.height / 2 118 | } 119 | 120 | pub fn y_end(&self) -> u32 { 121 | self.y + self.height 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | #[macro_use] 5 | extern crate derive_more; 6 | 7 | #[cfg(feature = "ipc")] 8 | #[macro_use] 9 | extern crate serde; 10 | 11 | mod branch; 12 | mod display; 13 | mod events; 14 | mod fork; 15 | mod geom; 16 | mod stack; 17 | mod tiler; 18 | mod window; 19 | mod workspace; 20 | 21 | pub use self::events::{Event, ForkUpdate, Placement}; 22 | pub use self::fork::Orientation; 23 | pub use self::geom::{Point, Rect}; 24 | pub use self::stack::StackMovement; 25 | pub use self::tiler::Tiler; 26 | pub use self::window::{WindowID, WindowPtr}; 27 | 28 | pub use qcell::TCellOwner; 29 | -------------------------------------------------------------------------------- /src/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | mod branch; 5 | mod fork; 6 | mod stack; 7 | mod window; 8 | 9 | pub use self::fork::Orientation; 10 | pub use self::window::{WindowID, WindowPtr}; 11 | 12 | pub(crate) use self::branch::{Branch, BranchRef}; 13 | pub(crate) use self::fork::{Fork, ForkPtr}; 14 | pub(crate) use self::stack::StackPtr; 15 | pub(crate) use self::window::Window; 16 | -------------------------------------------------------------------------------- /src/stack.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::branch::BranchRef; 5 | use crate::fork::ForkPtr; 6 | use crate::tiler::Tiler; 7 | use crate::window::{WindowID, WindowPtr}; 8 | use crate::Rect; 9 | use qcell::{TCell, TCellOwner}; 10 | use std::fmt::{self, Debug}; 11 | use std::rc::Rc; 12 | 13 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 14 | #[derive(Copy, Clone, Debug)] 15 | pub enum StackMovement { 16 | Left(WindowID), 17 | Right(WindowID), 18 | } 19 | 20 | #[derive(Deref, DerefMut)] 21 | pub(crate) struct StackPtr(Rc>>); 22 | impl Clone for StackPtr { 23 | fn clone(&self) -> StackPtr { 24 | StackPtr(self.0.clone()) 25 | } 26 | } 27 | 28 | impl StackPtr { 29 | pub fn new(window: &WindowPtr, parent: ForkPtr, t: &mut TCellOwner) -> Self { 30 | let workspace = window.ro(t).workspace; 31 | let ptr = StackPtr(Rc::new(TCell::new(Stack { 32 | area: window.ro(t).rect, 33 | active: window.clone(), 34 | parent, 35 | windows: vec![window.clone()], 36 | workspace, 37 | }))); 38 | 39 | window.rw(t).stack = Some(ptr.clone()); 40 | 41 | ptr 42 | } 43 | 44 | pub fn attach(&self, window: &WindowPtr, t: &mut TCellOwner) { 45 | window.rw(t).stack = Some(self.clone()); 46 | self.rw(t).windows.push(window.clone()); 47 | } 48 | 49 | pub fn detach(&self, tiler: &mut Tiler, window: &WindowPtr, t: &mut TCellOwner) { 50 | window.rw(t).stack = None; 51 | tiler.event_queue.stack_detach(self, window, t); 52 | 53 | let this = self.rw(t); 54 | 55 | if let Some(pos) = this.windows.iter().position(|w| Rc::ptr_eq(w, window)) { 56 | this.windows.remove(pos); 57 | 58 | // Set the focus window if the detached window was the active window. 59 | if Rc::ptr_eq(window, &this.active) { 60 | if let Some(to_focus) = this 61 | .windows 62 | .get(pos) 63 | .or_else(|| this.windows.get(pos - 1)) 64 | .cloned() 65 | { 66 | this.active = to_focus.clone(); 67 | tiler.event_queue.stack_raise_window(self, &to_focus, t); 68 | } 69 | } 70 | 71 | let this = self.rw(t); 72 | 73 | if this.windows.is_empty() { 74 | let parent = self.ro(t).parent.clone(); 75 | tiler.detach_branch(parent, BranchRef::Stack(self), t); 76 | tiler.event_queue.stack_destroy(self); 77 | } 78 | } 79 | } 80 | 81 | pub fn move_left(&self, t: &mut TCellOwner) -> Option { 82 | let this = self.rw(t); 83 | 84 | if let Some(pos) = this.active_window_position() { 85 | if pos != 0 { 86 | this.windows.swap(pos, pos - 1); 87 | return Some(StackMovement::Left(self.ro(t).active.id(t))); 88 | } 89 | } 90 | 91 | None 92 | } 93 | 94 | pub fn move_right(&self, t: &mut TCellOwner) -> Option { 95 | let this = self.rw(t); 96 | 97 | if let Some(pos) = this.active_window_position() { 98 | if pos != this.windows.len() - 1 { 99 | this.windows.swap(pos, pos + 1); 100 | return Some(StackMovement::Right(self.ro(t).active.id(t))); 101 | } 102 | } 103 | 104 | None 105 | } 106 | 107 | pub fn select_left(&self, t: &mut TCellOwner) -> Option> { 108 | let this = self.ro(t); 109 | 110 | let mut prev = None; 111 | 112 | for window in this.windows.iter() { 113 | if Rc::ptr_eq(&this.active, window) { 114 | return prev; 115 | } 116 | 117 | prev = Some(window.clone()); 118 | } 119 | 120 | None 121 | } 122 | 123 | pub fn select_right(&self, t: &mut TCellOwner) -> Option> { 124 | let this = self.ro(t); 125 | 126 | let mut windows = this.windows.iter(); 127 | 128 | while let Some(window) = windows.next() { 129 | if Rc::ptr_eq(window, &this.active) { 130 | return windows.next().cloned(); 131 | } 132 | } 133 | 134 | None 135 | } 136 | 137 | pub fn swap(&self, our: &WindowPtr, their: &WindowPtr, t: &mut TCellOwner) { 138 | for window in self.rw(t).windows.iter_mut() { 139 | if Rc::ptr_eq(window, our) { 140 | std::mem::swap(window, &mut their.clone()); 141 | } 142 | } 143 | } 144 | 145 | pub fn work_area_refresh(&self, tiler: &mut Tiler, t: &mut TCellOwner) { 146 | self.work_area_update(tiler, self.ro(t).area, t); 147 | } 148 | 149 | pub fn work_area_update(&self, tiler: &mut Tiler, area: Rect, t: &mut TCellOwner) { 150 | self.rw(t).area = area; 151 | for window in self.ro(t).windows.clone() { 152 | window.work_area_update(tiler, area, t); 153 | } 154 | 155 | tiler.event_queue.stack_update(self, t); 156 | } 157 | } 158 | 159 | pub(crate) struct Stack { 160 | pub area: Rect, 161 | pub active: WindowPtr, 162 | pub parent: ForkPtr, 163 | pub windows: Vec>, 164 | pub workspace: u32, 165 | } 166 | 167 | impl Stack { 168 | fn active_window_position(&self) -> Option { 169 | self.windows 170 | .iter() 171 | .position(|win| Rc::ptr_eq(&self.active, win)) 172 | } 173 | 174 | pub fn debug<'a>(&'a self, t: &'a TCellOwner) -> StackDebug<'a, T> { 175 | StackDebug::new(self, t) 176 | } 177 | } 178 | 179 | impl Drop for Stack { 180 | fn drop(&mut self) { 181 | tracing::debug!("Dropped stack"); 182 | } 183 | } 184 | 185 | pub struct StackDebug<'a, T: 'static> { 186 | stack: &'a Stack, 187 | t: &'a TCellOwner, 188 | } 189 | 190 | impl<'a, T: 'static> StackDebug<'a, T> { 191 | fn new(stack: &'a Stack, t: &'a TCellOwner) -> Self { 192 | Self { stack, t } 193 | } 194 | } 195 | 196 | impl<'a, T> Debug for StackDebug<'a, T> { 197 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 198 | let windows: Vec<_> = self 199 | .stack 200 | .windows 201 | .iter() 202 | .map(|window| window.id(self.t)) 203 | .collect(); 204 | 205 | fmt.debug_struct("Stack") 206 | .field("windows", &windows) 207 | .finish() 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/tiler.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::branch::{Branch, BranchRef}; 5 | use crate::display::DisplayPtr; 6 | use crate::events::EventQueue; 7 | use crate::fork::{Fork, ForkPtr, Orientation}; 8 | use crate::stack::{StackMovement, StackPtr}; 9 | use crate::window::{Window, WindowID, WindowPtr}; 10 | use crate::workspace::WorkspacePtr; 11 | use crate::{Event, Rect}; 12 | use either::Either; 13 | use qcell::{TCell, TCellOwner}; 14 | use std::collections::{BTreeMap, HashMap}; 15 | use std::fmt::{self, Debug}; 16 | use std::rc::Rc; 17 | 18 | type DistanceFn = fn(&Rect, &Rect) -> f64; 19 | type DirectionalConditionFn = fn(&Rect, &Rect) -> bool; 20 | 21 | pub enum Direction { 22 | Above, 23 | Below, 24 | Left, 25 | Right, 26 | } 27 | 28 | /// A tiling window manager 29 | pub struct Tiler { 30 | pub(crate) event_queue: EventQueue, 31 | active_changed: bool, 32 | active: Option>, 33 | active_workspace: u32, 34 | active_workspace_changed: bool, 35 | 36 | pub windows: BTreeMap>, 37 | forks: BTreeMap>, 38 | displays: BTreeMap>, 39 | workspaces: BTreeMap>, 40 | } 41 | 42 | impl Default for Tiler { 43 | fn default() -> Self { 44 | Self { 45 | event_queue: EventQueue::default(), 46 | active_changed: false, 47 | active: None, 48 | active_workspace: 0, 49 | active_workspace_changed: false, 50 | forks: BTreeMap::new(), 51 | windows: BTreeMap::new(), 52 | displays: BTreeMap::new(), 53 | workspaces: BTreeMap::new(), 54 | } 55 | } 56 | } 57 | 58 | impl Tiler { 59 | pub fn active_window(&self) -> Option<&WindowPtr> { 60 | self.active.as_ref() 61 | } 62 | 63 | /// Attach a window to the focused window in the tiler, and associate it with the tiler. 64 | pub fn attach(&mut self, window: &WindowPtr, t: &mut TCellOwner) { 65 | // Attach the window to the tiler in case it was not. 66 | self.windows.insert(window.id(t), window.clone()); 67 | 68 | if let Some(focus) = self.active_window().cloned() { 69 | tracing::debug!("attaching to focus window"); 70 | self.attach_to_window(window, &focus, t); 71 | return; 72 | } 73 | 74 | tracing::debug!("no active window: attaching to display instead"); 75 | 76 | self.set_active_window(window, t); 77 | 78 | let workspace = self 79 | .workspaces 80 | .get(&self.active_workspace) 81 | .expect("no workspace found to attach to") 82 | .clone(); 83 | 84 | self.attach_to_workspace(window, &workspace, t); 85 | } 86 | 87 | /// Attach a window to an existing window 88 | fn attach_to_window( 89 | &mut self, 90 | new_window: &WindowPtr, 91 | attach_to: &WindowPtr, 92 | t: &mut TCellOwner, 93 | ) { 94 | // If window is attached to stack, then attach new window to the same stack 95 | if let Some((stack, fork)) = attach_to.stack(t).zip(attach_to.fork(t)) { 96 | new_window.fork_set(fork.clone(), t); 97 | stack.attach(new_window, t); 98 | stack.work_area_update(self, stack.ro(t).area, t); 99 | return; 100 | } 101 | 102 | // If window is attached to a fork, attach to the same fork 103 | if let Some(fork) = attach_to.fork(t) { 104 | self.attach_to_window_in_fork(new_window, attach_to, &fork, t); 105 | return; 106 | } 107 | 108 | tracing::error!("attempted attach to window that's not attached to anything"); 109 | } 110 | 111 | /// Attach the `window` to the `attaching` window in the `fork`. 112 | fn attach_to_window_in_fork( 113 | &mut self, 114 | window: &WindowPtr, 115 | attaching: &WindowPtr, 116 | fork: &ForkPtr, 117 | t: &mut TCellOwner, 118 | ) { 119 | let workspace: u32; 120 | 121 | // If the right branch is empty, assign our new window to it. 122 | { 123 | let fork_ = fork.rw(t); 124 | 125 | workspace = fork_.workspace; 126 | 127 | if fork_.right.is_none() { 128 | fork_.right = Some(Branch::Window(window.clone())); 129 | window.fork_set(fork.clone(), t); 130 | return; 131 | } 132 | }; 133 | 134 | // Create a new fork branch and assign both windows to it. 135 | let new_fork = ForkPtr::new({ 136 | let area = Rect::new(1, 1, 1, 1); 137 | let branch = Branch::Window(attaching.clone()); 138 | let mut fork = Fork::new(area, branch, workspace); 139 | fork.right = Some(Branch::Window(window.clone())); 140 | fork 141 | }); 142 | 143 | self.fork_register(new_fork.clone(), t); 144 | 145 | attaching.fork_set(new_fork.clone(), t); 146 | window.fork_set(new_fork.clone(), t); 147 | 148 | let new_branch = Branch::Fork(new_fork.clone()); 149 | 150 | // Then assign the new branch to the fork where the window was. 151 | { 152 | let fork_ = fork.rw(t); 153 | match fork_.branch(BranchRef::Window(attaching)) { 154 | Some(Either::Left(branch)) | Some(Either::Right(branch)) => *branch = new_branch, 155 | None => tracing::error!("invalid parent fork association in window"), 156 | } 157 | } 158 | 159 | // Refresh work areas 160 | fork.work_area_refresh(self, t); 161 | 162 | // Reassign fork orientation and refresh again. TODO: Avoid redoing refresh 163 | new_fork.reset_orientation(self, t); 164 | new_fork.work_area_refresh(self, t); 165 | } 166 | 167 | /// Attach a window a tree on a display. 168 | fn attach_to_workspace( 169 | &mut self, 170 | window: &WindowPtr, 171 | workspace: &WorkspacePtr, 172 | t: &mut TCellOwner, 173 | ) { 174 | let area = workspace.area(t); 175 | 176 | // Assign window to an existing fork on the workspace. 177 | if let Some(fork) = workspace.fork(t) { 178 | if let Some(attach_to) = fork.largest_window(t) { 179 | self.attach_to_window_in_fork(window, &attach_to, &fork, t); 180 | fork.work_area_refresh(self, t); 181 | return; 182 | } 183 | } 184 | 185 | // Create a new fork and assign that, otherwise. 186 | let branch = Branch::Window(window.clone()); 187 | let fork = ForkPtr::new(Fork::new(area, branch, workspace.id(t))); 188 | self.fork_register(fork.clone(), t); 189 | 190 | window.fork_set(fork.clone(), t); 191 | 192 | let workspace = workspace.rw(t); 193 | workspace.focus = Some(window.clone()); 194 | workspace.fork = Some(fork.clone()); 195 | 196 | fork.work_area_refresh(self, t); 197 | } 198 | 199 | /// Detach a window from its tree, and removes its association with this tiler. 200 | pub fn detach(&mut self, window: &WindowPtr, t: &mut TCellOwner) { 201 | // Remove the window from management of the tiler. 202 | self.windows.remove(&window.id(t)); 203 | 204 | if let Some(stack) = window.stack(t) { 205 | window.fork_take(t); 206 | stack.detach(self, window, t); 207 | return; 208 | } 209 | 210 | if let Some(fork) = window.fork_take(t) { 211 | self.detach_branch(fork, BranchRef::Window(window), t); 212 | } 213 | 214 | // If window being detached is the active window, remove focus 215 | if let Some(active) = self.active.as_ref() { 216 | if Rc::ptr_eq(window, active) { 217 | self.active = None; 218 | self.active_changed = false; 219 | } 220 | } 221 | } 222 | 223 | /// Detach a window from a fork. 224 | fn detach_fork(&mut self, fork: ForkPtr, t: &mut TCellOwner) { 225 | eprintln!("requested to detach fork"); 226 | let mut detaching = Some(fork); 227 | 228 | while let Some(fork) = detaching.take() { 229 | self.event_queue.fork_destroy(&fork); 230 | self.forks.remove(&(Rc::as_ptr(&fork) as usize)); 231 | 232 | if let Some(parent) = fork.rw(t).parent.take() { 233 | let parent_ = parent.rw(t); 234 | if parent_.left_is(BranchRef::Fork(&fork)) { 235 | if let Some(right) = parent_.right.take() { 236 | parent_.left = right; 237 | } 238 | } else if parent_.right_is(BranchRef::Fork(&fork)) { 239 | parent_.right = None; 240 | } 241 | } else { 242 | for workspace in self.workspaces.values() { 243 | if let Some(ref root) = workspace.fork(t) { 244 | if Rc::ptr_eq(root, &fork) { 245 | workspace.rw(t).fork = None; 246 | return; 247 | } 248 | } 249 | } 250 | } 251 | } 252 | } 253 | 254 | /// Detach a window or stack from a fork 255 | pub(crate) fn detach_branch( 256 | &mut self, 257 | fork: ForkPtr, 258 | reference: BranchRef<'_, T>, 259 | t: &mut TCellOwner, 260 | ) { 261 | tracing::debug!("detaching branch from Fork({})", Rc::as_ptr(&fork) as usize); 262 | 263 | // After removing a window, it's possible to have a fork in a fork with an empty 264 | // branch on one side. When this happens, we will discard the parent fork and 265 | // place the grandchild in its place. Then the child association of the 266 | // grandparent fork is updated to point to the grandchild who is now a direct 267 | // child. 268 | fn reparent( 269 | tiler: &mut Tiler, 270 | grandparent: Option>, 271 | parent: ForkPtr, 272 | grandchild: ForkPtr, 273 | t: &mut TCellOwner, 274 | ) { 275 | match grandparent { 276 | Some(grandparent) => { 277 | // Update child association of the grandparent fork 278 | let grandparent_ = grandparent.rw(t); 279 | match grandparent_.branch(BranchRef::Fork(&parent)) { 280 | Some(Either::Left(branch)) | Some(Either::Right(branch)) => { 281 | *branch = Branch::Fork(grandchild.clone()) 282 | } 283 | None => tracing::error!("fork contained parent that doesn't own it"), 284 | } 285 | 286 | grandparent.work_area_refresh(tiler, t); 287 | 288 | // Update the parent association of the grandchild now a child 289 | grandchild.rw(t).parent = Some(grandparent); 290 | } 291 | None => { 292 | let mut display = None; 293 | 294 | for info in tiler.workspaces.values() { 295 | if let Some(ref workspace_fork) = info.ro(t).fork { 296 | if !Rc::ptr_eq(workspace_fork, &parent) { 297 | continue; 298 | } 299 | 300 | display = Some(info.ro(t).parent.ro(t).area); 301 | 302 | grandchild.rw(t).parent = None; 303 | info.rw(t).fork = Some(grandchild.clone()); 304 | break; 305 | } 306 | } 307 | 308 | if let Some(display) = display { 309 | grandchild.work_area_update(tiler, display, t); 310 | } 311 | } 312 | } 313 | 314 | // Remove the orphaned fork 315 | parent.rw(t).parent.take(); 316 | tiler.detach_fork(parent, t); 317 | } 318 | 319 | let fork_ = fork.rw(t); 320 | 321 | if fork_.left_is(reference) { 322 | tracing::debug!("detaching left branch of fork"); 323 | if let Some(right) = fork_.right.take() { 324 | tracing::debug!("right branch of fork was assigned to left branch"); 325 | fork_.left = right; 326 | 327 | if let Branch::Fork(ref grandchild) = fork_.left { 328 | tracing::debug!("reparenting left branch of fork into parent"); 329 | reparent( 330 | self, 331 | fork_.parent.clone(), 332 | fork.clone(), 333 | grandchild.clone(), 334 | t, 335 | ); 336 | } 337 | } else { 338 | tracing::debug!("fork is now childless"); 339 | self.detach_fork(fork, t); 340 | } 341 | } else if fork_.right_is(reference) { 342 | tracing::debug!("detaching right branch of fork"); 343 | fork_.right = None; 344 | 345 | if let Branch::Fork(ref grandchild) = fork_.left.clone() { 346 | tracing::debug!("reparenting left branch into parent fork"); 347 | reparent( 348 | self, 349 | fork_.parent.clone(), 350 | fork.clone(), 351 | grandchild.clone(), 352 | t, 353 | ); 354 | } 355 | } 356 | } 357 | 358 | /// Removes a display from the tree, and migrates its workspaces to another display 359 | pub fn display_detach(&mut self, display_id: u32, t: &mut TCellOwner) { 360 | // Get the active display to assign to. 361 | let active = self 362 | .workspaces 363 | .get(&self.active_workspace) 364 | .expect("active workspace doesn't exist") 365 | .ro(t) 366 | .parent 367 | .clone(); 368 | 369 | // Remove the display from the tiler. 370 | let display_ptr = ward::ward!(self.displays.remove(&display_id), else { 371 | tracing::error!("detach of non-existent display: {}", display_id); 372 | return; 373 | }); 374 | 375 | // Take ownership of its workspaces. 376 | let mut workspaces = HashMap::new(); 377 | std::mem::swap(&mut workspaces, &mut display_ptr.rw(t).workspaces); 378 | 379 | // Migrate workspaces. 380 | for workspace in workspaces.into_values() { 381 | active.assign_workspace(workspace, t); 382 | } 383 | } 384 | 385 | /// Creates or updates a display associated with the tree. 386 | pub fn display_update(&mut self, display: u32, area: Rect, t: &mut TCellOwner) { 387 | let display = self 388 | .displays 389 | .entry(display) 390 | .or_insert_with(|| DisplayPtr::new(area)) 391 | .clone(); 392 | 393 | display.work_area_update(self, area, t); 394 | } 395 | 396 | /// Retrieves the latest set of instructions for the window manager to carry out. 397 | pub fn events<'a>(&'a mut self, t: &'a mut TCellOwner) -> impl Iterator + 'a { 398 | let focus: Option = if self.active_changed { 399 | self.active_window().map(|a| Event::Focus(a.id(t))) 400 | } else { 401 | None 402 | }; 403 | 404 | self.active_changed = false; 405 | 406 | let workspace_switch = if self.active_workspace_changed { 407 | Some(Event::FocusWorkspace(self.active_workspace)) 408 | } else { 409 | None 410 | }; 411 | 412 | self.event_queue 413 | .consume_events() 414 | .chain(workspace_switch.into_iter()) 415 | .chain(focus.into_iter()) 416 | } 417 | 418 | /// Focus this window in the tree. 419 | pub fn focus(&mut self, window: &WindowPtr, t: &mut TCellOwner) { 420 | window.focus(self, t); 421 | } 422 | 423 | /// Move focus to the window above the active one. 424 | pub fn focus_above(&mut self, t: &mut TCellOwner) { 425 | match self.window_in_direction(Rect::distance_upward, Rect::is_below, t) { 426 | Some(active) => self.set_active_window(&active, t), 427 | None => self.focus_display_above(t), 428 | } 429 | } 430 | 431 | /// Move focus to the window below the active one. 432 | pub fn focus_below(&mut self, t: &mut TCellOwner) { 433 | match self.window_in_direction(Rect::distance_downward, Rect::is_above, t) { 434 | Some(active) => self.set_active_window(&active, t), 435 | None => self.focus_display_below(t), 436 | } 437 | } 438 | 439 | /// Move focus to the window left of the active one. 440 | pub fn focus_left(&mut self, t: &mut TCellOwner) { 441 | self.focus_with_stack(StackPtr::select_left, Self::focus_left_absolute, t); 442 | } 443 | 444 | /// Move focus to the left window, even if in a stack. 445 | pub fn focus_left_absolute(&mut self, t: &mut TCellOwner) { 446 | match self.window_in_direction(Rect::distance_westward, Rect::is_right, t) { 447 | Some(active) => self.set_active_window(&active, t), 448 | None => self.focus_display_left(t), 449 | } 450 | } 451 | 452 | /// Move focus to the window right of the active one. 453 | pub fn focus_right(&mut self, t: &mut TCellOwner) { 454 | self.focus_with_stack(StackPtr::select_right, Self::focus_right_absolute, t); 455 | } 456 | 457 | /// Move focus to the right window, even if in a stack. 458 | pub fn focus_right_absolute(&mut self, t: &mut TCellOwner) { 459 | match self.window_in_direction(Rect::distance_eastward, Rect::is_left, t) { 460 | Some(active) => self.set_active_window(&active, t), 461 | None => self.focus_display_right(t), 462 | } 463 | } 464 | 465 | /// Focus the active window on this display. 466 | fn focus_display(&mut self, display: DisplayPtr, t: &mut TCellOwner) { 467 | let display = display.ro(t); 468 | 469 | if let Some(active) = display.active { 470 | if let Some(workspace) = display.workspaces.get(&active) { 471 | if let Some(active) = workspace.ro(t).focus.clone() { 472 | self.set_active_window(&active, t); 473 | } 474 | } 475 | } 476 | } 477 | 478 | /// Move focus to the workspace on the display to the left of the active one. 479 | pub fn focus_display_left(&mut self, t: &mut TCellOwner) { 480 | if let Some(display) = self.display_in_direction(Rect::distance_westward, Rect::is_right, t) 481 | { 482 | self.focus_display(display, t); 483 | } 484 | } 485 | 486 | /// Move focus to the workspace on the display to the right of the active one. 487 | pub fn focus_display_right(&mut self, t: &mut TCellOwner) { 488 | if let Some(display) = self.display_in_direction(Rect::distance_eastward, Rect::is_left, t) 489 | { 490 | self.focus_display(display, t); 491 | } 492 | } 493 | 494 | /// Move focus to the workspace on the display above the active one. 495 | pub fn focus_display_above(&mut self, t: &mut TCellOwner) { 496 | if let Some(display) = self.display_in_direction(Rect::distance_upward, Rect::is_below, t) { 497 | self.focus_display(display, t); 498 | } 499 | } 500 | 501 | /// Move focus to the workspace on the display below the active one. 502 | pub fn focus_display_below(&mut self, t: &mut TCellOwner) { 503 | if let Some(display) = self.display_in_direction(Rect::distance_downward, Rect::is_above, t) 504 | { 505 | self.focus_display(display, t); 506 | } 507 | } 508 | 509 | /// Manage focus movements with consideration for in-stack movements. 510 | fn focus_with_stack( 511 | &mut self, 512 | stack_func: fn(&StackPtr, &mut TCellOwner) -> Option>, 513 | focus_func: fn(&mut Self, &mut TCellOwner), 514 | t: &mut TCellOwner, 515 | ) { 516 | let active = ward::ward!(self.active_window(), else { return }); 517 | 518 | // If window is in a stack, select the window to the left of the stack. 519 | if let Some(stack) = active.stack(t) { 520 | if let Some(left) = stack_func(&stack, t) { 521 | stack.rw(t).active = left.clone(); 522 | self.set_active_window(&left, t); 523 | return; 524 | } 525 | } 526 | 527 | focus_func(self, t); 528 | } 529 | 530 | /// Keep track of this fork directly in the tiler. 531 | fn fork_register(&mut self, fork: ForkPtr, t: &TCellOwner) { 532 | self.event_queue.fork_update(&fork, t); 533 | self.forks.insert(Rc::as_ptr(&fork) as usize, fork); 534 | } 535 | 536 | /// Resize a fork with a new split 537 | pub fn fork_resize(&mut self, fork: usize, split: u32, t: &mut TCellOwner) { 538 | if let Some(fork) = self.forks.get(&fork).cloned() { 539 | fork.resize(self, split, t); 540 | } 541 | } 542 | 543 | /// When moving vertically or horizontally, move active window out of the stack. 544 | fn move_from_stack( 545 | &mut self, 546 | active: &WindowPtr, 547 | fork: &ForkPtr, 548 | stack: &StackPtr, 549 | direction: Direction, 550 | t: &mut TCellOwner, 551 | ) { 552 | let (orientation, stack_on_left) = match direction { 553 | Direction::Above => (Orientation::Vertical, false), 554 | Direction::Below => (Orientation::Vertical, true), 555 | Direction::Left => (Orientation::Horizontal, false), 556 | Direction::Right => (Orientation::Horizontal, true), 557 | }; 558 | 559 | let area = stack.ro(t).area; 560 | let workspace = fork.ro(t).workspace; 561 | let windows = stack.ro(t).windows.len(); 562 | 563 | let branch = ward::ward!(fork.rw(t).branch(BranchRef::Stack(stack)), else { 564 | tracing::error!("invalid parent fork association of stacked window"); 565 | return; 566 | }); 567 | 568 | match branch { 569 | Either::Left(prev_branch) | Either::Right(prev_branch) => { 570 | if windows == 1 { 571 | *prev_branch = Branch::Window(active.clone()); 572 | self.event_queue.stack_destroy(stack); 573 | } else { 574 | let stack_branch = Branch::Stack(stack.clone()); 575 | let window_branch = Branch::Window(active.clone()); 576 | 577 | let (left, right) = if stack_on_left { 578 | (stack_branch, window_branch) 579 | } else { 580 | (window_branch, stack_branch) 581 | }; 582 | 583 | let new_fork = ForkPtr::new({ 584 | let mut fork = Fork::new(area, left, workspace); 585 | fork.right = Some(right); 586 | fork 587 | }); 588 | 589 | *prev_branch = Branch::Fork(new_fork.clone()); 590 | 591 | new_fork.rw(t).parent = Some(fork.clone()); 592 | self.fork_register(new_fork.clone(), t); 593 | new_fork.orientation_set(self, orientation, t); 594 | } 595 | } 596 | } 597 | 598 | fork.work_area_refresh(self, t); 599 | } 600 | 601 | /// When moving horizontally, check if a window is stacked and can be moved within the stack. 602 | fn move_horizontally( 603 | &mut self, 604 | stack_func: fn(&StackPtr, &mut TCellOwner) -> Option, 605 | else_func: fn(&mut Self, &mut TCellOwner), 606 | t: &mut TCellOwner, 607 | ) { 608 | let active = ward::ward!(self.active_window(), else { return }); 609 | 610 | // If window is in a stack, move the tab positioning in the stack 611 | if let Some(stack) = active.stack(t) { 612 | if let Some(movement) = stack_func(&stack, t) { 613 | self.event_queue.stack_movement(&stack, movement); 614 | return; 615 | } 616 | } 617 | 618 | else_func(self, t); 619 | } 620 | 621 | /// Move the active window up in the tree. 622 | pub fn move_left(&mut self, t: &mut TCellOwner) { 623 | self.move_horizontally(StackPtr::move_left, Self::move_left_absolute, t); 624 | } 625 | 626 | fn move_in_direction(&mut self, direction: Direction, t: &mut TCellOwner) { 627 | let active = ward::ward!(self.active_window().cloned(), else { return }); 628 | let fork = ward::ward!(active.fork(t), else { return }); 629 | 630 | // If in a stack, create a fork and make the window adjacent to the stack. 631 | if let Some(stack) = active.stack(t) { 632 | self.move_from_stack(&active, &fork, &stack, direction, t); 633 | return; 634 | } 635 | 636 | // Fetch nearest window in direction 637 | let (distance, filter): (DistanceFn, DirectionalConditionFn) = match direction { 638 | Direction::Above => (Rect::distance_upward, Rect::is_below), 639 | Direction::Below => (Rect::distance_downward, Rect::is_above), 640 | Direction::Left => (Rect::distance_westward, Rect::is_right), 641 | Direction::Right => (Rect::distance_eastward, Rect::is_left), 642 | }; 643 | 644 | if let Some(window) = self.window_in_direction(distance, filter, t) { 645 | let matched_fork = ward::ward!(window.fork(t), else { 646 | tracing::error!("cannot move into window that is forkless"); 647 | return; 648 | }); 649 | 650 | // If the window being attached is in the same fork, swap positions. 651 | if Rc::ptr_eq(&fork, &matched_fork) { 652 | let fork_ = fork.rw(t); 653 | if let Some(right) = fork_.right.as_mut() { 654 | std::mem::swap(right, &mut fork_.left); 655 | fork.work_area_refresh(self, t); 656 | return; 657 | } 658 | } 659 | 660 | // Detach and create a fork in new window. 661 | self.detach(&active, t); 662 | self.attach_to_window_in_fork(&active, &window, &matched_fork, t); 663 | self.set_active_window(&active, t); 664 | } 665 | 666 | // TODO: Move across displays if not found 667 | } 668 | 669 | /// Move the active window to the left, even if it is stacked. 670 | pub fn move_left_absolute(&mut self, t: &mut TCellOwner) { 671 | self.move_in_direction(Direction::Left, t); 672 | } 673 | 674 | /// Move the active window to the right in the tree. 675 | pub fn move_right(&mut self, t: &mut TCellOwner) { 676 | self.move_horizontally(StackPtr::move_right, Self::move_right_absolute, t); 677 | } 678 | 679 | /// Move the active window to the right, even if it is stacked. 680 | pub fn move_right_absolute(&mut self, t: &mut TCellOwner) { 681 | self.move_in_direction(Direction::Right, t); 682 | } 683 | 684 | /// Move the active window above in the tree. 685 | pub fn move_above(&mut self, t: &mut TCellOwner) { 686 | self.move_in_direction(Direction::Above, t) 687 | } 688 | 689 | /// Move the active window below in the tree. 690 | pub fn move_below(&mut self, t: &mut TCellOwner) { 691 | self.move_in_direction(Direction::Below, t); 692 | } 693 | 694 | /// Toggle the orientation of the active window. 695 | pub fn toggle_orientation(&mut self, t: &mut TCellOwner) { 696 | if let Some(active) = self.active_window() { 697 | if let Some(fork) = active.fork(t) { 698 | fork.toggle_orientation(self, t); 699 | } 700 | } 701 | } 702 | 703 | /// Set a new active window, and mark that we should notify the window manager. 704 | pub(crate) fn set_active_window(&mut self, window: &WindowPtr, t: &mut TCellOwner) { 705 | self.active = Some(window.clone()); 706 | self.active_changed = true; 707 | 708 | let workspace = window.ro(t).workspace; 709 | 710 | if self.active_workspace != workspace { 711 | self.workspace_switch(workspace, t); 712 | } 713 | } 714 | 715 | /// If a window is stacked, unstack it. If it is not stacked, stack it. 716 | pub fn stack_toggle(&mut self, t: &mut TCellOwner) { 717 | if let Some(active) = self.active_window().cloned() { 718 | active.stack_toggle(self, t); 719 | } 720 | } 721 | 722 | /// Swaps the tree location of this window with another. 723 | pub fn swap(&mut self, from: &WindowPtr, with: &WindowPtr, t: &mut TCellOwner) { 724 | from.swap_position_with(self, with, t); 725 | } 726 | 727 | /// Create a new pointer to a window managed by this tiler. 728 | pub fn window>(&mut self, id: I) -> WindowPtr { 729 | let id = id.into(); 730 | 731 | if let Some(window) = self.windows.get(&id) { 732 | return window.clone(); 733 | } 734 | 735 | let window = WindowPtr(Rc::new(TCell::new(Window::new(id)))); 736 | 737 | self.windows.insert(id, window.clone()); 738 | 739 | window 740 | } 741 | 742 | /// Locates the display adjacent to the active display. 743 | fn display_in_direction( 744 | &self, 745 | distance: DistanceFn, 746 | filter: DirectionalConditionFn, 747 | t: &mut TCellOwner, 748 | ) -> Option> { 749 | let active = ward::ward!(self.workspaces.get(&self.active_workspace), else { return None }); 750 | 751 | let active = &active.ro(t).parent; 752 | let active_rect = &active.ro(t).area; 753 | 754 | let mut least_distance = f64::MAX; 755 | let mut candidate = None; 756 | 757 | for display in self.displays.values() { 758 | if Rc::ptr_eq(display, active) { 759 | continue; 760 | } 761 | 762 | let this_rect = &display.ro(t).area; 763 | 764 | if filter(active_rect, this_rect) { 765 | continue; 766 | } 767 | 768 | let distance = distance(active_rect, this_rect); 769 | if distance < least_distance { 770 | least_distance = distance; 771 | candidate = Some(display.clone()); 772 | } 773 | } 774 | 775 | candidate 776 | } 777 | 778 | /// Locates the window adjacent to the active window in the active workspace that has 779 | /// the lowest distance for a given distance function. Ignores windows windows in the 780 | /// same stack. 781 | fn window_in_direction( 782 | &self, 783 | distance: DistanceFn, 784 | filter: DirectionalConditionFn, 785 | t: &TCellOwner, 786 | ) -> Option> { 787 | let active = ward::ward!(self.active_window(), else { return None }); 788 | 789 | let active_ = active.ro(t); 790 | let stack = active_.stack.as_ref(); 791 | let rect = active_.rect; 792 | let workspace = active_.workspace; 793 | 794 | let mut lowest_distance = f64::MAX; 795 | let mut candidate = None; 796 | 797 | for window in self.windows.values() { 798 | // Ignores windows from a different workspace. 799 | if window.ro(t).workspace != workspace { 800 | continue; 801 | } 802 | 803 | // Ignores same window. 804 | if Rc::ptr_eq(active, window) { 805 | continue; 806 | } 807 | 808 | // Ignores windows in the same stack. 809 | if let Some((active, this)) = stack.zip(window.ro(t).stack.as_ref()) { 810 | if Rc::ptr_eq(active, this) { 811 | continue; 812 | } 813 | } 814 | 815 | let this_rect = &window.ro(t).rect; 816 | 817 | // Avoid considering windows that meet this criteria. 818 | if filter(&rect, this_rect) { 819 | continue; 820 | } 821 | 822 | // The window with the least distance wins. 823 | let distance = distance(&rect, this_rect); 824 | if distance < lowest_distance { 825 | candidate = Some(window.clone()); 826 | lowest_distance = distance; 827 | } 828 | } 829 | 830 | candidate 831 | } 832 | 833 | /// Detaches a workspace from the tree. 834 | fn workspace_detach(&mut self, workspace: u32, t: &mut TCellOwner) { 835 | let workspace = ward::ward!(self.workspaces.remove(&workspace), else { 836 | tracing::error!("detached a workspace that didn't exist"); 837 | return; 838 | }); 839 | 840 | workspace 841 | .rw(t) 842 | .parent 843 | .clone() 844 | .remove_association(workspace, t); 845 | } 846 | 847 | /// Switching the workspace will hide all windows on other workspaces, show all 848 | /// visible windows for the given workspace, and focus the active window on the 849 | /// given workspace. 850 | pub fn workspace_switch(&mut self, workspace: u32, t: &mut TCellOwner) { 851 | if self.active_workspace == workspace { 852 | return; 853 | } 854 | 855 | self.active_workspace = workspace; 856 | self.active_workspace_changed = true; 857 | 858 | let mut window_events = HashMap::new(); 859 | 860 | std::mem::swap(&mut self.event_queue.windows, &mut window_events); 861 | 862 | for (id, window) in self.windows.iter() { 863 | let this = window.rw(t); 864 | 865 | let is_visible = this.visible; 866 | 867 | // If window's workspace is not the same as what is being switched to. 868 | if this.workspace != workspace { 869 | if is_visible { 870 | this.visible = false; 871 | window_events.entry(*id).or_default().visibility = Some(false); 872 | } 873 | 874 | continue; 875 | } 876 | 877 | // If window on switched workspace is the active window in a stack 878 | if let Some(stack) = this.stack.clone() { 879 | if Rc::ptr_eq(&stack.ro(t).active, window) { 880 | if !is_visible { 881 | window.rw(t).visible = true; 882 | window_events.entry(*id).or_default().visibility = Some(true); 883 | } 884 | } else if is_visible { 885 | window.rw(t).visible = false; 886 | window_events.entry(*id).or_default().visibility = Some(false); 887 | } 888 | 889 | continue; 890 | } 891 | 892 | // All other windows not in a stack 893 | if !is_visible { 894 | window.rw(t).visible = true; 895 | window_events.entry(*id).or_default().visibility = Some(true); 896 | } 897 | } 898 | 899 | std::mem::swap(&mut self.event_queue.windows, &mut window_events); 900 | 901 | let workspace = self 902 | .workspaces 903 | .get_mut(&workspace) 904 | .expect("no workspace assigned") 905 | .rw(t); 906 | 907 | if let Some(active) = workspace.focus.clone() { 908 | self.set_active_window(&active, t); 909 | } 910 | } 911 | 912 | /// Associate a workspace with a display, and creates the workspace if it didn't exist. 913 | pub fn workspace_update(&mut self, workspace: u32, display: u32, t: &mut TCellOwner) { 914 | let display_ = ward::ward!(self.displays.get(&display).cloned(), else { 915 | tracing::error!("cannot attach workspace to non-existent display"); 916 | return; 917 | }); 918 | 919 | match self.workspaces.get(&workspace).cloned() { 920 | Some(workspace) => { 921 | display_.assign_workspace(workspace, t); 922 | } 923 | None => { 924 | self.workspaces 925 | .insert(workspace, display_.create_workspace(workspace, t)); 926 | } 927 | } 928 | } 929 | 930 | pub fn debug<'a>(&'a self, t: &'a TCellOwner) -> TilerDisplay<'a, T> { 931 | TilerDisplay::new(self, t) 932 | } 933 | } 934 | 935 | pub struct TilerDisplay<'a, T: 'static> { 936 | pub tiler: &'a Tiler, 937 | pub t: &'a TCellOwner, 938 | } 939 | 940 | impl<'a, T> TilerDisplay<'a, T> { 941 | fn new(tiler: &'a Tiler, t: &'a TCellOwner) -> Self { 942 | Self { tiler, t } 943 | } 944 | } 945 | 946 | impl<'a, T> Debug for TilerDisplay<'a, T> { 947 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 948 | let active = self.tiler.active_window().map(|window| window.id(self.t)); 949 | 950 | let displays = self 951 | .tiler 952 | .displays 953 | .iter() 954 | .map(|(key, display)| (key, display.debug(self.t))) 955 | .collect::>(); 956 | 957 | fmt.debug_struct("Tiler") 958 | .field("active", &active) 959 | .field("displays", &displays) 960 | .finish() 961 | } 962 | } 963 | -------------------------------------------------------------------------------- /src/window.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::branch::{Branch, BranchRef}; 5 | use crate::fork::ForkPtr; 6 | use crate::stack::StackPtr; 7 | use crate::tiler::Tiler; 8 | use crate::{Placement, Rect}; 9 | use either::Either; 10 | use qcell::{TCell, TCellOwner}; 11 | use std::fmt::{self, Debug}; 12 | use std::rc::Rc; 13 | 14 | /// An ID assigned to a window by a window manager. 15 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 16 | #[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, From, Into)] 17 | pub struct WindowID(pub u32, pub u32); 18 | 19 | /// Pointer to reference-counted window managed by a `TCell`. 20 | #[derive(Deref, DerefMut)] 21 | pub struct WindowPtr(pub(crate) Rc>>); 22 | impl Clone for WindowPtr { 23 | fn clone(&self) -> WindowPtr { 24 | WindowPtr(self.0.clone()) 25 | } 26 | } 27 | 28 | impl WindowPtr { 29 | /// The ID assigned to the window by the window manager. 30 | pub fn id(&self, t: &TCellOwner) -> WindowID { 31 | self.ro(t).id 32 | } 33 | 34 | /// Focus this window in the tree. 35 | pub(crate) fn focus(&self, tiler: &mut Tiler, t: &mut TCellOwner) { 36 | if let Some(focus) = tiler.active_window() { 37 | if Rc::ptr_eq(focus, self) { 38 | return; 39 | } 40 | } 41 | 42 | if let Some(stack) = self.stack(t) { 43 | let mut visibility = Vec::new(); 44 | for this in stack.ro(t).windows.iter() { 45 | visibility.push((this.id(t), Rc::ptr_eq(self, this))); 46 | } 47 | 48 | for (id, show) in visibility { 49 | tiler.event_queue.windows.entry(id).or_default().visibility = Some(show); 50 | } 51 | } 52 | 53 | tiler.set_active_window(self, t) 54 | } 55 | 56 | /// Get a pointer to the parent fork assocation. 57 | pub(crate) fn fork(&self, t: &TCellOwner) -> Option> { 58 | self.ro(t).fork.clone() 59 | } 60 | 61 | /// Remove the parent fork association and return it. 62 | pub(crate) fn fork_take(&self, t: &mut TCellOwner) -> Option> { 63 | self.rw(t).fork.take() 64 | } 65 | 66 | /// Set the parent fork association for this window. 67 | pub(crate) fn fork_set(&self, fork: ForkPtr, t: &mut TCellOwner) { 68 | self.rw(t).workspace = fork.ro(t).workspace; 69 | self.rw(t).fork = Some(fork); 70 | } 71 | 72 | /// Get the pointer to the stack it may be associated with. 73 | pub(crate) fn stack(&self, t: &TCellOwner) -> Option> { 74 | self.ro(t).stack.clone() 75 | } 76 | 77 | /// If a window is stacked, unstack it. If it is not stacked, stack it. 78 | pub(crate) fn stack_toggle(&self, tiler: &mut Tiler, t: &mut TCellOwner) { 79 | if let Some(stack) = self.stack(t) { 80 | stack.detach(tiler, self, t); 81 | 82 | if stack.ro(t).windows.is_empty() { 83 | let fork = ward::ward!(self.fork(t), else { 84 | tracing::error!("window does not have a parent fork"); 85 | return; 86 | }); 87 | 88 | let fork_ = fork.rw(t); 89 | 90 | let branch = ward::ward!(fork_.branch(BranchRef::Stack(&stack)), else { 91 | tracing::error!("parent fork of window did not have a stack assocation for this window"); 92 | return; 93 | }); 94 | 95 | let (Either::Left(branch) | Either::Right(branch)) = branch; 96 | *branch = Branch::Window(self.clone()); 97 | tiler.event_queue.stack_destroy(&stack); 98 | } 99 | 100 | return; 101 | } 102 | 103 | let fork = ward::ward!(self.fork(t), else { 104 | tracing::error!("cannot stack because window does not have a parent fork"); 105 | return; 106 | }); 107 | 108 | let stack = StackPtr::new(self, fork.clone(), t); 109 | 110 | let branch = ward::ward!(fork.rw(t).branch(BranchRef::Window(self)), else { 111 | tracing::error!("cannot stack because window has invalid parent fork"); 112 | stack.detach(tiler, self, t); 113 | return; 114 | }); 115 | 116 | let (Either::Left(branch) | Either::Right(branch)) = branch; 117 | *branch = Branch::Stack(stack.clone()); 118 | 119 | tiler.event_queue.stack_update(&stack, t); 120 | tiler.event_queue.stack_assign(&stack, self, t); 121 | } 122 | 123 | /// Swaps the tree location of this window with another. 124 | pub(crate) fn swap_position_with( 125 | &self, 126 | tiler: &mut Tiler, 127 | other: &WindowPtr, 128 | t: &mut TCellOwner, 129 | ) { 130 | if let Some(stack) = self.stack(t) { 131 | stack.swap(self, other, t); 132 | stack.work_area_refresh(tiler, t); 133 | } else if let Some(fork) = self.fork(t) { 134 | fork.swap(self, other, t); 135 | fork.work_area_refresh(tiler, t); 136 | } 137 | 138 | if let Some(stack) = other.stack(t) { 139 | stack.swap(other, self, t); 140 | stack.work_area_refresh(tiler, t) 141 | } else if let Some(fork) = other.fork(t) { 142 | fork.swap(other, self, t); 143 | fork.work_area_refresh(tiler, t); 144 | } 145 | } 146 | 147 | /// Update the position and dimensions of this window. 148 | pub(crate) fn work_area_update(&self, tiler: &mut Tiler, area: Rect, t: &mut TCellOwner) { 149 | let this = self.rw(t); 150 | if this.rect != area { 151 | this.rect = area; 152 | } 153 | 154 | let id = this.id; 155 | let workspace = this.workspace; 156 | 157 | tiler.event_queue.windows.entry(id).or_default().place = Some(Placement { area, workspace }) 158 | } 159 | } 160 | 161 | pub struct Window { 162 | pub(crate) fork: Option>, 163 | pub(crate) id: WindowID, 164 | pub(crate) rect: Rect, 165 | pub(crate) stack: Option>, 166 | pub(crate) workspace: u32, 167 | pub(crate) visible: bool, 168 | } 169 | 170 | impl Window { 171 | pub(crate) fn new>(id: I) -> Self { 172 | Self { 173 | fork: None::>, 174 | id: id.into(), 175 | rect: Rect::new(1, 1, 1, 1), 176 | stack: None, 177 | workspace: 0, 178 | visible: true, 179 | } 180 | } 181 | 182 | pub fn debug<'a>(&'a self, t: &'a TCellOwner) -> WindowDebug<'a, T> { 183 | WindowDebug::new(self, t) 184 | } 185 | } 186 | 187 | impl Drop for Window { 188 | fn drop(&mut self) { 189 | tracing::debug!("dropped {:?}", self.id); 190 | } 191 | } 192 | 193 | pub struct WindowDebug<'a, T: 'static> { 194 | window: &'a Window, 195 | _t: &'a TCellOwner, 196 | } 197 | 198 | impl<'a, T> WindowDebug<'a, T> { 199 | fn new(window: &'a Window, t: &'a TCellOwner) -> Self { 200 | Self { window, _t: t } 201 | } 202 | } 203 | 204 | impl<'a, T> Debug for WindowDebug<'a, T> { 205 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 206 | fmt.debug_struct("Window") 207 | .field("id", &self.window.id) 208 | .field("fork", &self.window.fork.as_ref().map(|p| Rc::as_ptr(p))) 209 | .field("stack", &self.window.stack.as_ref().map(|p| Rc::as_ptr(p))) 210 | .field("workspace", &self.window.workspace) 211 | .field("rect", &self.window.rect) 212 | .finish() 213 | } 214 | } 215 | 216 | impl Debug for WindowID { 217 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 218 | write!(fmt, "WindowID({}, {})", self.0, self.1) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/workspace.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 System76 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | use crate::display::DisplayPtr; 5 | use crate::fork::ForkPtr; 6 | use crate::geom::Rect; 7 | use crate::window::WindowPtr; 8 | use qcell::{TCell, TCellOwner}; 9 | use std::fmt::{self, Debug}; 10 | use std::rc::Rc; 11 | 12 | /// A virtual workspace, which may be assigned to a display, and may have a focused window. 13 | #[derive(Deref, DerefMut)] 14 | pub(crate) struct WorkspacePtr(Rc>>); 15 | impl Clone for WorkspacePtr { 16 | fn clone(&self) -> WorkspacePtr { 17 | WorkspacePtr(self.0.clone()) 18 | } 19 | } 20 | 21 | impl WorkspacePtr { 22 | pub fn new(id: u32, parent: DisplayPtr) -> Self { 23 | Self(Rc::new(TCell::new(Workspace { 24 | id, 25 | focus: None, 26 | fork: None, 27 | parent, 28 | }))) 29 | } 30 | 31 | pub fn area(&self, t: &TCellOwner) -> Rect { 32 | self.ro(t).parent.area(t) 33 | } 34 | 35 | pub fn fork(&self, t: &TCellOwner) -> Option> { 36 | self.ro(t).fork.clone() 37 | } 38 | 39 | pub fn id(&self, t: &TCellOwner) -> u32 { 40 | self.ro(t).id 41 | } 42 | } 43 | 44 | /// A virtual workspace, which may be assigned to a display, and may have a focused window. 45 | pub(crate) struct Workspace { 46 | pub id: u32, 47 | pub focus: Option>, 48 | pub fork: Option>, 49 | pub parent: DisplayPtr, 50 | } 51 | 52 | impl Workspace { 53 | pub(crate) fn debug<'a>(&'a self, t: &'a TCellOwner) -> WorkspaceDebug<'a, T> { 54 | WorkspaceDebug::new(self, t) 55 | } 56 | } 57 | 58 | pub(crate) struct WorkspaceDebug<'a, T: 'static> { 59 | info: &'a Workspace, 60 | t: &'a TCellOwner, 61 | } 62 | 63 | impl<'a, T> WorkspaceDebug<'a, T> { 64 | pub fn new(info: &'a Workspace, t: &'a TCellOwner) -> Self { 65 | Self { info, t } 66 | } 67 | } 68 | 69 | impl<'a, T> Debug for WorkspaceDebug<'a, T> { 70 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 71 | let focus = self.info.focus.as_ref().map(|win| win.id(self.t)); 72 | fmt.debug_struct("Workspace") 73 | .field("focus", &focus) 74 | .field("fork", &self.info.fork.as_ref().map(|f| f.debug(self.t))) 75 | .finish() 76 | } 77 | } 78 | --------------------------------------------------------------------------------