├── .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