├── .gitignore ├── .reuse └── dep5 ├── COPYING ├── LICENSES ├── CC-BY-SA-4.0.txt ├── CC0-1.0.txt ├── GPL-3.0-or-later.txt └── MIT.txt ├── README.md ├── meson.build ├── protocol ├── meson.build ├── wlr-layer-shell-unstable-v1.xml ├── wlr-output-management-unstable-v1.xml └── wlr-screencopy-unstable-v1.xml ├── resources ├── head.ui ├── meson.build ├── resources.xml.in ├── style.css ├── wdisplays.desktop.in ├── wdisplays.svg └── wdisplays.ui ├── src ├── config.h.in ├── glviewport.c ├── glviewport.h ├── headform.c ├── headform.h ├── main.c ├── meson.build ├── outputs.c ├── overlay.c ├── render.c └── wdisplays.h └── wdisplays.png /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: wdisplays 3 | Upstream-Contact: Jason Francis 4 | Source: 5 | 6 | Files: README.md .gitignore 7 | Copyright: 2020 Jason Francis 8 | License: CC0-1.0 9 | 10 | Files: resources/style.css resources/wdisplays.desktop.in resources/*.ui 11 | resources/resources.xml.in 12 | Copyright: 2020 Jason Francis 13 | License: GPL-3.0-or-later 14 | 15 | Files: wdisplays.png resources/wdisplays.svg 16 | Copyright: 2020 Jason Francis 17 | License: CC-BY-SA-4.0 18 | 19 | Files: protocol/wlr-layer-shell-unstable-v1.xml 20 | Copyright: 2017 Drew DeVault 21 | License: MIT 22 | 23 | Files: protocol/wlr-output-management-unstable-v1.xml 24 | Copyright: 2019 Purism SPC 25 | License: MIT 26 | 27 | Files: protocol/wlr-screencopy-unstable-v1.xml 28 | Copyright: 2018 Simon Ser 29 | License: MIT 30 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | LICENSES/GPL-3.0-or-later.txt -------------------------------------------------------------------------------- /LICENSES/CC-BY-SA-4.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-ShareAlike 4.0 International Creative Commons 2 | Corporation ("Creative Commons") is not a law firm and does not provide legal 3 | services or legal advice. Distribution of Creative Commons public licenses 4 | does not create a lawyer-client or other relationship. Creative Commons makes 5 | its licenses and related information available on an "as-is" basis. Creative 6 | Commons gives no warranties regarding its licenses, any material licensed 7 | under their terms and conditions, or any related information. Creative Commons 8 | disclaims all liability for damages resulting from their use to the fullest 9 | extent possible. 10 | 11 | Using Creative Commons Public Licenses 12 | 13 | Creative Commons public licenses provide a standard set of terms and conditions 14 | that creators and other rights holders may use to share original works of 15 | authorship and other material subject to copyright and certain other rights 16 | specified in the public license below. The following considerations are for 17 | informational purposes only, are not exhaustive, and do not form part of our 18 | licenses. 19 | 20 | Considerations for licensors: Our public licenses are intended for use by 21 | those authorized to give the public permission to use material in ways otherwise 22 | restricted by copyright and certain other rights. Our licenses are irrevocable. 23 | Licensors should read and understand the terms and conditions of the license 24 | they choose before applying it. Licensors should also secure all rights necessary 25 | before applying our licenses so that the public can reuse the material as 26 | expected. Licensors should clearly mark any material not subject to the license. 27 | This includes other CC-licensed material, or material used under an exception 28 | or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors 29 | 30 | Considerations for the public: By using one of our public licenses, a licensor 31 | grants the public permission to use the licensed material under specified 32 | terms and conditions. If the licensor's permission is not necessary for any 33 | reason–for example, because of any applicable exception or limitation to copyright–then 34 | that use is not regulated by the license. Our licenses grant only permissions 35 | under copyright and certain other rights that a licensor has authority to 36 | grant. Use of the licensed material may still be restricted for other reasons, 37 | including because others have copyright or other rights in the material. A 38 | licensor may make special requests, such as asking that all changes be marked 39 | or described. 40 | 41 | Although not required by our licenses, you are encouraged to respect those 42 | requests where reasonable. More considerations for the public : wiki.creativecommons.org/Considerations_for_licensees 43 | 44 | Creative Commons Attribution-ShareAlike 4.0 International Public License 45 | 46 | By exercising the Licensed Rights (defined below), You accept and agree to 47 | be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 48 | 4.0 International Public License ("Public License"). To the extent this Public 49 | License may be interpreted as a contract, You are granted the Licensed Rights 50 | in consideration of Your acceptance of these terms and conditions, and the 51 | Licensor grants You such rights in consideration of benefits the Licensor 52 | receives from making the Licensed Material available under these terms and 53 | conditions. 54 | 55 | Section 1 – Definitions. 56 | 57 | a. Adapted Material means material subject to Copyright and Similar Rights 58 | that is derived from or based upon the Licensed Material and in which the 59 | Licensed Material is translated, altered, arranged, transformed, or otherwise 60 | modified in a manner requiring permission under the Copyright and Similar 61 | Rights held by the Licensor. For purposes of this Public License, where the 62 | Licensed Material is a musical work, performance, or sound recording, Adapted 63 | Material is always produced where the Licensed Material is synched in timed 64 | relation with a moving image. 65 | 66 | b. Adapter's License means the license You apply to Your Copyright and Similar 67 | Rights in Your contributions to Adapted Material in accordance with the terms 68 | and conditions of this Public License. 69 | 70 | c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, 71 | approved by Creative Commons as essentially the equivalent of this Public 72 | License. 73 | 74 | d. Copyright and Similar Rights means copyright and/or similar rights closely 75 | related to copyright including, without limitation, performance, broadcast, 76 | sound recording, and Sui Generis Database Rights, without regard to how the 77 | rights are labeled or categorized. For purposes of this Public License, the 78 | rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 79 | 80 | e. Effective Technological Measures means those measures that, in the absence 81 | of proper authority, may not be circumvented under laws fulfilling obligations 82 | under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, 83 | and/or similar international agreements. 84 | 85 | f. Exceptions and Limitations means fair use, fair dealing, and/or any other 86 | exception or limitation to Copyright and Similar Rights that applies to Your 87 | use of the Licensed Material. 88 | 89 | g. License Elements means the license attributes listed in the name of a Creative 90 | Commons Public License. The License Elements of this Public License are Attribution 91 | and ShareAlike. 92 | 93 | h. Licensed Material means the artistic or literary work, database, or other 94 | material to which the Licensor applied this Public License. 95 | 96 | i. Licensed Rights means the rights granted to You subject to the terms and 97 | conditions of this Public License, which are limited to all Copyright and 98 | Similar Rights that apply to Your use of the Licensed Material and that the 99 | Licensor has authority to license. 100 | 101 | j. Licensor means the individual(s) or entity(ies) granting rights under this 102 | Public License. 103 | 104 | k. Share means to provide material to the public by any means or process that 105 | requires permission under the Licensed Rights, such as reproduction, public 106 | display, public performance, distribution, dissemination, communication, or 107 | importation, and to make material available to the public including in ways 108 | that members of the public may access the material from a place and at a time 109 | individually chosen by them. 110 | 111 | l. Sui Generis Database Rights means rights other than copyright resulting 112 | from Directive 96/9/EC of the European Parliament and of the Council of 11 113 | March 1996 on the legal protection of databases, as amended and/or succeeded, 114 | as well as other essentially equivalent rights anywhere in the world. 115 | 116 | m. You means the individual or entity exercising the Licensed Rights under 117 | this Public License. Your has a corresponding meaning. 118 | 119 | Section 2 – Scope. 120 | 121 | a. License grant. 122 | 123 | 1. Subject to the terms and conditions of this Public License, the Licensor 124 | hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, 125 | irrevocable license to exercise the Licensed Rights in the Licensed Material 126 | to: 127 | 128 | A. reproduce and Share the Licensed Material, in whole or in part; and 129 | 130 | B. produce, reproduce, and Share Adapted Material. 131 | 132 | 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions 133 | and Limitations apply to Your use, this Public License does not apply, and 134 | You do not need to comply with its terms and conditions. 135 | 136 | 3. Term. The term of this Public License is specified in Section 6(a). 137 | 138 | 4. Media and formats; technical modifications allowed. The Licensor authorizes 139 | You to exercise the Licensed Rights in all media and formats whether now known 140 | or hereafter created, and to make technical modifications necessary to do 141 | so. The Licensor waives and/or agrees not to assert any right or authority 142 | to forbid You from making technical modifications necessary to exercise the 143 | Licensed Rights, including technical modifications necessary to circumvent 144 | Effective Technological Measures. For purposes of this Public License, simply 145 | making modifications authorized by this Section 2(a)(4) never produces Adapted 146 | Material. 147 | 148 | 5. Downstream recipients. 149 | 150 | A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed 151 | Material automatically receives an offer from the Licensor to exercise the 152 | Licensed Rights under the terms and conditions of this Public License. 153 | 154 | B. Additional offer from the Licensor – Adapted Material. Every recipient 155 | of Adapted Material from You automatically receives an offer from the Licensor 156 | to exercise the Licensed Rights in the Adapted Material under the conditions 157 | of the Adapter's License You apply. 158 | 159 | C. No downstream restrictions. You may not offer or impose any additional 160 | or different terms or conditions on, or apply any Effective Technological 161 | Measures to, the Licensed Material if doing so restricts exercise of the Licensed 162 | Rights by any recipient of the Licensed Material. 163 | 164 | 6. No endorsement. Nothing in this Public License constitutes or may be construed 165 | as permission to assert or imply that You are, or that Your use of the Licensed 166 | Material is, connected with, or sponsored, endorsed, or granted official status 167 | by, the Licensor or others designated to receive attribution as provided in 168 | Section 3(a)(1)(A)(i). 169 | 170 | b. Other rights. 171 | 172 | 1. Moral rights, such as the right of integrity, are not licensed under this 173 | Public License, nor are publicity, privacy, and/or other similar personality 174 | rights; however, to the extent possible, the Licensor waives and/or agrees 175 | not to assert any such rights held by the Licensor to the limited extent necessary 176 | to allow You to exercise the Licensed Rights, but not otherwise. 177 | 178 | 2. Patent and trademark rights are not licensed under this Public License. 179 | 180 | 3. To the extent possible, the Licensor waives any right to collect royalties 181 | from You for the exercise of the Licensed Rights, whether directly or through 182 | a collecting society under any voluntary or waivable statutory or compulsory 183 | licensing scheme. In all other cases the Licensor expressly reserves any right 184 | to collect such royalties. 185 | 186 | Section 3 – License Conditions. 187 | 188 | Your exercise of the Licensed Rights is expressly made subject to the following 189 | conditions. 190 | 191 | a. Attribution. 192 | 193 | 1. If You Share the Licensed Material (including in modified form), You must: 194 | 195 | A. retain the following if it is supplied by the Licensor with the Licensed 196 | Material: 197 | 198 | i. identification of the creator(s) of the Licensed Material and any others 199 | designated to receive attribution, in any reasonable manner requested by the 200 | Licensor (including by pseudonym if designated); 201 | 202 | ii. a copyright notice; 203 | 204 | iii. a notice that refers to this Public License; 205 | 206 | iv. a notice that refers to the disclaimer of warranties; 207 | 208 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 209 | 210 | B. indicate if You modified the Licensed Material and retain an indication 211 | of any previous modifications; and 212 | 213 | C. indicate the Licensed Material is licensed under this Public License, and 214 | include the text of, or the URI or hyperlink to, this Public License. 215 | 216 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner 217 | based on the medium, means, and context in which You Share the Licensed Material. 218 | For example, it may be reasonable to satisfy the conditions by providing a 219 | URI or hyperlink to a resource that includes the required information. 220 | 221 | 3. If requested by the Licensor, You must remove any of the information required 222 | by Section 3(a)(1)(A) to the extent reasonably practicable. 223 | 224 | b. ShareAlike.In addition to the conditions in Section 3(a), if You Share 225 | Adapted Material You produce, the following conditions also apply. 226 | 227 | 1. The Adapter's License You apply must be a Creative Commons license with 228 | the same License Elements, this version or later, or a BY-SA Compatible License. 229 | 230 | 2. You must include the text of, or the URI or hyperlink to, the Adapter's 231 | License You apply. You may satisfy this condition in any reasonable manner 232 | based on the medium, means, and context in which You Share Adapted Material. 233 | 234 | 3. You may not offer or impose any additional or different terms or conditions 235 | on, or apply any Effective Technological Measures to, Adapted Material that 236 | restrict exercise of the rights granted under the Adapter's License You apply. 237 | 238 | Section 4 – Sui Generis Database Rights. 239 | 240 | Where the Licensed Rights include Sui Generis Database Rights that apply to 241 | Your use of the Licensed Material: 242 | 243 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, 244 | reuse, reproduce, and Share all or a substantial portion of the contents of 245 | the database; 246 | 247 | b. if You include all or a substantial portion of the database contents in 248 | a database in which You have Sui Generis Database Rights, then the database 249 | in which You have Sui Generis Database Rights (but not its individual contents) 250 | is Adapted Material, including for purposes of Section 3(b); and 251 | 252 | c. You must comply with the conditions in Section 3(a) if You Share all or 253 | a substantial portion of the contents of the database. 254 | 255 | For the avoidance of doubt, this Section 4 supplements and does not replace 256 | Your obligations under this Public License where the Licensed Rights include 257 | other Copyright and Similar Rights. 258 | 259 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 260 | 261 | a. Unless otherwise separately undertaken by the Licensor, to the extent possible, 262 | the Licensor offers the Licensed Material as-is and as-available, and makes 263 | no representations or warranties of any kind concerning the Licensed Material, 264 | whether express, implied, statutory, or other. This includes, without limitation, 265 | warranties of title, merchantability, fitness for a particular purpose, non-infringement, 266 | absence of latent or other defects, accuracy, or the presence or absence of 267 | errors, whether or not known or discoverable. Where disclaimers of warranties 268 | are not allowed in full or in part, this disclaimer may not apply to You. 269 | 270 | b. To the extent possible, in no event will the Licensor be liable to You 271 | on any legal theory (including, without limitation, negligence) or otherwise 272 | for any direct, special, indirect, incidental, consequential, punitive, exemplary, 273 | or other losses, costs, expenses, or damages arising out of this Public License 274 | or use of the Licensed Material, even if the Licensor has been advised of 275 | the possibility of such losses, costs, expenses, or damages. Where a limitation 276 | of liability is not allowed in full or in part, this limitation may not apply 277 | to You. 278 | 279 | c. The disclaimer of warranties and limitation of liability provided above 280 | shall be interpreted in a manner that, to the extent possible, most closely 281 | approximates an absolute disclaimer and waiver of all liability. 282 | 283 | Section 6 – Term and Termination. 284 | 285 | a. This Public License applies for the term of the Copyright and Similar Rights 286 | licensed here. However, if You fail to comply with this Public License, then 287 | Your rights under this Public License terminate automatically. 288 | 289 | b. Where Your right to use the Licensed Material has terminated under Section 290 | 6(a), it reinstates: 291 | 292 | 1. automatically as of the date the violation is cured, provided it is cured 293 | within 30 days of Your discovery of the violation; or 294 | 295 | 2. upon express reinstatement by the Licensor. 296 | 297 | c. For the avoidance of doubt, this Section 6(b) does not affect any right 298 | the Licensor may have to seek remedies for Your violations of this Public 299 | License. 300 | 301 | d. For the avoidance of doubt, the Licensor may also offer the Licensed Material 302 | under separate terms or conditions or stop distributing the Licensed Material 303 | at any time; however, doing so will not terminate this Public License. 304 | 305 | e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 306 | 307 | Section 7 – Other Terms and Conditions. 308 | 309 | a. The Licensor shall not be bound by any additional or different terms or 310 | conditions communicated by You unless expressly agreed. 311 | 312 | b. Any arrangements, understandings, or agreements regarding the Licensed 313 | Material not stated herein are separate from and independent of the terms 314 | and conditions of this Public License. 315 | 316 | Section 8 – Interpretation. 317 | 318 | a. For the avoidance of doubt, this Public License does not, and shall not 319 | be interpreted to, reduce, limit, restrict, or impose conditions on any use 320 | of the Licensed Material that could lawfully be made without permission under 321 | this Public License. 322 | 323 | b. To the extent possible, if any provision of this Public License is deemed 324 | unenforceable, it shall be automatically reformed to the minimum extent necessary 325 | to make it enforceable. If the provision cannot be reformed, it shall be severed 326 | from this Public License without affecting the enforceability of the remaining 327 | terms and conditions. 328 | 329 | c. No term or condition of this Public License will be waived and no failure 330 | to comply consented to unless expressly agreed to by the Licensor. 331 | 332 | d. Nothing in this Public License constitutes or may be interpreted as a limitation 333 | upon, or waiver of, any privileges and immunities that apply to the Licensor 334 | or You, including from the legal processes of any jurisdiction or authority. 335 | 336 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative 337 | Commons may elect to apply one of its public licenses to material it publishes 338 | and in those instances will be considered the "Licensor." The text of the 339 | Creative Commons public licenses is dedicated to the public domain under the 340 | CC0 Public Domain Dedication. Except for the limited purpose of indicating 341 | that material is shared under a Creative Commons public license or as otherwise 342 | permitted by the Creative Commons policies published at creativecommons.org/policies, 343 | Creative Commons does not authorize the use of the trademark "Creative Commons" 344 | or any other trademark or logo of Creative Commons without its prior written 345 | consent including, without limitation, in connection with any unauthorized 346 | modifications to any of its public licenses or any other arrangements, understandings, 347 | or agreements concerning use of licensed material. For the avoidance of doubt, 348 | this paragraph does not form part of the public licenses. 349 | 350 | Creative Commons may be contacted at creativecommons.org. 351 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES 4 | NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE 5 | AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION 6 | ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE 7 | OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS 8 | LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION 9 | OR WORKS PROVIDED HEREUNDER. 10 | 11 | Statement of Purpose 12 | 13 | The laws of most jurisdictions throughout the world automatically confer exclusive 14 | Copyright and Related Rights (defined below) upon the creator and subsequent 15 | owner(s) (each and all, an "owner") of an original work of authorship and/or 16 | a database (each, a "Work"). 17 | 18 | Certain owners wish to permanently relinquish those rights to a Work for the 19 | purpose of contributing to a commons of creative, cultural and scientific 20 | works ("Commons") that the public can reliably and without fear of later claims 21 | of infringement build upon, modify, incorporate in other works, reuse and 22 | redistribute as freely as possible in any form whatsoever and for any purposes, 23 | including without limitation commercial purposes. These owners may contribute 24 | to the Commons to promote the ideal of a free culture and the further production 25 | of creative, cultural and scientific works, or to gain reputation or greater 26 | distribution for their Work in part through the use and efforts of others. 27 | 28 | For these and/or other purposes and motivations, and without any expectation 29 | of additional consideration or compensation, the person associating CC0 with 30 | a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 31 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 32 | and publicly distribute the Work under its terms, with knowledge of his or 33 | her Copyright and Related Rights in the Work and the meaning and intended 34 | legal effect of CC0 on those rights. 35 | 36 | 1. Copyright and Related Rights. A Work made available under CC0 may be protected 37 | by copyright and related or neighboring rights ("Copyright and Related Rights"). 38 | Copyright and Related Rights include, but are not limited to, the following: 39 | 40 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 41 | and translate a Work; 42 | 43 | ii. moral rights retained by the original author(s) and/or performer(s); 44 | 45 | iii. publicity and privacy rights pertaining to a person's image or likeness 46 | depicted in a Work; 47 | 48 | iv. rights protecting against unfair competition in regards to a Work, subject 49 | to the limitations in paragraph 4(a), below; 50 | 51 | v. rights protecting the extraction, dissemination, use and reuse of data 52 | in a Work; 53 | 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal protection 56 | of databases, and under any national implementation thereof, including any 57 | amended or successor version of such directive); and 58 | 59 | vii. other similar, equivalent or corresponding rights throughout the world 60 | based on applicable law or treaty, and any national implementations thereof. 61 | 62 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 63 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 64 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 65 | and Related Rights and associated claims and causes of action, whether now 66 | known or unknown (including existing as well as future claims and causes of 67 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 68 | duration provided by applicable law or treaty (including future time extensions), 69 | (iii) in any current or future medium and for any number of copies, and (iv) 70 | for any purpose whatsoever, including without limitation commercial, advertising 71 | or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the 72 | benefit of each member of the public at large and to the detriment of Affirmer's 73 | heirs and successors, fully intending that such Waiver shall not be subject 74 | to revocation, rescission, cancellation, termination, or any other legal or 75 | equitable action to disrupt the quiet enjoyment of the Work by the public 76 | as contemplated by Affirmer's express Statement of Purpose. 77 | 78 | 3. Public License Fallback. Should any part of the Waiver for any reason be 79 | judged legally invalid or ineffective under applicable law, then the Waiver 80 | shall be preserved to the maximum extent permitted taking into account Affirmer's 81 | express Statement of Purpose. In addition, to the extent the Waiver is so 82 | judged Affirmer hereby grants to each affected person a royalty-free, non 83 | transferable, non sublicensable, non exclusive, irrevocable and unconditional 84 | license to exercise Affirmer's Copyright and Related Rights in the Work (i) 85 | in all territories worldwide, (ii) for the maximum duration provided by applicable 86 | law or treaty (including future time extensions), (iii) in any current or 87 | future medium and for any number of copies, and (iv) for any purpose whatsoever, 88 | including without limitation commercial, advertising or promotional purposes 89 | (the "License"). The License shall be deemed effective as of the date CC0 90 | was applied by Affirmer to the Work. Should any part of the License for any 91 | reason be judged legally invalid or ineffective under applicable law, such 92 | partial invalidity or ineffectiveness shall not invalidate the remainder of 93 | the License, and in such case Affirmer hereby affirms that he or she will 94 | not (i) exercise any of his or her remaining Copyright and Related Rights 95 | in the Work or (ii) assert any associated claims and causes of action with 96 | respect to the Work, in either case contrary to Affirmer's express Statement 97 | of Purpose. 98 | 99 | 4. Limitations and Disclaimers. 100 | 101 | a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, 102 | licensed or otherwise affected by this document. 103 | 104 | b. Affirmer offers the Work as-is and makes no representations or warranties 105 | of any kind concerning the Work, express, implied, statutory or otherwise, 106 | including without limitation warranties of title, merchantability, fitness 107 | for a particular purpose, non infringement, or the absence of latent or other 108 | defects, accuracy, or the present or absence of errors, whether or not discoverable, 109 | all to the greatest extent permissible under applicable law. 110 | 111 | c. Affirmer disclaims responsibility for clearing rights of other persons 112 | that may apply to the Work or any use thereof, including without limitation 113 | any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims 114 | responsibility for obtaining any necessary consents, permissions or other 115 | rights required for any use of the Work. 116 | 117 | d. Affirmer understands and acknowledges that Creative Commons is not a party 118 | to this document and has no duty or obligation with respect to this CC0 or 119 | use of the Work. 120 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 19 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wdisplays 2 | 3 | ## Luis' Note 4 | 5 | Looks like the [original repository](https://github.com/cyclopsian/wdisplays.git) has disappeared, so since GPL v3 allows I've created a fork of it. Not sure I want to maintain it but for now it won't hurt to have it here. 6 | 7 | [![License: GPL 3.0 or later][license-img]][license-spdx] 8 | 9 | wdisplays is a graphical application for configuring displays in Wayland 10 | compositors. It borrows some code from [kanshi]. It should work in any 11 | compositor that implements the wlr-output-management-unstable-v1 protocol. 12 | Compositors that are known to support the protocol are [Sway] and [Wayfire]. 13 | The goal of this project is to allow precise adjustment of display settings in 14 | kiosks, digital signage, and other elaborate multi-monitor setups. 15 | 16 | ![Screenshot](wdisplays.png) 17 | 18 | # Installation 19 | 20 | [![Repology][repology-img]][repology-pkg] 21 | 22 | Check your distro for a `wdisplays` package. Known distro packages: 23 | 24 | - [Alpine](https://pkgs.alpinelinux.org/package/edge/testing/x86_64/wdisplays) 25 | - [Arch](https://aur.archlinux.org/packages/wdisplays-git/) 26 | - [Debian](https://packages.debian.org/sid/wdisplays) 27 | - [Fedora](https://copr.fedorainfracloud.org/coprs/wef/wdisplays/) 28 | - [FreeBSD](https://svnweb.freebsd.org/ports/head/x11/wdisplays/) 29 | - [Nix](https://github.com/NixOS/nixpkgs/tree/master/pkgs/tools/graphics/wdisplays) 30 | - [OpenSUSE](https://build.opensuse.org/package/show/home%3AMWh3/wdisplays) 31 | 32 | # Building 33 | 34 | Build requirements are: 35 | 36 | - meson 37 | - GTK+3 38 | - epoxy 39 | - wayland-client 40 | 41 | ```sh 42 | meson build 43 | ninja -C build 44 | sudo ninja -C build install 45 | ``` 46 | 47 | # Usage 48 | 49 | Displays can be moved around the virtual screen space by clicking and dragging 50 | them in the preview on the left panel. By default, they will snap to one 51 | another. Hold Shift while dragging to disable snapping. You can click and drag 52 | with the middle mouse button to pan. Zoom in and out either with the buttons on 53 | the top left, or by holding Ctrl and scrolling the mouse wheel. Fine tune your 54 | adjustments in the right panel, then click apply. 55 | 56 | There are some options available by clicking the menu button on the top left: 57 | 58 | - Automatically Apply Changes: Makes it so you don't have to hit apply. Disable 59 | this for making minor adjustments, but be careful, you may end up with an 60 | unusable setup. 61 | - Show Screen Contents: Shows a live preview of the screens in the left panel. 62 | Turn off to reduce energy usage. 63 | - Overlay Screen Names: Shows big names in the corner of all screens for easy 64 | identification. Disable if they get in the way. 65 | 66 | # FAQ 67 | 68 | ### What is this? 69 | 70 | It's intended to be the Wayland equivalent of an xrandr GUI, like [ARandR]. 71 | 72 | ### I'm using Sway, why aren't my display settings saved when I log out? 73 | 74 | Sway, like i3, doesn't save any settings unless you put them in the config 75 | file. See man `sway-output`. If you want to have multiple configurations 76 | depending on the monitors connected, you'll need to use an external program 77 | like [kanshi]. Integration with that and other external daemons is planned. 78 | 79 | ### How do I add support to my compositor? 80 | 81 | A minimal amount of code (approximately 150-200 LOC) is currently required to 82 | get support for this in wlroots compositors. See the diff here for a sample 83 | implementation on top of tinywl: [tinywl-output-management]. 84 | 85 | [kanshi]: https://github.com/emersion/kanshi 86 | [Sway]: https://swaywm.org 87 | [Wayfire]: https://wayfire.org 88 | [ARandR]: https://christian.amsuess.com/tools/arandr/ 89 | [tinywl-output-management]: https://git.sr.ht/~jf/tinywl-output-management/commit/87a45d89ae0e7975e2a59f84e960380dd2f5ac08 90 | 91 | [license-img]: https://img.shields.io/badge/License-GPL%203.0%20or%20later-blue.svg?logo=gnu 92 | [license-spdx]: https://spdx.org/licenses/GPL-3.0-or-later.html 93 | [repology-img]: https://repology.org/badge/tiny-repos/wdisplays.svg 94 | [repology-pkg]: https://repology.org/project/wdisplays/versions 95 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Jason Francis 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | project('network.cycles.wdisplays', 'c', license: 'MIT', version: '1.0') 5 | 6 | conf = configuration_data({ 7 | 'app_id': meson.project_name(), 8 | 'version': meson.project_version(), 9 | 'resource_prefix': '/' / '/'.join(meson.project_name().split('.')), 10 | }) 11 | 12 | subdir('protocol') 13 | subdir('resources') 14 | subdir('src') 15 | -------------------------------------------------------------------------------- /protocol/meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Jason Francis 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | wayland_scanner = find_program('wayland-scanner') 5 | wayland_client = dependency('wayland-client') 6 | wayland_protos = dependency('wayland-protocols', version: '>=1.17') 7 | 8 | wl_protocol_dir = wayland_protos.get_pkgconfig_variable('pkgdatadir') 9 | 10 | wayland_scanner_code = generator( 11 | wayland_scanner, 12 | output: '@BASENAME@-protocol.c', 13 | arguments: ['private-code', '@INPUT@', '@OUTPUT@'], 14 | ) 15 | 16 | wayland_scanner_client = generator( 17 | wayland_scanner, 18 | output: '@BASENAME@-client-protocol.h', 19 | arguments: ['client-header', '@INPUT@', '@OUTPUT@'], 20 | ) 21 | 22 | client_protocols = [ 23 | [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'], 24 | [wl_protocol_dir, 'stable/xdg-shell/xdg-shell.xml'], 25 | ['wlr-output-management-unstable-v1.xml'], 26 | ['wlr-screencopy-unstable-v1.xml'], 27 | ['wlr-layer-shell-unstable-v1.xml'] 28 | ] 29 | 30 | client_protos_src = [] 31 | client_protos_headers = [] 32 | 33 | foreach p : client_protocols 34 | xml = join_paths(p) 35 | client_protos_src += wayland_scanner_code.process(xml) 36 | client_protos_headers += wayland_scanner_client.process(xml) 37 | endforeach 38 | 39 | lib_client_protos = static_library( 40 | 'client_protos', 41 | client_protos_src + client_protos_headers, 42 | dependencies: [wayland_client] 43 | ) 44 | 45 | client_protos = declare_dependency( 46 | link_with: lib_client_protos, 47 | sources: client_protos_headers, 48 | ) 49 | -------------------------------------------------------------------------------- /protocol/wlr-layer-shell-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2017 Drew DeVault 5 | 6 | Permission to use, copy, modify, distribute, and sell this 7 | software and its documentation for any purpose is hereby granted 8 | without fee, provided that the above copyright notice appear in 9 | all copies and that both that copyright notice and this permission 10 | notice appear in supporting documentation, and that the name of 11 | the copyright holders not be used in advertising or publicity 12 | pertaining to distribution of the software without specific, 13 | written prior permission. The copyright holders make no 14 | representations about the suitability of this software for any 15 | purpose. It is provided "as is" without express or implied 16 | warranty. 17 | 18 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 19 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 22 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 23 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 24 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 25 | THIS SOFTWARE. 26 | 27 | 28 | 29 | 30 | Clients can use this interface to assign the surface_layer role to 31 | wl_surfaces. Such surfaces are assigned to a "layer" of the output and 32 | rendered with a defined z-depth respective to each other. They may also be 33 | anchored to the edges and corners of a screen and specify input handling 34 | semantics. This interface should be suitable for the implementation of 35 | many desktop shell components, and a broad number of other applications 36 | that interact with the desktop. 37 | 38 | 39 | 40 | 41 | Create a layer surface for an existing surface. This assigns the role of 42 | layer_surface, or raises a protocol error if another role is already 43 | assigned. 44 | 45 | Creating a layer surface from a wl_surface which has a buffer attached 46 | or committed is a client error, and any attempts by a client to attach 47 | or manipulate a buffer prior to the first layer_surface.configure call 48 | must also be treated as errors. 49 | 50 | You may pass NULL for output to allow the compositor to decide which 51 | output to use. Generally this will be the one that the user most 52 | recently interacted with. 53 | 54 | Clients can specify a namespace that defines the purpose of the layer 55 | surface. 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | These values indicate which layers a surface can be rendered in. They 73 | are ordered by z depth, bottom-most first. Traditional shell surfaces 74 | will typically be rendered between the bottom and top layers. 75 | Fullscreen shell surfaces are typically rendered at the top layer. 76 | Multiple surfaces can share a single layer, and ordering within a 77 | single layer is undefined. 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | An interface that may be implemented by a wl_surface, for surfaces that 90 | are designed to be rendered as a layer of a stacked desktop-like 91 | environment. 92 | 93 | Layer surface state (size, anchor, exclusive zone, margin, interactivity) 94 | is double-buffered, and will be applied at the time wl_surface.commit of 95 | the corresponding wl_surface is called. 96 | 97 | 98 | 99 | 100 | Sets the size of the surface in surface-local coordinates. The 101 | compositor will display the surface centered with respect to its 102 | anchors. 103 | 104 | If you pass 0 for either value, the compositor will assign it and 105 | inform you of the assignment in the configure event. You must set your 106 | anchor to opposite edges in the dimensions you omit; not doing so is a 107 | protocol error. Both values are 0 by default. 108 | 109 | Size is double-buffered, see wl_surface.commit. 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | Requests that the compositor anchor the surface to the specified edges 118 | and corners. If two orthoginal edges are specified (e.g. 'top' and 119 | 'left'), then the anchor point will be the intersection of the edges 120 | (e.g. the top left corner of the output); otherwise the anchor point 121 | will be centered on that edge, or in the center if none is specified. 122 | 123 | Anchor is double-buffered, see wl_surface.commit. 124 | 125 | 126 | 127 | 128 | 129 | 130 | Requests that the compositor avoids occluding an area of the surface 131 | with other surfaces. The compositor's use of this information is 132 | implementation-dependent - do not assume that this region will not 133 | actually be occluded. 134 | 135 | A positive value is only meaningful if the surface is anchored to an 136 | edge, rather than a corner. The zone is the number of surface-local 137 | coordinates from the edge that are considered exclusive. 138 | 139 | Surfaces that do not wish to have an exclusive zone may instead specify 140 | how they should interact with surfaces that do. If set to zero, the 141 | surface indicates that it would like to be moved to avoid occluding 142 | surfaces with a positive excluzive zone. If set to -1, the surface 143 | indicates that it would not like to be moved to accommodate for other 144 | surfaces, and the compositor should extend it all the way to the edges 145 | it is anchored to. 146 | 147 | For example, a panel might set its exclusive zone to 10, so that 148 | maximized shell surfaces are not shown on top of it. A notification 149 | might set its exclusive zone to 0, so that it is moved to avoid 150 | occluding the panel, but shell surfaces are shown underneath it. A 151 | wallpaper or lock screen might set their exclusive zone to -1, so that 152 | they stretch below or over the panel. 153 | 154 | The default value is 0. 155 | 156 | Exclusive zone is double-buffered, see wl_surface.commit. 157 | 158 | 159 | 160 | 161 | 162 | 163 | Requests that the surface be placed some distance away from the anchor 164 | point on the output, in surface-local coordinates. Setting this value 165 | for edges you are not anchored to has no effect. 166 | 167 | The exclusive zone includes the margin. 168 | 169 | Margin is double-buffered, see wl_surface.commit. 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | Set to 1 to request that the seat send keyboard events to this layer 180 | surface. For layers below the shell surface layer, the seat will use 181 | normal focus semantics. For layers above the shell surface layers, the 182 | seat will always give exclusive keyboard focus to the top-most layer 183 | which has keyboard interactivity set to true. 184 | 185 | Layer surfaces receive pointer, touch, and tablet events normally. If 186 | you do not want to receive them, set the input region on your surface 187 | to an empty region. 188 | 189 | Events is double-buffered, see wl_surface.commit. 190 | 191 | 192 | 193 | 194 | 195 | 196 | This assigns an xdg_popup's parent to this layer_surface. This popup 197 | should have been created via xdg_surface::get_popup with the parent set 198 | to NULL, and this request must be invoked before committing the popup's 199 | initial state. 200 | 201 | See the documentation of xdg_popup for more details about what an 202 | xdg_popup is and how it is used. 203 | 204 | 205 | 206 | 207 | 208 | 209 | When a configure event is received, if a client commits the 210 | surface in response to the configure event, then the client 211 | must make an ack_configure request sometime before the commit 212 | request, passing along the serial of the configure event. 213 | 214 | If the client receives multiple configure events before it 215 | can respond to one, it only has to ack the last configure event. 216 | 217 | A client is not required to commit immediately after sending 218 | an ack_configure request - it may even ack_configure several times 219 | before its next surface commit. 220 | 221 | A client may send multiple ack_configure requests before committing, but 222 | only the last request sent before a commit indicates which configure 223 | event the client really is responding to. 224 | 225 | 226 | 227 | 228 | 229 | 230 | This request destroys the layer surface. 231 | 232 | 233 | 234 | 235 | 236 | The configure event asks the client to resize its surface. 237 | 238 | Clients should arrange their surface for the new states, and then send 239 | an ack_configure request with the serial sent in this configure event at 240 | some point before committing the new surface. 241 | 242 | The client is free to dismiss all but the last configure event it 243 | received. 244 | 245 | The width and height arguments specify the size of the window in 246 | surface-local coordinates. 247 | 248 | The size is a hint, in the sense that the client is free to ignore it if 249 | it doesn't resize, pick a smaller size (to satisfy aspect ratio or 250 | resize in steps of NxM pixels). If the client picks a smaller size and 251 | is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the 252 | surface will be centered on this axis. 253 | 254 | If the width or height arguments are zero, it means the client should 255 | decide its own window dimension. 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | The closed event is sent by the compositor when the surface will no 265 | longer be shown. The output may have been destroyed or the user may 266 | have asked for it to be removed. Further changes to the surface will be 267 | ignored. The client should destroy the resource after receiving this 268 | event, and create a new surface if they so choose. 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | -------------------------------------------------------------------------------- /protocol/wlr-output-management-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2019 Purism SPC 5 | 6 | Permission to use, copy, modify, distribute, and sell this 7 | software and its documentation for any purpose is hereby granted 8 | without fee, provided that the above copyright notice appear in 9 | all copies and that both that copyright notice and this permission 10 | notice appear in supporting documentation, and that the name of 11 | the copyright holders not be used in advertising or publicity 12 | pertaining to distribution of the software without specific, 13 | written prior permission. The copyright holders make no 14 | representations about the suitability of this software for any 15 | purpose. It is provided "as is" without express or implied 16 | warranty. 17 | 18 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 19 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 22 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 23 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 24 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 25 | THIS SOFTWARE. 26 | 27 | 28 | 29 | This protocol exposes interfaces to obtain and modify output device 30 | configuration. 31 | 32 | Warning! The protocol described in this file is experimental and 33 | backward incompatible changes may be made. Backward compatible changes 34 | may be added together with the corresponding interface version bump. 35 | Backward incompatible changes are done by bumping the version number in 36 | the protocol and interface names and resetting the interface version. 37 | Once the protocol is to be declared stable, the 'z' prefix and the 38 | version number in the protocol and interface names are removed and the 39 | interface version number is reset. 40 | 41 | 42 | 43 | 44 | This interface is a manager that allows reading and writing the current 45 | output device configuration. 46 | 47 | Output devices that display pixels (e.g. a physical monitor or a virtual 48 | output in a window) are represented as heads. Heads cannot be created nor 49 | destroyed by the client, but they can be enabled or disabled and their 50 | properties can be changed. Each head may have one or more available modes. 51 | 52 | Whenever a head appears (e.g. a monitor is plugged in), it will be 53 | advertised via the head event. Immediately after the output manager is 54 | bound, all current heads are advertised. 55 | 56 | Whenever a head's properties change, the relevant wlr_output_head events 57 | will be sent. Not all head properties will be sent: only properties that 58 | have changed need to. 59 | 60 | Whenever a head disappears (e.g. a monitor is unplugged), a 61 | wlr_output_head.finished event will be sent. 62 | 63 | After one or more heads appear, change or disappear, the done event will 64 | be sent. It carries a serial which can be used in a create_configuration 65 | request to update heads properties. 66 | 67 | The information obtained from this protocol should only be used for output 68 | configuration purposes. This protocol is not designed to be a generic 69 | output property advertisement protocol for regular clients. Instead, 70 | protocols such as xdg-output should be used. 71 | 72 | 73 | 74 | 75 | This event introduces a new head. This happens whenever a new head 76 | appears (e.g. a monitor is plugged in) or after the output manager is 77 | bound. 78 | 79 | 80 | 81 | 82 | 83 | 84 | This event is sent after all information has been sent after binding to 85 | the output manager object and after any subsequent changes. This applies 86 | to child head and mode objects as well. In other words, this event is 87 | sent whenever a head or mode is created or destroyed and whenever one of 88 | their properties has been changed. Not all state is re-sent each time 89 | the current configuration changes: only the actual changes are sent. 90 | 91 | This allows changes to the output configuration to be seen as atomic, 92 | even if they happen via multiple events. 93 | 94 | A serial is sent to be used in a future create_configuration request. 95 | 96 | 97 | 98 | 99 | 100 | 101 | Create a new output configuration object. This allows to update head 102 | properties. 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | Indicates the client no longer wishes to receive events for output 111 | configuration changes. However the compositor may emit further events, 112 | until the finished event is emitted. 113 | 114 | The client must not send any more requests after this one. 115 | 116 | 117 | 118 | 119 | 120 | This event indicates that the compositor is done sending manager events. 121 | The compositor will destroy the object immediately after sending this 122 | event, so it will become invalid and the client should release any 123 | resources associated with it. 124 | 125 | 126 | 127 | 128 | 129 | 130 | A head is an output device. The difference between a wl_output object and 131 | a head is that heads are advertised even if they are turned off. A head 132 | object only advertises properties and cannot be used directly to change 133 | them. 134 | 135 | A head has some read-only properties: modes, name, description and 136 | physical_size. These cannot be changed by clients. 137 | 138 | Other properties can be updated via a wlr_output_configuration object. 139 | 140 | Properties sent via this interface are applied atomically via the 141 | wlr_output_manager.done event. No guarantees are made regarding the order 142 | in which properties are sent. 143 | 144 | 145 | 146 | 147 | This event describes the head name. 148 | 149 | The naming convention is compositor defined, but limited to alphanumeric 150 | characters and dashes (-). Each name is unique among all wlr_output_head 151 | objects, but if a wlr_output_head object is destroyed the same name may 152 | be reused later. The names will also remain consistent across sessions 153 | with the same hardware and software configuration. 154 | 155 | Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do 156 | not assume that the name is a reflection of an underlying DRM 157 | connector, X11 connection, etc. 158 | 159 | If the compositor implements the xdg-output protocol and this head is 160 | enabled, the xdg_output.name event must report the same name. 161 | 162 | The name event is sent after a wlr_output_head object is created. This 163 | event is only sent once per object, and the name does not change over 164 | the lifetime of the wlr_output_head object. 165 | 166 | 167 | 168 | 169 | 170 | 171 | This event describes a human-readable description of the head. 172 | 173 | The description is a UTF-8 string with no convention defined for its 174 | contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 175 | output via :1'. However, do not assume that the name is a reflection of 176 | the make, model, serial of the underlying DRM connector or the display 177 | name of the underlying X11 connection, etc. 178 | 179 | If the compositor implements xdg-output and this head is enabled, 180 | the xdg_output.description must report the same description. 181 | 182 | The description event is sent after a wlr_output_head object is created. 183 | This event is only sent once per object, and the description does not 184 | change over the lifetime of the wlr_output_head object. 185 | 186 | 187 | 188 | 189 | 190 | 191 | This event describes the physical size of the head. This event is only 192 | sent if the head has a physical size (e.g. is not a projector or a 193 | virtual device). 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | This event introduces a mode for this head. It is sent once per 202 | supported mode. 203 | 204 | 205 | 206 | 207 | 208 | 209 | This event describes whether the head is enabled. A disabled head is not 210 | mapped to a region of the global compositor space. 211 | 212 | When a head is disabled, some properties (current_mode, position, 213 | transform and scale) are irrelevant. 214 | 215 | 216 | 217 | 218 | 219 | 220 | This event describes the mode currently in use for this head. It is only 221 | sent if the output is enabled. 222 | 223 | 224 | 225 | 226 | 227 | 228 | This events describes the position of the head in the global compositor 229 | space. It is only sent if the output is enabled. 230 | 231 | 233 | 235 | 236 | 237 | 238 | 239 | This event describes the transformation currently applied to the head. 240 | It is only sent if the output is enabled. 241 | 242 | 243 | 244 | 245 | 246 | 247 | This events describes the scale of the head in the global compositor 248 | space. It is only sent if the output is enabled. 249 | 250 | 251 | 252 | 253 | 254 | 255 | The compositor will destroy the object immediately after sending this 256 | event, so it will become invalid and the client should release any 257 | resources associated with it. 258 | 259 | 260 | 261 | 262 | 263 | 264 | This object describes an output mode. 265 | 266 | Some heads don't support output modes, in which case modes won't be 267 | advertised. 268 | 269 | Properties sent via this interface are applied atomically via the 270 | wlr_output_manager.done event. No guarantees are made regarding the order 271 | in which properties are sent. 272 | 273 | 274 | 275 | 276 | This event describes the mode size. The size is given in physical 277 | hardware units of the output device. This is not necessarily the same as 278 | the output size in the global compositor space. For instance, the output 279 | may be scaled or transformed. 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | This event describes the mode's fixed vertical refresh rate. It is only 288 | sent if the mode has a fixed refresh rate. 289 | 290 | 291 | 292 | 293 | 294 | 295 | This event advertises this mode as preferred. 296 | 297 | 298 | 299 | 300 | 301 | The compositor will destroy the object immediately after sending this 302 | event, so it will become invalid and the client should release any 303 | resources associated with it. 304 | 305 | 306 | 307 | 308 | 309 | 310 | This object is used by the client to describe a full output configuration. 311 | 312 | First, the client needs to setup the output configuration. Each head can 313 | be either enabled (and configured) or disabled. It is a protocol error to 314 | send two enable_head or disable_head requests with the same head. It is a 315 | protocol error to omit a head in a configuration. 316 | 317 | Then, the client can apply or test the configuration. The compositor will 318 | then reply with a succeeded, failed or cancelled event. Finally the client 319 | should destroy the configuration object. 320 | 321 | 322 | 323 | 325 | 327 | 329 | 330 | 331 | 332 | 333 | Enable a head. This request creates a head configuration object that can 334 | be used to change the head's properties. 335 | 336 | 338 | 340 | 341 | 342 | 343 | 344 | Disable a head. 345 | 346 | 348 | 349 | 350 | 351 | 352 | Apply the new output configuration. 353 | 354 | In case the configuration is successfully applied, there is no guarantee 355 | that the new output state matches completely the requested 356 | configuration. For instance, a compositor might round the scale if it 357 | doesn't support fractional scaling. 358 | 359 | After this request has been sent, the compositor must respond with an 360 | succeeded, failed or cancelled event. Sending a request that isn't the 361 | destructor is a protocol error. 362 | 363 | 364 | 365 | 366 | 367 | Test the new output configuration. The configuration won't be applied, 368 | but will only be validated. 369 | 370 | Even if the compositor succeeds to test a configuration, applying it may 371 | fail. 372 | 373 | After this request has been sent, the compositor must respond with an 374 | succeeded, failed or cancelled event. Sending a request that isn't the 375 | destructor is a protocol error. 376 | 377 | 378 | 379 | 380 | 381 | Sent after the compositor has successfully applied the changes or 382 | tested them. 383 | 384 | Upon receiving this event, the client should destroy this object. 385 | 386 | If the current configuration has changed, events to describe the changes 387 | will be sent followed by a wlr_output_manager.done event. 388 | 389 | 390 | 391 | 392 | 393 | Sent if the compositor rejects the changes or failed to apply them. The 394 | compositor should revert any changes made by the apply request that 395 | triggered this event. 396 | 397 | Upon receiving this event, the client should destroy this object. 398 | 399 | 400 | 401 | 402 | 403 | Sent if the compositor cancels the configuration because the state of an 404 | output changed and the client has outdated information (e.g. after an 405 | output has been hotplugged). 406 | 407 | The client can create a new configuration with a newer serial and try 408 | again. 409 | 410 | Upon receiving this event, the client should destroy this object. 411 | 412 | 413 | 414 | 415 | 416 | Using this request a client can tell the compositor that it is not going 417 | to use the configuration object anymore. Any changes to the outputs 418 | that have not been applied will be discarded. 419 | 420 | This request also destroys wlr_output_configuration_head objects created 421 | via this object. 422 | 423 | 424 | 425 | 426 | 427 | 428 | This object is used by the client to update a single head's configuration. 429 | 430 | It is a protocol error to set the same property twice. 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | This request sets the head's mode. 444 | 445 | 446 | 447 | 448 | 449 | 450 | This request assigns a custom mode to the head. The size is given in 451 | physical hardware units of the output device. If set to zero, the 452 | refresh rate is unspecified. 453 | 454 | It is a protocol error to set both a mode and a custom mode. 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | This request sets the head's position in the global compositor space. 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | This request sets the head's transform. 472 | 473 | 474 | 475 | 476 | 477 | 478 | This request sets the head's scale. 479 | 480 | 481 | 482 | 483 | 484 | -------------------------------------------------------------------------------- /protocol/wlr-screencopy-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2018 Simon Ser 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice (including the next 14 | paragraph) shall be included in all copies or substantial portions of the 15 | Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | 25 | 26 | 27 | This protocol allows clients to ask the compositor to copy part of the 28 | screen content to a client buffer. 29 | 30 | Warning! The protocol described in this file is experimental and 31 | backward incompatible changes may be made. Backward compatible changes 32 | may be added together with the corresponding interface version bump. 33 | Backward incompatible changes are done by bumping the version number in 34 | the protocol and interface names and resetting the interface version. 35 | Once the protocol is to be declared stable, the 'z' prefix and the 36 | version number in the protocol and interface names are removed and the 37 | interface version number is reset. 38 | 39 | 40 | 41 | 42 | This object is a manager which offers requests to start capturing from a 43 | source. 44 | 45 | 46 | 47 | 48 | Capture the next frame of an entire output. 49 | 50 | 51 | 53 | 54 | 55 | 56 | 57 | 58 | Capture the next frame of an output's region. 59 | 60 | The region is given in output logical coordinates, see 61 | xdg_output.logical_size. The region will be clipped to the output's 62 | extents. 63 | 64 | 65 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | All objects created by the manager will still remain valid, until their 77 | appropriate destroy request has been called. 78 | 79 | 80 | 81 | 82 | 83 | 84 | This object represents a single frame. 85 | 86 | When created, a "buffer" event will be sent. The client will then be able 87 | to send a "copy" request. If the capture is successful, the compositor 88 | will send a "flags" followed by a "ready" event. 89 | 90 | If the capture failed, the "failed" event is sent. This can happen anytime 91 | before the "ready" event. 92 | 93 | Once either a "ready" or a "failed" event is received, the client should 94 | destroy the frame. 95 | 96 | 97 | 98 | 99 | Provides information about the frame's buffer. This event is sent once 100 | as soon as the frame is created. 101 | 102 | The client should then create a buffer with the provided attributes, and 103 | send a "copy" request. 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | Copy the frame to the supplied buffer. The buffer must have a the 114 | correct size, see zwlr_screencopy_frame_v1.buffer. The buffer needs to 115 | have a supported format. 116 | 117 | If the frame is successfully copied, a "flags" and a "ready" events are 118 | sent. Otherwise, a "failed" event is sent. 119 | 120 | 121 | 122 | 123 | 124 | 126 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | Provides flags about the frame. This event is sent once before the 137 | "ready" event. 138 | 139 | 140 | 141 | 142 | 143 | 144 | Called as soon as the frame is copied, indicating it is available 145 | for reading. This event includes the time at which presentation happened 146 | at. 147 | 148 | The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, 149 | each component being an unsigned 32-bit value. Whole seconds are in 150 | tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, 151 | and the additional fractional part in tv_nsec as nanoseconds. Hence, 152 | for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part 153 | may have an arbitrary offset at start. 154 | 155 | After receiving this event, the client should destroy the object. 156 | 157 | 159 | 161 | 163 | 164 | 165 | 166 | 167 | This event indicates that the attempted frame copy has failed. 168 | 169 | After receiving this event, the client should destroy the object. 170 | 171 | 172 | 173 | 174 | 175 | Destroys the frame. This request can be sent at any time by the client. 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /resources/head.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16383 7 | 1 8 | 10 9 | 10 | 11 | 16383 12 | 1 13 | 10 14 | 15 | 16 | 16383 17 | 1 18 | 10 19 | 20 | 21 | 2147483.647 22 | 1 23 | 10 24 | 25 | 26 | 0.01 27 | 99999 28 | 0.1 29 | 0.5 30 | 31 | 32 | 16383 33 | 1 34 | 10 35 | 36 | 398 | 399 | -------------------------------------------------------------------------------- /resources/meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Jason Francis 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | resources_xml = configure_file( 5 | input: 'resources.xml.in', 6 | output: 'resources.xml', 7 | configuration: conf 8 | ) 9 | 10 | gnome = import('gnome') 11 | resources = gnome.compile_resources( 12 | 'wdisplays-resources', resources_xml, 13 | source_dir : '.', 14 | c_name : 'wdisplays_resources') 15 | 16 | scour = find_program('scour', required: false) 17 | 18 | icon = 'wdisplays.svg' 19 | icondir = get_option('datadir') / 'icons' / 'hicolor' / 'scalable' / 'apps' 20 | 21 | if scour.found() 22 | custom_target('optimize-icon', 23 | input: icon, 24 | output: '@0@.svg'.format(meson.project_name()), 25 | command: [scour, 26 | '--enable-viewboxing', 27 | '--enable-comment-stripping', 28 | '--remove-descriptive-elements', 29 | '--enable-id-stripping', 30 | '--shorten-ids', 31 | '--create-groups', 32 | '--strip-xml-space', 33 | '--strip-xml-prolog', 34 | '--indent=none', 35 | '--no-line-breaks', 36 | '@INPUT@', '@OUTPUT@'], 37 | install: true, 38 | install_dir: icondir) 39 | else 40 | install_data(icon, install_dir: icondir) 41 | endif 42 | 43 | install_data( 44 | configure_file(input: 'wdisplays.desktop.in', 45 | output: '@0@.desktop'.format(meson.project_name()), 46 | configuration: conf), 47 | install_dir: get_option('datadir') / 'applications') 48 | -------------------------------------------------------------------------------- /resources/resources.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wdisplays.ui 5 | head.ui 6 | style.css 7 | 8 | 9 | -------------------------------------------------------------------------------- /resources/style.css: -------------------------------------------------------------------------------- 1 | spinner { 2 | opacity: 0; 3 | transition: opacity 200ms ease-in-out; 4 | background-color: rgba(64, 64, 64, 0.5); 5 | } 6 | 7 | spinner.visible { 8 | opacity: 1; 9 | } 10 | 11 | .output-overlay { 12 | font-size: 96px; 13 | background-color: @theme_selected_bg_color; 14 | color: @theme_selected_fg_color; 15 | border-radius: 8px; 16 | opacity: 0.9; 17 | padding: 8px; 18 | } 19 | 20 | .output-overlay .description { 21 | font-size: 12px; 22 | } 23 | -------------------------------------------------------------------------------- /resources/wdisplays.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=@version@ 3 | Type=Application 4 | Name=wdisplays 5 | Comment=Wlroots display configuration 6 | Exec=wdisplays 7 | Icon=@app_id@ 8 | -------------------------------------------------------------------------------- /resources/wdisplays.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 27 | 29 | 30 | 32 | image/svg+xml 33 | 35 | 36 | 37 | 38 | 39 | 41 | 44 | 48 | 49 | 51 | 55 | 59 | 60 | 63 | 67 | 71 | 75 | 79 | 83 | 87 | 88 | 91 | 95 | 99 | 100 | 109 | 111 | 115 | 119 | 120 | 130 | 132 | 136 | 140 | 141 | 150 | 152 | 156 | 160 | 161 | 165 | 168 | 174 | 175 | 183 | 187 | 191 | 195 | 199 | 203 | 207 | 208 | 216 | 220 | 224 | 228 | 232 | 236 | 240 | 241 | 251 | 262 | 272 | 283 | 293 | 304 | 305 | 353 | 363 | 370 | 377 | 384 | 391 | 398 | 405 | 412 | 419 | 430 | 437 | 444 | 451 | 458 | 465 | 472 | 479 | 486 | 493 | 500 | 501 | 507 | 516 | 525 | 534 | 543 | 552 | 561 | 570 | 579 | 588 | 597 | 606 | 615 | 621 | 627 | 633 | 639 | 644 | 649 | 655 | 661 | 662 | 663 | -------------------------------------------------------------------------------- /resources/wdisplays.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 1 8 | 10 9 | 10 | 11 | 1 12 | 10 13 | 14 | 15 | False 16 | wdisplays 17 | 18 | 19 | 20 | True 21 | False 22 | 23 | 24 | True 25 | False 26 | vertical 27 | 28 | 29 | False 30 | True 31 | start 32 | error 33 | True 34 | False 35 | 36 | 37 | 38 | False 39 | 6 40 | end 41 | 42 | 43 | 44 | 45 | 46 | False 47 | False 48 | 0 49 | 50 | 51 | 52 | 53 | False 54 | 16 55 | 56 | 57 | True 58 | False 59 | True 60 | 0 61 | 62 | 63 | True 64 | True 65 | 2 66 | 67 | 68 | 69 | 70 | True 71 | True 72 | 0 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | False 81 | True 82 | 0 83 | 84 | 85 | 86 | 87 | True 88 | True 89 | 400 90 | True 91 | 92 | 93 | True 94 | True 95 | canvas_horiz 96 | canvas_vert 97 | 100 98 | 300 99 | 100 | 101 | 102 | 103 | 104 | True 105 | False 106 | 107 | 108 | 109 | 110 | True 111 | False 112 | vertical 113 | 114 | 115 | True 116 | False 117 | center 118 | 8 119 | 8 120 | 8 121 | True 122 | heads_stack 123 | 124 | 125 | False 126 | True 127 | 0 128 | 129 | 130 | 131 | 132 | True 133 | False 134 | crossfade 135 | 136 | 137 | 138 | 139 | 140 | False 141 | True 142 | 1 143 | 144 | 145 | 146 | 147 | False 148 | False 149 | 150 | 151 | 152 | 153 | True 154 | True 155 | 1 156 | 157 | 158 | 159 | 160 | -1 161 | 162 | 163 | 164 | 165 | True 166 | False 167 | True 168 | True 169 | True 170 | 171 | 172 | True 173 | 174 | 175 | 176 | 177 | 178 | 179 | True 180 | False 181 | crossfade 182 | 183 | 184 | True 185 | False 186 | wdisplays 187 | False 188 | True 189 | 190 | 191 | True 192 | False 193 | 194 | 195 | True 196 | True 197 | True 198 | Zoom Out 199 | app.zoom-out 200 | 201 | 202 | 203 | True 204 | False 205 | zoom-out-symbolic 206 | 207 | 208 | 209 | 210 | True 211 | True 212 | 0 213 | True 214 | 215 | 216 | 217 | 218 | True 219 | True 220 | True 221 | Zoom Reset 222 | app.zoom-reset 223 | 224 | 225 | 226 | True 227 | True 228 | 1 229 | True 230 | 231 | 232 | 233 | 234 | True 235 | True 236 | True 237 | Zoom In 238 | app.zoom-in 239 | 240 | 241 | 242 | True 243 | False 244 | zoom-in-symbolic 245 | 246 | 247 | 248 | 249 | True 250 | True 251 | 2 252 | True 253 | 254 | 255 | 258 | 259 | 260 | 261 | 262 | True 263 | True 264 | True 265 | 266 | 267 | True 268 | False 269 | open-menu-symbolic 270 | 271 | 272 | 273 | 274 | end 275 | 1 276 | 277 | 278 | 279 | 280 | title 281 | 282 | 283 | 284 | 285 | True 286 | False 287 | 288 | 289 | True 290 | False 291 | Apply Changes? 292 | 293 | 294 | 295 | 296 | _Apply 297 | True 298 | True 299 | True 300 | True 301 | app.apply-changes 302 | 305 | 306 | 307 | end 308 | 309 | 310 | 311 | 312 | _Cancel 313 | True 314 | True 315 | True 316 | True 317 | app.cancel-changes 318 | 319 | 320 | 1 321 | 322 | 323 | 324 | 325 | apply 326 | 1 327 | 328 | 329 | 330 | 331 | 332 | 333 | -------------------------------------------------------------------------------- /src/config.h.in: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020 Jason Francis 2 | * SPDX-License-Identifier: GPL-3.0-or-later */ 3 | 4 | #ifndef WDISPLAY_CONFIG_H 5 | #define WDISPLAY_CONFIG_H 6 | 7 | #define WDISPLAYS_APP_ID "@app_id@" 8 | #define WDISPLAYS_VERSION "@version@" 9 | #define WDISPLAYS_RESOURCE_PREFIX "@resource_prefix@" 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /src/glviewport.c: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020 Jason Francis 2 | * SPDX-License-Identifier: GPL-3.0-or-later */ 3 | 4 | #include "glviewport.h" 5 | 6 | typedef struct _WdGLViewportPrivate { 7 | GtkAdjustment *hadjustment; 8 | GtkAdjustment *vadjustment; 9 | guint hscroll_policy : 1; 10 | guint vscroll_policy : 1; 11 | } WdGLViewportPrivate; 12 | 13 | enum { 14 | PROP_0, 15 | PROP_HADJUSTMENT, 16 | PROP_VADJUSTMENT, 17 | PROP_HSCROLL_POLICY, 18 | PROP_VSCROLL_POLICY 19 | }; 20 | 21 | static void wd_gl_viewport_set_property( 22 | GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); 23 | static void wd_gl_viewport_get_property( 24 | GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); 25 | 26 | G_DEFINE_TYPE_WITH_CODE(WdGLViewport, wd_gl_viewport, GTK_TYPE_GL_AREA, 27 | G_ADD_PRIVATE(WdGLViewport) 28 | G_IMPLEMENT_INTERFACE(GTK_TYPE_SCROLLABLE, NULL)) 29 | 30 | static void wd_gl_viewport_class_init(WdGLViewportClass *class) { 31 | GObjectClass *gobject_class = G_OBJECT_CLASS(class); 32 | 33 | gobject_class->set_property = wd_gl_viewport_set_property; 34 | gobject_class->get_property = wd_gl_viewport_get_property; 35 | 36 | g_object_class_override_property(gobject_class, PROP_HADJUSTMENT, "hadjustment"); 37 | g_object_class_override_property(gobject_class, PROP_VADJUSTMENT, "vadjustment"); 38 | g_object_class_override_property(gobject_class, PROP_HSCROLL_POLICY, "hscroll-policy"); 39 | g_object_class_override_property(gobject_class, PROP_VSCROLL_POLICY, "vscroll-policy"); 40 | } 41 | 42 | static void viewport_set_adjustment(GtkAdjustment *adjustment, 43 | GtkAdjustment **store) { 44 | if (!adjustment) { 45 | adjustment = gtk_adjustment_new(0., 0., 0., 0., 0., 0.); 46 | } 47 | if (adjustment != *store) { 48 | if (*store != NULL) { 49 | g_object_unref(*store); 50 | } 51 | *store = adjustment; 52 | g_object_ref_sink(adjustment); 53 | } 54 | } 55 | 56 | static void wd_gl_viewport_set_property( 57 | GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { 58 | WdGLViewport *viewport = WD_GL_VIEWPORT(object); 59 | WdGLViewportPrivate *priv = wd_gl_viewport_get_instance_private(viewport); 60 | 61 | switch (prop_id) { 62 | case PROP_HADJUSTMENT: 63 | viewport_set_adjustment(g_value_get_object(value), &priv->hadjustment); 64 | break; 65 | case PROP_VADJUSTMENT: 66 | viewport_set_adjustment(g_value_get_object(value), &priv->vadjustment); 67 | break; 68 | case PROP_HSCROLL_POLICY: 69 | if (priv->hscroll_policy != g_value_get_enum(value)) { 70 | priv->hscroll_policy = g_value_get_enum(value); 71 | g_object_notify_by_pspec(object, pspec); 72 | } 73 | break; 74 | case PROP_VSCROLL_POLICY: 75 | if (priv->vscroll_policy != g_value_get_enum(value)) { 76 | priv->vscroll_policy = g_value_get_enum(value); 77 | g_object_notify_by_pspec (object, pspec); 78 | } 79 | break; 80 | default: 81 | G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); 82 | break; 83 | } 84 | } 85 | 86 | static void wd_gl_viewport_get_property( 87 | GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { 88 | WdGLViewport *viewport = WD_GL_VIEWPORT(object); 89 | WdGLViewportPrivate *priv = wd_gl_viewport_get_instance_private(viewport); 90 | 91 | switch (prop_id) { 92 | case PROP_HADJUSTMENT: 93 | g_value_set_object(value, priv->hadjustment); 94 | break; 95 | case PROP_VADJUSTMENT: 96 | g_value_set_object(value, priv->vadjustment); 97 | break; 98 | case PROP_HSCROLL_POLICY: 99 | g_value_set_enum(value, priv->hscroll_policy); 100 | break; 101 | case PROP_VSCROLL_POLICY: 102 | g_value_set_enum(value, priv->vscroll_policy); 103 | break; 104 | default: 105 | G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); 106 | break; 107 | } 108 | } 109 | 110 | static void wd_gl_viewport_init(WdGLViewport *viewport) { 111 | } 112 | 113 | GtkWidget *wd_gl_viewport_new(void) { 114 | return gtk_widget_new(WD_TYPE_GL_VIEWPORT, NULL); 115 | } 116 | -------------------------------------------------------------------------------- /src/glviewport.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020 Jason Francis 2 | * SPDX-License-Identifier: GPL-3.0-or-later */ 3 | 4 | #ifndef WDISPLAY_GLVIEWPORT_H 5 | #define WDISPLAY_GLVIEWPORT_H 6 | 7 | #include 8 | 9 | G_BEGIN_DECLS 10 | 11 | #define WD_TYPE_GL_VIEWPORT (wd_gl_viewport_get_type()) 12 | G_DECLARE_DERIVABLE_TYPE( 13 | WdGLViewport, wd_gl_viewport, WD, GL_VIEWPORT,GtkGLArea) 14 | 15 | struct _WdGLViewportClass { 16 | GtkGLAreaClass parent_class; 17 | }; 18 | 19 | GtkWidget *wd_gl_viewport_new(void); 20 | 21 | G_END_DECLS 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /src/headform.c: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020 Jason Francis 2 | * SPDX-License-Identifier: GPL-3.0-or-later */ 3 | 4 | #include "headform.h" 5 | #include "wdisplays.h" 6 | 7 | typedef struct _WdHeadFormPrivate { 8 | GtkWidget *enabled; 9 | GtkWidget *description; 10 | GtkWidget *physical_size; 11 | GtkWidget *scale; 12 | GtkWidget *pos_x; 13 | GtkWidget *pos_y; 14 | GtkWidget *width; 15 | GtkWidget *height; 16 | GtkWidget *refresh; 17 | GtkWidget *mode_button; 18 | GtkWidget *rotate_button; 19 | GtkWidget *flipped; 20 | 21 | GAction *mode_action; 22 | GAction *rotate_action; 23 | } WdHeadFormPrivate; 24 | 25 | enum { 26 | CHANGED, 27 | LAST_SIGNAL 28 | }; 29 | 30 | static guint signals[LAST_SIGNAL]; 31 | 32 | G_DEFINE_TYPE_WITH_CODE(WdHeadForm, wd_head_form, GTK_TYPE_GRID, 33 | G_ADD_PRIVATE(WdHeadForm)) 34 | 35 | static const char *HEAD_PREFIX = "head"; 36 | static const char *MODE_PREFIX = "mode"; 37 | static const char *ROTATE_PREFIX = "rotate"; 38 | 39 | static void head_form_update_sensitivity(WdHeadForm *form) { 40 | WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form); 41 | 42 | bool enabled_toggled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->enabled)); 43 | 44 | g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(form)); 45 | for (GList *child = children; child != NULL; child = child->next) { 46 | GtkWidget *widget = GTK_WIDGET(child->data); 47 | if (widget != priv->enabled) { 48 | gtk_widget_set_sensitive(widget, enabled_toggled); 49 | } 50 | } 51 | } 52 | 53 | static GVariant *create_mode_variant(int32_t w, int32_t h, int32_t r) { 54 | GVariant * const children[] = { 55 | g_variant_new_int32(w), 56 | g_variant_new_int32(h), 57 | g_variant_new_int32(r), 58 | }; 59 | return g_variant_new_tuple(children, G_N_ELEMENTS(children)); 60 | } 61 | 62 | struct vid_mode { 63 | int32_t width; 64 | int32_t height; 65 | int32_t refresh; 66 | }; 67 | 68 | static void unpack_mode_variant(GVariant *value, struct vid_mode *mode) { 69 | g_autoptr(GVariant) width = g_variant_get_child_value(value, 0); 70 | mode->width = g_variant_get_int32(width); 71 | g_autoptr(GVariant) height = g_variant_get_child_value(value, 1); 72 | mode->height = g_variant_get_int32(height); 73 | g_autoptr(GVariant) refresh = g_variant_get_child_value(value, 2); 74 | mode->refresh = g_variant_get_int32(refresh); 75 | } 76 | 77 | static void enabled_toggled(GtkToggleButton *toggle, gpointer data) { 78 | WdHeadForm *form = WD_HEAD_FORM(data); 79 | head_form_update_sensitivity(form); 80 | g_signal_emit(form, signals[CHANGED], 0, WD_FIELD_ENABLED); 81 | } 82 | 83 | static void mode_spin_changed(GtkSpinButton *spin_button, gpointer data) { 84 | WdHeadForm *form = WD_HEAD_FORM(data); 85 | WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form); 86 | struct vid_mode mode; 87 | GVariant *value = g_action_get_state(priv->mode_action); 88 | unpack_mode_variant(value, &mode); 89 | if (strcmp(gtk_widget_get_name(GTK_WIDGET(spin_button)), "width") == 0) { 90 | mode.width = gtk_spin_button_get_value(spin_button); 91 | } else if (strcmp(gtk_widget_get_name(GTK_WIDGET(spin_button)), "height") == 0) { 92 | mode.height = gtk_spin_button_get_value(spin_button); 93 | } else if (strcmp(gtk_widget_get_name(GTK_WIDGET(spin_button)), "refresh") == 0) { 94 | mode.refresh = gtk_spin_button_get_value(spin_button) * 1000.; 95 | } 96 | g_action_activate(priv->mode_action, create_mode_variant(mode.width, mode.height, mode.refresh)); 97 | g_signal_emit(form, signals[CHANGED], 0, WD_FIELD_MODE); 98 | } 99 | 100 | static void position_spin_changed(GtkSpinButton *spin_button, gpointer data) { 101 | WdHeadForm *form = WD_HEAD_FORM(data); 102 | g_signal_emit(form, signals[CHANGED], 0, WD_FIELD_POSITION); 103 | } 104 | 105 | static void flipped_toggled(GtkToggleButton *toggle, gpointer data) { 106 | WdHeadForm *form = WD_HEAD_FORM(data); 107 | g_signal_emit(form, signals[CHANGED], 0, WD_FIELD_TRANSFORM); 108 | } 109 | 110 | static void wd_head_form_class_init(WdHeadFormClass *class) { 111 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class); 112 | 113 | signals[CHANGED] = g_signal_new("changed", 114 | G_OBJECT_CLASS_TYPE(class), 115 | G_SIGNAL_RUN_LAST, 116 | G_STRUCT_OFFSET(WdHeadFormClass, changed), 117 | NULL, NULL, NULL, 118 | G_TYPE_NONE, 1, G_TYPE_INT); 119 | 120 | gtk_widget_class_set_template_from_resource(widget_class, 121 | WDISPLAYS_RESOURCE_PREFIX "/head.ui"); 122 | 123 | gtk_widget_class_bind_template_callback(widget_class, enabled_toggled); 124 | gtk_widget_class_bind_template_callback(widget_class, mode_spin_changed); 125 | gtk_widget_class_bind_template_callback(widget_class, position_spin_changed); 126 | gtk_widget_class_bind_template_callback(widget_class, flipped_toggled); 127 | 128 | gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, enabled); 129 | gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, description); 130 | gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, physical_size); 131 | gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, scale); 132 | gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, pos_x); 133 | gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, pos_y); 134 | gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, width); 135 | gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, height); 136 | gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, refresh); 137 | gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, mode_button); 138 | gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, rotate_button); 139 | gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, flipped); 140 | gtk_widget_class_set_css_name(widget_class, "wd-head-form"); 141 | } 142 | 143 | static int32_t get_rotate_value(enum wl_output_transform transform) { 144 | if (transform == WL_OUTPUT_TRANSFORM_90 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_90) { 145 | return 90; 146 | } else if (transform == WL_OUTPUT_TRANSFORM_180 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_180) { 147 | return 180; 148 | } else if (transform == WL_OUTPUT_TRANSFORM_270 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_270) { 149 | return 270; 150 | } 151 | return 0; 152 | } 153 | 154 | static void rotate_selected(GSimpleAction *action, GVariant *param, gpointer data) { 155 | WdHeadForm *form = data; 156 | WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form); 157 | GMenuModel *menu = gtk_menu_button_get_menu_model(GTK_MENU_BUTTON(priv->rotate_button)); 158 | int items = g_menu_model_get_n_items(menu); 159 | for (int i = 0; i < items; i++) { 160 | g_autoptr(GVariant) target = g_menu_model_get_item_attribute_value(menu, i, G_MENU_ATTRIBUTE_TARGET, NULL); 161 | g_autoptr(GVariant) label = g_menu_model_get_item_attribute_value(menu, i, G_MENU_ATTRIBUTE_LABEL, NULL); 162 | if (g_variant_get_int32(target) == g_variant_get_int32(param)) { 163 | gtk_button_set_label(GTK_BUTTON(priv->rotate_button), g_variant_get_string(label, NULL)); 164 | break; 165 | } 166 | } 167 | g_simple_action_set_state(action, param); 168 | g_signal_emit(form, signals[CHANGED], 0, WD_FIELD_TRANSFORM); 169 | } 170 | 171 | static void mode_selected(GSimpleAction *action, GVariant *param, gpointer data) { 172 | WdHeadForm *form = data; 173 | WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form); 174 | struct vid_mode mode; 175 | unpack_mode_variant(param, &mode); 176 | 177 | g_simple_action_set_state(action, param); 178 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->width), mode.width); 179 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->height), mode.height); 180 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->refresh), mode.refresh / 1000.); 181 | g_signal_emit(form, signals[CHANGED], 0, WD_FIELD_MODE); 182 | } 183 | 184 | static void wd_head_form_init(WdHeadForm *form) { 185 | gtk_widget_init_template(GTK_WIDGET(form)); 186 | WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form); 187 | 188 | GSimpleActionGroup *head_actions = g_simple_action_group_new(); 189 | gtk_widget_insert_action_group(priv->mode_button, HEAD_PREFIX, G_ACTION_GROUP(head_actions)); 190 | gtk_widget_insert_action_group(priv->rotate_button, HEAD_PREFIX, G_ACTION_GROUP(head_actions)); 191 | 192 | GMenu *rotate_menu = g_menu_new(); 193 | g_menu_append(rotate_menu, "Don't Rotate", "head.rotate(0)"); 194 | g_menu_append(rotate_menu, "Rotate 90°", "head.rotate(90)"); 195 | g_menu_append(rotate_menu, "Rotate 180°", "head.rotate(180)"); 196 | g_menu_append(rotate_menu, "Rotate 270°", "head.rotate(270)"); 197 | gtk_menu_button_set_menu_model(GTK_MENU_BUTTON(priv->rotate_button), G_MENU_MODEL(rotate_menu)); 198 | 199 | static const GVariantType * const mode_types[] = { 200 | G_VARIANT_TYPE_INT32, 201 | G_VARIANT_TYPE_INT32, 202 | G_VARIANT_TYPE_INT32 203 | }; 204 | GSimpleAction *action = g_simple_action_new_stateful("mode", 205 | g_variant_type_new_tuple(mode_types, G_N_ELEMENTS(mode_types)), 206 | create_mode_variant(0, 0, 0)); 207 | g_action_map_add_action(G_ACTION_MAP(head_actions), G_ACTION(action)); 208 | g_signal_connect(action, "change-state", G_CALLBACK(mode_selected), form); 209 | g_object_unref(action); 210 | priv->mode_action = G_ACTION(action); 211 | 212 | action = g_simple_action_new_stateful(ROTATE_PREFIX, G_VARIANT_TYPE_INT32, 213 | g_variant_new_int32(0)); 214 | g_action_map_add_action(G_ACTION_MAP(head_actions), G_ACTION(action)); 215 | g_signal_connect(action, "change-state", G_CALLBACK(rotate_selected), form); 216 | g_object_unref(action); 217 | priv->rotate_action = G_ACTION(action); 218 | 219 | g_object_unref(head_actions); 220 | } 221 | 222 | void wd_head_form_update(WdHeadForm *form, const struct wd_head *head, 223 | enum wd_head_fields fields) { 224 | g_return_if_fail(form); 225 | g_return_if_fail(head); 226 | 227 | WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form); 228 | if (!fields) 229 | return; 230 | 231 | if (fields & WD_FIELD_DESCRIPTION) 232 | gtk_label_set_text(GTK_LABEL(priv->description), head->description); 233 | if (fields & WD_FIELD_PHYSICAL_SIZE) { 234 | g_autofree gchar *physical_str = g_strdup_printf("%dmm × %dmm", head->phys_width, head->phys_height); 235 | gtk_label_set_text(GTK_LABEL(priv->physical_size), physical_str); 236 | } 237 | if (fields & WD_FIELD_ENABLED) 238 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->enabled), head->enabled); 239 | if (fields & WD_FIELD_SCALE) 240 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->scale), head->scale); 241 | if (fields & WD_FIELD_POSITION) { 242 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->pos_x), head->x); 243 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->pos_y), head->y); 244 | } 245 | 246 | if (fields & WD_FIELD_MODE) { 247 | GMenu *mode_menu = g_menu_new(); 248 | struct wd_mode *mode; 249 | g_autofree gchar *action = g_strdup_printf("%s.%s", HEAD_PREFIX, MODE_PREFIX); 250 | wl_list_for_each(mode, &head->modes, link) { 251 | g_autofree gchar *name = g_strdup_printf("%d×%d@%0.3fHz", mode->width, mode->height, mode->refresh / 1000.); 252 | GMenuItem *item = g_menu_item_new(name, action); 253 | g_menu_item_set_attribute_value(item, G_MENU_ATTRIBUTE_TARGET, 254 | create_mode_variant(mode->width, mode->height, mode->refresh)); 255 | g_menu_append_item(mode_menu, item); 256 | } 257 | gtk_menu_button_set_menu_model(GTK_MENU_BUTTON(priv->mode_button), G_MENU_MODEL(mode_menu)); 258 | // Mode entries 259 | int w = head->custom_mode.width; 260 | int h = head->custom_mode.height; 261 | int r = head->custom_mode.refresh; 262 | if (head->enabled && head->mode != NULL) { 263 | w = head->mode->width; 264 | h = head->mode->height; 265 | r = head->mode->refresh; 266 | } else if (!head->enabled && w == 0 && h == 0) { 267 | struct wd_mode *mode; 268 | wl_list_for_each(mode, &head->modes, link) { 269 | if (mode->preferred) { 270 | w = mode->width; 271 | h = mode->height; 272 | r = mode->refresh; 273 | break; 274 | } 275 | } 276 | } 277 | 278 | g_action_change_state(priv->mode_action, create_mode_variant(w, h, r)); 279 | } 280 | 281 | if (fields & WD_FIELD_TRANSFORM) { 282 | int active_rotate = get_rotate_value(head->transform); 283 | g_action_change_state(priv->rotate_action, g_variant_new_int32(active_rotate)); 284 | 285 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->flipped), 286 | head->transform == WL_OUTPUT_TRANSFORM_FLIPPED 287 | || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_90 288 | || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_180 289 | || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_270); 290 | } 291 | 292 | // Sync state 293 | if (fields & WD_FIELD_ENABLED) { 294 | head_form_update_sensitivity(form); 295 | } 296 | g_signal_emit(form, signals[CHANGED], 0); 297 | } 298 | 299 | GtkWidget *wd_head_form_new(void) { 300 | return gtk_widget_new(WD_TYPE_HEAD_FORM, NULL); 301 | } 302 | 303 | gboolean wd_head_form_get_enabled(WdHeadForm *form) { 304 | WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form); 305 | return gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->enabled)); 306 | } 307 | 308 | gboolean wd_head_form_has_changes(WdHeadForm *form, const struct wd_head *head) { 309 | g_return_val_if_fail(form, FALSE); 310 | 311 | WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form); 312 | if (head->enabled != gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->enabled))) { 313 | return TRUE; 314 | } 315 | double old_scale = round(head->scale * 100.) / 100.; 316 | double new_scale = round(gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->scale)) * 100.) / 100.; 317 | if (old_scale != new_scale) { 318 | return TRUE; 319 | } 320 | if (head->x != gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->pos_x))) { 321 | return TRUE; 322 | } 323 | if (head->y != gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->pos_y))) { 324 | return TRUE; 325 | } 326 | int w = head->mode != NULL ? head->mode->width : head->custom_mode.width; 327 | if (w != gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->width))) { 328 | return TRUE; 329 | } 330 | int h = head->mode != NULL ? head->mode->height : head->custom_mode.height; 331 | if (h != gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->height))) { 332 | return TRUE; 333 | } 334 | int r = head->mode != NULL ? head->mode->refresh : head->custom_mode.refresh; 335 | if (r / 1000. != gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->refresh))) { 336 | return TRUE; 337 | } 338 | if (g_variant_get_int32(g_action_get_state(priv->rotate_action)) != get_rotate_value(head->transform)) { 339 | return TRUE; 340 | } 341 | bool flipped = head->transform == WL_OUTPUT_TRANSFORM_FLIPPED 342 | || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_90 343 | || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_180 344 | || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_270; 345 | if (flipped != gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->flipped))) { 346 | return TRUE; 347 | } 348 | return FALSE; 349 | } 350 | 351 | void wd_head_form_fill_config(WdHeadForm *form, struct wd_head_config *output) { 352 | g_return_if_fail(form); 353 | g_return_if_fail(output); 354 | 355 | WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form); 356 | output->enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->enabled)); 357 | output->scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->scale)); 358 | output->x = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->pos_x)); 359 | output->y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->pos_y)); 360 | output->width = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->width)); 361 | output->height = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->height)); 362 | output->refresh = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->refresh)) * 1000.; 363 | int32_t rotate = g_variant_get_int32(g_action_get_state(priv->rotate_action)); 364 | gboolean flipped = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->flipped)); 365 | switch (rotate) { 366 | case 0: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED : WL_OUTPUT_TRANSFORM_NORMAL; break; 367 | case 90: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_90 : WL_OUTPUT_TRANSFORM_90; break; 368 | case 180: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_180 : WL_OUTPUT_TRANSFORM_180; break; 369 | case 270: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_270 : WL_OUTPUT_TRANSFORM_270; break; 370 | } 371 | } 372 | 373 | void wd_head_form_get_dimensions(WdHeadForm *form, WdHeadDimensions *dimensions) { 374 | g_return_if_fail(form); 375 | g_return_if_fail(dimensions); 376 | 377 | WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form); 378 | 379 | dimensions->x = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->pos_x)); 380 | dimensions->y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->pos_y)); 381 | dimensions->w = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->width)); 382 | dimensions->h = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->height)); 383 | dimensions->scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->scale)); 384 | dimensions->rotation_id = g_variant_get_int32(g_action_get_state(priv->rotate_action)) / 90; 385 | dimensions->flipped = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->flipped)); 386 | } 387 | 388 | void wd_head_form_set_position(WdHeadForm *form, double x, double y) { 389 | g_return_if_fail(form); 390 | WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form); 391 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->pos_x), x); 392 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->pos_y), y); 393 | } 394 | -------------------------------------------------------------------------------- /src/headform.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020 Jason Francis 2 | * SPDX-License-Identifier: GPL-3.0-or-later */ 3 | 4 | #ifndef WDISPLAY_HEADFORM_H 5 | #define WDISPLAY_HEADFORM_H 6 | 7 | #include 8 | 9 | G_BEGIN_DECLS 10 | 11 | enum wd_head_fields { 12 | WD_FIELD_NAME = 1 << 0, 13 | WD_FIELD_ENABLED = 1 << 1, 14 | WD_FIELD_DESCRIPTION = 1 << 2, 15 | WD_FIELD_PHYSICAL_SIZE = 1 << 3, 16 | WD_FIELD_SCALE = 1 << 4, 17 | WD_FIELD_POSITION = 1 << 5, 18 | WD_FIELD_MODE = 1 << 6, 19 | WD_FIELD_TRANSFORM = 1 << 7, 20 | WD_FIELDS_ALL = (1 << 8) - 1 21 | }; 22 | 23 | #define WD_TYPE_HEAD_FORM (wd_head_form_get_type()) 24 | G_DECLARE_DERIVABLE_TYPE( 25 | WdHeadForm, wd_head_form, WD, HEAD_FORM, GtkGrid) 26 | 27 | struct _WdHeadFormClass { 28 | GtkGridClass parent_class; 29 | 30 | void (*changed)(WdHeadForm *form, enum wd_head_fields fields); 31 | }; 32 | 33 | struct wd_head; 34 | struct wd_head_config; 35 | 36 | typedef struct _WdHeadDimensions { 37 | gdouble x; 38 | gdouble y; 39 | gdouble w; 40 | gdouble h; 41 | gdouble scale; 42 | int rotation_id; 43 | gboolean flipped; 44 | } WdHeadDimensions; 45 | 46 | GtkWidget *wd_head_form_new(void); 47 | 48 | gboolean wd_head_form_get_enabled(WdHeadForm *form); 49 | gboolean wd_head_form_has_changes(WdHeadForm *form, const struct wd_head *head); 50 | void wd_head_form_update(WdHeadForm *form, const struct wd_head *head, 51 | enum wd_head_fields fields); 52 | void wd_head_form_fill_config(WdHeadForm *form, struct wd_head_config *output); 53 | void wd_head_form_get_dimensions(WdHeadForm *form, WdHeadDimensions *dimensions); 54 | void wd_head_form_set_position(WdHeadForm *form, double x, double y); 55 | 56 | G_END_DECLS 57 | 58 | #endif 59 | 60 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Jason Francis 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | cc = meson.get_compiler('c') 5 | m_dep = cc.find_library('m', required : false) 6 | rt_dep = cc.find_library('rt', required : false) 7 | gdk = dependency('gdk-3.0', version: '>= 3.24') 8 | gtk = dependency('gtk+-3.0', version: '>= 3.24') 9 | assert(gdk.get_pkgconfig_variable('targets').split().contains('wayland'), 'Wayland GDK backend not present') 10 | epoxy = dependency('epoxy') 11 | 12 | configure_file(input: 'config.h.in', output: 'config.h', configuration: conf) 13 | 14 | executable( 15 | 'wdisplays', 16 | [ 17 | 'main.c', 18 | 'glviewport.c', 19 | 'headform.c', 20 | 'outputs.c', 21 | 'overlay.c', 22 | 'render.c', 23 | resources, 24 | ], 25 | dependencies : [ 26 | m_dep, 27 | rt_dep, 28 | wayland_client, 29 | client_protos, 30 | epoxy, 31 | gtk 32 | ], 33 | install: true 34 | ) 35 | -------------------------------------------------------------------------------- /src/outputs.c: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2019 Jason Francis 2 | * SPDX-License-Identifier: GPL-3.0-or-later */ 3 | /* SPDX-FileCopyrightText: 2017-2019 Simon Ser 4 | * SPDX-License-Identifier: MIT */ 5 | 6 | /* 7 | * Parts of this file are taken from emersion/kanshi: 8 | * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/main.c 9 | */ 10 | 11 | #define _GNU_SOURCE 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "wdisplays.h" 25 | 26 | #include "wlr-output-management-unstable-v1-client-protocol.h" 27 | #include "xdg-output-unstable-v1-client-protocol.h" 28 | #include "wlr-screencopy-unstable-v1-client-protocol.h" 29 | #include "wlr-layer-shell-unstable-v1-client-protocol.h" 30 | 31 | static void noop() { 32 | // This space is intentionally left blank 33 | } 34 | 35 | struct wd_pending_config { 36 | struct wd_state *state; 37 | struct wl_list *outputs; 38 | }; 39 | 40 | static void destroy_pending(struct wd_pending_config *pending) { 41 | struct wd_head_config *output, *tmp; 42 | wl_list_for_each_safe(output, tmp, pending->outputs, link) { 43 | wl_list_remove(&output->link); 44 | free(output); 45 | } 46 | free(pending->outputs); 47 | free(pending); 48 | } 49 | 50 | static void config_handle_succeeded(void *data, 51 | struct zwlr_output_configuration_v1 *config) { 52 | struct wd_pending_config *pending = data; 53 | zwlr_output_configuration_v1_destroy(config); 54 | wd_ui_apply_done(pending->state, pending->outputs); 55 | destroy_pending(pending); 56 | } 57 | 58 | static void config_handle_failed(void *data, 59 | struct zwlr_output_configuration_v1 *config) { 60 | struct wd_pending_config *pending = data; 61 | zwlr_output_configuration_v1_destroy(config); 62 | wd_ui_apply_done(pending->state, NULL); 63 | wd_ui_show_error(pending->state, 64 | "The display server was not able to process your changes."); 65 | destroy_pending(pending); 66 | } 67 | 68 | static void config_handle_cancelled(void *data, 69 | struct zwlr_output_configuration_v1 *config) { 70 | struct wd_pending_config *pending = data; 71 | zwlr_output_configuration_v1_destroy(config); 72 | wd_ui_apply_done(pending->state, NULL); 73 | wd_ui_show_error(pending->state, 74 | "The display configuration was modified by the server before updates were processed. " 75 | "Please check the configuration and apply the changes again."); 76 | destroy_pending(pending); 77 | } 78 | 79 | static const struct zwlr_output_configuration_v1_listener config_listener = { 80 | .succeeded = config_handle_succeeded, 81 | .failed = config_handle_failed, 82 | .cancelled = config_handle_cancelled, 83 | }; 84 | 85 | void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs, 86 | struct wl_display *display) { 87 | struct zwlr_output_configuration_v1 *config = 88 | zwlr_output_manager_v1_create_configuration(state->output_manager, state->serial); 89 | 90 | struct wd_pending_config *pending = calloc(1, sizeof(*pending)); 91 | pending->state = state; 92 | pending->outputs = new_outputs; 93 | 94 | zwlr_output_configuration_v1_add_listener(config, &config_listener, pending); 95 | 96 | ssize_t i = -1; 97 | struct wd_head_config *output; 98 | wl_list_for_each(output, new_outputs, link) { 99 | i++; 100 | struct wd_head *head = output->head; 101 | 102 | if (!output->enabled && output->enabled != head->enabled) { 103 | zwlr_output_configuration_v1_disable_head(config, head->wlr_head); 104 | continue; 105 | } 106 | 107 | struct zwlr_output_configuration_head_v1 *config_head = zwlr_output_configuration_v1_enable_head(config, head->wlr_head); 108 | 109 | const struct wd_mode *selected_mode = NULL; 110 | const struct wd_mode *mode; 111 | wl_list_for_each(mode, &head->modes, link) { 112 | if (mode->width == output->width && mode->height == output->height && mode->refresh == output->refresh) { 113 | selected_mode = mode; 114 | break; 115 | } 116 | } 117 | if (selected_mode != NULL) { 118 | if (output->enabled != head->enabled || selected_mode != head->mode) { 119 | zwlr_output_configuration_head_v1_set_mode(config_head, selected_mode->wlr_mode); 120 | } 121 | } else if (output->enabled != head->enabled 122 | || output->width != head->custom_mode.width 123 | || output->height != head->custom_mode.height 124 | || output->refresh != head->custom_mode.refresh) { 125 | zwlr_output_configuration_head_v1_set_custom_mode(config_head, 126 | output->width, output->height, output->refresh); 127 | } 128 | if (output->enabled != head->enabled || output->x != head->x || output->y != head->y) { 129 | zwlr_output_configuration_head_v1_set_position(config_head, output->x, output->y); 130 | } 131 | if (output->enabled != head->enabled || output->scale != head->scale) { 132 | zwlr_output_configuration_head_v1_set_scale(config_head, wl_fixed_from_double(output->scale)); 133 | } 134 | if (output->enabled != head->enabled || output->transform != head->transform) { 135 | zwlr_output_configuration_head_v1_set_transform(config_head, output->transform); 136 | } 137 | } 138 | 139 | zwlr_output_configuration_v1_apply(config); 140 | 141 | wl_display_roundtrip(display); 142 | } 143 | 144 | static void wd_frame_destroy(struct wd_frame *frame) { 145 | if (frame->pixels != NULL) 146 | munmap(frame->pixels, frame->height * frame->stride); 147 | if (frame->buffer != NULL) 148 | wl_buffer_destroy(frame->buffer); 149 | if (frame->pool != NULL) 150 | wl_shm_pool_destroy(frame->pool); 151 | if (frame->capture_fd != -1) 152 | close(frame->capture_fd); 153 | if (frame->wlr_frame != NULL) 154 | zwlr_screencopy_frame_v1_destroy(frame->wlr_frame); 155 | 156 | wl_list_remove(&frame->link); 157 | free(frame); 158 | } 159 | 160 | static int create_shm_file(size_t size, const char *fmt, ...) { 161 | char *shm_name = NULL; 162 | int fd = -1; 163 | 164 | va_list vl; 165 | va_start(vl, fmt); 166 | int result = vasprintf(&shm_name, fmt, vl); 167 | va_end(vl); 168 | 169 | if (result == -1) { 170 | fprintf(stderr, "asprintf: %s\n", strerror(errno)); 171 | shm_name = NULL; 172 | return -1; 173 | } 174 | 175 | fd = shm_open(shm_name, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); 176 | if (fd == -1) { 177 | fprintf(stderr, "shm_open: %s\n", strerror(errno)); 178 | free(shm_name); 179 | return -1; 180 | } 181 | shm_unlink(shm_name); 182 | free(shm_name); 183 | 184 | if (ftruncate(fd, size) == -1) { 185 | fprintf(stderr, "ftruncate: %s\n", strerror(errno)); 186 | close(fd); 187 | return -1; 188 | } 189 | return fd; 190 | } 191 | 192 | static void capture_buffer(void *data, 193 | struct zwlr_screencopy_frame_v1 *copy_frame, 194 | uint32_t format, uint32_t width, uint32_t height, uint32_t stride) { 195 | struct wd_frame *frame = data; 196 | 197 | if (format != WL_SHM_FORMAT_ARGB8888 && format != WL_SHM_FORMAT_XRGB8888 && 198 | format != WL_SHM_FORMAT_ABGR8888 && format != WL_SHM_FORMAT_XBGR8888) { 199 | goto err; 200 | } 201 | 202 | size_t size = stride * height; 203 | frame->capture_fd = create_shm_file(size, "/wd-%s", frame->output->name); 204 | if (frame->capture_fd == -1) { 205 | goto err; 206 | } 207 | 208 | frame->pool = wl_shm_create_pool(frame->output->state->shm, 209 | frame->capture_fd, size); 210 | frame->buffer = wl_shm_pool_create_buffer(frame->pool, 0, 211 | width, height, stride, format); 212 | zwlr_screencopy_frame_v1_copy(copy_frame, frame->buffer); 213 | frame->stride = stride; 214 | frame->width = width; 215 | frame->height = height; 216 | frame->swap_rgb = format == WL_SHM_FORMAT_ABGR8888 217 | || format == WL_SHM_FORMAT_XBGR8888; 218 | 219 | return; 220 | err: 221 | wd_frame_destroy(frame); 222 | } 223 | 224 | static void capture_flags(void *data, 225 | struct zwlr_screencopy_frame_v1 *wlr_frame, 226 | uint32_t flags) { 227 | struct wd_frame *frame = data; 228 | frame->y_invert = !!(flags & ZWLR_SCREENCOPY_FRAME_V1_FLAGS_Y_INVERT); 229 | } 230 | 231 | static void capture_ready(void *data, 232 | struct zwlr_screencopy_frame_v1 *wlr_frame, 233 | uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec) { 234 | struct wd_frame *frame = data; 235 | 236 | frame->pixels = mmap(NULL, frame->stride * frame->height, 237 | PROT_READ, MAP_SHARED, frame->capture_fd, 0); 238 | if (frame->pixels == MAP_FAILED) { 239 | frame->pixels = NULL; 240 | fprintf(stderr, "mmap: %d: %s\n", frame->capture_fd, strerror(errno)); 241 | wd_frame_destroy(frame); 242 | return; 243 | } else { 244 | uint64_t tv_sec = (uint64_t) tv_sec_hi << 32 | tv_sec_lo; 245 | frame->tick = (tv_sec * 1000000) + (tv_nsec / 1000); 246 | } 247 | 248 | zwlr_screencopy_frame_v1_destroy(frame->wlr_frame); 249 | frame->wlr_frame = NULL; 250 | 251 | struct wd_frame *frame_iter, *frame_tmp; 252 | wl_list_for_each_safe(frame_iter, frame_tmp, &frame->output->frames, link) { 253 | if (frame != frame_iter) { 254 | wd_frame_destroy(frame_iter); 255 | } 256 | } 257 | } 258 | 259 | static void capture_failed(void *data, 260 | struct zwlr_screencopy_frame_v1 *wlr_frame) { 261 | struct wd_frame *frame = data; 262 | wd_frame_destroy(frame); 263 | } 264 | 265 | struct zwlr_screencopy_frame_v1_listener capture_listener = { 266 | .buffer = capture_buffer, 267 | .flags = capture_flags, 268 | .ready = capture_ready, 269 | .failed = capture_failed 270 | }; 271 | 272 | static bool has_pending_captures(struct wd_state *state) { 273 | struct wd_output *output; 274 | wl_list_for_each(output, &state->outputs, link) { 275 | struct wd_frame *frame; 276 | wl_list_for_each(frame, &output->frames, link) { 277 | if (frame->pixels == NULL) { 278 | return true; 279 | } 280 | } 281 | } 282 | return false; 283 | } 284 | 285 | void wd_capture_frame(struct wd_state *state) { 286 | if (state->copy_manager == NULL || has_pending_captures(state) 287 | || !state->capture) { 288 | return; 289 | } 290 | 291 | struct wd_output *output; 292 | wl_list_for_each(output, &state->outputs, link) { 293 | struct wd_frame *frame = calloc(1, sizeof(*frame)); 294 | frame->output = output; 295 | frame->capture_fd = -1; 296 | frame->wlr_frame = 297 | zwlr_screencopy_manager_v1_capture_output(state->copy_manager, 1, 298 | output->wl_output); 299 | zwlr_screencopy_frame_v1_add_listener(frame->wlr_frame, &capture_listener, 300 | frame); 301 | wl_list_insert(&output->frames, &frame->link); 302 | } 303 | } 304 | 305 | static void wd_output_destroy(struct wd_output *output) { 306 | struct wd_frame *frame, *frame_tmp; 307 | wl_list_for_each_safe(frame, frame_tmp, &output->frames, link) { 308 | wd_frame_destroy(frame); 309 | } 310 | if (output->state->layer_shell != NULL) { 311 | wd_destroy_overlay(output); 312 | } 313 | zxdg_output_v1_destroy(output->xdg_output); 314 | free(output->name); 315 | free(output); 316 | } 317 | 318 | static void wd_mode_destroy(struct wd_mode* mode) { 319 | zwlr_output_mode_v1_destroy(mode->wlr_mode); 320 | free(mode); 321 | } 322 | 323 | static void wd_head_destroy(struct wd_head *head) { 324 | if (head->state->clicked == head->render) { 325 | head->state->clicked = NULL; 326 | } 327 | if (head->render != NULL) { 328 | wl_list_remove(&head->render->link); 329 | free(head->render); 330 | head->render = NULL; 331 | } 332 | struct wd_mode *mode, *mode_tmp; 333 | wl_list_for_each_safe(mode, mode_tmp, &head->modes, link) { 334 | zwlr_output_mode_v1_destroy(mode->wlr_mode); 335 | free(mode); 336 | } 337 | zwlr_output_head_v1_destroy(head->wlr_head); 338 | free(head->name); 339 | free(head->description); 340 | free(head); 341 | } 342 | 343 | static void mode_handle_size(void *data, struct zwlr_output_mode_v1 *wlr_mode, 344 | int32_t width, int32_t height) { 345 | struct wd_mode *mode = data; 346 | mode->width = width; 347 | mode->height = height; 348 | } 349 | 350 | static void mode_handle_refresh(void *data, 351 | struct zwlr_output_mode_v1 *wlr_mode, int32_t refresh) { 352 | struct wd_mode *mode = data; 353 | mode->refresh = refresh; 354 | } 355 | 356 | static void mode_handle_preferred(void *data, 357 | struct zwlr_output_mode_v1 *wlr_mode) { 358 | struct wd_mode *mode = data; 359 | mode->preferred = true; 360 | } 361 | 362 | static void mode_handle_finished(void *data, 363 | struct zwlr_output_mode_v1 *wlr_mode) { 364 | struct wd_mode *mode = data; 365 | wl_list_remove(&mode->link); 366 | wd_mode_destroy(mode); 367 | } 368 | 369 | static const struct zwlr_output_mode_v1_listener mode_listener = { 370 | .size = mode_handle_size, 371 | .refresh = mode_handle_refresh, 372 | .preferred = mode_handle_preferred, 373 | .finished = mode_handle_finished, 374 | }; 375 | 376 | static void head_handle_name(void *data, 377 | struct zwlr_output_head_v1 *wlr_head, const char *name) { 378 | struct wd_head *head = data; 379 | head->name = strdup(name); 380 | wd_ui_reset_head(head, WD_FIELD_NAME); 381 | } 382 | 383 | static void head_handle_description(void *data, 384 | struct zwlr_output_head_v1 *wlr_head, const char *description) { 385 | struct wd_head *head = data; 386 | head->description = strdup(description); 387 | wd_ui_reset_head(head, WD_FIELD_DESCRIPTION); 388 | } 389 | 390 | static void head_handle_physical_size(void *data, 391 | struct zwlr_output_head_v1 *wlr_head, int32_t width, int32_t height) { 392 | struct wd_head *head = data; 393 | head->phys_width = width; 394 | head->phys_height = height; 395 | wd_ui_reset_head(head, WD_FIELD_PHYSICAL_SIZE); 396 | } 397 | 398 | static void head_handle_mode(void *data, 399 | struct zwlr_output_head_v1 *wlr_head, 400 | struct zwlr_output_mode_v1 *wlr_mode) { 401 | struct wd_head *head = data; 402 | 403 | struct wd_mode *mode = calloc(1, sizeof(*mode)); 404 | mode->head = head; 405 | mode->wlr_mode = wlr_mode; 406 | wl_list_insert(head->modes.prev, &mode->link); 407 | 408 | zwlr_output_mode_v1_add_listener(wlr_mode, &mode_listener, mode); 409 | } 410 | 411 | static void head_handle_enabled(void *data, 412 | struct zwlr_output_head_v1 *wlr_head, int32_t enabled) { 413 | struct wd_head *head = data; 414 | head->enabled = !!enabled; 415 | if (!enabled) { 416 | head->output = NULL; 417 | } 418 | wd_ui_reset_head(head, WD_FIELD_ENABLED); 419 | } 420 | 421 | static void head_handle_current_mode(void *data, 422 | struct zwlr_output_head_v1 *wlr_head, 423 | struct zwlr_output_mode_v1 *wlr_mode) { 424 | struct wd_head *head = data; 425 | struct wd_mode *mode; 426 | wl_list_for_each(mode, &head->modes, link) { 427 | if (mode->wlr_mode == wlr_mode) { 428 | head->mode = mode; 429 | wd_ui_reset_head(head, WD_FIELD_MODE); 430 | return; 431 | } 432 | } 433 | fprintf(stderr, "received unknown current_mode\n"); 434 | head->mode = NULL; 435 | } 436 | 437 | static void head_handle_position(void *data, 438 | struct zwlr_output_head_v1 *wlr_head, int32_t x, int32_t y) { 439 | struct wd_head *head = data; 440 | head->x = x; 441 | head->y = y; 442 | wd_ui_reset_head(head, WD_FIELD_POSITION); 443 | } 444 | 445 | static void head_handle_transform(void *data, 446 | struct zwlr_output_head_v1 *wlr_head, int32_t transform) { 447 | struct wd_head *head = data; 448 | head->transform = transform; 449 | wd_ui_reset_head(head, WD_FIELD_TRANSFORM); 450 | } 451 | 452 | static void head_handle_scale(void *data, 453 | struct zwlr_output_head_v1 *wlr_head, wl_fixed_t scale) { 454 | struct wd_head *head = data; 455 | head->scale = wl_fixed_to_double(scale); 456 | wd_ui_reset_head(head, WD_FIELD_SCALE); 457 | } 458 | 459 | static void head_handle_finished(void *data, 460 | struct zwlr_output_head_v1 *wlr_head) { 461 | struct wd_head *head = data; 462 | struct wd_state *state = head->state; 463 | wl_list_remove(&head->link); 464 | wd_head_destroy(head); 465 | 466 | uint32_t counter = 0; 467 | wl_list_for_each(head, &state->heads, link) { 468 | if (head->id != counter) { 469 | head->id = counter; 470 | if (head->output != NULL) { 471 | wd_redraw_overlay(head->output); 472 | } 473 | } 474 | counter++; 475 | } 476 | } 477 | 478 | static const struct zwlr_output_head_v1_listener head_listener = { 479 | .name = head_handle_name, 480 | .description = head_handle_description, 481 | .physical_size = head_handle_physical_size, 482 | .mode = head_handle_mode, 483 | .enabled = head_handle_enabled, 484 | .current_mode = head_handle_current_mode, 485 | .position = head_handle_position, 486 | .transform = head_handle_transform, 487 | .scale = head_handle_scale, 488 | .finished = head_handle_finished, 489 | }; 490 | 491 | static void output_manager_handle_head(void *data, 492 | struct zwlr_output_manager_v1 *manager, 493 | struct zwlr_output_head_v1 *wlr_head) { 494 | struct wd_state *state = data; 495 | 496 | struct wd_head *head = calloc(1, sizeof(*head)); 497 | head->state = state; 498 | head->wlr_head = wlr_head; 499 | head->scale = 1.0; 500 | head->id = wl_list_length(&state->heads); 501 | wl_list_init(&head->modes); 502 | wl_list_insert(&state->heads, &head->link); 503 | 504 | zwlr_output_head_v1_add_listener(wlr_head, &head_listener, head); 505 | } 506 | 507 | static void output_manager_handle_done(void *data, 508 | struct zwlr_output_manager_v1 *manager, uint32_t serial) { 509 | struct wd_state *state = data; 510 | state->serial = serial; 511 | 512 | assert(wl_list_length(&state->heads) <= HEADS_MAX); 513 | 514 | struct wd_head *head = data; 515 | wl_list_for_each(head, &state->heads, link) { 516 | if (!head->enabled && head->mode == NULL && !wl_list_empty(&head->modes)) { 517 | struct wd_mode *mode = wl_container_of(head->modes.prev, mode, link); 518 | head->custom_mode.width = mode->width; 519 | head->custom_mode.height = mode->height; 520 | head->custom_mode.refresh = mode->refresh; 521 | } 522 | } 523 | wd_ui_reset_heads(state); 524 | } 525 | 526 | static const struct zwlr_output_manager_v1_listener output_manager_listener = { 527 | .head = output_manager_handle_head, 528 | .done = output_manager_handle_done, 529 | .finished = noop, 530 | }; 531 | static void registry_handle_global(void *data, struct wl_registry *registry, 532 | uint32_t name, const char *interface, uint32_t version) { 533 | struct wd_state *state = data; 534 | 535 | if (strcmp(interface, zwlr_output_manager_v1_interface.name) == 0) { 536 | state->output_manager = wl_registry_bind(registry, name, 537 | &zwlr_output_manager_v1_interface, 1); 538 | zwlr_output_manager_v1_add_listener(state->output_manager, 539 | &output_manager_listener, state); 540 | } else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { 541 | state->xdg_output_manager = wl_registry_bind(registry, name, 542 | &zxdg_output_manager_v1_interface, 2); 543 | } else if(strcmp(interface, zwlr_screencopy_manager_v1_interface.name) == 0) { 544 | state->copy_manager = wl_registry_bind(registry, name, 545 | &zwlr_screencopy_manager_v1_interface, 1); 546 | } else if(strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { 547 | state->layer_shell = wl_registry_bind(registry, name, 548 | &zwlr_layer_shell_v1_interface, 1); 549 | } else if(strcmp(interface, wl_shm_interface.name) == 0) { 550 | state->shm = wl_registry_bind(registry, name, &wl_shm_interface, 1); 551 | } 552 | } 553 | 554 | static const struct wl_registry_listener registry_listener = { 555 | .global = registry_handle_global, 556 | .global_remove = noop, 557 | }; 558 | 559 | void wd_add_output_management_listener(struct wd_state *state, struct 560 | wl_display *display) { 561 | struct wl_registry *registry = wl_display_get_registry(display); 562 | wl_registry_add_listener(registry, ®istry_listener, state); 563 | 564 | wl_display_dispatch(display); 565 | wl_display_roundtrip(display); 566 | } 567 | 568 | struct wd_head *wd_find_head(struct wd_state *state, 569 | struct wd_output *output) { 570 | struct wd_head *head; 571 | wl_list_for_each(head, &state->heads, link) { 572 | if (output->name != NULL && strcmp(output->name, head->name) == 0) { 573 | head->output = output; 574 | return head; 575 | } 576 | } 577 | return NULL; 578 | } 579 | 580 | static void output_logical_position(void *data, struct zxdg_output_v1 *zxdg_output_v1, 581 | int32_t x, int32_t y) { 582 | struct wd_output *output = data; 583 | struct wd_head *head = wd_find_head(output->state, output); 584 | if (head != NULL) { 585 | head->x = x; 586 | head->y = y; 587 | wd_ui_reset_head(head, WD_FIELD_POSITION); 588 | } 589 | } 590 | 591 | static void output_name(void *data, struct zxdg_output_v1 *zxdg_output_v1, 592 | const char *name) { 593 | struct wd_output *output = data; 594 | if (output->name != NULL) { 595 | free(output->name); 596 | } 597 | output->name = strdup(name); 598 | struct wd_head *head = wd_find_head(output->state, output); 599 | if (head != NULL) { 600 | wd_ui_reset_head(head, WD_FIELD_NAME); 601 | } 602 | } 603 | 604 | static const struct zxdg_output_v1_listener output_listener = { 605 | .logical_position = output_logical_position, 606 | .logical_size = noop, 607 | .done = noop, 608 | .name = output_name, 609 | .description = noop 610 | }; 611 | 612 | void wd_add_output(struct wd_state *state, struct wl_output *wl_output, 613 | struct wl_display *display) { 614 | struct wd_output *output = calloc(1, sizeof(*output)); 615 | output->state = state; 616 | output->wl_output = wl_output; 617 | output->xdg_output = zxdg_output_manager_v1_get_xdg_output( 618 | state->xdg_output_manager, wl_output); 619 | wl_list_init(&output->frames); 620 | zxdg_output_v1_add_listener(output->xdg_output, &output_listener, output); 621 | wl_list_insert(output->state->outputs.prev, &output->link); 622 | if (state->layer_shell != NULL && state->show_overlay) { 623 | wl_display_roundtrip(display); 624 | wd_create_overlay(output); 625 | } 626 | } 627 | 628 | void wd_remove_output(struct wd_state *state, struct wl_output *wl_output, 629 | struct wl_display *display) { 630 | struct wd_output *output, *output_tmp; 631 | wl_list_for_each_safe(output, output_tmp, &state->outputs, link) { 632 | if (output->wl_output == wl_output) { 633 | wl_list_remove(&output->link); 634 | wd_output_destroy(output); 635 | break; 636 | } 637 | } 638 | wd_capture_wait(state, display); 639 | } 640 | 641 | struct wd_output *wd_find_output(struct wd_state *state, struct wd_head 642 | *head) { 643 | if (!head->enabled) { 644 | return NULL; 645 | } 646 | if (head->output != NULL) { 647 | return head->output; 648 | } 649 | struct wd_output *output; 650 | wl_list_for_each(output, &state->outputs, link) { 651 | if (output->name != NULL && strcmp(output->name, head->name) == 0) { 652 | head->output = output; 653 | return output; 654 | } 655 | } 656 | head->output = NULL; 657 | return NULL; 658 | } 659 | 660 | struct wd_state *wd_state_create(void) { 661 | struct wd_state *state = calloc(1, sizeof(*state)); 662 | state->zoom = 1.; 663 | state->capture = true; 664 | state->show_overlay = true; 665 | wl_list_init(&state->heads); 666 | wl_list_init(&state->outputs); 667 | wl_list_init(&state->render.heads); 668 | return state; 669 | } 670 | 671 | void wd_capture_wait(struct wd_state *state, struct wl_display *display) { 672 | wl_display_flush(display); 673 | while (has_pending_captures(state)) { 674 | if (wl_display_dispatch(display) == -1) { 675 | break; 676 | } 677 | } 678 | } 679 | 680 | void wd_state_destroy(struct wd_state *state) { 681 | struct wd_head *head, *head_tmp; 682 | wl_list_for_each_safe(head, head_tmp, &state->heads, link) { 683 | wd_head_destroy(head); 684 | } 685 | struct wd_output *output, *output_tmp; 686 | wl_list_for_each_safe(output, output_tmp, &state->outputs, link) { 687 | wd_output_destroy(output); 688 | } 689 | if (state->layer_shell != NULL) { 690 | zwlr_layer_shell_v1_destroy(state->layer_shell); 691 | } 692 | if (state->copy_manager != NULL) { 693 | zwlr_screencopy_manager_v1_destroy(state->copy_manager); 694 | } 695 | zwlr_output_manager_v1_destroy(state->output_manager); 696 | zxdg_output_manager_v1_destroy(state->xdg_output_manager); 697 | wl_shm_destroy(state->shm); 698 | free(state); 699 | } 700 | -------------------------------------------------------------------------------- /src/overlay.c: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020 Jason Francis 2 | * SPDX-License-Identifier: GPL-3.0-or-later */ 3 | 4 | #define _GNU_SOURCE 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include "wdisplays.h" 13 | 14 | #include "wlr-layer-shell-unstable-v1-client-protocol.h" 15 | 16 | #define SCREEN_MARGIN_PERCENT 0.02 17 | 18 | static void layer_surface_configure(void *data, 19 | struct zwlr_layer_surface_v1 *surface, 20 | uint32_t serial, uint32_t width, uint32_t height) { 21 | struct wd_output *output = data; 22 | gtk_widget_set_size_request(output->overlay_window, width, height); 23 | zwlr_layer_surface_v1_ack_configure(surface, serial); 24 | } 25 | 26 | static void layer_surface_closed(void *data, 27 | struct zwlr_layer_surface_v1 *surface) { 28 | } 29 | 30 | static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { 31 | .configure = layer_surface_configure, 32 | .closed = layer_surface_closed, 33 | }; 34 | 35 | static inline int min(int a, int b) { 36 | return a < b ? a : b; 37 | } 38 | 39 | static PangoLayout *create_text_layout(struct wd_head *head, 40 | PangoContext *pango, GtkStyleContext *style) { 41 | GtkStyleContext *desc_style = gtk_style_context_new(); 42 | gtk_style_context_set_screen(desc_style, 43 | gtk_style_context_get_screen(style)); 44 | GtkWidgetPath *desc_path = gtk_widget_path_copy( 45 | gtk_style_context_get_path(style)); 46 | gtk_widget_path_append_type(desc_path, G_TYPE_NONE); 47 | gtk_style_context_set_path(desc_style, desc_path); 48 | gtk_style_context_add_class(desc_style, "description"); 49 | 50 | double desc_font_size = 16.; 51 | gtk_style_context_get(desc_style, GTK_STATE_FLAG_NORMAL, 52 | "font-size", &desc_font_size, NULL); 53 | 54 | g_autofree gchar *str = g_strdup_printf("%s\n%s", 55 | head->name, (int) (desc_font_size * PANGO_SCALE), head->description); 56 | PangoLayout *layout = pango_layout_new(pango); 57 | 58 | pango_layout_set_markup(layout, str, -1); 59 | return layout; 60 | } 61 | 62 | static void resize(struct wd_output *output) { 63 | struct wd_head *head = wd_find_head(output->state, output); 64 | 65 | uint32_t screen_width = head->custom_mode.width; 66 | uint32_t screen_height = head->custom_mode.height; 67 | if (head->mode != NULL) { 68 | screen_width = head->mode->width; 69 | screen_height = head->mode->height; 70 | } 71 | uint32_t margin = min(screen_width, screen_height) * SCREEN_MARGIN_PERCENT; 72 | 73 | GdkWindow *window = gtk_widget_get_window(output->overlay_window); 74 | PangoContext *pango = gtk_widget_get_pango_context(output->overlay_window); 75 | GtkStyleContext *style_ctx = gtk_widget_get_style_context( 76 | output->overlay_window); 77 | PangoLayout *layout = create_text_layout(head, pango, style_ctx); 78 | 79 | int width; 80 | int height; 81 | pango_layout_get_pixel_size(layout, &width, &height); 82 | g_object_unref(layout); 83 | 84 | 85 | GtkBorder padding; 86 | gtk_style_context_get_padding(style_ctx, GTK_STATE_FLAG_NORMAL, &padding); 87 | 88 | width = min(width, screen_width - margin * 2) 89 | + padding.left + padding.right; 90 | height = min(height, screen_height - margin * 2) 91 | + padding.top + padding.bottom; 92 | 93 | zwlr_layer_surface_v1_set_margin(output->overlay_layer_surface, 94 | margin, margin, margin, margin); 95 | zwlr_layer_surface_v1_set_size(output->overlay_layer_surface, 96 | width, height); 97 | 98 | struct wl_surface *surface = gdk_wayland_window_get_wl_surface(window); 99 | wl_surface_commit(surface); 100 | 101 | GdkDisplay *display = gdk_window_get_display(window); 102 | wl_display_roundtrip(gdk_wayland_display_get_wl_display(display)); 103 | } 104 | 105 | void wd_redraw_overlay(struct wd_output *output) { 106 | if (output->overlay_window != NULL) { 107 | resize(output); 108 | gtk_widget_queue_draw(output->overlay_window); 109 | } 110 | } 111 | 112 | void window_realize(GtkWidget *widget, gpointer data) { 113 | GdkWindow *window = gtk_widget_get_window(widget); 114 | gdk_wayland_window_set_use_custom_surface(window); 115 | } 116 | 117 | void window_map(GtkWidget *widget, gpointer data) { 118 | struct wd_output *output = data; 119 | 120 | GdkWindow *window = gtk_widget_get_window(widget); 121 | cairo_region_t *region = cairo_region_create(); 122 | gdk_window_input_shape_combine_region(window, region, 0, 0); 123 | cairo_region_destroy(region); 124 | 125 | struct wl_surface *surface = gdk_wayland_window_get_wl_surface(window); 126 | 127 | output->overlay_layer_surface = zwlr_layer_shell_v1_get_layer_surface( 128 | output->state->layer_shell, surface, output->wl_output, 129 | ZWLR_LAYER_SHELL_V1_LAYER_TOP, "output-overlay"); 130 | 131 | zwlr_layer_surface_v1_add_listener(output->overlay_layer_surface, 132 | &layer_surface_listener, output); 133 | 134 | zwlr_layer_surface_v1_set_anchor(output->overlay_layer_surface, 135 | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | 136 | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); 137 | 138 | resize(output); 139 | } 140 | 141 | void window_unmap(GtkWidget *widget, gpointer data) { 142 | struct wd_output *output = data; 143 | zwlr_layer_surface_v1_destroy(output->overlay_layer_surface); 144 | } 145 | 146 | gboolean window_draw(GtkWidget *widget, cairo_t *cr, gpointer data) { 147 | struct wd_output *output = data; 148 | struct wd_head *head = wd_find_head(output->state, output); 149 | 150 | GtkStyleContext *style_ctx = gtk_widget_get_style_context(widget); 151 | GdkRGBA fg; 152 | gtk_style_context_get_color(style_ctx, GTK_STATE_FLAG_NORMAL, &fg); 153 | 154 | int width = gtk_widget_get_allocated_width(widget); 155 | int height = gtk_widget_get_allocated_height(widget); 156 | gtk_render_background(style_ctx, cr, 0, 0, width, height); 157 | 158 | GtkBorder padding; 159 | gtk_style_context_get_padding(style_ctx, GTK_STATE_FLAG_NORMAL, &padding); 160 | PangoContext *pango = gtk_widget_get_pango_context(widget); 161 | PangoLayout *layout = create_text_layout(head, pango, style_ctx); 162 | 163 | gdk_cairo_set_source_rgba(cr, &fg); 164 | cairo_move_to(cr, padding.left, padding.top); 165 | pango_cairo_show_layout(cr, layout); 166 | g_object_unref(layout); 167 | return TRUE; 168 | } 169 | 170 | void wd_create_overlay(struct wd_output *output) { 171 | output->overlay_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); 172 | gtk_window_set_decorated(GTK_WINDOW(output->overlay_window), FALSE); 173 | gtk_window_set_resizable(GTK_WINDOW(output->overlay_window), FALSE); 174 | gtk_widget_add_events(output->overlay_window, GDK_STRUCTURE_MASK); 175 | 176 | g_signal_connect(output->overlay_window, "realize", 177 | G_CALLBACK(window_realize), output); 178 | g_signal_connect(output->overlay_window, "map", 179 | G_CALLBACK(window_map), output); 180 | g_signal_connect(output->overlay_window, "unmap", 181 | G_CALLBACK(window_unmap), output); 182 | g_signal_connect(output->overlay_window, "draw", 183 | G_CALLBACK(window_draw), output); 184 | 185 | GtkStyleContext *style_ctx = gtk_widget_get_style_context( 186 | output->overlay_window); 187 | gtk_style_context_add_class(style_ctx, "output-overlay"); 188 | gtk_widget_show(output->overlay_window); 189 | } 190 | 191 | void wd_destroy_overlay(struct wd_output *output) { 192 | if (output->overlay_window != NULL) { 193 | gtk_widget_destroy(output->overlay_window); 194 | output->overlay_window = NULL; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/render.c: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2020 Jason Francis 2 | * SPDX-License-Identifier: GPL-3.0-or-later */ 3 | 4 | #include "wdisplays.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define BT_UV_VERT_SIZE (2 + 2) 13 | #define BT_UV_QUAD_SIZE (6 * BT_UV_VERT_SIZE) 14 | #define BT_UV_MAX (BT_COLOR_QUAD_SIZE * HEADS_MAX) 15 | 16 | #define BT_COLOR_VERT_SIZE (2 + 4) 17 | #define BT_COLOR_QUAD_SIZE (6 * BT_COLOR_VERT_SIZE) 18 | #define BT_COLOR_MAX (BT_COLOR_QUAD_SIZE * HEADS_MAX) 19 | 20 | #define BT_LINE_VERT_SIZE (2 + 4) 21 | #define BT_LINE_QUAD_SIZE (8 * BT_LINE_VERT_SIZE) 22 | #define BT_LINE_EXT_SIZE (24 * BT_LINE_VERT_SIZE) 23 | #define BT_LINE_MAX (BT_LINE_EXT_SIZE * (HEADS_MAX + 1)) 24 | 25 | enum gl_buffers { 26 | TEXTURE_BUFFER, 27 | COLOR_BUFFER, 28 | LINE_BUFFER, 29 | NUM_BUFFERS 30 | }; 31 | 32 | struct wd_gl_data { 33 | GLuint color_program; 34 | GLuint color_vertex_shader; 35 | GLuint color_fragment_shader; 36 | GLuint color_position_attribute; 37 | GLuint color_color_attribute; 38 | GLuint color_screen_size_uniform; 39 | 40 | GLuint texture_program; 41 | GLuint texture_vertex_shader; 42 | GLuint texture_fragment_shader; 43 | GLuint texture_position_attribute; 44 | GLuint texture_uv_attribute; 45 | GLuint texture_screen_size_uniform; 46 | GLuint texture_texture_uniform; 47 | GLuint texture_color_transform_uniform; 48 | 49 | GLuint buffers[NUM_BUFFERS]; 50 | 51 | unsigned texture_count; 52 | GLuint textures[HEADS_MAX]; 53 | 54 | float verts[BT_LINE_MAX]; 55 | }; 56 | 57 | static const char *color_vertex_shader_src = "\ 58 | precision mediump float;\n\ 59 | attribute vec2 position;\n\ 60 | attribute vec4 color;\n\ 61 | varying vec4 color_out;\n\ 62 | uniform vec2 screen_size;\n\ 63 | void main(void) {\n\ 64 | vec2 screen_pos = (position / screen_size * 2. - 1.) * vec2(1., -1.);\n\ 65 | gl_Position = vec4(screen_pos, 0., 1.);\n\ 66 | color_out = color;\n\ 67 | }"; 68 | 69 | static const char *color_fragment_shader_src = "\ 70 | precision mediump float;\n\ 71 | varying vec4 color_out;\n\ 72 | void main(void) {\n\ 73 | gl_FragColor = color_out;\n\ 74 | }"; 75 | 76 | static const char *texture_vertex_shader_src = "\ 77 | precision mediump float;\n\ 78 | attribute vec2 position;\n\ 79 | attribute vec2 uv;\n\ 80 | varying vec2 uv_out;\n\ 81 | uniform vec2 screen_size;\n\ 82 | void main(void) {\n\ 83 | vec2 screen_pos = (position / screen_size * 2. - 1.) * vec2(1., -1.);\n\ 84 | gl_Position = vec4(screen_pos, 0., 1.);\n\ 85 | uv_out = uv;\n\ 86 | }"; 87 | 88 | static const char *texture_fragment_shader_src = "\ 89 | precision mediump float;\n\ 90 | varying vec2 uv_out;\n\ 91 | uniform sampler2D texture;\n\ 92 | uniform mat4 color_transform;\n\ 93 | void main(void) {\n\ 94 | gl_FragColor = texture2D(texture, uv_out) * color_transform;\n\ 95 | }"; 96 | 97 | static GLuint gl_make_shader(GLenum type, const char *src) { 98 | GLuint shader = glCreateShader(type); 99 | glShaderSource(shader, 1, &src, NULL); 100 | glCompileShader(shader); 101 | GLint status; 102 | glGetShaderiv(shader, GL_COMPILE_STATUS, &status); 103 | if (status == GL_FALSE) { 104 | GLsizei length; 105 | glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length); 106 | GLchar *log = "Failed"; 107 | if (length > 0) { 108 | log = malloc(length); 109 | glGetShaderInfoLog(shader, length, NULL, log); 110 | } 111 | fprintf(stderr, "glCompileShader: %s\n", log); 112 | if (length > 0) { 113 | free(log); 114 | } 115 | } 116 | return shader; 117 | } 118 | 119 | static void gl_link_and_validate(GLint program) { 120 | GLint status; 121 | 122 | glLinkProgram(program); 123 | glGetProgramiv(program, GL_LINK_STATUS, &status); 124 | if (status == GL_FALSE) { 125 | GLsizei length; 126 | glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length); 127 | GLchar *log = malloc(length); 128 | glGetProgramInfoLog(program, length, NULL, log); 129 | fprintf(stderr, "glLinkProgram: %s\n", log); 130 | free(log); 131 | return; 132 | } 133 | glValidateProgram(program); 134 | glGetProgramiv(program, GL_VALIDATE_STATUS, &status); 135 | if (status == GL_FALSE) { 136 | GLsizei length; 137 | glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length); 138 | GLchar *log = malloc(length); 139 | glGetProgramInfoLog(program, length, NULL, log); 140 | fprintf(stderr, "glValidateProgram: %s\n", log); 141 | free(log); 142 | } 143 | } 144 | 145 | struct wd_gl_data *wd_gl_setup(void) { 146 | struct wd_gl_data *res = calloc(1, sizeof(struct wd_gl_data)); 147 | res->color_program = glCreateProgram(); 148 | 149 | res->color_vertex_shader = gl_make_shader(GL_VERTEX_SHADER, 150 | color_vertex_shader_src); 151 | glAttachShader(res->color_program, res->color_vertex_shader); 152 | res->color_fragment_shader = gl_make_shader(GL_FRAGMENT_SHADER, 153 | color_fragment_shader_src); 154 | glAttachShader(res->color_program, res->color_fragment_shader); 155 | gl_link_and_validate(res->color_program); 156 | 157 | res->color_position_attribute = glGetAttribLocation(res->color_program, 158 | "position"); 159 | res->color_color_attribute = glGetAttribLocation(res->color_program, 160 | "color"); 161 | res->color_screen_size_uniform = glGetUniformLocation(res->color_program, 162 | "screen_size"); 163 | 164 | res->texture_program = glCreateProgram(); 165 | 166 | res->texture_vertex_shader = gl_make_shader(GL_VERTEX_SHADER, 167 | texture_vertex_shader_src); 168 | glAttachShader(res->texture_program, res->texture_vertex_shader); 169 | res->texture_fragment_shader = gl_make_shader(GL_FRAGMENT_SHADER, 170 | texture_fragment_shader_src); 171 | glAttachShader(res->texture_program, res->texture_fragment_shader); 172 | gl_link_and_validate(res->texture_program); 173 | 174 | res->texture_position_attribute = glGetAttribLocation(res->texture_program, 175 | "position"); 176 | res->texture_uv_attribute = glGetAttribLocation(res->texture_program, 177 | "uv"); 178 | res->texture_screen_size_uniform = glGetUniformLocation(res->texture_program, 179 | "screen_size"); 180 | res->texture_texture_uniform = glGetUniformLocation(res->texture_program, 181 | "texture"); 182 | res->texture_color_transform_uniform = glGetUniformLocation( 183 | res->texture_program, "color_transform"); 184 | 185 | glGenBuffers(NUM_BUFFERS, res->buffers); 186 | glBindBuffer(GL_ARRAY_BUFFER, res->buffers[TEXTURE_BUFFER]); 187 | glBufferData(GL_ARRAY_BUFFER, BT_UV_MAX * sizeof(float), 188 | NULL, GL_DYNAMIC_DRAW); 189 | 190 | glBindBuffer(GL_ARRAY_BUFFER, res->buffers[COLOR_BUFFER]); 191 | glBufferData(GL_ARRAY_BUFFER, BT_COLOR_MAX * sizeof(float), 192 | NULL, GL_DYNAMIC_DRAW); 193 | 194 | glBindBuffer(GL_ARRAY_BUFFER, res->buffers[LINE_BUFFER]); 195 | glBufferData(GL_ARRAY_BUFFER, BT_LINE_MAX * sizeof(float), 196 | NULL, GL_DYNAMIC_DRAW); 197 | 198 | return res; 199 | } 200 | 201 | static const GLfloat TRANSFORM_RGB[16] = { 202 | 1, 0, 0, 0, 203 | 0, 1, 0, 0, 204 | 0, 0, 1, 0, 205 | 0, 0, 0, 1}; 206 | 207 | static const GLfloat TRANSFORM_BGR[16] = { 208 | 0, 0, 1, 0, 209 | 0, 1, 0, 0, 210 | 1, 0, 0, 0, 211 | 0, 0, 0, 1}; 212 | 213 | #define PUSH_POINT_COLOR(_start, _a, _b, _color, _alpha) \ 214 | *((_start)++) = (_a);\ 215 | *((_start)++) = (_b);\ 216 | *((_start)++) = ((_color)[0]);\ 217 | *((_start)++) = ((_color)[1]);\ 218 | *((_start)++) = ((_color)[2]);\ 219 | *((_start)++) = (_alpha); 220 | 221 | #define PUSH_POINT_UV(_start, _a, _b, _c, _d) \ 222 | *((_start)++) = (_a);\ 223 | *((_start)++) = (_b);\ 224 | *((_start)++) = (_c);\ 225 | *((_start)++) = (_d); 226 | 227 | static inline float lerp(float x, float y, float a) { 228 | return x * (1.f - a) + y * a; 229 | } 230 | 231 | static inline void lerp_color(float out[3], float x[3], float y[3], float a) { 232 | out[0] = lerp(x[0], y[0], a); 233 | out[1] = lerp(x[1], y[1], a); 234 | out[2] = lerp(x[2], y[2], a); 235 | out[3] = lerp(x[3], y[3], a); 236 | } 237 | 238 | static inline float ease(float d) { 239 | d *= 2.f; 240 | if (d <= 1.f) { 241 | d = d * d; 242 | } else { 243 | d -= 1.f; 244 | d = d * (2.f - d) + 1.f; 245 | } 246 | d /= 2.f; 247 | return d; 248 | } 249 | 250 | void wd_gl_render(struct wd_gl_data *res, struct wd_render_data *info, 251 | uint64_t tick) { 252 | unsigned int tri_verts = 0; 253 | 254 | unsigned int head_count = wl_list_length(&info->heads); 255 | if (head_count >= HEADS_MAX) 256 | head_count = HEADS_MAX; 257 | 258 | if (head_count > res->texture_count) { 259 | glGenTextures(head_count - res->texture_count, 260 | res->textures + res->texture_count); 261 | for (int i = res->texture_count; i < head_count; i++) { 262 | glBindTexture(GL_TEXTURE_2D, res->textures[i]); 263 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, 264 | GL_LINEAR_MIPMAP_LINEAR); 265 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 266 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 267 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 268 | } 269 | glBindTexture(GL_TEXTURE_2D, 0); 270 | res->texture_count = head_count; 271 | } 272 | 273 | struct wd_render_head_data *head; 274 | int i = 0; 275 | wl_list_for_each_reverse(head, &info->heads, link) { 276 | float *tri_ptr = res->verts + i * BT_UV_QUAD_SIZE; 277 | float x1 = head->active.x_invert ? head->x2 : head->x1; 278 | float y1 = head->y_invert ? head->y2 : head->y1; 279 | float x2 = head->active.x_invert ? head->x1 : head->x2; 280 | float y2 = head->y_invert ? head->y1 : head->y2; 281 | 282 | float sa = 0.f; 283 | float sb = 1.f; 284 | float sc = sb; 285 | float sd = sa; 286 | float ta = 0.f; 287 | float tb = ta; 288 | float tc = 1.f; 289 | float td = tc; 290 | for (int i = 0; i < head->active.rotation; i++) { 291 | float tmp = sd; 292 | sd = sc; 293 | sc = sb; 294 | sb = sa; 295 | sa = tmp; 296 | 297 | tmp = td; 298 | td = tc; 299 | tc = tb; 300 | tb = ta; 301 | ta = tmp; 302 | } 303 | 304 | PUSH_POINT_UV(tri_ptr, x1, y1, sa, ta) 305 | PUSH_POINT_UV(tri_ptr, x2, y1, sb, tb) 306 | PUSH_POINT_UV(tri_ptr, x1, y2, sd, td) 307 | PUSH_POINT_UV(tri_ptr, x1, y2, sd, td) 308 | PUSH_POINT_UV(tri_ptr, x2, y1, sb, tb) 309 | PUSH_POINT_UV(tri_ptr, x2, y2, sc, tc) 310 | 311 | tri_verts += 6; 312 | i++; 313 | if (i >= HEADS_MAX) 314 | break; 315 | } 316 | 317 | glClearColor(info->bg_color[0], info->bg_color[1], info->bg_color[2], 1.f); 318 | glClear(GL_COLOR_BUFFER_BIT); 319 | 320 | float screen_size[2] = { info->viewport_width, info->viewport_height }; 321 | 322 | if (tri_verts > 0) { 323 | glUseProgram(res->texture_program); 324 | glBindBuffer(GL_ARRAY_BUFFER, res->buffers[TEXTURE_BUFFER]); 325 | glBufferSubData(GL_ARRAY_BUFFER, 0, 326 | tri_verts * BT_UV_VERT_SIZE * sizeof(float), res->verts); 327 | glEnableVertexAttribArray(res->texture_position_attribute); 328 | glEnableVertexAttribArray(res->texture_uv_attribute); 329 | glVertexAttribPointer(res->texture_position_attribute, 330 | 2, GL_FLOAT, GL_FALSE, 331 | BT_UV_VERT_SIZE * sizeof(float), (void *) (0 * sizeof(float))); 332 | glVertexAttribPointer(res->texture_uv_attribute, 2, GL_FLOAT, GL_FALSE, 333 | BT_UV_VERT_SIZE * sizeof(float), (void *) (2 * sizeof(float))); 334 | glUniform2fv(res->texture_screen_size_uniform, 1, screen_size); 335 | glUniform1i(res->texture_texture_uniform, 0); 336 | glActiveTexture(GL_TEXTURE0); 337 | 338 | i = 0; 339 | wl_list_for_each_reverse(head, &info->heads, link) { 340 | glBindTexture(GL_TEXTURE_2D, res->textures[i]); 341 | if (head->updated_at == tick) { 342 | glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, head->tex_stride / 4); 343 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 344 | head->tex_width, head->tex_height, 345 | 0, GL_RGBA, GL_UNSIGNED_BYTE, head->pixels); 346 | glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, 0); 347 | glGenerateMipmap(GL_TEXTURE_2D); 348 | } 349 | glUniformMatrix4fv(res->texture_color_transform_uniform, 1, GL_FALSE, 350 | head->swap_rgb ? TRANSFORM_RGB : TRANSFORM_BGR); 351 | glDrawArrays(GL_TRIANGLES, i * 6, 6); 352 | i++; 353 | if (i >= HEADS_MAX) 354 | break; 355 | } 356 | } 357 | 358 | tri_verts = 0; 359 | 360 | int j = 0; 361 | i = 0; 362 | bool any_clicked = false; 363 | uint64_t click_begin = 0; 364 | wl_list_for_each_reverse(head, &info->heads, link) { 365 | any_clicked = head->clicked || any_clicked; 366 | if (head->click_begin > click_begin) 367 | click_begin = head->click_begin; 368 | if (head->hovered || tick < head->hover_begin + HOVER_USECS) { 369 | float *tri_ptr = res->verts + j++ * BT_COLOR_QUAD_SIZE; 370 | float x1 = head->x1; 371 | float y1 = head->y1; 372 | float x2 = head->x2; 373 | float y2 = head->y2; 374 | 375 | float *color = info->selection_color; 376 | float d = fminf( 377 | (tick - head->hover_begin) / (double) HOVER_USECS, 1.f); 378 | if (!head->hovered) 379 | d = 1.f - d; 380 | float alpha = color[3] * ease(d) * .5f; 381 | 382 | PUSH_POINT_COLOR(tri_ptr, x1, y1, color, alpha) 383 | PUSH_POINT_COLOR(tri_ptr, x2, y1, color, alpha) 384 | PUSH_POINT_COLOR(tri_ptr, x1, y2, color, alpha) 385 | PUSH_POINT_COLOR(tri_ptr, x1, y2, color, alpha) 386 | PUSH_POINT_COLOR(tri_ptr, x2, y1, color, alpha) 387 | PUSH_POINT_COLOR(tri_ptr, x2, y2, color, alpha) 388 | 389 | tri_verts += 6; 390 | } 391 | i++; 392 | if (i >= HEADS_MAX) 393 | break; 394 | } 395 | 396 | if (tri_verts > 0) { 397 | glEnable(GL_BLEND); 398 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 399 | glUseProgram(res->color_program); 400 | glBindBuffer(GL_ARRAY_BUFFER, res->buffers[COLOR_BUFFER]); 401 | glBufferSubData(GL_ARRAY_BUFFER, 0, 402 | tri_verts * BT_COLOR_VERT_SIZE * sizeof(float), res->verts); 403 | glEnableVertexAttribArray(res->color_position_attribute); 404 | glEnableVertexAttribArray(res->color_color_attribute); 405 | glVertexAttribPointer(res->color_position_attribute, 2, GL_FLOAT, GL_FALSE, 406 | BT_COLOR_VERT_SIZE * sizeof(float), (void *) (0 * sizeof(float))); 407 | glVertexAttribPointer(res->color_color_attribute, 4, GL_FLOAT, GL_FALSE, 408 | BT_COLOR_VERT_SIZE * sizeof(float), (void *) (2 * sizeof(float))); 409 | glUniform2fv(res->color_screen_size_uniform, 1, screen_size); 410 | glDrawArrays(GL_TRIANGLES, 0, tri_verts); 411 | glDisable(GL_BLEND); 412 | } 413 | 414 | unsigned int line_verts = 0; 415 | i = 0; 416 | float *line_ptr = res->verts; 417 | if (any_clicked || (click_begin && tick < click_begin + HOVER_USECS)) { 418 | const float ox = -info->scroll_x - info->x_origin; 419 | const float oy = -info->scroll_y - info->y_origin; 420 | const float sx = screen_size[0]; 421 | const float sy = screen_size[1]; 422 | 423 | float color[4]; 424 | lerp_color(color, info->selection_color, info->fg_color, .5f); 425 | float d = fminf( 426 | (tick - click_begin) / (double) HOVER_USECS, 1.f); 427 | if (!any_clicked) 428 | d = 1.f - d; 429 | float alpha = color[3] * ease(d) * .5f; 430 | 431 | PUSH_POINT_COLOR(line_ptr, ox, oy, color, alpha) 432 | PUSH_POINT_COLOR(line_ptr, sx, oy, color, alpha) 433 | PUSH_POINT_COLOR(line_ptr, ox, oy, color, alpha) 434 | PUSH_POINT_COLOR(line_ptr, ox, sy, color, alpha) 435 | 436 | line_verts += 4; 437 | } 438 | wl_list_for_each(head, &info->heads, link) { 439 | float x1 = head->x1; 440 | float y1 = head->y1; 441 | float x2 = head->x2; 442 | float y2 = head->y2; 443 | 444 | float *color = info->fg_color; 445 | float alpha = color[3] * (head->clicked ? .5f : .25f); 446 | 447 | PUSH_POINT_COLOR(line_ptr, x1, y1, color, alpha) 448 | PUSH_POINT_COLOR(line_ptr, x2, y1, color, alpha) 449 | PUSH_POINT_COLOR(line_ptr, x2, y1, color, alpha) 450 | PUSH_POINT_COLOR(line_ptr, x2, y2, color, alpha) 451 | PUSH_POINT_COLOR(line_ptr, x2, y2, color, alpha) 452 | PUSH_POINT_COLOR(line_ptr, x1, y2, color, alpha) 453 | PUSH_POINT_COLOR(line_ptr, x1, y2, color, alpha) 454 | PUSH_POINT_COLOR(line_ptr, x1, y1, color, alpha) 455 | 456 | line_verts += 8; 457 | 458 | if (any_clicked || (click_begin && tick < click_begin + HOVER_USECS)) { 459 | float d = fminf( 460 | (tick - click_begin) / (double) HOVER_USECS, 1.f); 461 | if (!any_clicked) 462 | d = 1.f - d; 463 | alpha = color[3] * ease(d) * (head->clicked ? .15f : .075f); 464 | 465 | const float sx = screen_size[0]; 466 | const float sy = screen_size[1]; 467 | 468 | PUSH_POINT_COLOR(line_ptr, 0, y1, color, alpha) 469 | PUSH_POINT_COLOR(line_ptr, x1, y1, color, alpha) 470 | PUSH_POINT_COLOR(line_ptr, x1, 0, color, alpha) 471 | PUSH_POINT_COLOR(line_ptr, x1, y1, color, alpha) 472 | 473 | PUSH_POINT_COLOR(line_ptr, sx, y1, color, alpha) 474 | PUSH_POINT_COLOR(line_ptr, x2, y1, color, alpha) 475 | PUSH_POINT_COLOR(line_ptr, x2, 0, color, alpha) 476 | PUSH_POINT_COLOR(line_ptr, x2, y1, color, alpha) 477 | 478 | PUSH_POINT_COLOR(line_ptr, sx, y2, color, alpha) 479 | PUSH_POINT_COLOR(line_ptr, x2, y2, color, alpha) 480 | PUSH_POINT_COLOR(line_ptr, x2, sy, color, alpha) 481 | PUSH_POINT_COLOR(line_ptr, x2, y2, color, alpha) 482 | 483 | PUSH_POINT_COLOR(line_ptr, 0, y2, color, alpha) 484 | PUSH_POINT_COLOR(line_ptr, x1, y2, color, alpha) 485 | PUSH_POINT_COLOR(line_ptr, x1, sy, color, alpha) 486 | PUSH_POINT_COLOR(line_ptr, x1, y2, color, alpha) 487 | 488 | line_verts += 16; 489 | } 490 | 491 | i++; 492 | if (i >= HEADS_MAX) 493 | break; 494 | } 495 | 496 | if (line_verts > 0) { 497 | glEnable(GL_BLEND); 498 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 499 | glUseProgram(res->color_program); 500 | glBindBuffer(GL_ARRAY_BUFFER, res->buffers[LINE_BUFFER]); 501 | glBufferSubData(GL_ARRAY_BUFFER, 0, 502 | line_verts * BT_LINE_VERT_SIZE * sizeof(float), res->verts); 503 | glEnableVertexAttribArray(res->color_position_attribute); 504 | glEnableVertexAttribArray(res->color_color_attribute); 505 | glVertexAttribPointer(res->color_position_attribute, 2, GL_FLOAT, GL_FALSE, 506 | BT_LINE_VERT_SIZE * sizeof(float), (void *) (0 * sizeof(float))); 507 | glVertexAttribPointer(res->color_color_attribute, 4, GL_FLOAT, GL_FALSE, 508 | BT_LINE_VERT_SIZE * sizeof(float), (void *) (2 * sizeof(float))); 509 | glUniform2fv(res->color_screen_size_uniform, 1, screen_size); 510 | glDrawArrays(GL_LINES, 0, line_verts); 511 | glDisable(GL_BLEND); 512 | } 513 | } 514 | 515 | void wd_gl_cleanup(struct wd_gl_data *res) { 516 | glDeleteBuffers(NUM_BUFFERS, res->buffers); 517 | glDeleteShader(res->texture_fragment_shader); 518 | glDeleteShader(res->texture_vertex_shader); 519 | glDeleteProgram(res->texture_program); 520 | 521 | glDeleteShader(res->color_fragment_shader); 522 | glDeleteShader(res->color_vertex_shader); 523 | glDeleteProgram(res->color_program); 524 | 525 | free(res); 526 | } 527 | -------------------------------------------------------------------------------- /src/wdisplays.h: -------------------------------------------------------------------------------- 1 | /* SPDX-FileCopyrightText: 2019 Jason Francis 2 | * SPDX-License-Identifier: GPL-3.0-or-later */ 3 | /* SPDX-FileCopyrightText: 2017-2019 Simon Ser 4 | * SPDX-License-Identifier: MIT */ 5 | 6 | /* 7 | * Parts of this file are taken from emersion/kanshi: 8 | * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/include/kanshi.h 9 | * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/include/config.h 10 | */ 11 | 12 | #ifndef WDISPLAY_WDISPLAY_H 13 | #define WDISPLAY_WDISPLAY_H 14 | 15 | #include "config.h" 16 | 17 | #define HEADS_MAX 64 18 | #define HOVER_USECS (100 * 1000) 19 | 20 | #include 21 | #include 22 | 23 | #include "headform.h" 24 | 25 | struct zxdg_output_v1; 26 | struct zxdg_output_manager_v1; 27 | struct zwlr_output_mode_v1; 28 | struct zwlr_output_head_v1; 29 | struct zwlr_output_manager_v1; 30 | struct zwlr_screencopy_manager_v1; 31 | struct zwlr_screencopy_frame_v1; 32 | struct zwlr_layer_shell_v1; 33 | struct zwlr_layer_surface_v1; 34 | 35 | struct _GtkWidget; 36 | typedef struct _GtkWidget GtkWidget; 37 | struct _GtkBuilder; 38 | typedef struct _GtkBuilder GtkBuilder; 39 | struct _GdkCursor; 40 | typedef struct _GdkCursor GdkCursor; 41 | struct _cairo_surface; 42 | typedef struct _cairo_surface cairo_surface_t; 43 | 44 | struct wd_output { 45 | struct wd_state *state; 46 | struct zxdg_output_v1 *xdg_output; 47 | struct wl_output *wl_output; 48 | struct wl_list link; 49 | 50 | char *name; 51 | struct wl_list frames; 52 | GtkWidget *overlay_window; 53 | struct zwlr_layer_surface_v1 *overlay_layer_surface; 54 | }; 55 | 56 | struct wd_frame { 57 | struct wd_output *output; 58 | struct zwlr_screencopy_frame_v1 *wlr_frame; 59 | 60 | struct wl_list link; 61 | int capture_fd; 62 | unsigned stride; 63 | unsigned width; 64 | unsigned height; 65 | struct wl_shm_pool *pool; 66 | struct wl_buffer *buffer; 67 | uint8_t *pixels; 68 | uint64_t tick; 69 | bool y_invert; 70 | bool swap_rgb; 71 | }; 72 | 73 | struct wd_head_config { 74 | struct wl_list link; 75 | 76 | struct wd_head *head; 77 | bool enabled; 78 | int32_t width; 79 | int32_t height; 80 | int32_t refresh; // mHz 81 | int32_t x; 82 | int32_t y; 83 | double scale; 84 | enum wl_output_transform transform; 85 | }; 86 | 87 | struct wd_mode { 88 | struct wd_head *head; 89 | struct zwlr_output_mode_v1 *wlr_mode; 90 | struct wl_list link; 91 | 92 | int32_t width, height; 93 | int32_t refresh; // mHz 94 | bool preferred; 95 | }; 96 | 97 | struct wd_head { 98 | struct wd_state *state; 99 | struct zwlr_output_head_v1 *wlr_head; 100 | struct wl_list link; 101 | 102 | struct wd_output *output; 103 | struct wd_render_head_data *render; 104 | cairo_surface_t *surface; 105 | 106 | uint32_t id; 107 | char *name, *description; 108 | int32_t phys_width, phys_height; // mm 109 | struct wl_list modes; 110 | 111 | bool enabled; 112 | struct wd_mode *mode; 113 | struct { 114 | int32_t width, height; 115 | int32_t refresh; 116 | } custom_mode; 117 | int32_t x, y; 118 | enum wl_output_transform transform; 119 | double scale; 120 | }; 121 | 122 | struct wd_gl_data; 123 | 124 | struct wd_render_head_flags { 125 | uint8_t rotation; 126 | bool x_invert; 127 | }; 128 | 129 | struct wd_render_head_data { 130 | struct wl_list link; 131 | uint64_t updated_at; 132 | uint64_t hover_begin; 133 | uint64_t click_begin; 134 | 135 | float x1; 136 | float y1; 137 | float x2; 138 | float y2; 139 | 140 | struct wd_render_head_flags queued; 141 | struct wd_render_head_flags active; 142 | 143 | uint8_t *pixels; 144 | unsigned tex_stride; 145 | unsigned tex_width; 146 | unsigned tex_height; 147 | 148 | bool preview; 149 | bool y_invert; 150 | bool swap_rgb; 151 | bool hovered; 152 | bool clicked; 153 | }; 154 | 155 | struct wd_render_data { 156 | float fg_color[4]; 157 | float bg_color[4]; 158 | float border_color[4]; 159 | float selection_color[4]; 160 | unsigned int viewport_width; 161 | unsigned int viewport_height; 162 | unsigned int width; 163 | unsigned int height; 164 | int scroll_x; 165 | int scroll_y; 166 | int x_origin; 167 | int y_origin; 168 | uint64_t updated_at; 169 | 170 | struct wl_list heads; 171 | }; 172 | 173 | struct wd_point { 174 | double x; 175 | double y; 176 | }; 177 | 178 | struct wd_state { 179 | struct zxdg_output_manager_v1 *xdg_output_manager; 180 | struct zwlr_output_manager_v1 *output_manager; 181 | struct zwlr_screencopy_manager_v1 *copy_manager; 182 | struct zwlr_layer_shell_v1 *layer_shell; 183 | struct wl_shm *shm; 184 | struct wl_list heads; 185 | struct wl_list outputs; 186 | uint32_t serial; 187 | 188 | bool apply_pending; 189 | bool autoapply; 190 | bool capture; 191 | bool show_overlay; 192 | double zoom; 193 | 194 | unsigned int apply_idle; 195 | unsigned int reset_idle; 196 | 197 | struct wd_render_head_data *clicked; 198 | struct wd_point drag_start; 199 | struct wd_point head_drag_start; /* 0-1 range in head rect */ 200 | bool panning; 201 | struct wd_point pan_start; 202 | 203 | GtkWidget *main_box; 204 | GtkWidget *header_stack; 205 | GtkWidget *stack_switcher; 206 | GtkWidget *stack; 207 | GtkWidget *scroller; 208 | GtkWidget *canvas; 209 | GtkWidget *spinner; 210 | GtkWidget *zoom_out; 211 | GtkWidget *zoom_reset; 212 | GtkWidget *zoom_in; 213 | GtkWidget *overlay; 214 | GtkWidget *info_bar; 215 | GtkWidget *info_label; 216 | GtkWidget *menu_button; 217 | 218 | GdkCursor *grab_cursor; 219 | GdkCursor *grabbing_cursor; 220 | GdkCursor *move_cursor; 221 | 222 | unsigned int canvas_tick; 223 | struct wd_gl_data *gl_data; 224 | struct wd_render_data render; 225 | }; 226 | 227 | 228 | /* 229 | * Creates the application state structure. 230 | */ 231 | struct wd_state *wd_state_create(void); 232 | 233 | /* 234 | * Frees the application state structure. 235 | */ 236 | void wd_state_destroy(struct wd_state *state); 237 | 238 | /* 239 | * Displays an error message and then exits the program. 240 | */ 241 | void wd_fatal_error(int status, const char *message); 242 | 243 | /* 244 | * Add an output to the list of screen captured outputs. 245 | */ 246 | void wd_add_output(struct wd_state *state, struct wl_output *wl_output, struct wl_display *display); 247 | 248 | /* 249 | * Remove an output from the list of screen captured outputs. 250 | */ 251 | void wd_remove_output(struct wd_state *state, struct wl_output *wl_output, struct wl_display *display); 252 | 253 | /* 254 | * Finds the output associated with a given head. Can return NULL if the head's 255 | * output is disabled. 256 | */ 257 | struct wd_output *wd_find_output(struct wd_state *state, struct wd_head *head); 258 | 259 | /* 260 | * Finds the head associated with a given output. 261 | */ 262 | struct wd_head *wd_find_head(struct wd_state *state, struct wd_output *output); 263 | /* 264 | * Starts listening for output management events from the compositor. 265 | */ 266 | void wd_add_output_management_listener(struct wd_state *state, struct wl_display *display); 267 | 268 | /* 269 | * Sends updated display configuration back to the compositor. 270 | */ 271 | void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs, struct wl_display *display); 272 | 273 | /* 274 | * Queues capture of the next frame of all screens. 275 | */ 276 | void wd_capture_frame(struct wd_state *state); 277 | 278 | /* 279 | * Blocks until all captures are finished. 280 | */ 281 | void wd_capture_wait(struct wd_state *state, struct wl_display *display); 282 | 283 | /* 284 | * Updates the UI stack of all heads. Does not update individual head forms. 285 | * Useful for when a display is plugged/unplugged and we want to add/remove 286 | * a page, but we don't want to wipe out user's changes on the other pages. 287 | */ 288 | void wd_ui_reset_heads(struct wd_state *state); 289 | 290 | /* 291 | * Updates the UI form for a single head. Useful for when the compositor 292 | * notifies us of updated configuration caused by another program. 293 | */ 294 | void wd_ui_reset_head(const struct wd_head *head, enum wd_head_fields fields); 295 | 296 | /* 297 | * Updates the stack and all forms to the last known server state. 298 | */ 299 | void wd_ui_reset_all(struct wd_state *state); 300 | 301 | /* 302 | * Reactivates the GUI after the display configuration updates. 303 | */ 304 | void wd_ui_apply_done(struct wd_state *state, struct wl_list *outputs); 305 | 306 | /* 307 | * Reactivates the GUI after the display configuration updates. 308 | */ 309 | void wd_ui_show_error(struct wd_state *state, const char *message); 310 | 311 | /* 312 | * Compiles the GL shaders. 313 | */ 314 | struct wd_gl_data *wd_gl_setup(void); 315 | 316 | /* 317 | * Renders the GL scene. 318 | */ 319 | void wd_gl_render(struct wd_gl_data *res, struct wd_render_data *info, uint64_t tick); 320 | 321 | /* 322 | * Destroys the GL shaders. 323 | */ 324 | void wd_gl_cleanup(struct wd_gl_data *res); 325 | 326 | /* 327 | * Create an overlay on the screen that contains a textual description of the 328 | * output. This is to help the user identify the outputs visually. 329 | */ 330 | void wd_create_overlay(struct wd_output *output); 331 | 332 | /* 333 | * Forces redrawing of the screen overlay on the given output. 334 | */ 335 | void wd_redraw_overlay(struct wd_output *output); 336 | 337 | /* 338 | * Destroys the screen overlay on the given output. 339 | */ 340 | void wd_destroy_overlay(struct wd_output *output); 341 | 342 | #endif 343 | -------------------------------------------------------------------------------- /wdisplays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luispabon/wdisplays/7f2eac0d2aa81b5f495da7950fd5a94683f7868e/wdisplays.png --------------------------------------------------------------------------------