├── .github └── workflows │ └── build.yml ├── .gitignore ├── .vscode └── launch.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── ipc.xml ├── magma-ipc ├── Cargo.toml └── src │ ├── ipc │ ├── mod.rs │ └── workspaces.rs │ └── main.rs ├── resources └── cursor.rgba └── src ├── backends ├── mod.rs ├── udev.rs └── winit.rs ├── config ├── mod.rs └── types.rs ├── handlers ├── compositor.rs ├── input.rs ├── mod.rs └── xdg_shell.rs ├── input.rs ├── ipc ├── mod.rs └── workspaces.rs ├── main.rs ├── state.rs └── utils ├── binarytree.rs ├── focus.rs ├── mod.rs ├── protocols ├── mod.rs └── screencopy │ ├── frame.rs │ └── mod.rs ├── render.rs ├── tiling.rs └── workspaces.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Build 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: System dependencies 12 | run: sudo apt-get update; sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | profile: minimal 16 | toolchain: stable 17 | override: true 18 | - uses: actions-rs/cargo@v1 19 | with: 20 | command: build 21 | args: --release 22 | - name: Archive release artifacts 23 | uses: actions/upload-artifact@v3 24 | with: 25 | name: release artifact 26 | path: target/release/magma 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'magma'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=magma", 15 | "--package=magma" 16 | ], 17 | "filter": { 18 | "name": "magma", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": ["--winit"], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'magma'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=magma", 34 | "--package=magma" 35 | ], 36 | "filter": { 37 | "name": "magma", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "magma" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ron = "0.8" 10 | serde = { version = "1", features = ["derive"] } 11 | xdg = "^2.1" 12 | tracing = "0.1.37" 13 | tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } 14 | smithay-drm-extras = { git = "https://github.com/Smithay/smithay.git"} 15 | wayland-scanner = "0.30.0" 16 | wayland-backend = "0.1.2" 17 | 18 | [dependencies.smithay] 19 | git = "https://github.com/Smithay/smithay.git" 20 | default-features = false 21 | features = [ 22 | "backend_winit", 23 | "wayland_frontend", 24 | "desktop", 25 | "backend_session_libseat", 26 | "backend_drm", 27 | "renderer_multi", 28 | "backend_gbm", 29 | "backend_udev", 30 | "backend_libinput", 31 | ] 32 | 33 | [workspace] 34 | members = [ 35 | "magma-ipc", 36 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # magma 2 | 3 | archived in favour of [MagmaEWM](https://github.com/MagmaEWM/MagmaEWM) 4 | -------------------------------------------------------------------------------- /ipc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | This is the protocol defenition for Magma's IPC, it is intended to be used with a wrapper 4 | 5 | HackOS © 2023 6 | 7 | 8 | 9 | 10 | subscribe to workspace events 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | active workspace changed 19 | 20 | 21 | 22 | occupied workspaces changed 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /magma-ipc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "magma-ipc" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | wayland-client = "0.30.1" 10 | wayland-scanner = "0.30.0" 11 | wayland-backend = "0.1.2" -------------------------------------------------------------------------------- /magma-ipc/src/ipc/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod generated { 2 | use wayland_client; 3 | 4 | pub mod __interfaces { 5 | wayland_scanner::generate_interfaces!("../ipc.xml"); 6 | } 7 | use self::__interfaces::*; 8 | 9 | wayland_scanner::generate_client_code!("../ipc.xml"); 10 | } 11 | 12 | pub mod workspaces; -------------------------------------------------------------------------------- /magma-ipc/src/ipc/workspaces.rs: -------------------------------------------------------------------------------- 1 | use super::generated::workspaces::Event; 2 | 3 | impl Into for Event { 4 | fn into(self) -> String { 5 | match self { 6 | Event::ActiveWorkspace { id: _ } => "active_workspace".to_owned(), 7 | Event::OccupiedWorkspaces { occupied: _ } => "occupied_workspaces".to_owned(), 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /magma-ipc/src/main.rs: -------------------------------------------------------------------------------- 1 | use ipc::generated::{workspaces::{Workspaces, Event as WorkspacesEvent}, magma_ipc::MagmaIpc}; 2 | use wayland_client::{Connection, Dispatch, protocol::wl_registry, QueueHandle, globals::{registry_queue_init, GlobalListContents}}; 3 | 4 | mod ipc; 5 | 6 | 7 | struct State(String); 8 | 9 | impl Dispatch for State { 10 | fn event( 11 | _state: &mut Self, 12 | _: &wl_registry::WlRegistry, 13 | _event: wl_registry::Event, 14 | _: &GlobalListContents, 15 | _: &Connection, 16 | _: &QueueHandle, 17 | ) {} 18 | } 19 | 20 | impl Dispatch for State { 21 | fn event( 22 | _state: &mut Self, 23 | _proxy: &MagmaIpc, 24 | _event: ::Event, 25 | _data: &(), 26 | _conn: &Connection, 27 | _qhandle: &QueueHandle, 28 | ) { 29 | } 30 | } 31 | 32 | 33 | impl Dispatch for State { 34 | fn event( 35 | state: &mut Self, 36 | _proxy: &Workspaces, 37 | event: WorkspacesEvent, 38 | _data: &(), 39 | _conn: &Connection, 40 | _qhandle: &QueueHandle, 41 | ) { 42 | match event { 43 | WorkspacesEvent::ActiveWorkspace { id } => { 44 | if "active_workspace" == state.0 { 45 | println!("{}", id) 46 | } 47 | }, 48 | WorkspacesEvent::OccupiedWorkspaces { occupied } => { 49 | if "occupied_workspaces" == state.0 { 50 | println!("{:?}", occupied) 51 | } 52 | }, 53 | } 54 | } 55 | } 56 | 57 | fn main() { 58 | let conn = Connection::connect_to_env().unwrap(); 59 | let mut event_queue = conn.new_event_queue(); 60 | let qh = event_queue.handle(); 61 | let (globals, _queue) = registry_queue_init::(&conn).unwrap(); 62 | let ipc: MagmaIpc = globals.bind::(&qh, 1..=1, ()).unwrap(); 63 | 64 | let category = std::env::args().nth(1); 65 | 66 | match category.as_ref().map(|s| &s[..]) { 67 | Some("workspace") => { 68 | ipc.workspaces(&qh, ()); 69 | } 70 | Some(_) => { 71 | todo!() 72 | } 73 | None => { 74 | todo!() 75 | } 76 | } 77 | let mut state = State(std::env::args().nth(2).expect("item is necessary")); 78 | 79 | if let Some(watch) = std::env::args().nth(3) { 80 | if watch == "watch" { 81 | loop { 82 | event_queue.blocking_dispatch(&mut state).unwrap(); 83 | } 84 | } 85 | } else { 86 | event_queue.roundtrip(&mut state).unwrap(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /resources/cursor.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackedOS/magma/04ff3ab40b5e499f41c7c425e483ac2c5d470c95/resources/cursor.rgba -------------------------------------------------------------------------------- /src/backends/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod udev; 2 | pub mod winit; 3 | -------------------------------------------------------------------------------- /src/backends/udev.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet}, 3 | os::fd::FromRawFd, 4 | path::PathBuf, 5 | time::{Duration}, 6 | }; 7 | 8 | use smithay::{ 9 | backend::{ 10 | allocator::{ 11 | gbm::{self, GbmAllocator, GbmBufferFlags, GbmDevice}, 12 | Fourcc, dmabuf::Dmabuf, 13 | }, 14 | drm::{self, DrmDevice, DrmDeviceFd, DrmNode, NodeType, compositor::{DrmCompositor}, DrmError}, 15 | egl::{EGLDevice, EGLDisplay}, 16 | libinput::{LibinputInputBackend, LibinputSessionInterface}, 17 | renderer::{ 18 | element::{texture::{TextureBuffer, TextureRenderElement}, surface::WaylandSurfaceRenderElement, AsRenderElements}, 19 | gles::{GlesRenderer, GlesTexture}, 20 | multigpu::{gbm::GbmGlesBackend, GpuManager, MultiRenderer, MultiTexture}, ImportDma, self, Bind, Offscreen, BufferType, ExportMem, 21 | }, 22 | session::{libseat::LibSeatSession, Event as SessionEvent, Session}, 23 | udev::{self, UdevBackend, UdevEvent}, SwapBuffersError, 24 | }, 25 | desktop::{space::SpaceElement, layer_map_for_output, LayerSurface}, 26 | output::{Mode as WlMode, Output, PhysicalProperties}, 27 | reexports::{ 28 | calloop::{EventLoop, LoopHandle, RegistrationToken, timer::{Timer, TimeoutAction}}, 29 | drm::{control::{crtc::{self, Handle}, ModeTypeFlags}, Device as DrmDeviceTrait, SystemError}, 30 | input::Libinput, 31 | nix::fcntl::OFlag, 32 | wayland_server::{Display, DisplayHandle, backend::GlobalId, protocol::{wl_output::WlOutput, wl_shm}}, wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1, 33 | }, 34 | utils::{DeviceFd, Scale, Transform, Size, Rectangle, Point}, wayland::{shell::wlr_layer::Layer, dmabuf::{DmabufGlobal, DmabufState, DmabufHandler, ImportError, DmabufFeedbackBuilder, DmabufFeedback}, shm}, delegate_dmabuf, 35 | }; 36 | use smithay_drm_extras::{ 37 | drm_scanner::{self, DrmScanEvent, DrmScanner}, 38 | edid::EdidInfo, 39 | }; 40 | use tracing::{error, info, warn, trace}; 41 | 42 | use crate::{ 43 | state::{Backend, CalloopData, MagmaState}, 44 | utils::{render::CustomRenderElements, protocols::screencopy::{ScreencopyManagerState, frame::Screencopy, ScreencopyHandler}}, delegate_screencopy_manager, 45 | }; 46 | pub type GbmDrmCompositor = DrmCompositor< 47 | GbmAllocator, 48 | GbmDevice, 49 | (), 50 | DrmDeviceFd, 51 | >; 52 | const SUPPORTED_FORMATS: &[Fourcc] = &[ 53 | Fourcc::Abgr2101010, 54 | Fourcc::Argb2101010, 55 | Fourcc::Abgr8888, 56 | Fourcc::Argb8888, 57 | ]; 58 | 59 | static CURSOR_DATA: &[u8] = include_bytes!("../../resources/cursor.rgba"); 60 | 61 | pub struct UdevData { 62 | pub session: LibSeatSession, 63 | handle: LoopHandle<'static, CalloopData>, 64 | primary_gpu: DrmNode, 65 | gpus: GpuManager>, 66 | devices: HashMap, 67 | dmabuf_state: Option<(DmabufState, DmabufGlobal)>, 68 | } 69 | 70 | impl DmabufHandler for MagmaState { 71 | fn dmabuf_state(&mut self) -> &mut DmabufState { 72 | &mut self.backend_data.dmabuf_state.as_mut().unwrap().0 73 | } 74 | 75 | fn dmabuf_imported(&mut self, _global: &DmabufGlobal, dmabuf: Dmabuf) -> Result<(), ImportError> { 76 | self.backend_data 77 | .gpus 78 | .single_renderer(&self.backend_data.primary_gpu) 79 | .and_then(|mut renderer| renderer.import_dmabuf(&dmabuf, None)) 80 | .map(|_| ()) 81 | .map_err(|_| ImportError::Failed) 82 | } 83 | } 84 | delegate_dmabuf!(MagmaState); 85 | 86 | impl Backend for UdevData { 87 | fn seat_name(&self) -> String { 88 | self.session.seat() 89 | } 90 | } 91 | pub struct Device { 92 | pub surfaces: HashMap, 93 | pub gbm: GbmDevice, 94 | pub drm: DrmDevice, 95 | pub drm_scanner: DrmScanner, 96 | pub render_node: DrmNode, 97 | pub registration_token: RegistrationToken, 98 | } 99 | 100 | pub fn init_udev() { 101 | let mut event_loop: EventLoop> = EventLoop::try_new().unwrap(); 102 | let mut display: Display> = Display::new().unwrap(); 103 | 104 | /* 105 | * Initialize session 106 | */ 107 | let (session, notifier) = match LibSeatSession::new() { 108 | Ok(ret) => ret, 109 | Err(err) => { 110 | error!("Could not initialize a session: {}", err); 111 | return; 112 | } 113 | }; 114 | 115 | /* 116 | * Initialize the compositor 117 | */ 118 | let (primary_gpu, _) = primary_gpu(&session.seat()); 119 | info!("Using {} as primary gpu.", primary_gpu); 120 | 121 | let gpus = GpuManager::new(Default::default()).unwrap(); 122 | 123 | let data = UdevData { 124 | handle: event_loop.handle(), 125 | session, 126 | primary_gpu, 127 | gpus, 128 | devices: HashMap::new(), 129 | dmabuf_state: None, 130 | }; 131 | 132 | let mut state = MagmaState::new(event_loop.handle(), event_loop.get_signal(), &mut display, data); 133 | ScreencopyManagerState::new::>(&display.handle()); 134 | 135 | /* 136 | * Add input source 137 | */ 138 | let mut libinput_context = Libinput::new_with_udev::>( 139 | state.backend_data.session.clone().into(), 140 | ); 141 | libinput_context 142 | .udev_assign_seat(&state.backend_data.session.seat()) 143 | .unwrap(); 144 | 145 | let libinput_backend = LibinputInputBackend::new(libinput_context.clone()); 146 | 147 | event_loop 148 | .handle() 149 | .insert_source(libinput_backend, move |event, _, calloopdata| { 150 | if let Some(vt) = calloopdata.state.process_input_event_udev(event) { 151 | info!(to = vt, "Trying to switch vt"); 152 | if let Err(err) = calloopdata.state.backend_data.session.change_vt(vt) { 153 | error!(vt, "Error switching vt: {}", err); 154 | } 155 | } 156 | }) 157 | .unwrap(); 158 | 159 | event_loop 160 | .handle() 161 | .insert_source(notifier, move |event, _, data| { 162 | match event { 163 | SessionEvent::PauseSession => { 164 | libinput_context.suspend(); 165 | info!("pausing session"); 166 | 167 | for backend in data.state.backend_data.devices.values() { 168 | backend.drm.pause(); 169 | } 170 | } 171 | SessionEvent::ActivateSession => { 172 | info!("resuming session"); 173 | 174 | if let Err(err) = libinput_context.resume() { 175 | error!("Failed to resume libinput context: {:?}", err); 176 | } 177 | for (node, backend) in data 178 | .state 179 | .backend_data 180 | .devices 181 | .iter_mut() 182 | .map(|(handle, backend)| (*handle, backend)) 183 | { 184 | backend.drm.activate(); 185 | for (crtc, surface) in backend.surfaces.iter_mut().map(|(handle, surface)| (*handle, surface)) { 186 | if let Err(err) = surface.compositor.surface().reset_state() { 187 | warn!("Failed to reset drm surface state: {}", err); 188 | } 189 | // reset the buffers after resume to trigger a full redraw 190 | // this is important after a vt switch as the primary plane 191 | // has no content and damage tracking may prevent a redraw 192 | // otherwise 193 | surface.compositor.reset_buffers(); 194 | data.state.loop_handle.insert_idle(move |data| { 195 | if let Some(err) = data.state.render(node, crtc, None).err() { 196 | if let SwapBuffersError::ContextLost(_) = err { 197 | info!("Context lost on device {}, re-creating", node); 198 | data.state.on_device_removed(node); 199 | data.state.on_device_added(node, node.dev_path().unwrap(), &mut data.display); 200 | } 201 | } 202 | }); 203 | 204 | } 205 | } 206 | } 207 | } 208 | }) 209 | .unwrap(); 210 | 211 | /* 212 | * Initialize Udev 213 | */ 214 | 215 | let backend = UdevBackend::new(&state.seat_name).unwrap(); 216 | for (device_id, path) in backend.device_list() { 217 | state.on_udev_event( 218 | UdevEvent::Added { 219 | device_id, 220 | path: path.to_owned(), 221 | }, 222 | &mut display, 223 | ); 224 | } 225 | 226 | let renderer = state.backend_data.gpus.single_renderer(&primary_gpu).unwrap(); 227 | // init dmabuf support with format list from our primary gpu 228 | let dmabuf_formats = renderer.dmabuf_formats().collect::>(); 229 | let default_feedback = DmabufFeedbackBuilder::new(primary_gpu.dev_id(), dmabuf_formats) 230 | .build() 231 | .unwrap(); 232 | let mut dmabuf_state = DmabufState::new(); 233 | let global = dmabuf_state 234 | .create_global_with_default_feedback::>(&display.handle(), &default_feedback); 235 | state.backend_data.dmabuf_state = Some((dmabuf_state, global)); 236 | 237 | let gpus = &mut state.backend_data.gpus; 238 | state.backend_data.devices.values_mut().for_each(|backend_data| { 239 | // Update the per drm surface dmabuf feedback 240 | backend_data.surfaces.values_mut().for_each(|surface_data| { 241 | surface_data.dmabuf_feedback = surface_data.dmabuf_feedback.take().or_else(|| { 242 | get_surface_dmabuf_feedback( 243 | primary_gpu, 244 | surface_data.render_node, 245 | gpus, 246 | &surface_data.compositor, 247 | ) 248 | }); 249 | }); 250 | }); 251 | 252 | event_loop 253 | .handle() 254 | .insert_source(backend, |event, _, calloopdata| { 255 | calloopdata 256 | .state 257 | .on_udev_event(event, &mut calloopdata.display) 258 | }) 259 | .unwrap(); 260 | 261 | let mut calloopdata = CalloopData { state, display }; 262 | 263 | std::env::set_var("WAYLAND_DISPLAY", &calloopdata.state.socket_name); 264 | calloopdata.state.config.autostart.push("dbus-update-activation-environment --all".to_string()); 265 | for command in &calloopdata.state.config.autostart { 266 | if let Err(err) = std::process::Command::new("/bin/sh") 267 | .arg("-c") 268 | .arg(command) 269 | .spawn() 270 | { 271 | info!("{} {} {}", err, "Failed to spawn \"{}\"", command); 272 | } 273 | } 274 | 275 | event_loop 276 | .run(None, &mut calloopdata, move |data| { 277 | data.state.workspaces.all_windows().for_each(|e| e.refresh()); 278 | data.state.popup_manager.cleanup(); 279 | data.display.flush_clients().unwrap(); 280 | }) 281 | .unwrap(); 282 | } 283 | 284 | pub fn primary_gpu(seat: &str) -> (DrmNode, PathBuf) { 285 | // TODO: can't this be in smithay? 286 | // primary_gpu() does the same thing anyway just without `NodeType::Render` check 287 | // so perhaps `primary_gpu(seat, node_type)`? 288 | udev::primary_gpu(seat) 289 | .unwrap() 290 | .and_then(|p| { 291 | DrmNode::from_path(&p) 292 | .ok()? 293 | .node_with_type(NodeType::Render)? 294 | .ok() 295 | .map(|node| (node, p)) 296 | }) 297 | .unwrap_or_else(|| { 298 | udev::all_gpus(seat) 299 | .unwrap() 300 | .into_iter() 301 | .find_map(|p| { 302 | DrmNode::from_path(&p) 303 | .ok()? 304 | .node_with_type(NodeType::Render)? 305 | .ok() 306 | .map(|node| (node, p)) 307 | }) 308 | .expect("No GPU!") 309 | }) 310 | } 311 | 312 | // Drm 313 | impl MagmaState { 314 | pub fn on_drm_event( 315 | &mut self, 316 | node: DrmNode, 317 | event: drm::DrmEvent, 318 | _meta: &mut Option, 319 | ) { 320 | match event { 321 | drm::DrmEvent::VBlank(crtc) => { 322 | let device = self.backend_data.devices.get_mut(&node).unwrap(); 323 | let surface = device.surfaces.get_mut(&crtc).unwrap(); 324 | surface.compositor.frame_submitted().ok(); 325 | self.render( 326 | node, 327 | crtc, 328 | None 329 | ).ok(); 330 | 331 | } 332 | drm::DrmEvent::Error(_) => {} 333 | } 334 | } 335 | 336 | pub fn on_connector_event( 337 | &mut self, 338 | node: DrmNode, 339 | event: drm_scanner::DrmScanEvent, 340 | display: &mut Display>, 341 | ) { 342 | let device = if let Some(device) = self.backend_data.devices.get_mut(&node) { 343 | device 344 | } else { 345 | error!("Received connector event for unknown device: {:?}", node); 346 | return; 347 | }; 348 | 349 | match event { 350 | DrmScanEvent::Connected { 351 | connector, 352 | crtc: Some(crtc), 353 | } => { 354 | let mut renderer = self 355 | .backend_data 356 | .gpus 357 | .single_renderer(&device.render_node) 358 | .unwrap(); 359 | 360 | let name = format!( 361 | "{}-{}", 362 | connector.interface().as_str(), 363 | connector.interface_id() 364 | ); 365 | 366 | let drm_mode = if self.config.outputs.contains_key(&name) { 367 | let output_config = &self.config.outputs[&name]; 368 | *connector 369 | .modes() 370 | .iter() 371 | .filter(|mode| { 372 | let (x, y) = mode.size(); 373 | Size::from((x as i32, y as i32)) == output_config.mode_size() 374 | }) 375 | // and then select the closest refresh rate (e.g. to match 59.98 as 60) 376 | .min_by_key(|mode| { 377 | let refresh_rate = WlMode::from(**mode).refresh; 378 | (output_config.mode_refresh() as i32 - refresh_rate as i32).abs() 379 | }).expect("No matching mode found for output config") 380 | } else { 381 | *connector 382 | .modes() 383 | .iter() 384 | .find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) 385 | .unwrap_or(&connector.modes()[0]) 386 | }; 387 | 388 | let drm_surface = device.drm 389 | .create_surface(crtc, drm_mode, &[connector.handle()]) 390 | .unwrap(); 391 | 392 | 393 | let (make, model) = EdidInfo::for_connector(&device.drm, connector.handle()) 394 | .map(|info| (info.manufacturer, info.model)) 395 | .unwrap_or_else(|| ("Unknown".into(), "Unknown".into())); 396 | 397 | let (w, h) = connector.size().unwrap_or((0, 0)); 398 | let output = Output::new( 399 | name, 400 | PhysicalProperties { 401 | size: (w as i32, h as i32).into(), 402 | subpixel: smithay::output::Subpixel::Unknown, 403 | make, 404 | model, 405 | }, 406 | ); 407 | let global = output.create_global::>(&display.handle()); 408 | let output_mode = WlMode::from(drm_mode); 409 | output.set_preferred(output_mode); 410 | output.change_current_state( 411 | Some(output_mode), 412 | Some(Transform::Normal), 413 | Some(smithay::output::Scale::Integer(1)), 414 | None, 415 | ); 416 | let render_formats = renderer.as_mut().egl_context().dmabuf_render_formats().clone(); 417 | let gbm_allocator = GbmAllocator::new(device.gbm.clone(), GbmBufferFlags::RENDERING); 418 | 419 | let driver = match device.drm.get_driver() { 420 | Ok(driver) => driver, 421 | Err(err) => { 422 | warn!("Failed to query drm driver: {}", err); 423 | return; 424 | } 425 | }; 426 | 427 | let mut planes = match drm_surface.planes() { 428 | Ok(planes) => planes, 429 | Err(err) => { 430 | warn!("Failed to query surface planes: {}", err); 431 | return; 432 | } 433 | }; 434 | 435 | // Using an overlay plane on a nvidia card breaks 436 | if driver.name().to_string_lossy().to_lowercase().contains("nvidia") 437 | || driver 438 | .description() 439 | .to_string_lossy() 440 | .to_lowercase() 441 | .contains("nvidia") 442 | { 443 | planes.overlay = vec![]; 444 | } 445 | 446 | let compositor = GbmDrmCompositor::new( 447 | &output, 448 | drm_surface, 449 | Some(planes), 450 | gbm_allocator, 451 | device.gbm.clone(), 452 | SUPPORTED_FORMATS, 453 | render_formats, 454 | device.drm.cursor_size(), 455 | Some(device.gbm.clone()) 456 | 457 | ).unwrap(); 458 | 459 | let pointer_texture = TextureBuffer::from_memory( 460 | &mut renderer, 461 | CURSOR_DATA, 462 | Fourcc::Abgr8888, 463 | (64, 64), 464 | false, 465 | 1, 466 | Transform::Normal, 467 | None, 468 | ) 469 | .unwrap(); 470 | 471 | let dmabuf_feedback = get_surface_dmabuf_feedback( 472 | self.backend_data.primary_gpu, 473 | device.render_node, 474 | &mut self.backend_data.gpus, 475 | &compositor, 476 | ); 477 | 478 | let surface = Surface { 479 | _dh: display.handle(), 480 | _device_id: node, 481 | render_node: device.render_node, 482 | global, 483 | compositor, 484 | dmabuf_feedback, 485 | output: output.clone(), 486 | pointer_texture, 487 | }; 488 | 489 | for workspace in self.workspaces.iter() { 490 | workspace.remove_outputs(); 491 | workspace.add_output(output.clone()) 492 | } 493 | 494 | device.surfaces.insert(crtc, surface); 495 | 496 | self.render( 497 | node, 498 | crtc, 499 | None 500 | ).ok(); 501 | } 502 | DrmScanEvent::Disconnected { 503 | crtc: Some(crtc), .. 504 | } => { 505 | device.surfaces.remove(&crtc); 506 | } 507 | _ => {} 508 | } 509 | } 510 | } 511 | 512 | // Udev 513 | impl MagmaState { 514 | pub fn on_udev_event(&mut self, event: UdevEvent, display: &mut Display>) { 515 | match event { 516 | UdevEvent::Added { device_id, path } => { 517 | if let Ok(node) = DrmNode::from_dev_id(device_id) { 518 | self.on_device_added(node, path, display); 519 | } 520 | } 521 | UdevEvent::Changed { device_id } => { 522 | if let Ok(node) = DrmNode::from_dev_id(device_id) { 523 | self.on_device_changed(node, display); 524 | } 525 | } 526 | UdevEvent::Removed { device_id } => { 527 | if let Ok(node) = DrmNode::from_dev_id(device_id) { 528 | self.on_device_removed(node); 529 | } 530 | } 531 | } 532 | } 533 | 534 | fn on_device_added( 535 | &mut self, 536 | node: DrmNode, 537 | path: PathBuf, 538 | display: &mut Display>, 539 | ) { 540 | let fd = self 541 | .backend_data 542 | .session 543 | .open( 544 | &path, 545 | OFlag::O_RDWR | OFlag::O_CLOEXEC | OFlag::O_NOCTTY | OFlag::O_NONBLOCK, 546 | ) 547 | .unwrap(); 548 | 549 | let fd = DrmDeviceFd::new(unsafe { DeviceFd::from_raw_fd(fd) }); 550 | 551 | let (drm, drm_notifier) = drm::DrmDevice::new(fd, true).unwrap(); 552 | 553 | let gbm = gbm::GbmDevice::new(drm.device_fd().clone()).unwrap(); 554 | 555 | // Make sure display is dropped before we call add_node 556 | let render_node = 557 | match EGLDevice::device_for_display(&EGLDisplay::new(gbm.clone()).unwrap()) 558 | .ok() 559 | .and_then(|x| x.try_get_render_node().ok().flatten()) 560 | { 561 | Some(node) => node, 562 | None => node, 563 | }; 564 | 565 | self.backend_data 566 | .gpus 567 | .as_mut() 568 | .add_node(render_node, gbm.clone()) 569 | .unwrap(); 570 | 571 | let registration_token = self.backend_data 572 | .handle 573 | .insert_source(drm_notifier, move |event, meta, calloopdata| { 574 | calloopdata 575 | .state 576 | .on_drm_event(node, event, meta); 577 | }) 578 | .unwrap(); 579 | 580 | self.backend_data.devices.insert( 581 | node, 582 | Device { 583 | drm, 584 | gbm, 585 | drm_scanner: Default::default(), 586 | surfaces: Default::default(), 587 | render_node, 588 | registration_token, 589 | }, 590 | ); 591 | 592 | self.on_device_changed(node, display); 593 | } 594 | 595 | fn on_device_changed(&mut self, node: DrmNode, display: &mut Display>) { 596 | if let Some(device) = self.backend_data.devices.get_mut(&node) { 597 | for event in device.drm_scanner.scan_connectors(&device.drm) { 598 | self.on_connector_event(node, event, display); 599 | } 600 | } 601 | } 602 | 603 | fn on_device_removed(&mut self, node: DrmNode) { 604 | if let Some(device) = self.backend_data.devices.get_mut(&node) { 605 | self.backend_data 606 | .gpus 607 | .as_mut() 608 | .remove_node(&device.render_node); 609 | 610 | for surface in device.surfaces.values() { 611 | self.dh.disable_global::>(surface.global.clone()); 612 | } 613 | } 614 | } 615 | } 616 | 617 | pub struct Surface { 618 | _dh: DisplayHandle, 619 | _device_id: DrmNode, 620 | render_node: DrmNode, 621 | global: GlobalId, 622 | compositor: GbmDrmCompositor, 623 | dmabuf_feedback: Option, 624 | output: Output, 625 | pointer_texture: TextureBuffer, 626 | } 627 | 628 | impl MagmaState { 629 | pub fn render( 630 | &mut self, 631 | node: DrmNode, 632 | crtc: Handle, 633 | screencopy: Option, 634 | ) -> Result 635 | { 636 | let device = self.backend_data.devices.get_mut(&node).unwrap(); 637 | let surface = device.surfaces.get_mut(&crtc).unwrap(); 638 | let mut renderer = self.backend_data.gpus.single_renderer(&device.render_node).unwrap(); 639 | let output = self.workspaces.current().outputs().next().unwrap(); 640 | 641 | let mut renderelements: Vec>> = vec![]; 642 | 643 | renderelements.append(&mut vec![CustomRenderElements::>::from( 644 | TextureRenderElement::from_texture_buffer( 645 | self.pointer_location.to_physical(Scale::from(1.0)), 646 | &surface.pointer_texture, 647 | None, 648 | None, 649 | None, 650 | ), 651 | )]); 652 | 653 | let layer_map = layer_map_for_output(&output); 654 | let (lower, upper): (Vec<&LayerSurface>, Vec<&LayerSurface>) = layer_map 655 | .layers() 656 | .rev() 657 | .partition(|s| matches!(s.layer(), Layer::Background | Layer::Bottom)); 658 | 659 | renderelements.extend( 660 | upper 661 | .into_iter() 662 | .filter_map(|surface| { 663 | layer_map 664 | .layer_geometry(surface) 665 | .map(|geo| (geo.loc, surface)) 666 | }) 667 | .flat_map(|(loc, surface)| { 668 | AsRenderElements::>::render_elements::>>( 669 | surface, 670 | &mut renderer, 671 | loc.to_physical_precise_round(1), 672 | Scale::from(1.0), 673 | ) 674 | .into_iter() 675 | .map(CustomRenderElements::Surface) 676 | }), 677 | ); 678 | 679 | renderelements.extend(self.workspaces.current().render_elements(&mut renderer)); 680 | 681 | renderelements.extend( 682 | lower 683 | .into_iter() 684 | .filter_map(|surface| { 685 | layer_map 686 | .layer_geometry(surface) 687 | .map(|geo| (geo.loc, surface)) 688 | }) 689 | .flat_map(|(loc, surface)| { 690 | AsRenderElements::>::render_elements::>>( 691 | surface, 692 | &mut renderer, 693 | loc.to_physical_precise_round(1), 694 | Scale::from(1.0), 695 | ) 696 | .into_iter() 697 | .map(CustomRenderElements::Surface) 698 | }), 699 | ); 700 | 701 | let frame_result = surface.compositor 702 | .render_frame::<_, _, GlesTexture>( 703 | &mut renderer, 704 | &renderelements, 705 | [0.1, 0.1, 0.1, 1.0], 706 | ) 707 | .unwrap(); 708 | 709 | // Copy framebuffer for screencopy. 710 | if let Some(mut screencopy) = screencopy { 711 | // Mark entire buffer as damaged. 712 | let region = screencopy.region(); 713 | if let Some(damage) = frame_result.damage.clone() { 714 | screencopy.damage(&damage); 715 | } 716 | 717 | let shm_buffer = screencopy.buffer(); 718 | 719 | // Ignore unknown buffer types. 720 | let buffer_type = renderer::buffer_type(shm_buffer); 721 | if !matches!(buffer_type, Some(BufferType::Shm)) { 722 | warn!("Unsupported buffer type: {:?}", buffer_type); 723 | } else 724 | { 725 | // Create and bind an offscreen render buffer. 726 | let buffer_dimensions = renderer::buffer_dimensions(shm_buffer).unwrap(); 727 | let offscreen_buffer = Offscreen::::create_buffer(&mut renderer,Fourcc::Argb8888,buffer_dimensions).unwrap(); 728 | renderer.bind(offscreen_buffer).unwrap(); 729 | 730 | let output = &screencopy.output; 731 | let scale = output.current_scale().fractional_scale(); 732 | let output_size = output.current_mode().unwrap().size; 733 | let transform = output.current_transform(); 734 | 735 | // Calculate drawing area after output transform. 736 | let damage = transform.transform_rect_in(region, &output_size); 737 | 738 | frame_result.blit_frame_result(damage.size, transform, scale, &mut renderer, [damage], []).unwrap(); 739 | 740 | let region = Rectangle { loc: Point::from((region.loc.x, region.loc.y)), size: Size::from((region.size.w, region.size.h)) }; 741 | let mapping = renderer.copy_framebuffer(region, Fourcc::Argb8888).unwrap(); 742 | let buffer = renderer.map_texture(&mapping); 743 | // shm_buffer. 744 | // Copy offscreen buffer's content to the SHM buffer. 745 | shm::with_buffer_contents_mut(shm_buffer, |shm_buffer_ptr, shm_len, buffer_data| { 746 | // Ensure SHM buffer is in an acceptable format. 747 | if dbg!(buffer_data.format) != wl_shm::Format::Argb8888 748 | || buffer_data.stride != region.size.w * 4 749 | || buffer_data.height != region.size.h 750 | || shm_len as i32 != buffer_data.stride * buffer_data.height 751 | { 752 | error!("Invalid buffer format"); 753 | return; 754 | } 755 | 756 | // Copy the offscreen buffer's content to the SHM buffer. 757 | unsafe { shm_buffer_ptr.copy_from(buffer.unwrap().as_ptr(), shm_len) }; 758 | }).unwrap(); 759 | } 760 | 761 | // Mark screencopy frame as successful. 762 | screencopy.submit(); 763 | } 764 | let rendered = frame_result.damage.is_some(); 765 | let mut result = Ok(rendered); 766 | if rendered { 767 | let queueresult = surface.compositor.queue_frame(()).map_err(Into::::into); 768 | if queueresult.is_err() {result = Err(queueresult.unwrap_err());} 769 | } 770 | 771 | let reschedule = match &result { 772 | Ok(has_rendered) => !has_rendered, 773 | Err(err) => { 774 | warn!("Error during rendering: {:?}", err); 775 | match err { 776 | SwapBuffersError::AlreadySwapped => false, 777 | SwapBuffersError::TemporaryFailure(err) => !matches!( 778 | err.downcast_ref::(), 779 | Some(&DrmError::DeviceInactive) 780 | | Some(&DrmError::Access { 781 | source: SystemError::PermissionDenied, 782 | .. 783 | }) 784 | ), 785 | SwapBuffersError::ContextLost(err) => {warn!("Rendering loop lost: {}", err); false}, 786 | } 787 | } 788 | }; 789 | 790 | if reschedule { 791 | let output_refresh = match output.current_mode() { 792 | Some(mode) => mode.refresh, 793 | None => return result, 794 | }; 795 | // If reschedule is true we either hit a temporary failure or more likely rendering 796 | // did not cause any damage on the output. In this case we just re-schedule a repaint 797 | // after approx. one frame to re-test for damage. 798 | let reschedule_duration = Duration::from_millis((1_000_000f32 / output_refresh as f32) as u64); 799 | trace!( 800 | "reschedule repaint timer with delay {:?} on {:?}", 801 | reschedule_duration, 802 | crtc, 803 | ); 804 | let timer = Timer::from_duration(reschedule_duration); 805 | self.loop_handle 806 | .insert_source(timer, move |_, _, data| { 807 | data.state.render(node, crtc,None).ok(); 808 | TimeoutAction::Drop 809 | }) 810 | .expect("failed to schedule frame timer"); 811 | } 812 | 813 | self.workspaces.current().windows().for_each(|window| { 814 | window.send_frame( 815 | &output, 816 | self.start_time.elapsed(), 817 | Some(Duration::ZERO), 818 | |_, _| Some(output.clone()), 819 | ); 820 | }); 821 | for layer_surface in layer_map.layers() { 822 | layer_surface.send_frame( 823 | &output, 824 | self.start_time.elapsed(), 825 | Some(Duration::ZERO), 826 | |_, _| Some(output.clone()), 827 | ); 828 | } 829 | 830 | result 831 | } 832 | } 833 | 834 | fn get_surface_dmabuf_feedback( 835 | primary_gpu: DrmNode, 836 | render_node: DrmNode, 837 | gpus: &mut GpuManager>, 838 | compositor: &GbmDrmCompositor, 839 | ) -> Option { 840 | let primary_formats = gpus 841 | .single_renderer(&primary_gpu) 842 | .ok()? 843 | .dmabuf_formats() 844 | .collect::>(); 845 | 846 | let render_formats = gpus 847 | .single_renderer(&render_node) 848 | .ok()? 849 | .dmabuf_formats() 850 | .collect::>(); 851 | 852 | let all_render_formats = primary_formats 853 | .iter() 854 | .chain(render_formats.iter()) 855 | .copied() 856 | .collect::>(); 857 | 858 | let surface = compositor.surface(); 859 | let planes = surface.planes().unwrap(); 860 | // We limit the scan-out trache to formats we can also render from 861 | // so that there is always a fallback render path available in case 862 | // the supplied buffer can not be scanned out directly 863 | let planes_formats = surface 864 | .supported_formats(planes.primary.handle) 865 | .unwrap() 866 | .into_iter() 867 | .chain( 868 | planes 869 | .overlay 870 | .iter() 871 | .flat_map(|p| surface.supported_formats(p.handle).unwrap()), 872 | ) 873 | .collect::>() 874 | .intersection(&all_render_formats) 875 | .copied() 876 | .collect::>(); 877 | 878 | let builder = DmabufFeedbackBuilder::new(primary_gpu.dev_id(), primary_formats); 879 | let _render_feedback = builder 880 | .clone() 881 | .add_preference_tranche(render_node.dev_id(), None, render_formats.clone()) 882 | .build() 883 | .unwrap(); 884 | 885 | let _scanout_feedback = builder 886 | .add_preference_tranche( 887 | surface.device_fd().dev_id().unwrap(), 888 | Some(zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout), 889 | planes_formats, 890 | ) 891 | .add_preference_tranche(render_node.dev_id(), None, render_formats) 892 | .build() 893 | .unwrap(); 894 | 895 | Some(DrmSurfaceDmabufFeedback { 896 | _render_feedback, 897 | _scanout_feedback, 898 | }) 899 | } 900 | 901 | struct DrmSurfaceDmabufFeedback { 902 | _render_feedback: DmabufFeedback, 903 | _scanout_feedback: DmabufFeedback, 904 | } 905 | 906 | impl ScreencopyHandler for MagmaState { 907 | fn output(&mut self, output: &WlOutput) -> &Output { 908 | self.workspaces.outputs().find(|o| o.owns(output)).unwrap() 909 | } 910 | 911 | fn frame(&mut self, frame: Screencopy) { 912 | for (node, device) in &self.backend_data.devices { 913 | for (crtc, surface) in &device.surfaces { 914 | if surface.output == frame.output { 915 | self.render(*node, *crtc, Some(frame)).ok(); 916 | return; 917 | } 918 | } 919 | } 920 | } 921 | } 922 | 923 | delegate_screencopy_manager!(MagmaState); -------------------------------------------------------------------------------- /src/backends/winit.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use smithay::{ 4 | backend::{ 5 | renderer::{ 6 | damage::OutputDamageTracker, element::{surface::WaylandSurfaceRenderElement, AsRenderElements}, 7 | gles::GlesRenderer, 8 | }, 9 | winit::{self, WinitError, WinitEvent, WinitEventLoop, WinitGraphicsBackend}, 10 | }, 11 | desktop::{space::SpaceElement, layer_map_for_output, LayerSurface}, 12 | output::{Mode, Output, PhysicalProperties, Subpixel}, 13 | reexports::{ 14 | calloop::{ 15 | timer::{TimeoutAction, Timer}, 16 | EventLoop, 17 | }, 18 | wayland_server::Display, 19 | }, 20 | utils::{Rectangle, Transform, Scale}, wayland::shell::wlr_layer::Layer, 21 | }; 22 | 23 | pub struct WinitData { 24 | backend: WinitGraphicsBackend, 25 | damage_tracker: OutputDamageTracker, 26 | } 27 | 28 | impl Backend for WinitData { 29 | fn seat_name(&self) -> String { 30 | "winit".to_string() 31 | } 32 | } 33 | use crate::{state::Backend, CalloopData, MagmaState}; 34 | 35 | pub fn init_winit() { 36 | let mut event_loop: EventLoop> = EventLoop::try_new().unwrap(); 37 | 38 | let mut display: Display> = Display::new().unwrap(); 39 | 40 | let (backend, mut winit) = winit::init().unwrap(); 41 | 42 | let mode = Mode { 43 | size: backend.window_size().physical_size, 44 | refresh: 60_000, 45 | }; 46 | 47 | let output = Output::new( 48 | "winit".to_string(), 49 | PhysicalProperties { 50 | size: (0, 0).into(), 51 | subpixel: Subpixel::Unknown, 52 | make: "Smithay".into(), 53 | model: "Winit".into(), 54 | }, 55 | ); 56 | let _global = output.create_global::>(&display.handle()); 57 | output.change_current_state( 58 | Some(mode), 59 | Some(Transform::Flipped180), 60 | None, 61 | Some((0, 0).into()), 62 | ); 63 | output.set_preferred(mode); 64 | 65 | let damage_tracked_renderer = OutputDamageTracker::from_output(&output); 66 | 67 | let winitdata = WinitData { 68 | backend, 69 | damage_tracker: damage_tracked_renderer, 70 | }; 71 | let state = MagmaState::new(event_loop.handle(), event_loop.get_signal(), &mut display, winitdata); 72 | 73 | let mut data = CalloopData { 74 | display, 75 | state: state, 76 | }; 77 | 78 | let state = &mut data.state; 79 | 80 | // map output to every workspace 81 | for workspace in state.workspaces.iter() { 82 | workspace.add_output(output.clone()); 83 | } 84 | 85 | std::env::set_var("WAYLAND_DISPLAY", &state.socket_name); 86 | 87 | let mut full_redraw = 0u8; 88 | 89 | let timer = Timer::immediate(); 90 | 91 | event_loop 92 | .handle() 93 | .insert_source(timer, move |_, _, data| { 94 | winit_dispatch(&mut winit, data, &output, &mut full_redraw); 95 | TimeoutAction::ToDuration(Duration::from_millis(16)) 96 | }) 97 | .unwrap(); 98 | 99 | event_loop 100 | .run(None, &mut data, move |_| { 101 | // Magma is running 102 | }) 103 | .unwrap(); 104 | } 105 | 106 | pub fn winit_dispatch( 107 | winit: &mut WinitEventLoop, 108 | data: &mut CalloopData, 109 | output: &Output, 110 | full_redraw: &mut u8, 111 | ) { 112 | let display = &mut data.display; 113 | let state = &mut data.state; 114 | 115 | let res = winit.dispatch_new_events(|event| match event { 116 | WinitEvent::Resized { size, .. } => { 117 | output.change_current_state( 118 | Some(Mode { 119 | size, 120 | refresh: 60_000, 121 | }), 122 | None, 123 | None, 124 | None, 125 | ); 126 | } 127 | WinitEvent::Input(event) => state.process_input_event(event), 128 | _ => (), 129 | }); 130 | 131 | let winitdata = &mut state.backend_data; 132 | 133 | if let Err(WinitError::WindowClosed) = res { 134 | // Stop the loop 135 | state.loop_signal.stop(); 136 | } else { 137 | res.unwrap(); 138 | } 139 | 140 | *full_redraw = full_redraw.saturating_sub(1); 141 | 142 | let size = winitdata.backend.window_size().physical_size; 143 | let damage = Rectangle::from_loc_and_size((0, 0), size); 144 | 145 | winitdata.backend.bind().unwrap(); 146 | 147 | let mut renderelements: Vec> = vec![]; 148 | 149 | let workspace = state.workspaces.current_mut(); 150 | let output = workspace.outputs().next().unwrap(); 151 | let layer_map = layer_map_for_output(&output); 152 | let (lower, upper): (Vec<&LayerSurface>, Vec<&LayerSurface>) = layer_map 153 | .layers() 154 | .rev() 155 | .partition(|s| matches!(s.layer(), Layer::Background | Layer::Bottom)); 156 | 157 | renderelements.extend( 158 | upper 159 | .into_iter() 160 | .filter_map(|surface| { 161 | layer_map 162 | .layer_geometry(surface) 163 | .map(|geo| (geo.loc, surface)) 164 | }) 165 | .flat_map(|(loc, surface)| { 166 | AsRenderElements::::render_elements::>( 167 | surface, 168 | winitdata.backend.renderer(), 169 | loc.to_physical_precise_round(1), 170 | Scale::from(1.0), 171 | ) 172 | }), 173 | ); 174 | 175 | renderelements.extend(workspace 176 | .render_elements(winitdata.backend.renderer())); 177 | 178 | renderelements.extend( 179 | lower 180 | .into_iter() 181 | .filter_map(|surface| { 182 | layer_map 183 | .layer_geometry(surface) 184 | .map(|geo| (geo.loc, surface)) 185 | }) 186 | .flat_map(|(loc, surface)| { 187 | AsRenderElements::::render_elements::>( 188 | surface, 189 | winitdata.backend.renderer(), 190 | loc.to_physical_precise_round(1), 191 | Scale::from(1.0), 192 | ) 193 | }), 194 | ); 195 | winitdata 196 | .damage_tracker 197 | .render_output( 198 | winitdata.backend.renderer(), 199 | 0, 200 | &renderelements, 201 | [0.1, 0.1, 0.1, 1.0], 202 | ) 203 | .unwrap(); 204 | 205 | winitdata.backend.submit(Some(&[damage])).unwrap(); 206 | 207 | workspace.windows().for_each(|window| { 208 | window.send_frame( 209 | output, 210 | state.start_time.elapsed(), 211 | Some(Duration::ZERO), 212 | |_, _| Some(output.clone()), 213 | ) 214 | }); 215 | 216 | workspace.windows().for_each(|e| e.refresh()); 217 | state.popup_manager.cleanup(); 218 | display.flush_clients().unwrap(); 219 | } 220 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fs::OpenOptions}; 2 | 3 | use serde::Deserialize; 4 | use smithay::{output::Mode, utils::{Size, Physical}}; 5 | 6 | use self::types::{deserialize_KeyModifiers, deserialize_Keysym, XkbConfig}; 7 | 8 | mod types; 9 | #[derive(Debug, Deserialize)] 10 | pub struct Config { 11 | pub workspaces: u8, 12 | pub keybindings: HashMap, 13 | 14 | #[serde(default = "default_gaps")] 15 | pub gaps: (i32, i32), 16 | #[serde(default = "default_outputs")] 17 | pub outputs: HashMap, 18 | #[serde(default = "default_autostart")] 19 | pub autostart: Vec, 20 | 21 | pub xkb: XkbConfig, 22 | } 23 | 24 | #[derive(Debug, Deserialize, Clone)] 25 | pub struct OutputConfig ((i32, i32), Option); 26 | 27 | 28 | impl OutputConfig { 29 | pub fn mode_size(&self) -> Size { 30 | self.0.into() 31 | } 32 | 33 | pub fn mode_refresh(&self) -> u32 { 34 | self.1.unwrap_or(60_000) 35 | } 36 | 37 | pub fn output_mode(&self) -> Mode { 38 | Mode { 39 | size: self.mode_size(), 40 | refresh: self.mode_refresh() as i32, 41 | } 42 | } 43 | } 44 | 45 | impl Config { 46 | pub fn load() -> Config { 47 | let xdg = xdg::BaseDirectories::new().ok(); 48 | let locations = if let Some(base) = xdg { 49 | vec![ 50 | base.get_config_file("magma.ron"), 51 | base.get_config_file("magma/config.ron"), 52 | ] 53 | } else { 54 | vec![] 55 | }; 56 | 57 | for path in locations { 58 | dbg!("Trying config location: {}", path.display()); 59 | if path.exists() { 60 | dbg!("Using config at {}", path.display()); 61 | return ron::de::from_reader(OpenOptions::new().read(true).open(path).unwrap()) 62 | .expect("Malformed config file"); 63 | } 64 | } 65 | panic!("No config file found") 66 | } 67 | } 68 | fn default_gaps() -> (i32, i32) { 69 | (5, 5) 70 | } 71 | fn default_autostart() -> Vec { 72 | vec![] 73 | } 74 | fn default_outputs() -> HashMap { 75 | HashMap::new() 76 | } 77 | 78 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] 79 | pub enum KeyModifier { 80 | Ctrl, 81 | Alt, 82 | Shift, 83 | Super, 84 | } 85 | 86 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 87 | pub struct KeyModifiers { 88 | ctrl: bool, 89 | alt: bool, 90 | shift: bool, 91 | logo: bool, 92 | } 93 | 94 | /// Describtion of a key combination that might be 95 | /// handled by the compositor. 96 | #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Hash)] 97 | #[serde(deny_unknown_fields)] 98 | pub struct KeyPattern { 99 | /// What modifiers are expected to be pressed alongside the key 100 | #[serde(deserialize_with = "deserialize_KeyModifiers")] 101 | pub modifiers: KeyModifiers, 102 | /// The actual key, that was pressed 103 | #[serde(deserialize_with = "deserialize_Keysym")] 104 | pub key: u32, 105 | } 106 | 107 | #[derive(Debug, Deserialize, Clone, PartialEq, Eq)] 108 | pub enum Action { 109 | Quit, 110 | Debug, 111 | Close, 112 | Workspace(u8), 113 | MoveWindowToWorkspace(u8), 114 | MoveWindowAndSwitchToWorkspace(u8), 115 | ToggleWindowFloating, 116 | VTSwitch(i32), 117 | Spawn(String), 118 | } 119 | -------------------------------------------------------------------------------- /src/config/types.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use smithay::input::keyboard::{keysyms as KeySyms, xkb, Keysym, ModifiersState, XkbConfig as WlXkbConfig}; 3 | 4 | use super::{KeyModifier, KeyModifiers}; 5 | 6 | #[derive(Deserialize)] 7 | #[serde(transparent)] 8 | pub struct KeyModifiersDef(Vec); 9 | 10 | impl From for KeyModifiers { 11 | fn from(src: KeyModifiersDef) -> Self { 12 | src.0.into_iter().fold( 13 | KeyModifiers { 14 | ctrl: false, 15 | alt: false, 16 | shift: false, 17 | logo: false, 18 | }, 19 | |mut modis, modi: KeyModifier| { 20 | modis += modi; 21 | modis 22 | }, 23 | ) 24 | } 25 | } 26 | 27 | #[allow(non_snake_case)] 28 | pub fn deserialize_KeyModifiers<'de, D>(deserializer: D) -> Result 29 | where 30 | D: serde::Deserializer<'de>, 31 | { 32 | KeyModifiersDef::deserialize(deserializer).map(Into::into) 33 | } 34 | 35 | #[allow(non_snake_case)] 36 | pub fn deserialize_Keysym<'de, D>(deserializer: D) -> Result 37 | where 38 | D: serde::Deserializer<'de>, 39 | { 40 | use serde::de::{Error, Unexpected}; 41 | 42 | let name = String::deserialize(deserializer)?; 43 | //let name = format!("KEY_{}", code); 44 | match xkb::keysym_from_name(&name, xkb::KEYSYM_NO_FLAGS) { 45 | KeySyms::KEY_NoSymbol => match xkb::keysym_from_name(&name, xkb::KEYSYM_CASE_INSENSITIVE) { 46 | KeySyms::KEY_NoSymbol => Err(::invalid_value( 47 | Unexpected::Str(&name), 48 | &"One of the keysym names of xkbcommon.h without the 'KEY_' prefix", 49 | )), 50 | x => { 51 | dbg!( 52 | "Key-Binding '{}' only matched case insensitive for {:?}", 53 | name, 54 | xkb::keysym_get_name(x) 55 | ); 56 | Ok(x) 57 | } 58 | }, 59 | x => Ok(x), 60 | } 61 | } 62 | 63 | impl std::ops::AddAssign for KeyModifiers { 64 | fn add_assign(&mut self, rhs: KeyModifier) { 65 | match rhs { 66 | KeyModifier::Ctrl => self.ctrl = true, 67 | KeyModifier::Alt => self.alt = true, 68 | KeyModifier::Shift => self.shift = true, 69 | KeyModifier::Super => self.logo = true, 70 | }; 71 | } 72 | } 73 | 74 | impl PartialEq for KeyModifiers { 75 | fn eq(&self, other: &ModifiersState) -> bool { 76 | self.ctrl == other.ctrl 77 | && self.alt == other.alt 78 | && self.shift == other.shift 79 | && self.logo == other.logo 80 | } 81 | } 82 | 83 | #[derive(Debug, Clone, Deserialize)] 84 | pub struct XkbConfig { 85 | pub rules: String, 86 | pub model: String, 87 | pub layout: String, 88 | pub variant: String, 89 | pub options: Option, 90 | } 91 | 92 | impl Default for XkbConfig { 93 | fn default() -> XkbConfig { 94 | XkbConfig { 95 | rules: String::new(), 96 | model: String::new(), 97 | layout: String::new(), 98 | variant: String::new(), 99 | options: None, 100 | } 101 | } 102 | } 103 | 104 | impl<'a> Into> for &'a XkbConfig { 105 | fn into(self) -> WlXkbConfig<'a> { 106 | WlXkbConfig { 107 | rules: &self.rules, 108 | model: &self.model, 109 | layout: &self.layout, 110 | variant: &self.variant, 111 | options: self.options.clone(), 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /src/handlers/compositor.rs: -------------------------------------------------------------------------------- 1 | use smithay::{ 2 | backend::renderer::utils::on_commit_buffer_handler, 3 | delegate_compositor, delegate_shm, 4 | reexports::wayland_server::protocol::wl_surface::WlSurface, 5 | wayland::{ 6 | buffer::BufferHandler, 7 | compositor::{get_parent, is_sync_subsurface, CompositorHandler, CompositorState}, 8 | shm::{ShmHandler, ShmState}, 9 | }, 10 | }; 11 | 12 | use crate::state::{Backend, MagmaState}; 13 | 14 | use super::xdg_shell; 15 | 16 | impl CompositorHandler for MagmaState { 17 | fn compositor_state(&mut self) -> &mut CompositorState { 18 | &mut self.compositor_state 19 | } 20 | 21 | fn commit(&mut self, surface: &WlSurface) { 22 | on_commit_buffer_handler(surface); 23 | if !is_sync_subsurface(surface) { 24 | let mut root = surface.clone(); 25 | while let Some(parent) = get_parent(&root) { 26 | root = parent; 27 | } 28 | if let Some(window) = self 29 | .workspaces 30 | .all_windows() 31 | .find(|w| w.toplevel().wl_surface() == &root) 32 | { 33 | window.on_commit(); 34 | } 35 | }; 36 | self.popup_manager.commit(surface); 37 | xdg_shell::handle_commit(&self.workspaces, surface, &self.popup_manager); 38 | } 39 | } 40 | 41 | delegate_compositor!(@ MagmaState); 42 | 43 | impl BufferHandler for MagmaState { 44 | fn buffer_destroyed( 45 | &mut self, 46 | _buffer: &smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer, 47 | ) { 48 | } 49 | } 50 | 51 | impl ShmHandler for MagmaState { 52 | fn shm_state(&self) -> &ShmState { 53 | &self.shm_state 54 | } 55 | } 56 | 57 | delegate_shm!(@ MagmaState); 58 | -------------------------------------------------------------------------------- /src/handlers/input.rs: -------------------------------------------------------------------------------- 1 | use tracing::info; 2 | 3 | use crate::{ 4 | config::Action, 5 | state::{Backend, MagmaState}, 6 | }; 7 | 8 | impl MagmaState { 9 | pub fn handle_action(&mut self, action: Action) { 10 | match action { 11 | Action::Quit => self.loop_signal.stop(), 12 | Action::Debug => todo!(), 13 | Action::Close => { 14 | if let Some(d) = self 15 | .workspaces 16 | .current() 17 | .window_under(self.pointer_location) 18 | { 19 | d.0.toplevel().send_close() 20 | } 21 | } 22 | Action::Workspace(id) => { 23 | self.workspaces.activate(id, &mut self.ipc_manager); 24 | self.set_input_focus_auto(); 25 | }, 26 | Action::MoveWindowToWorkspace(id) => { 27 | let window = self 28 | .workspaces 29 | .current() 30 | .window_under(self.pointer_location) 31 | .map(|d| d.0.clone()); 32 | 33 | if let Some(window) = window { 34 | self.workspaces 35 | .move_window_to_workspace(&window, id, self.config.gaps); 36 | } 37 | self.ipc_manager.update_occupied_workspaces(&mut self.workspaces); 38 | } 39 | Action::MoveWindowAndSwitchToWorkspace(u8) => { 40 | self.handle_action(Action::MoveWindowToWorkspace(u8)); 41 | self.handle_action(Action::Workspace(u8)); 42 | } 43 | Action::ToggleWindowFloating => todo!(), 44 | Action::Spawn(command) => { 45 | if let Err(err) = std::process::Command::new("/bin/sh") 46 | .arg("-c") 47 | .arg(command.clone()) 48 | .spawn() 49 | { 50 | info!("{} {} {}", err, "Failed to spawn \"{}\"", command); 51 | } 52 | } 53 | Action::VTSwitch(_) => {info!("VTSwitch is not used in Winit backend.")}, 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod compositor; 2 | pub mod input; 3 | pub mod xdg_shell; 4 | 5 | // 6 | // Wl Seat 7 | // 8 | 9 | use smithay::desktop::{layer_map_for_output, LayerSurface}; 10 | use smithay::input::{SeatHandler, SeatState}; 11 | 12 | use smithay::output::Output; 13 | use smithay::reexports::wayland_server::Resource; 14 | use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; 15 | use smithay::wayland::data_device::{ 16 | ClientDndGrabHandler, DataDeviceHandler, ServerDndGrabHandler, set_data_device_focus, 17 | }; 18 | use smithay::wayland::primary_selection::{PrimarySelectionHandler, set_primary_focus}; 19 | use smithay::wayland::seat::WaylandFocus; 20 | use smithay::wayland::shell::wlr_layer::{WlrLayerShellHandler, WlrLayerShellState, LayerSurface as WlrLayerSurface, Layer}; 21 | use smithay::{delegate_data_device, delegate_output, delegate_seat, delegate_layer_shell, delegate_primary_selection}; 22 | 23 | use crate::state::{Backend, MagmaState}; 24 | use crate::utils::focus::FocusTarget; 25 | 26 | impl SeatHandler for MagmaState { 27 | type KeyboardFocus = FocusTarget; 28 | type PointerFocus = FocusTarget; 29 | 30 | fn seat_state(&mut self) -> &mut SeatState> { 31 | &mut self.seat_state 32 | } 33 | 34 | fn cursor_image( 35 | &mut self, 36 | _seat: &smithay::input::Seat, 37 | _image: smithay::input::pointer::CursorImageStatus, 38 | ) { 39 | } 40 | fn focus_changed(&mut self, seat: &smithay::input::Seat, focused: Option<&FocusTarget>) { 41 | let dh = &self.dh; 42 | 43 | let focus = focused 44 | .and_then(WaylandFocus::wl_surface) 45 | .and_then(|s| dh.get_client(s.id()).ok()); 46 | set_data_device_focus(dh, seat, focus.clone()); 47 | set_primary_focus(dh, seat, focus); 48 | 49 | if let Some(focus_target) = focused { 50 | match focus_target { 51 | FocusTarget::Window(w) => { 52 | for window in self.workspaces.all_windows(){ 53 | if window.eq(w){ 54 | window.set_activated(true); 55 | }else{ 56 | window.set_activated(false); 57 | } 58 | window.toplevel().send_configure(); 59 | } 60 | }, 61 | FocusTarget::LayerSurface(_) => { 62 | for window in self.workspaces.all_windows() { 63 | window.set_activated(false); 64 | window.toplevel().send_configure(); 65 | } 66 | }, 67 | FocusTarget::Popup(_) => {}, 68 | }; 69 | } 70 | } 71 | } 72 | 73 | delegate_seat!(@ MagmaState); 74 | 75 | // 76 | // Wl Data Device 77 | // 78 | 79 | impl DataDeviceHandler for MagmaState { 80 | fn data_device_state(&self) -> &smithay::wayland::data_device::DataDeviceState { 81 | &self.data_device_state 82 | } 83 | } 84 | 85 | impl ClientDndGrabHandler for MagmaState {} 86 | impl ServerDndGrabHandler for MagmaState {} 87 | 88 | delegate_data_device!(@ MagmaState); 89 | 90 | impl PrimarySelectionHandler for MagmaState { 91 | fn primary_selection_state(&self) -> &smithay::wayland::primary_selection::PrimarySelectionState { 92 | &self.primary_selection_state 93 | } 94 | } 95 | 96 | delegate_primary_selection!(@ MagmaState); 97 | 98 | // 99 | // Wl Output & Xdg Output 100 | // 101 | 102 | delegate_output!(@ MagmaState); 103 | 104 | impl WlrLayerShellHandler for MagmaState{ 105 | fn shell_state(&mut self) -> &mut WlrLayerShellState { 106 | &mut self.layer_shell_state 107 | } 108 | 109 | fn new_layer_surface( 110 | &mut self, 111 | surface: WlrLayerSurface, 112 | output: Option, 113 | _layer: Layer, 114 | namespace: String, 115 | ) { 116 | let output = output.as_ref().and_then(Output::from_resource).unwrap_or_else(|| { 117 | self.workspaces.current().outputs().next().unwrap().clone() 118 | }); 119 | let mut map = layer_map_for_output(&output); 120 | let layer_surface = LayerSurface::new(surface, namespace); 121 | map.map_layer(&layer_surface).unwrap(); 122 | self.set_input_focus(FocusTarget::LayerSurface(layer_surface)) 123 | } 124 | 125 | fn layer_destroyed(&mut self, surface: WlrLayerSurface) { 126 | if let Some((mut map, layer)) = self.workspaces.outputs().find_map(|o| { 127 | let map = layer_map_for_output(o); 128 | let layer = map 129 | .layers() 130 | .find(|&layer| layer.layer_surface() == &surface) 131 | .cloned(); 132 | layer.map(|layer| (map, layer)) 133 | }) { 134 | map.unmap_layer(&layer); 135 | } 136 | self.set_input_focus_auto() 137 | } 138 | } 139 | 140 | delegate_layer_shell!(@ MagmaState); -------------------------------------------------------------------------------- /src/handlers/xdg_shell.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Mutex; 2 | 3 | use smithay::{ 4 | delegate_xdg_decoration, delegate_xdg_shell, 5 | desktop::{Window, PopupKind, PopupManager, layer_map_for_output, WindowSurfaceType}, 6 | reexports::{ 7 | wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode, 8 | wayland_server::protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, 9 | }, 10 | utils::Serial, 11 | wayland::{ 12 | compositor::with_states, 13 | shell::{xdg::{ 14 | decoration::XdgDecorationHandler, PopupSurface, PositionerState, ToplevelSurface, 15 | XdgShellHandler, XdgShellState, XdgToplevelSurfaceRoleAttributes, XdgPopupSurfaceData, 16 | }, wlr_layer::LayerSurfaceData}, 17 | }, 18 | }; 19 | use tracing::warn; 20 | 21 | use crate::{ 22 | state::{Backend, MagmaState}, 23 | utils::{ 24 | tiling::{bsp_layout, WindowLayoutEvent}, 25 | workspaces::Workspaces, focus::FocusTarget, 26 | }, 27 | }; 28 | 29 | impl XdgShellHandler for MagmaState { 30 | fn xdg_shell_state(&mut self) -> &mut XdgShellState { 31 | &mut self.xdg_shell_state 32 | } 33 | 34 | fn new_toplevel(&mut self, surface: ToplevelSurface) { 35 | let window = Window::new(surface); 36 | bsp_layout( 37 | self.workspaces.current_mut(), 38 | window.clone(), 39 | WindowLayoutEvent::Added, 40 | self.config.gaps, 41 | ); 42 | self.set_input_focus(FocusTarget::Window(window)); 43 | self.ipc_manager.update_occupied_workspaces(&mut self.workspaces); 44 | } 45 | fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { 46 | let window = self 47 | .workspaces 48 | .all_windows() 49 | .find(|w| w.toplevel() == &surface) 50 | .unwrap() 51 | .clone(); 52 | 53 | let workspace = self.workspaces.workspace_from_window(&window).unwrap(); 54 | bsp_layout( 55 | workspace, 56 | window, 57 | WindowLayoutEvent::Removed, 58 | self.config.gaps, 59 | ); 60 | 61 | self.set_input_focus_auto(); 62 | self.ipc_manager.update_occupied_workspaces(&mut self.workspaces); 63 | } 64 | fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) { 65 | surface.with_pending_state(|state| { 66 | 67 | state.geometry = positioner.get_geometry(); 68 | }); 69 | if let Err(err) = self.popup_manager.track_popup(PopupKind::from(surface)) { 70 | warn!("Failed to track popup: {}", err); 71 | } 72 | } 73 | 74 | fn grab(&mut self, _surface: PopupSurface, _seat: WlSeat, _serial: Serial) { 75 | // TODO 76 | } 77 | } 78 | 79 | delegate_xdg_shell!(@ MagmaState); 80 | 81 | /// Should be called on `WlSurface::commit` 82 | pub fn handle_commit(workspaces: &Workspaces, surface: &WlSurface, popup_manager: &PopupManager) -> Option<()> { 83 | if let Some(window) = workspaces 84 | .all_windows() 85 | .find(|w| w.toplevel().wl_surface() == surface) 86 | { 87 | let initial_configure_sent = with_states(surface, |states| { 88 | states 89 | .data_map 90 | .get::>() 91 | .unwrap() 92 | .lock() 93 | .unwrap() 94 | .initial_configure_sent 95 | }); 96 | if !initial_configure_sent { 97 | window.toplevel().send_configure(); 98 | } 99 | } 100 | 101 | if let Some(popup) = popup_manager.find_popup(surface) { 102 | let PopupKind::Xdg(ref popup) = popup; 103 | let initial_configure_sent = with_states(surface, |states| { 104 | states 105 | .data_map 106 | .get::() 107 | .unwrap() 108 | .lock() 109 | .unwrap() 110 | .initial_configure_sent 111 | }); 112 | if !initial_configure_sent { 113 | // NOTE: This should never fail as the initial configure is always 114 | // allowed. 115 | popup.send_configure().expect("initial configure failed"); 116 | } 117 | }; 118 | 119 | if let Some(output) = workspaces.current().outputs().find(|o| { 120 | let map = layer_map_for_output(o); 121 | map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) 122 | .is_some() 123 | }) { 124 | let initial_configure_sent = with_states(surface, |states| { 125 | states 126 | .data_map 127 | .get::() 128 | .unwrap() 129 | .lock() 130 | .unwrap() 131 | .initial_configure_sent 132 | }); 133 | let mut map = layer_map_for_output(output); 134 | 135 | // arrange the layers before sending the initial configure 136 | // to respect any size the client may have sent 137 | map.arrange(); 138 | // send the initial configure if relevant 139 | if !initial_configure_sent { 140 | let layer = map 141 | .layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) 142 | .unwrap(); 143 | 144 | layer.layer_surface().send_configure(); 145 | } 146 | }; 147 | 148 | Some(()) 149 | } 150 | 151 | // Disable decorations 152 | impl XdgDecorationHandler for MagmaState { 153 | fn new_decoration(&mut self, toplevel: ToplevelSurface) { 154 | toplevel.with_pending_state(|state| { 155 | // Advertise server side decoration 156 | state.decoration_mode = Some(Mode::ServerSide); 157 | }); 158 | toplevel.send_configure(); 159 | } 160 | 161 | fn request_mode( 162 | &mut self, 163 | _toplevel: ToplevelSurface, 164 | _mode: smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode, 165 | ) { 166 | } 167 | 168 | fn unset_mode(&mut self, _toplevel: ToplevelSurface) {} 169 | } 170 | 171 | delegate_xdg_decoration!(@ MagmaState); 172 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | use smithay::{ 2 | backend::input::{ 3 | self, AbsolutePositionEvent, Axis, AxisSource, Event, InputBackend, InputEvent, KeyState, 4 | KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, 5 | }, 6 | input::{ 7 | keyboard::{FilterResult, xkb}, 8 | pointer::{AxisFrame, ButtonEvent, MotionEvent, RelativeMotionEvent}, 9 | }, 10 | utils::{Logical, Point, SERIAL_COUNTER}, 11 | }; 12 | 13 | use crate::{state::{Backend, MagmaState}, utils::focus::FocusTarget, backends::udev::UdevData, config::Action}; 14 | impl MagmaState { 15 | pub fn process_input_event_udev(&mut self, event: InputEvent) -> Option { 16 | match event { 17 | InputEvent::Keyboard { event, .. } => { 18 | let serial = SERIAL_COUNTER.next_serial(); 19 | let time = Event::time_msec(&event); 20 | 21 | if let Some(action) = self.seat.get_keyboard().unwrap().input( 22 | self, 23 | event.key_code(), 24 | event.state(), 25 | serial, 26 | time, 27 | |data, modifiers, handle| { 28 | for (binding, action) in data.config.keybindings.iter() { 29 | if event.state() == KeyState::Pressed 30 | && binding.modifiers == *modifiers 31 | && handle.raw_syms().contains(&binding.key) 32 | { 33 | return FilterResult::Intercept(action.clone()); 34 | } else if (xkb::KEY_XF86Switch_VT_1..=xkb::KEY_XF86Switch_VT_12).contains(&handle.modified_sym()) { 35 | // VTSwitch 36 | let vt = (&handle.modified_sym() - xkb::KEY_XF86Switch_VT_1 + 1) as i32; 37 | return FilterResult::Intercept(Action::VTSwitch(vt)); 38 | } 39 | } 40 | FilterResult::Forward 41 | }, 42 | ) { 43 | match action { 44 | Action::VTSwitch(vt) => return Some(vt), 45 | _ => self.handle_action(action), 46 | } 47 | }; 48 | None 49 | } 50 | event => {self.process_input_event(event); None} 51 | } 52 | } 53 | } 54 | 55 | 56 | impl MagmaState { 57 | pub fn process_input_event(&mut self, event: InputEvent) { 58 | match event { 59 | InputEvent::Keyboard { event, .. } => { 60 | let serial = SERIAL_COUNTER.next_serial(); 61 | let time = Event::time_msec(&event); 62 | 63 | if let Some(action) = self.seat.get_keyboard().unwrap().input( 64 | self, 65 | event.key_code(), 66 | event.state(), 67 | serial, 68 | time, 69 | |data, modifiers, handle| { 70 | for (binding, action) in data.config.keybindings.iter() { 71 | if event.state() == KeyState::Pressed 72 | && binding.modifiers == *modifiers 73 | && handle.raw_syms().contains(&binding.key) 74 | { 75 | return FilterResult::Intercept(action.clone()); 76 | } 77 | } 78 | FilterResult::Forward 79 | }, 80 | ) { 81 | self.handle_action(action); 82 | }; 83 | } 84 | InputEvent::PointerMotion { event } => { 85 | let serial = SERIAL_COUNTER.next_serial(); 86 | let delta = (event.delta_x(), event.delta_y()).into(); 87 | self.pointer_location += delta; 88 | 89 | // clamp to screen limits 90 | // this event is never generated by winit 91 | self.pointer_location = self.clamp_coords(self.pointer_location); 92 | 93 | let under = self.surface_under(); 94 | 95 | self.set_input_focus_auto(); 96 | 97 | if let Some(ptr) = self.seat.get_pointer() { 98 | ptr.motion( 99 | self, 100 | under.clone(), 101 | &MotionEvent { 102 | location: self.pointer_location, 103 | serial, 104 | time: event.time_msec(), 105 | }, 106 | ); 107 | 108 | ptr.relative_motion( 109 | self, 110 | under, 111 | &RelativeMotionEvent { 112 | delta, 113 | delta_unaccel: event.delta_unaccel(), 114 | utime: event.time(), 115 | }, 116 | ) 117 | } 118 | } 119 | InputEvent::PointerMotionAbsolute { event, .. } => { 120 | let output = self.workspaces.current().outputs().next().unwrap().clone(); 121 | 122 | let output_geo = self.workspaces.current().output_geometry(&output).unwrap(); 123 | 124 | let pos = event.position_transformed(output_geo.size) + output_geo.loc.to_f64(); 125 | 126 | let serial = SERIAL_COUNTER.next_serial(); 127 | 128 | let pointer = self.seat.get_pointer().unwrap(); 129 | 130 | self.pointer_location = self.clamp_coords(pos); 131 | 132 | let under = self.surface_under(); 133 | 134 | self.set_input_focus_auto(); 135 | 136 | pointer.motion( 137 | self, 138 | under, 139 | &MotionEvent { 140 | location: pos, 141 | serial, 142 | time: event.time_msec(), 143 | }, 144 | ); 145 | } 146 | InputEvent::PointerButton { event, .. } => { 147 | let pointer = self.seat.get_pointer().unwrap(); 148 | 149 | let serial = SERIAL_COUNTER.next_serial(); 150 | 151 | let button = event.button_code(); 152 | 153 | let button_state = event.state(); 154 | 155 | self.set_input_focus_auto(); 156 | 157 | pointer.button( 158 | self, 159 | &ButtonEvent { 160 | button, 161 | state: button_state, 162 | serial, 163 | time: event.time_msec(), 164 | }, 165 | ); 166 | } 167 | InputEvent::PointerAxis { event, .. } => { 168 | let horizontal_amount = 169 | event.amount(input::Axis::Horizontal).unwrap_or_else(|| { 170 | event 171 | .amount_discrete(input::Axis::Horizontal) 172 | .unwrap_or(0.0) 173 | * 3.0 174 | }); 175 | let vertical_amount = event.amount(input::Axis::Vertical).unwrap_or_else(|| { 176 | event.amount_discrete(input::Axis::Vertical).unwrap_or(0.0) * 3.0 177 | }); 178 | let horizontal_amount_discrete = event.amount_discrete(input::Axis::Horizontal); 179 | let vertical_amount_discrete = event.amount_discrete(input::Axis::Vertical); 180 | 181 | { 182 | let mut frame = AxisFrame::new(event.time_msec()).source(event.source()); 183 | if horizontal_amount != 0.0 { 184 | frame = frame.value(Axis::Horizontal, horizontal_amount); 185 | if let Some(discrete) = horizontal_amount_discrete { 186 | frame = frame.discrete(Axis::Horizontal, discrete as i32); 187 | } 188 | } else if event.source() == AxisSource::Finger { 189 | frame = frame.stop(Axis::Horizontal); 190 | } 191 | if vertical_amount != 0.0 { 192 | frame = frame.value(Axis::Vertical, vertical_amount); 193 | if let Some(discrete) = vertical_amount_discrete { 194 | frame = frame.discrete(Axis::Vertical, discrete as i32); 195 | } 196 | } else if event.source() == AxisSource::Finger { 197 | frame = frame.stop(Axis::Vertical); 198 | } 199 | self.seat.get_pointer().unwrap().axis(self, frame); 200 | } 201 | } 202 | _ => {} 203 | } 204 | } 205 | 206 | fn clamp_coords(&self, pos: Point) -> Point { 207 | if self.workspaces.current().outputs().next().is_none() { 208 | return pos; 209 | } 210 | 211 | let (pos_x, pos_y) = pos.into(); 212 | let (max_x, max_y) = self.workspaces.current().output_geometry(self.workspaces.current().outputs().next().unwrap()).unwrap().size.into(); 213 | let clamped_x = pos_x.max(0.0).min(max_x as f64); 214 | let clamped_y = pos_y.max(0.0).min(max_y as f64); 215 | (clamped_x, clamped_y).into() 216 | } 217 | 218 | pub fn set_input_focus(&mut self, target: FocusTarget){ 219 | let keyboard = self.seat.get_keyboard().unwrap(); 220 | let serial = SERIAL_COUNTER.next_serial(); 221 | keyboard.set_focus(self, Some(target), serial); 222 | } 223 | 224 | pub fn set_input_focus_auto(&mut self){ 225 | let under = self.surface_under(); 226 | if let Some(d) = under.clone() { 227 | self.set_input_focus(d.0); 228 | } 229 | } 230 | } 231 | 232 | -------------------------------------------------------------------------------- /src/ipc/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod generated { 2 | use smithay::reexports::wayland_server; 3 | 4 | pub mod __interfaces { 5 | wayland_scanner::generate_interfaces!("ipc.xml"); 6 | } 7 | use self::__interfaces::*; 8 | 9 | wayland_scanner::generate_server_code!("ipc.xml"); 10 | } 11 | mod workspaces; 12 | use smithay::reexports::wayland_server::{GlobalDispatch, Dispatch, DisplayHandle, Client, New, DataInit}; 13 | 14 | use self::generated::{magma_ipc::{MagmaIpc, Request}, workspaces::Workspaces}; 15 | 16 | 17 | pub struct MagmaIpcManager { 18 | pub workspace_handles: Vec, 19 | } 20 | 21 | impl MagmaIpcManager { 22 | pub fn new(display: &DisplayHandle) -> Self 23 | where 24 | D: GlobalDispatch, 25 | D: Dispatch, 26 | D: Dispatch, 27 | D: MagmaIpcHandler, 28 | D: 'static, 29 | { 30 | display.create_global::(1, ()); 31 | 32 | Self { 33 | workspace_handles: Vec::new(), 34 | } 35 | } 36 | } 37 | 38 | impl GlobalDispatch for MagmaIpcManager 39 | where 40 | D: GlobalDispatch, 41 | D: Dispatch, 42 | D: Dispatch, 43 | D: MagmaIpcHandler, 44 | D: 'static, 45 | { 46 | fn bind( 47 | _state: &mut D, 48 | _display: &DisplayHandle, 49 | _client: &Client, 50 | manager: New, 51 | _manager_state: &(), 52 | data_init: &mut DataInit<'_, D>, 53 | ) { 54 | data_init.init(manager, ()); 55 | } 56 | } 57 | 58 | impl Dispatch for MagmaIpcManager 59 | where 60 | D: GlobalDispatch, 61 | D: Dispatch, 62 | D: Dispatch, 63 | D: MagmaIpcHandler, 64 | D: 'static, 65 | { 66 | fn request( 67 | state: &mut D, 68 | _client: &Client, 69 | _resource: &MagmaIpc, 70 | request: Request, 71 | _data: &(), 72 | _dhandle: &DisplayHandle, 73 | data_init: &mut DataInit<'_, D>, 74 | ) { 75 | match request { 76 | Request::Workspaces { id } => state.register_workspace(data_init.init(id, ())), 77 | }; 78 | } 79 | } 80 | 81 | 82 | #[macro_export] 83 | macro_rules! delegate_magma_ipc { 84 | ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { 85 | smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ 86 | $crate::ipc::generated::magma_ipc::MagmaIpc: () 87 | ] => $crate::ipc::MagmaIpcManager); 88 | 89 | smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ 90 | $crate::ipc::generated::magma_ipc::MagmaIpc: () 91 | ] => $crate::ipc::MagmaIpcManager); 92 | 93 | smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ 94 | $crate::ipc::generated::workspaces::Workspaces: () 95 | ] => $crate::ipc::MagmaIpcManager); 96 | }; 97 | } 98 | 99 | pub trait MagmaIpcHandler { 100 | fn register_workspace(&mut self, workspace: Workspaces); 101 | } -------------------------------------------------------------------------------- /src/ipc/workspaces.rs: -------------------------------------------------------------------------------- 1 | use smithay::reexports::wayland_server::Dispatch; 2 | 3 | use crate::utils::workspaces::Workspaces as CompWorkspaces; 4 | 5 | use super::{generated::workspaces::Workspaces, MagmaIpcManager, MagmaIpcHandler}; 6 | 7 | impl Dispatch for MagmaIpcManager 8 | where 9 | D: Dispatch, 10 | D: MagmaIpcHandler, 11 | D: 'static, { 12 | fn request( 13 | _state: &mut D, 14 | _client: &smithay::reexports::wayland_server::Client, 15 | _resource: &Workspaces, 16 | _request: ::Request, 17 | _data: &(), 18 | _dhandle: &smithay::reexports::wayland_server::DisplayHandle, 19 | _data_init: &mut smithay::reexports::wayland_server::DataInit<'_, D>, 20 | ) { 21 | 22 | } 23 | } 24 | 25 | impl MagmaIpcManager { 26 | pub fn update_active_workspace(&mut self, id: u32) { 27 | for workspace_handle in self.workspace_handles.iter() { 28 | workspace_handle.active_workspace(id); 29 | } 30 | } 31 | 32 | pub fn update_occupied_workspaces(&mut self, workspaces: &mut CompWorkspaces) { 33 | for workspace_handle in self.workspace_handles.iter() { 34 | let mut occupied = vec![]; 35 | for (id, workspace) in workspaces.iter().enumerate() { 36 | if workspace.windows().next().is_some() { 37 | occupied.push(id as u8); 38 | } 39 | } 40 | workspace_handle.occupied_workspaces(occupied); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use state::{CalloopData, MagmaState}; 2 | use tracing::{error, info}; 3 | 4 | use crate::backends::{udev, winit}; 5 | 6 | mod backends; 7 | mod config; 8 | mod handlers; 9 | mod input; 10 | mod ipc; 11 | mod state; 12 | mod utils; 13 | 14 | static POSSIBLE_BACKENDS: &[&str] = &[ 15 | "--winit : Run magma as a X11 or Wayland client using winit.", 16 | "--tty-udev : Run magma as a tty udev client (requires root if without logind).", 17 | ]; 18 | fn main() { 19 | if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() { 20 | tracing_subscriber::fmt().with_env_filter(env_filter).init(); 21 | } else { 22 | tracing_subscriber::fmt().init(); 23 | } 24 | 25 | let arg = ::std::env::args().nth(1); 26 | match arg.as_ref().map(|s| &s[..]) { 27 | Some("--winit") => { 28 | info!("Starting magmawn with winit backend"); 29 | winit::init_winit(); 30 | } 31 | Some("--tty-udev") => { 32 | info!("Starting magma on a tty using udev"); 33 | udev::init_udev(); 34 | } 35 | Some(other) => { 36 | error!("Unknown backend: {}", other); 37 | } 38 | None => { 39 | println!("USAGE: magma --backend"); 40 | println!(); 41 | println!("Possible backends are:"); 42 | for b in POSSIBLE_BACKENDS { 43 | println!("\t{}", b); 44 | } 45 | } 46 | } 47 | 48 | info!("Magma is shutting down"); 49 | } 50 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsString, os::fd::AsRawFd, sync::Arc, time::Instant}; 2 | 3 | use smithay::{ 4 | desktop::{Window, PopupManager, layer_map_for_output}, 5 | input::{Seat, SeatState, keyboard::XkbConfig}, 6 | reexports::{ 7 | calloop::{generic::Generic, Interest, LoopSignal, Mode, PostAction, LoopHandle}, 8 | wayland_server::{ 9 | backend::{ClientData, ClientId, DisconnectReason}, 10 | Display, DisplayHandle, 11 | }, 12 | }, 13 | utils::{Logical, Point}, 14 | wayland::{ 15 | compositor::CompositorState, 16 | data_device::DataDeviceState, 17 | output::OutputManagerState, 18 | shell::{xdg::{decoration::XdgDecorationState, XdgShellState}, wlr_layer::{WlrLayerShellState, Layer as WlrLayer}}, 19 | shm::ShmState, 20 | socket::ListeningSocketSource, primary_selection::PrimarySelectionState, 21 | }, 22 | }; 23 | use tracing::warn; 24 | 25 | use crate::{config::Config, utils::{workspaces::Workspaces, focus::FocusTarget}, ipc::{MagmaIpcManager, MagmaIpcHandler}, delegate_magma_ipc}; 26 | 27 | pub struct CalloopData { 28 | pub state: MagmaState, 29 | pub display: Display>, 30 | } 31 | 32 | pub trait Backend { 33 | fn seat_name(&self) -> String; 34 | } 35 | 36 | pub struct MagmaState { 37 | pub dh: DisplayHandle, 38 | pub backend_data: BackendData, 39 | pub loop_handle: LoopHandle<'static, CalloopData>, 40 | pub config: Config, 41 | pub start_time: Instant, 42 | pub socket_name: OsString, 43 | pub seat_name: String, 44 | pub loop_signal: LoopSignal, 45 | pub workspaces: Workspaces, 46 | 47 | pub compositor_state: CompositorState, 48 | pub xdg_shell_state: XdgShellState, 49 | pub xdg_decoration_state: XdgDecorationState, 50 | pub shm_state: ShmState, 51 | pub output_manager_state: OutputManagerState, 52 | pub seat_state: SeatState>, 53 | pub data_device_state: DataDeviceState, 54 | pub primary_selection_state: PrimarySelectionState, 55 | pub popup_manager: PopupManager, 56 | pub layer_shell_state: WlrLayerShellState, 57 | pub seat: Seat, 58 | 59 | pub pointer_location: Point, 60 | 61 | pub ipc_manager: MagmaIpcManager, 62 | } 63 | 64 | impl MagmaState { 65 | pub fn new( 66 | mut loop_handle: LoopHandle<'static, CalloopData>, 67 | loop_signal: LoopSignal, 68 | display: &mut Display>, 69 | backend_data: BackendData, 70 | ) -> Self { 71 | let start_time = Instant::now(); 72 | 73 | let dh = display.handle(); 74 | 75 | let config = Config::load(); 76 | 77 | let compositor_state = CompositorState::new::(&dh); 78 | let xdg_shell_state = XdgShellState::new::(&dh); 79 | let xdg_decoration_state = XdgDecorationState::new::(&dh); 80 | let shm_state = ShmState::new::(&dh, vec![]); 81 | let output_manager_state = OutputManagerState::new_with_xdg_output::(&dh); 82 | let mut seat_state = SeatState::new(); 83 | let data_device_state = DataDeviceState::new::(&dh); 84 | let primary_selection_state = PrimarySelectionState::new::(&dh); 85 | let layer_shell_state = WlrLayerShellState::new::(&dh); 86 | let seat_name = backend_data.seat_name(); 87 | let mut seat = seat_state.new_wl_seat(&dh, seat_name.clone()); 88 | let conf = config.xkb.clone(); 89 | if let Err(err) = seat.add_keyboard((&conf).into(), 200, 25) { 90 | warn!( 91 | ?err, 92 | "Failed to load provided xkb config. Trying default...", 93 | ); 94 | seat.add_keyboard(XkbConfig::default(), 200, 25) 95 | .expect("Failed to load xkb configuration files"); 96 | } 97 | seat.add_pointer(); 98 | 99 | let workspaces = Workspaces::new(config.workspaces); 100 | 101 | let socket_name = Self::init_wayland_listener(&mut loop_handle, display); 102 | 103 | let ipc_manager = MagmaIpcManager::new::(&dh); 104 | 105 | Self { 106 | loop_handle, 107 | dh, 108 | backend_data, 109 | config, 110 | start_time, 111 | seat_name, 112 | socket_name, 113 | workspaces, 114 | compositor_state, 115 | xdg_shell_state, 116 | xdg_decoration_state, 117 | loop_signal, 118 | shm_state, 119 | output_manager_state, 120 | seat_state, 121 | data_device_state, 122 | primary_selection_state, 123 | layer_shell_state, 124 | seat, 125 | pointer_location: Point::from((0.0, 0.0)), 126 | popup_manager: PopupManager::default(), 127 | ipc_manager, 128 | } 129 | } 130 | fn init_wayland_listener( 131 | handle: &mut LoopHandle<'static, CalloopData>, 132 | display: &mut Display>, 133 | ) -> OsString { 134 | // Creates a new listening socket, automatically choosing the next available `wayland` socket name. 135 | let listening_socket = ListeningSocketSource::new_auto().unwrap(); 136 | 137 | // Get the name of the listening socket. 138 | // Clients will connect to this socket. 139 | let socket_name = listening_socket.socket_name().to_os_string(); 140 | 141 | handle 142 | .insert_source(listening_socket, move |client_stream, _, state| { 143 | // Inside the callback, you should insert the client into the display. 144 | // 145 | // You may also associate some data with the client when inserting the client. 146 | state 147 | .display 148 | .handle() 149 | .insert_client(client_stream, Arc::new(ClientState)) 150 | .unwrap(); 151 | }) 152 | .expect("Failed to init the wayland event source."); 153 | 154 | // You also need to add the display itself to the event loop, so that client events will be processed by wayland-server. 155 | handle 156 | .insert_source( 157 | Generic::new( 158 | display.backend().poll_fd().as_raw_fd(), 159 | Interest::READ, 160 | Mode::Level, 161 | ), 162 | |_, _, state| { 163 | state.display.dispatch_clients(&mut state.state).unwrap(); 164 | Ok(PostAction::Continue) 165 | }, 166 | ) 167 | .unwrap(); 168 | 169 | socket_name 170 | } 171 | 172 | pub fn window_under(&mut self) -> Option<(Window, Point)> { 173 | let pos = self.pointer_location; 174 | self.workspaces 175 | .current() 176 | .window_under(pos) 177 | .map(|(w, p)| (w.clone(), p)) 178 | } 179 | pub fn surface_under(&self) -> Option<(FocusTarget, Point)> { 180 | let pos = self.pointer_location; 181 | let output = self.workspaces.current().outputs().find(|o| { 182 | let geometry = self.workspaces.current().output_geometry(o).unwrap(); 183 | geometry.contains(pos.to_i32_round()) 184 | })?; 185 | let output_geo = self.workspaces.current().output_geometry(output).unwrap(); 186 | let layers = layer_map_for_output(output); 187 | 188 | let mut under = None; 189 | if let Some(layer) = layers 190 | .layer_under(WlrLayer::Overlay, pos) 191 | .or_else(|| layers.layer_under(WlrLayer::Top, pos)) 192 | { 193 | let layer_loc = layers.layer_geometry(layer).unwrap().loc; 194 | under = Some((layer.clone().into(), output_geo.loc + layer_loc)) 195 | } else if let Some((window, location)) = self.workspaces.current().window_under(pos) { 196 | under = Some((window.clone().into(), location)); 197 | } else if let Some(layer) = layers 198 | .layer_under(WlrLayer::Bottom, pos) 199 | .or_else(|| layers.layer_under(WlrLayer::Background, pos)) 200 | { 201 | let layer_loc = layers.layer_geometry(layer).unwrap().loc; 202 | under = Some((layer.clone().into(), output_geo.loc + layer_loc)); 203 | }; 204 | under 205 | } 206 | } 207 | 208 | pub struct ClientState; 209 | impl ClientData for ClientState { 210 | fn initialized(&self, _client_id: ClientId) {} 211 | fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {} 212 | } 213 | 214 | delegate_magma_ipc!(@ MagmaState); 215 | 216 | impl MagmaIpcHandler for MagmaState { 217 | fn register_workspace(&mut self, workspace: crate::ipc::generated::workspaces::Workspaces) { 218 | self.ipc_manager.workspace_handles.push(workspace); 219 | self.ipc_manager.update_active_workspace(self.workspaces.current.into()); 220 | self.ipc_manager.update_occupied_workspaces(&mut self.workspaces); 221 | } 222 | } -------------------------------------------------------------------------------- /src/utils/binarytree.rs: -------------------------------------------------------------------------------- 1 | use smithay::desktop::Window; 2 | use std::fmt::Debug; 3 | use std::{cell::RefCell, rc::Rc}; 4 | 5 | use super::workspaces::MagmaWindow; 6 | 7 | #[derive(Clone)] 8 | pub enum BinaryTree { 9 | Empty, 10 | Window(Rc>), 11 | Split { 12 | split: HorizontalOrVertical, 13 | ratio: f32, 14 | left: Box, 15 | right: Box, 16 | }, 17 | } 18 | 19 | impl Debug for BinaryTree { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | match self { 22 | Self::Empty => write!(f, "Empty"), 23 | Self::Window(w) => w.borrow().rec.fmt(f), 24 | Self::Split { 25 | left, 26 | right, 27 | split, 28 | ratio, 29 | } => f 30 | .debug_struct("Split") 31 | .field("split", split) 32 | .field("ratio", ratio) 33 | .field("left", left) 34 | .field("right", right) 35 | .finish(), 36 | } 37 | } 38 | } 39 | 40 | #[derive(Debug, Clone, Copy, PartialEq)] 41 | pub enum HorizontalOrVertical { 42 | Horizontal, 43 | Vertical, 44 | } 45 | 46 | impl BinaryTree { 47 | pub fn new() -> Self { 48 | BinaryTree::Empty 49 | } 50 | 51 | pub fn insert( 52 | &mut self, 53 | window: Rc>, 54 | splitnew: HorizontalOrVertical, 55 | rationew: f32, 56 | ) { 57 | match self { 58 | BinaryTree::Empty => { 59 | *self = BinaryTree::Window(window); 60 | } 61 | BinaryTree::Window(w) => { 62 | *self = BinaryTree::Split { 63 | left: Box::new(BinaryTree::Window(w.clone())), 64 | right: Box::new(BinaryTree::Window(window)), 65 | split: splitnew, 66 | ratio: rationew, 67 | }; 68 | } 69 | BinaryTree::Split { 70 | left: _, 71 | right, 72 | split: _, 73 | ratio: _, 74 | } => { 75 | right.insert(window, splitnew, rationew); 76 | } 77 | } 78 | } 79 | 80 | pub fn remove(&mut self, window: &Window) { 81 | match self { 82 | BinaryTree::Empty => {} 83 | BinaryTree::Window(w) => { 84 | // Should only happen if this is the root 85 | if w.borrow().window == *window { 86 | *self = BinaryTree::Empty; 87 | } 88 | } 89 | BinaryTree::Split { 90 | left, 91 | right, 92 | split: _, 93 | ratio: _, 94 | } => { 95 | if let BinaryTree::Window(w) = left.as_ref() { 96 | if w.borrow().window == *window { 97 | *self = *right.clone(); 98 | return; 99 | } 100 | } 101 | if let BinaryTree::Window(w) = right.as_ref() { 102 | if w.borrow().window == *window { 103 | *self = *left.clone(); 104 | return; 105 | } 106 | } 107 | left.remove(window); 108 | right.remove(window); 109 | } 110 | } 111 | } 112 | 113 | pub fn next_split(&self) -> HorizontalOrVertical { 114 | match self { 115 | BinaryTree::Empty => HorizontalOrVertical::Horizontal, 116 | BinaryTree::Window(_w) => HorizontalOrVertical::Horizontal, 117 | BinaryTree::Split { 118 | left: _, 119 | right, 120 | split, 121 | ratio: _, 122 | } => { 123 | if let BinaryTree::Split { 124 | left: _, 125 | right: _, 126 | split: _, 127 | ratio: _, 128 | } = right.as_ref() 129 | { 130 | right.next_split() 131 | } else { 132 | if *split == HorizontalOrVertical::Horizontal { 133 | HorizontalOrVertical::Vertical 134 | } else { 135 | HorizontalOrVertical::Horizontal 136 | } 137 | } 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/utils/focus.rs: -------------------------------------------------------------------------------- 1 | use smithay::desktop::Window; 2 | pub use smithay::{ 3 | backend::input::KeyState, 4 | desktop::{LayerSurface, PopupKind}, 5 | input::{ 6 | keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, 7 | pointer::{AxisFrame, ButtonEvent, MotionEvent, PointerTarget, RelativeMotionEvent}, 8 | Seat, 9 | }, 10 | reexports::wayland_server::{backend::ObjectId, protocol::wl_surface::WlSurface, Resource}, 11 | utils::{IsAlive, Serial}, 12 | wayland::seat::WaylandFocus, 13 | }; 14 | 15 | use crate::{ 16 | state::{MagmaState, Backend}, 17 | }; 18 | 19 | #[derive(Debug, Clone, PartialEq)] 20 | pub enum FocusTarget { 21 | Window(Window), 22 | LayerSurface(LayerSurface), 23 | Popup(PopupKind), 24 | } 25 | 26 | impl IsAlive for FocusTarget { 27 | fn alive(&self) -> bool { 28 | match self { 29 | FocusTarget::Window(w) => w.alive(), 30 | FocusTarget::LayerSurface(l) => l.alive(), 31 | FocusTarget::Popup(p) => p.alive(), 32 | } 33 | } 34 | } 35 | 36 | impl From for WlSurface { 37 | fn from(target: FocusTarget) -> Self { 38 | target.wl_surface().unwrap() 39 | } 40 | } 41 | 42 | impl PointerTarget> for FocusTarget { 43 | fn enter( 44 | &self, 45 | seat: &Seat>, 46 | data: &mut MagmaState, 47 | event: &MotionEvent, 48 | ) { 49 | match self { 50 | FocusTarget::Window(w) => PointerTarget::enter(w, seat, data, event), 51 | FocusTarget::LayerSurface(l) => PointerTarget::enter(l, seat, data, event), 52 | FocusTarget::Popup(p) => PointerTarget::enter(p.wl_surface(), seat, data, event), 53 | } 54 | } 55 | fn motion( 56 | &self, 57 | seat: &Seat>, 58 | data: &mut MagmaState, 59 | event: &MotionEvent, 60 | ) { 61 | match self { 62 | FocusTarget::Window(w) => PointerTarget::motion(w, seat, data, event), 63 | FocusTarget::LayerSurface(l) => PointerTarget::motion(l, seat, data, event), 64 | FocusTarget::Popup(p) => PointerTarget::motion(p.wl_surface(), seat, data, event), 65 | } 66 | } 67 | fn relative_motion( 68 | &self, 69 | seat: &Seat>, 70 | data: &mut MagmaState, 71 | event: &RelativeMotionEvent, 72 | ) { 73 | match self { 74 | FocusTarget::Window(w) => PointerTarget::relative_motion(w, seat, data, event), 75 | FocusTarget::LayerSurface(l) => PointerTarget::relative_motion(l, seat, data, event), 76 | FocusTarget::Popup(p) => PointerTarget::relative_motion(p.wl_surface(), seat, data, event), 77 | } 78 | } 79 | fn button( 80 | &self, 81 | seat: &Seat>, 82 | data: &mut MagmaState, 83 | event: &ButtonEvent, 84 | ) { 85 | match self { 86 | FocusTarget::Window(w) => PointerTarget::button(w, seat, data, event), 87 | FocusTarget::LayerSurface(l) => PointerTarget::button(l, seat, data, event), 88 | FocusTarget::Popup(p) => PointerTarget::button(p.wl_surface(), seat, data, event), 89 | } 90 | } 91 | fn axis( 92 | &self, 93 | seat: &Seat>, 94 | data: &mut MagmaState, 95 | frame: AxisFrame, 96 | ) { 97 | match self { 98 | FocusTarget::Window(w) => PointerTarget::axis(w, seat, data, frame), 99 | FocusTarget::LayerSurface(l) => PointerTarget::axis(l, seat, data, frame), 100 | FocusTarget::Popup(p) => PointerTarget::axis(p.wl_surface(), seat, data, frame), 101 | } 102 | } 103 | fn leave( 104 | &self, 105 | seat: &Seat>, 106 | data: &mut MagmaState, 107 | serial: Serial, 108 | time: u32, 109 | ) { 110 | match self { 111 | FocusTarget::Window(w) => PointerTarget::leave(w, seat, data, serial, time), 112 | FocusTarget::LayerSurface(l) => PointerTarget::leave(l, seat, data, serial, time), 113 | FocusTarget::Popup(p) => PointerTarget::leave(p.wl_surface(), seat, data, serial, time), 114 | } 115 | } 116 | } 117 | 118 | impl KeyboardTarget> for FocusTarget { 119 | fn enter( 120 | &self, 121 | seat: &Seat>, 122 | data: &mut MagmaState, 123 | keys: Vec>, 124 | serial: Serial, 125 | ) { 126 | match self { 127 | FocusTarget::Window(w) => KeyboardTarget::enter(w, seat, data, keys, serial), 128 | FocusTarget::LayerSurface(l) => KeyboardTarget::enter(l, seat, data, keys, serial), 129 | FocusTarget::Popup(p) => KeyboardTarget::enter(p.wl_surface(), seat, data, keys, serial), 130 | } 131 | } 132 | fn leave( 133 | &self, 134 | seat: &Seat>, 135 | data: &mut MagmaState, 136 | serial: Serial, 137 | ) { 138 | match self { 139 | FocusTarget::Window(w) => KeyboardTarget::leave(w, seat, data, serial), 140 | FocusTarget::LayerSurface(l) => KeyboardTarget::leave(l, seat, data, serial), 141 | FocusTarget::Popup(p) => KeyboardTarget::leave(p.wl_surface(), seat, data, serial), 142 | } 143 | } 144 | fn key( 145 | &self, 146 | seat: &Seat>, 147 | data: &mut MagmaState, 148 | key: KeysymHandle<'_>, 149 | state: KeyState, 150 | serial: Serial, 151 | time: u32, 152 | ) { 153 | match self { 154 | FocusTarget::Window(w) => KeyboardTarget::key(w, seat, data, key, state, serial, time), 155 | FocusTarget::LayerSurface(l) => KeyboardTarget::key(l, seat, data, key, state, serial, time), 156 | FocusTarget::Popup(p) => { 157 | KeyboardTarget::key(p.wl_surface(), seat, data, key, state, serial, time) 158 | } 159 | } 160 | } 161 | fn modifiers( 162 | &self, 163 | seat: &Seat>, 164 | data: &mut MagmaState, 165 | modifiers: ModifiersState, 166 | serial: Serial, 167 | ) { 168 | match self { 169 | FocusTarget::Window(w) => KeyboardTarget::modifiers(w, seat, data, modifiers, serial), 170 | FocusTarget::LayerSurface(l) => KeyboardTarget::modifiers(l, seat, data, modifiers, serial), 171 | FocusTarget::Popup(p) => KeyboardTarget::modifiers(p.wl_surface(), seat, data, modifiers, serial), 172 | } 173 | } 174 | } 175 | 176 | impl WaylandFocus for FocusTarget { 177 | fn wl_surface(&self) -> Option { 178 | match self { 179 | FocusTarget::Window(w) => w.wl_surface(), 180 | FocusTarget::LayerSurface(l) => Some(l.wl_surface().clone()), 181 | FocusTarget::Popup(p) => Some(p.wl_surface().clone()), 182 | } 183 | } 184 | fn same_client_as(&self, object_id: &ObjectId) -> bool { 185 | match self { 186 | FocusTarget::Window(w) => w.same_client_as(object_id), 187 | FocusTarget::LayerSurface(l) => l.wl_surface().id().same_client_as(object_id), 188 | FocusTarget::Popup(p) => p.wl_surface().id().same_client_as(object_id), 189 | } 190 | } 191 | } 192 | 193 | impl From for FocusTarget { 194 | fn from(w: Window) -> Self { 195 | FocusTarget::Window(w) 196 | } 197 | } 198 | 199 | impl From for FocusTarget { 200 | fn from(l: LayerSurface) -> Self { 201 | FocusTarget::LayerSurface(l) 202 | } 203 | } 204 | 205 | impl From for FocusTarget { 206 | fn from(p: PopupKind) -> Self { 207 | FocusTarget::Popup(p) 208 | } 209 | } -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod binarytree; 2 | pub mod render; 3 | pub mod tiling; 4 | pub mod workspaces; 5 | pub mod focus; 6 | pub mod protocols; -------------------------------------------------------------------------------- /src/utils/protocols/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod screencopy; -------------------------------------------------------------------------------- /src/utils/protocols/screencopy/frame.rs: -------------------------------------------------------------------------------- 1 | //! wlr-screencopy frame. 2 | 3 | use std::time::UNIX_EPOCH; 4 | 5 | use smithay::output::Output; 6 | use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::{ 7 | Flags, Request, ZwlrScreencopyFrameV1, 8 | }; 9 | use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer; 10 | use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle}; 11 | use smithay::utils::{Physical, Rectangle}; 12 | 13 | use crate::utils::protocols::screencopy::{ScreencopyHandler, ScreencopyManagerState}; 14 | 15 | pub struct ScreencopyFrameState { 16 | pub rect: Rectangle, 17 | pub overlay_cursor: bool, 18 | pub output: Output, 19 | } 20 | 21 | impl Dispatch for ScreencopyManagerState 22 | where 23 | D: Dispatch, 24 | D: ScreencopyHandler, 25 | D: 'static, 26 | { 27 | fn request( 28 | state: &mut D, 29 | _client: &Client, 30 | frame: &ZwlrScreencopyFrameV1, 31 | request: Request, 32 | data: &ScreencopyFrameState, 33 | _display: &DisplayHandle, 34 | _data_init: &mut DataInit<'_, D>, 35 | ) { 36 | let (buffer, send_damage) = match request { 37 | Request::Copy { buffer } => (buffer, false), 38 | Request::CopyWithDamage { buffer } => (buffer, true), 39 | Request::Destroy => return, 40 | _ => unreachable!(), 41 | }; 42 | 43 | state.frame(Screencopy { 44 | send_damage, 45 | buffer, 46 | frame: frame.clone(), 47 | region: data.rect, 48 | submitted: false, 49 | output: data.output.clone(), 50 | }); 51 | } 52 | } 53 | 54 | /// Screencopy frame. 55 | pub struct Screencopy { 56 | region: Rectangle, 57 | frame: ZwlrScreencopyFrameV1, 58 | send_damage: bool, 59 | buffer: WlBuffer, 60 | submitted: bool, 61 | pub output: Output, 62 | } 63 | 64 | impl Drop for Screencopy { 65 | fn drop(&mut self) { 66 | if !self.submitted { 67 | self.frame.failed(); 68 | } 69 | } 70 | } 71 | 72 | impl Screencopy { 73 | /// Get the target buffer to copy to. 74 | pub fn buffer(&self) -> &WlBuffer { 75 | &self.buffer 76 | } 77 | 78 | /// Get the region which should be copied. 79 | pub fn region(&self) -> Rectangle { 80 | self.region 81 | } 82 | 83 | /// Mark damaged regions of the screencopy buffer. 84 | pub fn damage(&mut self, damage: &[Rectangle]) { 85 | if !self.send_damage { 86 | return; 87 | } 88 | 89 | for Rectangle { loc, size } in damage { 90 | self.frame.damage(loc.x as u32, loc.y as u32, size.w as u32, size.h as u32); 91 | } 92 | } 93 | 94 | /// Submit the copied content. 95 | pub fn submit(mut self) { 96 | // Notify client that buffer is ordinary. 97 | self.frame.flags(Flags::empty()); 98 | 99 | // Notify client about successful copy. 100 | let now = UNIX_EPOCH.elapsed().unwrap(); 101 | let secs = now.as_secs(); 102 | self.frame.ready((secs >> 32) as u32, secs as u32, now.subsec_nanos()); 103 | 104 | // Mark frame as submitted to ensure destructor isn't run. 105 | self.submitted = true; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/utils/protocols/screencopy/mod.rs: -------------------------------------------------------------------------------- 1 | //! wlr-screencopy protocol. 2 | 3 | use _screencopy::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1; 4 | use _screencopy::zwlr_screencopy_manager_v1::{Request, ZwlrScreencopyManagerV1}; 5 | use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server as _screencopy; 6 | use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; 7 | use smithay::reexports::wayland_server::protocol::wl_shm; 8 | use smithay::reexports::wayland_server::{ 9 | Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, 10 | }; 11 | use smithay::utils::Rectangle; 12 | 13 | use smithay::output::Output; 14 | use crate::utils::protocols::screencopy::frame::{Screencopy, ScreencopyFrameState}; 15 | 16 | pub mod frame; 17 | 18 | const MANAGER_VERSION: u32 = 3; 19 | 20 | pub struct ScreencopyManagerState; 21 | 22 | impl ScreencopyManagerState { 23 | pub fn new(display: &DisplayHandle) -> Self 24 | where 25 | D: GlobalDispatch, 26 | D: Dispatch, 27 | D: Dispatch, 28 | D: ScreencopyHandler, 29 | D: 'static, 30 | { 31 | display.create_global::(MANAGER_VERSION, ()); 32 | 33 | Self 34 | } 35 | } 36 | 37 | impl GlobalDispatch for ScreencopyManagerState 38 | where 39 | D: GlobalDispatch, 40 | D: Dispatch, 41 | D: Dispatch, 42 | D: ScreencopyHandler, 43 | D: 'static, 44 | { 45 | fn bind( 46 | _state: &mut D, 47 | _display: &DisplayHandle, 48 | _client: &Client, 49 | manager: New, 50 | _manager_state: &(), 51 | data_init: &mut DataInit<'_, D>, 52 | ) { 53 | data_init.init(manager, ()); 54 | } 55 | } 56 | 57 | impl Dispatch for ScreencopyManagerState 58 | where 59 | D: GlobalDispatch, 60 | D: Dispatch, 61 | D: Dispatch, 62 | D: ScreencopyHandler, 63 | D: 'static, 64 | { 65 | fn request( 66 | state: &mut D, 67 | _client: &Client, 68 | manager: &ZwlrScreencopyManagerV1, 69 | request: Request, 70 | _data: &(), 71 | _display: &DisplayHandle, 72 | data_init: &mut DataInit<'_, D>, 73 | ) { 74 | let (frame, overlay_cursor, rect, output) = match request { 75 | Request::CaptureOutput { frame, overlay_cursor, output } => { 76 | let output = state.output(&output); 77 | let rect = 78 | Rectangle::from_loc_and_size((0, 0), output.current_mode().unwrap().size); 79 | (frame, overlay_cursor, rect, output.clone()) 80 | }, 81 | Request::CaptureOutputRegion { frame, overlay_cursor, x, y, width, height, output } => { 82 | let rect = Rectangle::from_loc_and_size((x, y), (width, height)); 83 | 84 | // Translate logical rect to physical framebuffer coordinates. 85 | let output = state.output(&output); 86 | let output_transform = output.current_transform(); 87 | let rotated_rect = output_transform.transform_rect_in(rect, &output.current_mode().unwrap().size); 88 | 89 | // Clamp captured region to the output. 90 | let clamped_rect = rotated_rect 91 | .intersection(Rectangle::from_loc_and_size( 92 | (0, 0), 93 | output.current_mode().unwrap().size, 94 | )) 95 | .unwrap_or_default(); 96 | 97 | (frame, overlay_cursor, clamped_rect, output.clone()) 98 | }, 99 | Request::Destroy => return, 100 | _ => unreachable!(), 101 | }; 102 | 103 | // Create the frame. 104 | let overlay_cursor = overlay_cursor != 0; 105 | let frame = data_init.init(frame, ScreencopyFrameState { overlay_cursor, rect, output }); 106 | 107 | // Send desired SHM buffer parameters. 108 | frame.buffer( 109 | wl_shm::Format::Argb8888, 110 | rect.size.w as u32, 111 | rect.size.h as u32, 112 | rect.size.w as u32 * 4, 113 | ); 114 | 115 | if manager.version() >= 3 { 116 | // Notify client that all supported buffers were enumerated. 117 | frame.buffer_done(); 118 | } 119 | } 120 | } 121 | 122 | /// Handler trait for wlr-screencopy. 123 | pub trait ScreencopyHandler { 124 | /// Get the physical size of an output. 125 | fn output(&mut self, output: &WlOutput) -> &Output; 126 | 127 | /// Handle new screencopy request. 128 | fn frame(&mut self, frame: Screencopy); 129 | } 130 | 131 | #[allow(missing_docs)] 132 | #[macro_export] 133 | macro_rules! delegate_screencopy_manager { 134 | ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { 135 | smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ 136 | smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1: () 137 | ] => $crate::utils::protocols::screencopy::ScreencopyManagerState); 138 | 139 | smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ 140 | smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1: () 141 | ] => $crate::utils::protocols::screencopy::ScreencopyManagerState); 142 | 143 | smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ 144 | smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1: $crate::utils::protocols::screencopy::frame::ScreencopyFrameState 145 | ] => $crate::utils::protocols::screencopy::ScreencopyManagerState); 146 | }; 147 | } 148 | -------------------------------------------------------------------------------- /src/utils/render.rs: -------------------------------------------------------------------------------- 1 | use smithay::{ 2 | backend::renderer::{ 3 | element::{surface::WaylandSurfaceRenderElement, texture::TextureRenderElement}, 4 | ImportAll, ImportMem, Renderer, 5 | }, 6 | render_elements, 7 | }; 8 | 9 | render_elements! { 10 | pub CustomRenderElements where 11 | R: ImportAll + ImportMem; 12 | Texture=TextureRenderElement<::TextureId>, 13 | Surface=WaylandSurfaceRenderElement, 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/tiling.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use smithay::{ 4 | desktop::{Window, layer_map_for_output}, 5 | utils::{Logical, Physical, Point, Rectangle, Size}, 6 | }; 7 | 8 | use super::{ 9 | binarytree::{BinaryTree, HorizontalOrVertical}, 10 | workspaces::{MagmaWindow, Workspace}, 11 | }; 12 | 13 | pub enum WindowLayoutEvent { 14 | Added, 15 | Removed, 16 | } 17 | 18 | pub fn bsp_layout( 19 | workspace: &mut Workspace, 20 | window: Window, 21 | event: WindowLayoutEvent, 22 | gaps: (i32, i32), 23 | ) { 24 | let output = layer_map_for_output(workspace 25 | .outputs() 26 | .next() 27 | .unwrap()).non_exclusive_zone(); 28 | 29 | match event { 30 | WindowLayoutEvent::Added => { 31 | let window = Rc::new(RefCell::new(MagmaWindow { 32 | window, 33 | rec: Rectangle { 34 | loc: Point::from((gaps.0 + output.loc.x, gaps.0 + output.loc.y)), 35 | size: Size::from((output.size.w - (gaps.0 * 2), output.size.h - (gaps.0 * 2))), 36 | }, 37 | })); 38 | workspace.add_window(window); 39 | 40 | bsp_update_layout(workspace, gaps); 41 | } 42 | WindowLayoutEvent::Removed => { 43 | workspace.remove_window(&window); 44 | bsp_update_layout(workspace, gaps); 45 | } 46 | } 47 | dbg!(workspace.layout_tree.clone()); 48 | } 49 | 50 | pub fn bsp_update_layout(workspace: &mut Workspace, gaps: (i32, i32)) { 51 | //recalculate the size and location of the windows 52 | 53 | let output = layer_map_for_output(workspace 54 | .outputs() 55 | .next() 56 | .unwrap()).non_exclusive_zone(); 57 | 58 | let output_full = workspace.outputs().next().unwrap().current_mode().unwrap().size; 59 | 60 | match &mut workspace.layout_tree { 61 | BinaryTree::Empty => {} 62 | BinaryTree::Window(w) => { 63 | w.borrow_mut().rec = Rectangle { 64 | loc: Point::from((gaps.0 + gaps.1 + output.loc.x, gaps.0 + gaps.1 + output.loc.y)), 65 | size: Size::from(( 66 | output.size.w - ((gaps.0 + gaps.1) * 2), 67 | output.size.h - ((gaps.0 + gaps.1) * 2), 68 | )), 69 | }; 70 | } 71 | BinaryTree::Split { 72 | left, 73 | right, 74 | split, 75 | ratio, 76 | } => { 77 | if let BinaryTree::Window(w) = left.as_mut() { 78 | generate_layout( 79 | right.as_mut(), 80 | &w, 81 | Rectangle { 82 | loc: Point::from((gaps.0 + output.loc.x, gaps.0 + output.loc.y)), 83 | size: Size::from((output.size.w - (gaps.0 * 2), output.size.h - (gaps.0 * 2))), 84 | }, 85 | *split, 86 | *ratio, 87 | Size::from((output_full.w - gaps.0, output_full.h - gaps.0)), 88 | gaps, 89 | ) 90 | } 91 | } 92 | } 93 | for magmawindow in workspace.magmawindows() { 94 | let xdg_toplevel = magmawindow.window.toplevel(); 95 | xdg_toplevel.with_pending_state(|state| { 96 | state.size = Some(magmawindow.rec.size); 97 | }); 98 | xdg_toplevel.send_configure(); 99 | } 100 | } 101 | 102 | pub fn generate_layout( 103 | tree: &mut BinaryTree, 104 | lastwin: &Rc>, 105 | lastgeo: Rectangle, 106 | split: HorizontalOrVertical, 107 | ratio: f32, 108 | output: Size, 109 | gaps: (i32, i32), 110 | ) { 111 | let size; 112 | match split { 113 | HorizontalOrVertical::Horizontal => { 114 | size = Size::from(((lastgeo.size.w as f32 * ratio) as i32, lastgeo.size.h)); 115 | } 116 | HorizontalOrVertical::Vertical => { 117 | size = Size::from((lastgeo.size.w, (lastgeo.size.h as f32 * ratio) as i32)); 118 | } 119 | } 120 | 121 | let loc: Point; 122 | match split { 123 | HorizontalOrVertical::Horizontal => { 124 | loc = Point::from((lastgeo.loc.x, output.h - size.h)); 125 | } 126 | HorizontalOrVertical::Vertical => { 127 | loc = Point::from((output.w - size.w, lastgeo.loc.y)); 128 | } 129 | } 130 | 131 | let recgapped = Rectangle { 132 | size: Size::from((size.w - (gaps.1 * 2), (size.h - (gaps.1 * 2)))), 133 | loc: Point::from((loc.x + gaps.1, loc.y + gaps.1)), 134 | }; 135 | 136 | lastwin.borrow_mut().rec = recgapped; 137 | 138 | let loc; 139 | match split { 140 | HorizontalOrVertical::Horizontal => { 141 | loc = Point::from((output.w - size.w, lastgeo.loc.y)); 142 | } 143 | HorizontalOrVertical::Vertical => { 144 | loc = Point::from((lastgeo.loc.x, output.h - size.h)); 145 | } 146 | } 147 | 148 | let rec = Rectangle { size, loc }; 149 | let recgapped = Rectangle { 150 | size: Size::from((size.w - (gaps.1 * 2), (size.h - (gaps.1 * 2)))), 151 | loc: Point::from((loc.x + gaps.1, loc.y + gaps.1)), 152 | }; 153 | match tree { 154 | BinaryTree::Empty => {} 155 | BinaryTree::Window(w) => w.borrow_mut().rec = recgapped, 156 | BinaryTree::Split { 157 | split, 158 | ratio, 159 | left, 160 | right, 161 | } => { 162 | if let BinaryTree::Window(w) = left.as_mut() { 163 | w.borrow_mut().rec = rec; 164 | generate_layout(right.as_mut(), &w, rec, *split, *ratio, output, gaps) 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/utils/workspaces.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::{Ref, RefCell}, 3 | rc::Rc, 4 | }; 5 | 6 | use smithay::{ 7 | backend::renderer::{ 8 | element::{surface::WaylandSurfaceRenderElement, AsRenderElements}, 9 | ImportAll, Renderer, Texture, 10 | }, 11 | desktop::{space::SpaceElement, Window}, 12 | output::Output, 13 | utils::{Logical, Point, Rectangle, Scale, Transform}, 14 | }; 15 | 16 | use crate::ipc::MagmaIpcManager; 17 | 18 | use super::{binarytree::BinaryTree, tiling::bsp_update_layout}; 19 | 20 | #[derive(Debug, PartialEq, Clone)] 21 | pub struct MagmaWindow { 22 | pub window: Window, 23 | pub rec: Rectangle, 24 | } 25 | impl MagmaWindow { 26 | fn bbox(&self) -> Rectangle { 27 | let mut bbox = self.window.bbox(); 28 | bbox.loc += self.rec.loc - self.window.geometry().loc; 29 | bbox 30 | } 31 | 32 | fn render_location(&self) -> Point { 33 | self.rec.loc - self.window.geometry().loc 34 | } 35 | } 36 | pub struct Workspace { 37 | windows: Vec>>, 38 | outputs: Vec, 39 | pub layout_tree: BinaryTree, 40 | } 41 | 42 | impl Workspace { 43 | pub fn new() -> Self { 44 | Workspace { 45 | windows: Vec::new(), 46 | outputs: Vec::new(), 47 | layout_tree: BinaryTree::new(), 48 | } 49 | } 50 | 51 | pub fn windows(&self) -> impl Iterator> { 52 | self.windows 53 | .iter() 54 | .map(|w| Ref::map(w.borrow(), |hw| &hw.window)) 55 | } 56 | 57 | pub fn magmawindows(&self) -> impl Iterator> { 58 | self.windows.iter().map(|w| Ref::map(w.borrow(), |hw| hw)) 59 | } 60 | 61 | pub fn add_window(&mut self, window: Rc>) { 62 | // add window to vec and remap if exists 63 | self.windows 64 | .retain(|w| &w.borrow().window != &window.borrow().window); 65 | self.windows.push(window.clone()); 66 | self.layout_tree 67 | .insert(window, self.layout_tree.next_split(), 0.5); 68 | } 69 | 70 | pub fn remove_window(&mut self, window: &Window) -> Option>> { 71 | let mut removed = None; 72 | self.windows.retain(|w| { 73 | if &w.borrow().window == window { 74 | removed = Some(w.clone()); 75 | false 76 | } else { 77 | true 78 | } 79 | }); 80 | self.layout_tree.remove(&window); 81 | removed 82 | } 83 | 84 | pub fn render_elements<'a, R: Renderer + ImportAll, C: From>>( 85 | &self, 86 | renderer: &mut R, 87 | ) -> Vec 88 | where 89 | ::TextureId: Texture + 'static, 90 | { 91 | let mut render_elements: Vec = Vec::new(); 92 | for element in &self.windows { 93 | render_elements.append(&mut element.borrow().window.render_elements( 94 | renderer, 95 | element.borrow().render_location().to_physical(1), 96 | Scale::from(1.0), 97 | )); 98 | } 99 | render_elements 100 | } 101 | 102 | pub fn outputs(&self) -> impl Iterator { 103 | self.outputs.iter() 104 | } 105 | 106 | pub fn add_output(&mut self, output: Output) { 107 | self.outputs.push(output); 108 | } 109 | 110 | pub fn remove_outputs(&mut self) { 111 | self.outputs.clear() 112 | } 113 | 114 | pub fn output_geometry(&self, o: &Output) -> Option> { 115 | if !self.outputs.contains(o) { 116 | return None; 117 | } 118 | 119 | let transform: Transform = o.current_transform(); 120 | o.current_mode().map(|mode| { 121 | Rectangle::from_loc_and_size( 122 | (0, 0), 123 | transform 124 | .transform_size(mode.size) 125 | .to_f64() 126 | .to_logical(o.current_scale().fractional_scale()) 127 | .to_i32_ceil(), 128 | ) 129 | }) 130 | } 131 | 132 | pub fn window_under>>( 133 | &self, 134 | point: P, 135 | ) -> Option<(Ref<'_, Window>, Point)> { 136 | let point = point.into(); 137 | self.windows 138 | .iter() 139 | .filter(|e| e.borrow().bbox().to_f64().contains(point)) 140 | .find_map(|e| { 141 | // we need to offset the point to the location where the surface is actually drawn 142 | let render_location = e.borrow().render_location(); 143 | if e.borrow() 144 | .window 145 | .is_in_input_region(&(point - render_location.to_f64())) 146 | { 147 | Some((Ref::map(e.borrow(), |hw| &hw.window), render_location)) 148 | } else { 149 | None 150 | } 151 | }) 152 | } 153 | 154 | pub fn contains_window(&self, window: &Window) -> bool { 155 | self.windows.iter().any(|w| &w.borrow().window == window) 156 | } 157 | } 158 | 159 | pub struct Workspaces { 160 | workspaces: Vec, 161 | pub current: u8, 162 | } 163 | 164 | impl Workspaces { 165 | pub fn new(workspaceamount: u8) -> Self { 166 | Workspaces { 167 | workspaces: (0..workspaceamount).map(|_| Workspace::new()).collect(), 168 | current: 0, 169 | } 170 | } 171 | 172 | pub fn outputs (&self) -> impl Iterator { 173 | self.workspaces.iter().flat_map(|w| w.outputs()) 174 | } 175 | 176 | pub fn iter(&mut self) -> impl Iterator { 177 | self.workspaces.iter_mut() 178 | } 179 | 180 | pub fn current_mut(&mut self) -> &mut Workspace { 181 | &mut self.workspaces[self.current as usize] 182 | } 183 | 184 | pub fn current(&self) -> &Workspace { 185 | &self.workspaces[self.current as usize] 186 | } 187 | 188 | pub fn all_windows(&self) -> impl Iterator> { 189 | self.workspaces.iter().flat_map(|w| w.windows()) 190 | } 191 | 192 | pub fn workspace_from_window(&mut self, window: &Window) -> Option<&mut Workspace> { 193 | self.workspaces 194 | .iter_mut() 195 | .find(|w| w.contains_window(window)) 196 | } 197 | 198 | pub fn activate(&mut self, id: u8, magma_ipc_manager: &mut MagmaIpcManager) { 199 | self.current = id; 200 | magma_ipc_manager.update_active_workspace(id.into()); 201 | } 202 | pub fn move_window_to_workspace(&mut self, window: &Window, workspace: u8, gaps: (i32, i32)) { 203 | let mut removed = None; 204 | if let Some(ws) = self.workspace_from_window(window) { 205 | removed = ws.remove_window(window); 206 | bsp_update_layout(ws, gaps) 207 | } 208 | if let Some(removed) = removed { 209 | self.workspaces[workspace as usize].add_window(removed); 210 | bsp_update_layout(&mut self.workspaces[workspace as usize], gaps) 211 | } 212 | } 213 | } 214 | --------------------------------------------------------------------------------