├── .gitignore ├── LICENSE ├── 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 ├── style.css └── wdisplays.ui ├── src ├── glviewport.c ├── glviewport.h ├── main.c ├── meson.build ├── outputs.c ├── overlay.c ├── render.c └── wdisplays.h └── wdisplays.png /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019 cyclopsian 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wdisplays 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://spdx.org/licenses/MIT.html) 4 | 5 | wdisplays is a graphical application for configuring displays in Wayland 6 | compositors. It borrows some code from [kanshi]. It should work in any 7 | compositor that implements the wlr-output-management-unstable-v1 protocol, 8 | including [sway]. The goal of this project is to allow precise adjustment of 9 | display settings in kiosks, digital signage, and other elaborate multi-monitor 10 | setups. 11 | 12 | ![Screenshot](wdisplays.png) 13 | 14 | # Building 15 | 16 | Build requirements are: 17 | 18 | - meson 19 | - GTK+3 20 | - epoxy 21 | - wayland-client 22 | 23 | ```sh 24 | meson build 25 | ninja -C build 26 | sudo ninja -C build install 27 | ``` 28 | 29 | Binaries are not available. Only building from source is supported, and only 30 | if you're using wlroots compiled from master. 31 | 32 | # Usage 33 | 34 | Displays can be moved around the virtual screen space by clicking and dragging 35 | them in the preview on the left panel. By default, they will snap to one 36 | another. Hold Shift while dragging to disable snapping. You can click and drag 37 | with the middle mouse button to pan. Zoom in and out either with the buttons on 38 | the top left, or by holding Ctrl and scrolling the mouse wheel. Fine tune your 39 | adjustments in the right panel, then click apply. 40 | 41 | There are some options available by clicking the menu button on the top left: 42 | 43 | - Automatically Apply Changes: Makes it so you don't have to hit apply. Disable 44 | this for making minor adjustments, but be careful, you may end up with an 45 | unusable setup. 46 | - Show Screen Contents: Shows a live preview of the screens in the left panel. 47 | Turn off to reduce energy usage. 48 | - Overlay Screen Names: Shows big names in the corner of all screens for easy 49 | identification. Disable if they get in the way. 50 | 51 | # FAQ (Fervently Anticpiated Quandaries) 52 | 53 | ### What is this? 54 | 55 | It's intended to be the Wayland equivalent of an xrandr GUI, like [ARandR]. 56 | 57 | ### Help, I get errors and/or crashes! 58 | 59 | Make sure your wlroots and sway are up-to-date. Particularly, you need a git 60 | revision of wlroots from [this commit](https://github.com/swaywm/wlroots/commit/724b5e1b8d742a8429f4431ae1a55d7d26cb92ae) 61 | (or later) or your compositor may crash when adding/removing displays. 62 | Alternatively, you can try to disable the "Show Screen Contents" option. 63 | 64 | ### I'm using Sway, why aren't my display settings saved when I log out? 65 | 66 | Sway, like i3, doesn't save any settings unless you put them in the config 67 | file. See man `sway-output`. If you want to have multiple configurations 68 | depending on the monitors connected, you'll need to use an external program 69 | like [kanshi]. 70 | 71 | [kanshi]: https://github.com/emersion/kanshi 72 | [sway]: https://github.com/swaywm/sway 73 | [ARandR]: https://christian.amsuess.com/tools/arandr/ 74 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('wdisplays', 'c') 2 | 3 | subdir('protocol') 4 | subdir('resources') 5 | subdir('src') 6 | -------------------------------------------------------------------------------- /protocol/meson.build: -------------------------------------------------------------------------------- 1 | wayland_scanner = find_program('wayland-scanner') 2 | wayland_client = dependency('wayland-client') 3 | wayland_protos = dependency('wayland-protocols', version: '>=1.17') 4 | 5 | wl_protocol_dir = wayland_protos.get_pkgconfig_variable('pkgdatadir') 6 | 7 | wayland_scanner_code = generator( 8 | wayland_scanner, 9 | output: '@BASENAME@-protocol.c', 10 | arguments: ['private-code', '@INPUT@', '@OUTPUT@'], 11 | ) 12 | 13 | wayland_scanner_client = generator( 14 | wayland_scanner, 15 | output: '@BASENAME@-client-protocol.h', 16 | arguments: ['client-header', '@INPUT@', '@OUTPUT@'], 17 | ) 18 | 19 | client_protocols = [ 20 | [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'], 21 | [wl_protocol_dir, 'stable/xdg-shell/xdg-shell.xml'], 22 | ['wlr-output-management-unstable-v1.xml'], 23 | ['wlr-screencopy-unstable-v1.xml'], 24 | ['wlr-layer-shell-unstable-v1.xml'] 25 | ] 26 | 27 | client_protos_src = [] 28 | client_protos_headers = [] 29 | 30 | foreach p : client_protocols 31 | xml = join_paths(p) 32 | client_protos_src += wayland_scanner_code.process(xml) 33 | client_protos_headers += wayland_scanner_client.process(xml) 34 | endforeach 35 | 36 | lib_client_protos = static_library( 37 | 'client_protos', 38 | client_protos_src + client_protos_headers, 39 | dependencies: [wayland_client] 40 | ) 41 | 42 | client_protos = declare_dependency( 43 | link_with: lib_client_protos, 44 | sources: client_protos_headers, 45 | ) 46 | -------------------------------------------------------------------------------- /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 | False 12 | 13 | 14 | True 15 | False 16 | 10 17 | 10 18 | 10 19 | 10 20 | vertical 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 16383 29 | 1 30 | 10 31 | 32 | 33 | 16383 34 | 1 35 | 10 36 | 37 | 38 | 2147483.647 39 | 1 40 | 10 41 | 42 | 43 | 0.01 44 | 99999 45 | 0.1 46 | 0.5 47 | 48 | 49 | 16383 50 | 1 51 | 10 52 | 53 | 54 | True 55 | False 56 | 8 57 | 8 58 | 8 59 | 8 60 | 8 61 | 16 62 | True 63 | 64 | 65 | _Enabled 66 | True 67 | True 68 | False 69 | start 70 | True 71 | True 72 | 73 | 74 | 75 | 1 76 | 0 77 | 78 | 79 | 80 | 81 | True 82 | False 83 | True 84 | word-char 85 | end 86 | 0 87 | 88 | 89 | 1 90 | 1 91 | 92 | 93 | 94 | 95 | True 96 | True 97 | start 98 | 9 99 | scale_adjustment 100 | 2 101 | 1 102 | 103 | 104 | 105 | 1 106 | 3 107 | 108 | 109 | 110 | 111 | True 112 | False 113 | DPI _Scale 114 | True 115 | scale 116 | 1 117 | 118 | 119 | 0 120 | 3 121 | 122 | 123 | 124 | 125 | True 126 | False 127 | _Position 128 | True 129 | pos_x 130 | 1 131 | 132 | 133 | 0 134 | 4 135 | 136 | 137 | 138 | 139 | True 140 | False 141 | Description 142 | 1 143 | 144 | 145 | 0 146 | 1 147 | 148 | 149 | 150 | 151 | True 152 | False 153 | True 154 | word-char 155 | end 156 | 0 157 | 158 | 159 | 1 160 | 2 161 | 162 | 163 | 164 | 165 | True 166 | False 167 | Physical Size 168 | 1 169 | 170 | 171 | 0 172 | 2 173 | 174 | 175 | 176 | 177 | True 178 | False 179 | Si_ze 180 | True 181 | width 182 | 1 183 | 184 | 185 | 0 186 | 5 187 | 188 | 189 | 190 | 191 | True 192 | False 193 | start 194 | 8 195 | 196 | 197 | True 198 | True 199 | 9 200 | number 201 | refresh_adjustment 202 | 3 203 | True 204 | if-valid 205 | 206 | 207 | False 208 | True 209 | 0 210 | 211 | 212 | 213 | 214 | True 215 | False 216 | Hz 217 | 218 | 219 | False 220 | True 221 | 1 222 | 223 | 224 | 225 | 226 | 1 227 | 6 228 | 229 | 230 | 231 | 232 | True 233 | False 234 | _Refresh Rate 235 | True 236 | 1 237 | 238 | 239 | 0 240 | 6 241 | 242 | 243 | 244 | 245 | True 246 | True 247 | True 248 | transforms 249 | 250 | 251 | 252 | 253 | 254 | 1 255 | 7 256 | 257 | 258 | 259 | 260 | True 261 | False 262 | _Transform 263 | True 264 | 1 265 | 266 | 267 | 0 268 | 7 269 | 270 | 271 | 272 | 273 | _Flipped 274 | True 275 | True 276 | False 277 | start 278 | True 279 | True 280 | 281 | 282 | 283 | 1 284 | 8 285 | 286 | 287 | 288 | 289 | True 290 | False 291 | 8 292 | 293 | 294 | True 295 | True 296 | 6 297 | 0 298 | number 299 | pos_x_adjustment 300 | True 301 | if-valid 302 | 303 | 304 | 0 305 | 0 306 | 307 | 308 | 309 | 310 | True 311 | True 312 | 6 313 | 0 314 | number 315 | pos_y_adjustment 316 | True 317 | if-valid 318 | 319 | 320 | 2 321 | 0 322 | 323 | 324 | 325 | 326 | True 327 | True 328 | 4 329 | 0 330 | number 331 | width_adjustment 332 | True 333 | if-valid 334 | 335 | 336 | 0 337 | 1 338 | 339 | 340 | 341 | 342 | 20 343 | True 344 | False 345 | × 346 | 347 | 348 | 1 349 | 1 350 | 351 | 352 | 353 | 354 | True 355 | True 356 | 4 357 | 0 358 | number 359 | height_adjustment 360 | True 361 | if-valid 362 | 363 | 364 | 2 365 | 1 366 | 367 | 368 | 369 | 370 | True 371 | True 372 | True 373 | Select Mode Preset 374 | 8 375 | 8 376 | modes 377 | 378 | 379 | True 380 | False 381 | view-more-symbolic 382 | 383 | 384 | 385 | 386 | 3 387 | 1 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 1 399 | 4 400 | 2 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | False 412 | rotate_button 413 | 414 | 415 | True 416 | False 417 | 10 418 | 10 419 | 10 420 | 10 421 | vertical 422 | 423 | 424 | True 425 | True 426 | True 427 | transform.rotate_0 428 | Don't Rotate 429 | 430 | 431 | False 432 | True 433 | 0 434 | 435 | 436 | 437 | 438 | True 439 | True 440 | True 441 | transform.rotate_90 442 | Rotate 90° 443 | 444 | 445 | False 446 | True 447 | 1 448 | 449 | 450 | 451 | 452 | True 453 | True 454 | True 455 | transform.rotate_180 456 | Rotate 180° 457 | 458 | 459 | False 460 | True 461 | 2 462 | 463 | 464 | 465 | 466 | True 467 | True 468 | True 469 | transform.rotate_270 470 | Rotate 270° 471 | 472 | 473 | False 474 | True 475 | 3 476 | 477 | 478 | 479 | 480 | 481 | 482 | -------------------------------------------------------------------------------- /resources/meson.build: -------------------------------------------------------------------------------- 1 | 2 | gnome = import('gnome') 3 | resources = gnome.compile_resources( 4 | 'waydisplay-resources', 'resources.xml', 5 | source_dir : '.', 6 | c_name : 'waydisplay_resources') 7 | 8 | -------------------------------------------------------------------------------- /resources/resources.xml: -------------------------------------------------------------------------------- 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.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 1 8 | 10 9 | 10 | 11 | 1 12 | 10 13 | 14 | 15 | False 16 | 17 | 18 | True 19 | False 20 | 10 21 | 10 22 | 10 23 | 10 24 | vertical 25 | 26 | 27 | True 28 | True 29 | True 30 | app.auto-apply 31 | _Automatically Apply Changes 32 | 33 | 34 | False 35 | True 36 | 0 37 | 38 | 39 | 40 | 41 | True 42 | True 43 | True 44 | app.capture-screens 45 | Show Screen Contents 46 | 47 | 48 | False 49 | True 50 | 1 51 | 52 | 53 | 54 | 55 | True 56 | True 57 | True 58 | app.show-overlay 59 | Overlay Screen Names 60 | 61 | 62 | False 63 | True 64 | 2 65 | 66 | 67 | 68 | 69 | 70 | 71 | False 72 | Waydisplay 73 | 74 | 75 | 76 | True 77 | False 78 | 79 | 80 | True 81 | False 82 | vertical 83 | 84 | 85 | False 86 | True 87 | start 88 | error 89 | True 90 | False 91 | 92 | 93 | 94 | False 95 | 6 96 | end 97 | 98 | 99 | 100 | 101 | 102 | False 103 | False 104 | 0 105 | 106 | 107 | 108 | 109 | False 110 | 16 111 | 112 | 113 | True 114 | False 115 | True 116 | 0 117 | 118 | 119 | True 120 | True 121 | 2 122 | 123 | 124 | 125 | 126 | True 127 | True 128 | 0 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | False 137 | True 138 | 0 139 | 140 | 141 | 142 | 143 | True 144 | True 145 | 400 146 | True 147 | 148 | 149 | True 150 | True 151 | canvas_horiz 152 | canvas_vert 153 | 400 154 | 300 155 | 156 | 157 | 158 | 159 | 160 | True 161 | False 162 | 163 | 164 | 165 | 166 | True 167 | False 168 | vertical 169 | 170 | 171 | True 172 | False 173 | center 174 | 8 175 | 8 176 | 8 177 | 8 178 | 8 179 | True 180 | heads_stack 181 | 182 | 183 | False 184 | True 185 | 0 186 | 187 | 188 | 189 | 190 | True 191 | False 192 | crossfade 193 | 194 | 195 | 196 | 197 | 198 | False 199 | True 200 | 1 201 | 202 | 203 | 204 | 205 | False 206 | False 207 | 208 | 209 | 210 | 211 | True 212 | True 213 | 1 214 | 215 | 216 | 217 | 218 | -1 219 | 220 | 221 | 222 | 223 | True 224 | False 225 | True 226 | True 227 | True 228 | 229 | 230 | True 231 | 232 | 233 | 234 | 235 | 236 | 237 | True 238 | False 239 | crossfade 240 | 241 | 242 | True 243 | False 244 | wdisplays 245 | False 246 | True 247 | 248 | 249 | True 250 | False 251 | expand 252 | 253 | 254 | True 255 | True 256 | True 257 | Zoom Out 258 | 259 | 260 | 261 | True 262 | False 263 | zoom-out-symbolic 264 | 265 | 266 | 267 | 268 | 269 | True 270 | True 271 | 0 272 | True 273 | 274 | 275 | 276 | 277 | True 278 | True 279 | True 280 | Zoom Reset 281 | 282 | 283 | 284 | 285 | True 286 | True 287 | 1 288 | True 289 | 290 | 291 | 292 | 293 | True 294 | True 295 | True 296 | Zoom In 297 | 298 | 299 | 300 | True 301 | False 302 | zoom-in-symbolic 303 | 304 | 305 | 306 | 307 | 308 | True 309 | True 310 | 2 311 | True 312 | 313 | 314 | 315 | 316 | 317 | 318 | True 319 | True 320 | True 321 | main_menu 322 | 323 | 324 | True 325 | False 326 | open-menu-symbolic 327 | 328 | 329 | 330 | 331 | end 332 | 1 333 | 334 | 335 | 336 | 337 | title 338 | 339 | 340 | 341 | 342 | True 343 | False 344 | 345 | 346 | True 347 | False 348 | Apply Changes? 349 | 350 | 351 | 352 | 353 | _Apply 354 | True 355 | True 356 | True 357 | True 358 | 359 | 362 | 363 | 364 | end 365 | 366 | 367 | 368 | 369 | _Cancel 370 | True 371 | True 372 | True 373 | True 374 | 375 | 376 | 377 | 1 378 | 379 | 380 | 381 | 382 | apply 383 | 1 384 | 385 | 386 | 387 | 388 | 389 | 390 | -------------------------------------------------------------------------------- /src/glviewport.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 cyclopsian 3 | 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY 19 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | 24 | #include "glviewport.h" 25 | 26 | typedef struct _WdGLViewportPrivate { 27 | GtkAdjustment *hadjustment; 28 | GtkAdjustment *vadjustment; 29 | guint hscroll_policy : 1; 30 | guint vscroll_policy : 1; 31 | } WdGLViewportPrivate; 32 | 33 | enum { 34 | PROP_0, 35 | PROP_HADJUSTMENT, 36 | PROP_VADJUSTMENT, 37 | PROP_HSCROLL_POLICY, 38 | PROP_VSCROLL_POLICY 39 | }; 40 | 41 | static void wd_gl_viewport_set_property( 42 | GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); 43 | static void wd_gl_viewport_get_property( 44 | GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); 45 | 46 | G_DEFINE_TYPE_WITH_CODE(WdGLViewport, wd_gl_viewport, GTK_TYPE_GL_AREA, 47 | G_ADD_PRIVATE(WdGLViewport) 48 | G_IMPLEMENT_INTERFACE(GTK_TYPE_SCROLLABLE, NULL)) 49 | 50 | static void wd_gl_viewport_class_init(WdGLViewportClass *class) { 51 | GObjectClass *gobject_class = G_OBJECT_CLASS(class); 52 | 53 | gobject_class->set_property = wd_gl_viewport_set_property; 54 | gobject_class->get_property = wd_gl_viewport_get_property; 55 | 56 | g_object_class_override_property(gobject_class, PROP_HADJUSTMENT, "hadjustment"); 57 | g_object_class_override_property(gobject_class, PROP_VADJUSTMENT, "vadjustment"); 58 | g_object_class_override_property(gobject_class, PROP_HSCROLL_POLICY, "hscroll-policy"); 59 | g_object_class_override_property(gobject_class, PROP_VSCROLL_POLICY, "vscroll-policy"); 60 | } 61 | 62 | static void viewport_set_adjustment(GtkAdjustment *adjustment, 63 | GtkAdjustment **store) { 64 | if (!adjustment) { 65 | adjustment = gtk_adjustment_new(0., 0., 0., 0., 0., 0.); 66 | } 67 | if (adjustment != *store) { 68 | if (*store != NULL) { 69 | g_object_unref(*store); 70 | } 71 | *store = adjustment; 72 | g_object_ref_sink(adjustment); 73 | } 74 | } 75 | 76 | static void wd_gl_viewport_set_property( 77 | GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { 78 | WdGLViewport *viewport = WD_GL_VIEWPORT(object); 79 | WdGLViewportPrivate *priv = wd_gl_viewport_get_instance_private(viewport); 80 | 81 | switch (prop_id) { 82 | case PROP_HADJUSTMENT: 83 | viewport_set_adjustment(g_value_get_object(value), &priv->hadjustment); 84 | break; 85 | case PROP_VADJUSTMENT: 86 | viewport_set_adjustment(g_value_get_object(value), &priv->vadjustment); 87 | break; 88 | case PROP_HSCROLL_POLICY: 89 | if (priv->hscroll_policy != g_value_get_enum(value)) { 90 | priv->hscroll_policy = g_value_get_enum(value); 91 | g_object_notify_by_pspec(object, pspec); 92 | } 93 | break; 94 | case PROP_VSCROLL_POLICY: 95 | if (priv->vscroll_policy != g_value_get_enum(value)) { 96 | priv->vscroll_policy = g_value_get_enum(value); 97 | g_object_notify_by_pspec (object, pspec); 98 | } 99 | break; 100 | default: 101 | G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); 102 | break; 103 | } 104 | } 105 | 106 | static void wd_gl_viewport_get_property( 107 | GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { 108 | WdGLViewport *viewport = WD_GL_VIEWPORT(object); 109 | WdGLViewportPrivate *priv = wd_gl_viewport_get_instance_private(viewport); 110 | 111 | switch (prop_id) { 112 | case PROP_HADJUSTMENT: 113 | g_value_set_object(value, priv->hadjustment); 114 | break; 115 | case PROP_VADJUSTMENT: 116 | g_value_set_object(value, priv->vadjustment); 117 | break; 118 | case PROP_HSCROLL_POLICY: 119 | g_value_set_enum(value, priv->hscroll_policy); 120 | break; 121 | case PROP_VSCROLL_POLICY: 122 | g_value_set_enum(value, priv->vscroll_policy); 123 | break; 124 | default: 125 | G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); 126 | break; 127 | } 128 | } 129 | 130 | static void wd_gl_viewport_init(WdGLViewport *viewport) { 131 | } 132 | 133 | GtkWidget *wd_gl_viewport_new(void) { 134 | return gtk_widget_new(WD_TYPE_GL_VIEWPORT, NULL); 135 | } 136 | -------------------------------------------------------------------------------- /src/glviewport.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 cyclopsian 3 | 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY 19 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | 24 | #ifndef WDISPLAY_GLVIEWPORT_H 25 | #define WDISPLAY_GLVIEWPORT_H 26 | 27 | #include 28 | 29 | G_BEGIN_DECLS 30 | 31 | #define WD_TYPE_GL_VIEWPORT (wd_gl_viewport_get_type()) 32 | G_DECLARE_DERIVABLE_TYPE( 33 | WdGLViewport, wd_gl_viewport, WD, GL_VIEWPORT,GtkGLArea) 34 | 35 | struct _WdGLViewportClass { 36 | GtkGLAreaClass parent_class; 37 | }; 38 | 39 | GtkWidget *wd_gl_viewport_new(void); 40 | 41 | G_END_DECLS 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 cyclopsian 3 | 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY 19 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | 24 | #include 25 | #include 26 | 27 | #include "wdisplays.h" 28 | #include "glviewport.h" 29 | 30 | __attribute__((noreturn)) void wd_fatal_error(int status, const char *message) { 31 | GtkWindow *parent = gtk_application_get_active_window(GTK_APPLICATION(g_application_get_default())); 32 | GtkWidget *dialog = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", message); 33 | gtk_dialog_run(GTK_DIALOG(dialog)); 34 | gtk_widget_destroy(dialog); 35 | exit(status); 36 | } 37 | 38 | #define DEFAULT_ZOOM 0.1 39 | #define MIN_ZOOM (1./1000.) 40 | #define MAX_ZOOM 1000. 41 | #define CANVAS_MARGIN 40 42 | 43 | static const char *MODE_PREFIX = "mode"; 44 | static const char *TRANSFORM_PREFIX = "transform"; 45 | static const char *APP_PREFIX = "app"; 46 | 47 | #define NUM_ROTATIONS 4 48 | static const char *ROTATE_IDS[NUM_ROTATIONS] = { 49 | "rotate_0", "rotate_90", "rotate_180", "rotate_270" 50 | }; 51 | 52 | static int get_rotate_index(enum wl_output_transform transform) { 53 | if (transform == WL_OUTPUT_TRANSFORM_90 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_90) { 54 | return 1; 55 | } else if (transform == WL_OUTPUT_TRANSFORM_180 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_180) { 56 | return 2; 57 | } else if (transform == WL_OUTPUT_TRANSFORM_270 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_270) { 58 | return 3; 59 | } 60 | return 0; 61 | } 62 | 63 | static bool has_changes(const struct wd_state *state) { 64 | g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); 65 | for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { 66 | GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder")); 67 | const struct wd_head *head = g_object_get_data(G_OBJECT(form_iter->data), "head"); 68 | if (head->enabled != gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "enabled")))) { 69 | return TRUE; 70 | } 71 | double old_scale = round(head->scale * 100.) / 100.; 72 | double new_scale = round(gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale"))) * 100.) / 100.; 73 | if (old_scale != new_scale) { 74 | return TRUE; 75 | } 76 | if (head->x != gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_x")))) { 77 | return TRUE; 78 | } 79 | if (head->y != gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_y")))) { 80 | return TRUE; 81 | } 82 | int w = head->mode != NULL ? head->mode->width : head->custom_mode.width; 83 | if (w != gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "width")))) { 84 | return TRUE; 85 | } 86 | int h = head->mode != NULL ? head->mode->height : head->custom_mode.height; 87 | if (h != gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "height")))) { 88 | return TRUE; 89 | } 90 | int r = head->mode != NULL ? head->mode->refresh : head->custom_mode.refresh; 91 | if (r / 1000. != gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "refresh")))) { 92 | return TRUE; 93 | } 94 | for (int i = 0; i < NUM_ROTATIONS; i++) { 95 | GtkWidget *rotate = GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[i])); 96 | gboolean selected; 97 | g_object_get(rotate, "active", &selected, NULL); 98 | if (selected) { 99 | if (i != get_rotate_index(head->transform)) { 100 | return TRUE; 101 | } 102 | break; 103 | } 104 | } 105 | bool flipped = head->transform == WL_OUTPUT_TRANSFORM_FLIPPED 106 | || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_90 107 | || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_180 108 | || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_270; 109 | if (flipped != gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "flipped")))) { 110 | return TRUE; 111 | } 112 | } 113 | return FALSE; 114 | } 115 | 116 | void fill_output_from_form(struct wd_head_config *output, GtkWidget *form) { 117 | GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); 118 | output->head = g_object_get_data(G_OBJECT(form), "head"); 119 | output->enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "enabled"))); 120 | output->scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale"))); 121 | output->x = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_x"))); 122 | output->y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_y"))); 123 | output->width = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "width"))); 124 | output->height = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "height"))); 125 | output->refresh = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "refresh"))) * 1000.; 126 | gboolean flipped = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "flipped"))); 127 | for (int i = 0; i < NUM_ROTATIONS; i++) { 128 | GtkWidget *rotate = GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[i])); 129 | gboolean selected; 130 | g_object_get(rotate, "active", &selected, NULL); 131 | if (selected) { 132 | switch (i) { 133 | case 0: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED : WL_OUTPUT_TRANSFORM_NORMAL; break; 134 | case 1: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_90 : WL_OUTPUT_TRANSFORM_90; break; 135 | case 2: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_180 : WL_OUTPUT_TRANSFORM_180; break; 136 | case 3: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_270 : WL_OUTPUT_TRANSFORM_270; break; 137 | } 138 | break; 139 | } 140 | } 141 | } 142 | 143 | static gboolean send_apply(gpointer data) { 144 | struct wd_state *state = data; 145 | struct wl_list *outputs = calloc(1, sizeof(*outputs)); 146 | wl_list_init(outputs); 147 | g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); 148 | for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { 149 | struct wd_head_config *output = calloc(1, sizeof(*output)); 150 | wl_list_insert(outputs, &output->link); 151 | fill_output_from_form(output, GTK_WIDGET(form_iter->data)); 152 | } 153 | GdkWindow *window = gtk_widget_get_window(state->stack); 154 | GdkDisplay *display = gdk_window_get_display(window); 155 | struct wl_display *wl_display = gdk_wayland_display_get_wl_display(display); 156 | wd_apply_state(state, outputs, wl_display); 157 | state->apply_pending = FALSE; 158 | return FALSE; 159 | } 160 | 161 | static void apply_state(struct wd_state *state) { 162 | gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), "title"); 163 | if (!state->autoapply) { 164 | gtk_style_context_add_class(gtk_widget_get_style_context(state->spinner), "visible"); 165 | gtk_overlay_set_overlay_pass_through(GTK_OVERLAY(state->overlay), state->spinner, FALSE); 166 | gtk_spinner_start(GTK_SPINNER(state->spinner)); 167 | 168 | gtk_widget_set_sensitive(state->stack_switcher, FALSE); 169 | gtk_widget_set_sensitive(state->stack, FALSE); 170 | gtk_widget_set_sensitive(state->zoom_in, FALSE); 171 | gtk_widget_set_sensitive(state->zoom_reset, FALSE); 172 | gtk_widget_set_sensitive(state->zoom_out, FALSE); 173 | gtk_widget_set_sensitive(state->menu_button, FALSE); 174 | } 175 | 176 | /* queue this once per iteration in order to prevent duplicate updates */ 177 | if (!state->apply_pending) { 178 | state->apply_pending = TRUE; 179 | g_idle_add(send_apply, state); 180 | } 181 | } 182 | 183 | static gboolean apply_done_reset(gpointer data) { 184 | wd_ui_reset_all(data); 185 | return FALSE; 186 | } 187 | 188 | static void update_scroll_size(struct wd_state *state) { 189 | state->render.viewport_width = gtk_widget_get_allocated_width(state->canvas); 190 | state->render.viewport_height = gtk_widget_get_allocated_height(state->canvas); 191 | 192 | GtkAdjustment *scroll_x_adj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller)); 193 | GtkAdjustment *scroll_y_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller)); 194 | int scroll_x_upper = state->render.width; 195 | int scroll_y_upper = state->render.height; 196 | gtk_adjustment_set_upper(scroll_x_adj, MAX(0, scroll_x_upper)); 197 | gtk_adjustment_set_upper(scroll_y_adj, MAX(0, scroll_y_upper)); 198 | gtk_adjustment_set_page_size(scroll_x_adj, state->render.viewport_width); 199 | gtk_adjustment_set_page_size(scroll_y_adj, state->render.viewport_height); 200 | gtk_adjustment_set_page_increment(scroll_x_adj, state->render.viewport_width); 201 | gtk_adjustment_set_page_increment(scroll_y_adj, state->render.viewport_height); 202 | gtk_adjustment_set_step_increment(scroll_x_adj, state->render.viewport_width / 10); 203 | gtk_adjustment_set_step_increment(scroll_y_adj, state->render.viewport_height / 10); 204 | double x = gtk_adjustment_get_value(scroll_x_adj); 205 | double y = gtk_adjustment_get_value(scroll_y_adj); 206 | gtk_adjustment_set_value(scroll_x_adj, MIN(x, scroll_x_upper)); 207 | gtk_adjustment_set_value(scroll_y_adj, MIN(y, scroll_y_upper)); 208 | } 209 | 210 | /* 211 | * Recalculates the desired canvas size, accounting for zoom + margins. 212 | */ 213 | static void update_canvas_size(struct wd_state *state) { 214 | int xmin = 0; 215 | int xmax = 0; 216 | int ymin = 0; 217 | int ymax = 0; 218 | 219 | g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); 220 | for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { 221 | GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder")); 222 | gboolean enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "enabled"))); 223 | if (enabled) { 224 | int x1 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_x"))); 225 | int y1 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_y"))); 226 | int w = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "width"))); 227 | int h = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "height"))); 228 | int x2 = x1 + w; 229 | int y2 = y1 + w; 230 | double scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale"))); 231 | if (scale > 0.) { 232 | w /= scale; 233 | h /= scale; 234 | } 235 | xmin = MIN(xmin, x1); 236 | xmax = MAX(xmax, x2); 237 | ymin = MIN(ymin, y1); 238 | ymax = MAX(ymax, y2); 239 | } 240 | } 241 | // update canvas sizings 242 | state->render.x_origin = floor(xmin * state->zoom) - CANVAS_MARGIN; 243 | state->render.y_origin = floor(ymin * state->zoom) - CANVAS_MARGIN; 244 | state->render.width = ceil((xmax - xmin) * state->zoom) + CANVAS_MARGIN * 2; 245 | state->render.height = ceil((ymax - ymin) * state->zoom) + CANVAS_MARGIN * 2; 246 | 247 | update_scroll_size(state); 248 | } 249 | 250 | static void cache_scroll(struct wd_state *state) { 251 | GtkAdjustment *scroll_x_adj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller)); 252 | GtkAdjustment *scroll_y_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller)); 253 | state->render.scroll_x = gtk_adjustment_get_value(scroll_x_adj); 254 | state->render.scroll_y = gtk_adjustment_get_value(scroll_y_adj); 255 | } 256 | 257 | static gboolean redraw_canvas(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer data); 258 | 259 | static void update_tick_callback(struct wd_state *state) { 260 | bool any_animate = FALSE; 261 | struct wd_render_head_data *render; 262 | wl_list_for_each(render, &state->render.heads, link) { 263 | if (state->render.updated_at < render->hover_begin + HOVER_USECS 264 | || state->render.updated_at < render->click_begin + HOVER_USECS) { 265 | any_animate = TRUE; 266 | break; 267 | } 268 | } 269 | if (!any_animate && !state->capture) { 270 | if (state->canvas_tick != -1) { 271 | gtk_widget_remove_tick_callback(state->canvas, state->canvas_tick); 272 | state->canvas_tick = -1; 273 | } 274 | } else if (state->canvas_tick == -1) { 275 | state->canvas_tick = 276 | gtk_widget_add_tick_callback(state->canvas, redraw_canvas, state, NULL); 277 | } 278 | gtk_gl_area_queue_render(GTK_GL_AREA(state->canvas)); 279 | gtk_gl_area_set_auto_render(GTK_GL_AREA(state->canvas), state->capture); 280 | } 281 | 282 | static void update_cursor(struct wd_state *state) { 283 | bool any_hovered = FALSE; 284 | struct wd_head *head; 285 | wl_list_for_each(head, &state->heads, link) { 286 | struct wd_render_head_data *render = head->render; 287 | if (render != NULL && render->hovered) { 288 | any_hovered = TRUE; 289 | break; 290 | } 291 | } 292 | GdkWindow *window = gtk_widget_get_window(state->canvas); 293 | if (any_hovered) { 294 | gdk_window_set_cursor(window, state->grab_cursor); 295 | } else if (state->clicked != NULL) { 296 | gdk_window_set_cursor(window, state->grabbing_cursor); 297 | } else if (state->panning) { 298 | gdk_window_set_cursor(window, state->move_cursor); 299 | } else { 300 | gdk_window_set_cursor(window, NULL); 301 | } 302 | } 303 | 304 | static inline void flip_anim(uint64_t *timer, uint64_t tick) { 305 | uint64_t animate_end = *timer + HOVER_USECS; 306 | if (tick < animate_end) { 307 | *timer = tick - (animate_end - tick); 308 | } else { 309 | *timer = tick; 310 | } 311 | } 312 | 313 | static void update_hovered(struct wd_state *state) { 314 | GdkDisplay *display = gdk_display_get_default(); 315 | GdkWindow *window = gtk_widget_get_window(state->canvas); 316 | if (!gtk_widget_get_realized(state->canvas)) { 317 | return; 318 | } 319 | GdkFrameClock *clock = gtk_widget_get_frame_clock(state->canvas); 320 | uint64_t tick = gdk_frame_clock_get_frame_time(clock); 321 | g_autoptr(GList) seats = gdk_display_list_seats(display); 322 | bool any_hovered = FALSE; 323 | struct wd_render_head_data *render; 324 | wl_list_for_each(render, &state->render.heads, link) { 325 | bool init_hovered = render->hovered; 326 | render->hovered = FALSE; 327 | if (any_hovered) { 328 | continue; 329 | } 330 | if (state->clicked == render) { 331 | render->hovered = TRUE; 332 | any_hovered = TRUE; 333 | } else if (state->clicked == NULL) { 334 | for (GList *iter = seats; iter != NULL; iter = iter->next) { 335 | double mouse_x; 336 | double mouse_y; 337 | 338 | GdkDevice *pointer = gdk_seat_get_pointer(GDK_SEAT(iter->data)); 339 | gdk_window_get_device_position_double(window, pointer, &mouse_x, &mouse_y, NULL); 340 | if (mouse_x >= render->x1 && mouse_x < render->x2 && 341 | mouse_y >= render->y1 && mouse_y < render->y2) { 342 | render->hovered = TRUE; 343 | any_hovered = TRUE; 344 | break; 345 | } 346 | } 347 | } 348 | if (init_hovered != render->hovered) { 349 | flip_anim(&render->hover_begin, tick); 350 | } 351 | } 352 | update_cursor(state); 353 | update_tick_callback(state); 354 | } 355 | 356 | static inline void color_to_float_array(GtkStyleContext *ctx, 357 | const char *color_name, float out[4]) { 358 | GdkRGBA color; 359 | gtk_style_context_lookup_color(ctx, color_name, &color); 360 | out[0] = color.red; 361 | out[1] = color.green; 362 | out[2] = color.blue; 363 | out[3] = color.alpha; 364 | } 365 | 366 | static unsigned form_get_rotation(GtkWidget *form) { 367 | GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); 368 | unsigned rot; 369 | for (rot = 0; rot < NUM_ROTATIONS; rot++) { 370 | GtkWidget *rotate = GTK_WIDGET(gtk_builder_get_object(builder, 371 | ROTATE_IDS[rot])); 372 | gboolean selected; 373 | g_object_get(rotate, "active", &selected, NULL); 374 | if (selected) { 375 | return rot; 376 | } 377 | } 378 | return -1; 379 | } 380 | 381 | #define SWAP(_type, _a, _b) { _type _tmp = (_a); (_a) = (_b); (_b) = _tmp; } 382 | 383 | static void queue_canvas_draw(struct wd_state *state) { 384 | GtkStyleContext *style_ctx = gtk_widget_get_style_context(state->canvas); 385 | color_to_float_array(style_ctx, 386 | "theme_fg_color", state->render.fg_color); 387 | color_to_float_array(style_ctx, 388 | "theme_bg_color", state->render.bg_color); 389 | color_to_float_array(style_ctx, 390 | "borders", state->render.border_color); 391 | color_to_float_array(style_ctx, 392 | "theme_selected_bg_color", state->render.selection_color); 393 | 394 | cache_scroll(state); 395 | 396 | g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); 397 | for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { 398 | GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder")); 399 | gboolean enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "enabled"))); 400 | if (enabled) { 401 | int x = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_x"))); 402 | int y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "pos_y"))); 403 | int w = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "width"))); 404 | int h = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "height"))); 405 | double scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale"))); 406 | if (scale <= 0.) 407 | scale = 1.; 408 | 409 | struct wd_head *head = g_object_get_data(G_OBJECT(form_iter->data), "head"); 410 | if (head->render == NULL) { 411 | head->render = calloc(1, sizeof(*head->render)); 412 | wl_list_insert(&state->render.heads, &head->render->link); 413 | } 414 | struct wd_render_head_data *render = head->render; 415 | render->queued.rotation = form_get_rotation(GTK_WIDGET(form_iter->data)); 416 | if (render->queued.rotation & 1) { 417 | SWAP(int, w, h); 418 | } 419 | render->queued.x_invert = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "flipped"))); 420 | render->x1 = floor(x * state->zoom - state->render.scroll_x - state->render.x_origin); 421 | render->y1 = floor(y * state->zoom - state->render.scroll_y - state->render.y_origin); 422 | render->x2 = floor(render->x1 + w * state->zoom / scale); 423 | render->y2 = floor(render->y1 + h * state->zoom / scale); 424 | } 425 | } 426 | gtk_gl_area_queue_render(GTK_GL_AREA(state->canvas)); 427 | } 428 | 429 | // BEGIN FORM CALLBACKS 430 | static void show_apply(struct wd_state *state) { 431 | const gchar *page = "title"; 432 | if (has_changes(state)) { 433 | if (state->autoapply) { 434 | apply_state(state); 435 | } else { 436 | page = "apply"; 437 | } 438 | } 439 | gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), page); 440 | } 441 | 442 | static void update_ui(struct wd_state *state) { 443 | show_apply(state); 444 | update_canvas_size(state); 445 | queue_canvas_draw(state); 446 | } 447 | 448 | static void update_sensitivity(GtkWidget *form) { 449 | GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); 450 | GtkWidget *enabled = GTK_WIDGET(gtk_builder_get_object(builder, "enabled")); 451 | bool enabled_toggled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(enabled)); 452 | 453 | g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(form)); 454 | for (GList *child = children; child != NULL; child = child->next) { 455 | GtkWidget *widget = GTK_WIDGET(child->data); 456 | if (widget != enabled) { 457 | gtk_widget_set_sensitive(widget, enabled_toggled); 458 | } 459 | } 460 | } 461 | 462 | static void select_rotate_option(GtkWidget *form, GtkWidget *model_button) { 463 | GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); 464 | GtkWidget *rotate_button = GTK_WIDGET(gtk_builder_get_object(builder, "rotate_button")); 465 | for (int i = 0; i < NUM_ROTATIONS; i++) { 466 | GtkWidget *rotate = GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[i])); 467 | gboolean selected = model_button == rotate; 468 | g_object_set(rotate, "active", selected, NULL); 469 | if (selected) { 470 | g_autofree gchar *rotate_text = NULL; 471 | g_object_get(rotate, "text", &rotate_text, NULL); 472 | gtk_button_set_label(GTK_BUTTON(rotate_button), rotate_text); 473 | } 474 | } 475 | } 476 | 477 | static void rotate_selected(GSimpleAction *action, GVariant *param, gpointer data) { 478 | select_rotate_option(GTK_WIDGET(data), g_object_get_data(G_OBJECT(action), "widget")); 479 | const struct wd_head *head = g_object_get_data(G_OBJECT(data), "head"); 480 | update_ui(head->state); 481 | } 482 | 483 | static void select_mode_option(GtkWidget *form, int32_t w, int32_t h, int32_t r) { 484 | GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); 485 | GtkWidget *mode_box = GTK_WIDGET(gtk_builder_get_object(builder, "mode_box")); 486 | g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(mode_box)); 487 | for (GList *child = children; child != NULL; child = child->next) { 488 | const struct wd_mode *mode = g_object_get_data(G_OBJECT(child->data), "mode"); 489 | g_object_set(child->data, "active", w == mode->width && h == mode->height && r == mode->refresh, NULL); 490 | } 491 | } 492 | 493 | static void update_mode_entries(GtkWidget *form, int32_t w, int32_t h, int32_t r) { 494 | GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); 495 | GtkWidget *width = GTK_WIDGET(gtk_builder_get_object(builder, "width")); 496 | GtkWidget *height = GTK_WIDGET(gtk_builder_get_object(builder, "height")); 497 | GtkWidget *refresh = GTK_WIDGET(gtk_builder_get_object(builder, "refresh")); 498 | 499 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(width), w); 500 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(height), h); 501 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(refresh), r / 1000.); 502 | } 503 | 504 | static void mode_selected(GSimpleAction *action, GVariant *param, gpointer data) { 505 | GtkWidget *form = data; 506 | const struct wd_head *head = g_object_get_data(G_OBJECT(form), "head"); 507 | const struct wd_mode *mode = g_object_get_data(G_OBJECT(action), "mode"); 508 | 509 | update_mode_entries(form, mode->width, mode->height, mode->refresh); 510 | select_mode_option(form, mode->width, mode->height, mode->refresh); 511 | update_ui(head->state); 512 | } 513 | // END FORM CALLBACKS 514 | 515 | static void clear_menu(GtkWidget *box, GActionMap *action_map) { 516 | g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(box)); 517 | for (GList *child = children; child != NULL; child = child->next) { 518 | g_action_map_remove_action(action_map, strchr(gtk_actionable_get_action_name(GTK_ACTIONABLE(child->data)), '.') + 1); 519 | gtk_container_remove(GTK_CONTAINER(box), GTK_WIDGET(child->data)); 520 | } 521 | } 522 | 523 | static void update_head_form(GtkWidget *form, unsigned int fields) { 524 | GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); 525 | GtkWidget *description = GTK_WIDGET(gtk_builder_get_object(builder, "description")); 526 | GtkWidget *physical_size = GTK_WIDGET(gtk_builder_get_object(builder, "physical_size")); 527 | GtkWidget *enabled = GTK_WIDGET(gtk_builder_get_object(builder, "enabled")); 528 | GtkWidget *scale = GTK_WIDGET(gtk_builder_get_object(builder, "scale")); 529 | GtkWidget *pos_x = GTK_WIDGET(gtk_builder_get_object(builder, "pos_x")); 530 | GtkWidget *pos_y = GTK_WIDGET(gtk_builder_get_object(builder, "pos_y")); 531 | GtkWidget *mode_box = GTK_WIDGET(gtk_builder_get_object(builder, "mode_box")); 532 | GtkWidget *flipped = GTK_WIDGET(gtk_builder_get_object(builder, "flipped")); 533 | const struct wd_head *head = g_object_get_data(G_OBJECT(form), "head"); 534 | 535 | if (fields & WD_FIELD_NAME) { 536 | gtk_container_child_set(GTK_CONTAINER(head->state->stack), form, "title", head->name, NULL); 537 | } 538 | if (fields & WD_FIELD_DESCRIPTION) { 539 | gtk_label_set_text(GTK_LABEL(description), head->description); 540 | } 541 | if (fields & WD_FIELD_PHYSICAL_SIZE) { 542 | g_autofree gchar *physical_str = g_strdup_printf("%dmm × %dmm", head->phys_width, head->phys_height); 543 | gtk_label_set_text(GTK_LABEL(physical_size), physical_str); 544 | } 545 | if (fields & WD_FIELD_ENABLED) { 546 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(enabled), head->enabled); 547 | } 548 | if (fields & WD_FIELD_SCALE) { 549 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(scale), head->scale); 550 | } 551 | if (fields & WD_FIELD_POSITION) { 552 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(pos_x), head->x); 553 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(pos_y), head->y); 554 | } 555 | 556 | if (fields & WD_FIELD_MODE) { 557 | GActionMap *mode_actions = G_ACTION_MAP(g_object_get_data(G_OBJECT(form), "mode-group")); 558 | clear_menu(mode_box, mode_actions); 559 | struct wd_mode *mode; 560 | wl_list_for_each(mode, &head->modes, link) { 561 | g_autofree gchar *name = g_strdup_printf("%d×%d@%0.3fHz", mode->width, mode->height, mode->refresh / 1000.); 562 | GSimpleAction *action = g_simple_action_new(name, NULL); 563 | g_action_map_add_action(G_ACTION_MAP(mode_actions), G_ACTION(action)); 564 | g_signal_connect(action, "activate", G_CALLBACK(mode_selected), form); 565 | g_object_set_data(G_OBJECT(action), "mode", mode); 566 | g_object_unref(action); 567 | 568 | GtkWidget *button = gtk_model_button_new(); 569 | g_autoptr(GString) prefixed_name = g_string_new(MODE_PREFIX); 570 | g_string_append(prefixed_name, "."); 571 | g_string_append(prefixed_name, name); 572 | gtk_actionable_set_action_name(GTK_ACTIONABLE(button), prefixed_name->str); 573 | g_object_set(button, "role", GTK_BUTTON_ROLE_RADIO, "text", name, NULL); 574 | gtk_box_pack_start(GTK_BOX(mode_box), button, FALSE, FALSE, 0); 575 | g_object_set_data(G_OBJECT(button), "mode", mode); 576 | gtk_widget_show_all(button); 577 | } 578 | // Mode entries 579 | int w = head->custom_mode.width; 580 | int h = head->custom_mode.height; 581 | int r = head->custom_mode.refresh; 582 | if (head->enabled && head->mode != NULL) { 583 | w = head->mode->width; 584 | h = head->mode->height; 585 | r = head->mode->refresh; 586 | } else if (!head->enabled && w == 0 && h == 0) { 587 | struct wd_mode *mode; 588 | wl_list_for_each(mode, &head->modes, link) { 589 | if (mode->preferred) { 590 | w = mode->width; 591 | h = mode->height; 592 | r = mode->refresh; 593 | break; 594 | } 595 | } 596 | } 597 | 598 | update_mode_entries(form, w, h, r); 599 | select_mode_option(form, w, h, r); 600 | gtk_widget_show_all(mode_box); 601 | } 602 | 603 | if (fields & WD_FIELD_TRANSFORM) { 604 | int active_rotate = get_rotate_index(head->transform); 605 | select_rotate_option(form, GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[active_rotate]))); 606 | 607 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(flipped), 608 | head->transform == WL_OUTPUT_TRANSFORM_FLIPPED 609 | || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_90 610 | || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_180 611 | || head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_270); 612 | } 613 | 614 | // Sync state 615 | if (fields & WD_FIELD_ENABLED) { 616 | update_sensitivity(form); 617 | } 618 | update_ui(head->state); 619 | } 620 | 621 | void wd_ui_reset_heads(struct wd_state *state) { 622 | if (state->stack == NULL) { 623 | return; 624 | } 625 | 626 | g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); 627 | GList *form_iter = forms; 628 | struct wd_head *head; 629 | int i = 0; 630 | wl_list_for_each(head, &state->heads, link) { 631 | GtkBuilder *builder; 632 | GtkWidget *form; 633 | if (form_iter == NULL) { 634 | builder = gtk_builder_new_from_resource("/head.ui"); 635 | form = GTK_WIDGET(gtk_builder_get_object(builder, "form")); 636 | g_object_set_data(G_OBJECT(form), "builder", builder); 637 | g_object_set_data(G_OBJECT(form), "head", head); 638 | g_autofree gchar *page_name = g_strdup_printf("%d", i); 639 | gtk_stack_add_titled(GTK_STACK(state->stack), form, page_name, head->name); 640 | 641 | GtkWidget *mode_button = GTK_WIDGET(gtk_builder_get_object(builder, "mode_button")); 642 | GtkWidget *rotate_button = GTK_WIDGET(gtk_builder_get_object(builder, "rotate_button")); 643 | 644 | GSimpleActionGroup *mode_actions = g_simple_action_group_new(); 645 | gtk_widget_insert_action_group(mode_button, MODE_PREFIX, G_ACTION_GROUP(mode_actions)); 646 | g_object_set_data(G_OBJECT(form), "mode-group", mode_actions); 647 | g_object_unref(mode_actions); 648 | 649 | GSimpleActionGroup *transform_actions = g_simple_action_group_new(); 650 | gtk_widget_insert_action_group(rotate_button, TRANSFORM_PREFIX, G_ACTION_GROUP(transform_actions)); 651 | g_object_unref(transform_actions); 652 | 653 | for (int i = 0; i < NUM_ROTATIONS; i++) { 654 | GtkWidget *button = GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[i])); 655 | g_object_set(button, "role", GTK_BUTTON_ROLE_RADIO, NULL); 656 | GSimpleAction *action = g_simple_action_new(ROTATE_IDS[i], NULL); 657 | g_action_map_add_action(G_ACTION_MAP(transform_actions), G_ACTION(action)); 658 | g_signal_connect(action, "activate", G_CALLBACK(rotate_selected), form); 659 | g_object_set_data(G_OBJECT(action), "widget", button); 660 | g_object_unref(action); 661 | } 662 | update_head_form(form, WD_FIELDS_ALL); 663 | 664 | gtk_widget_show_all(form); 665 | 666 | g_signal_connect_swapped(gtk_builder_get_object(builder, "enabled"), "toggled", G_CALLBACK(update_sensitivity), form); 667 | g_signal_connect_swapped(gtk_builder_get_object(builder, "enabled"), "toggled", G_CALLBACK(update_ui), state); 668 | g_signal_connect_swapped(gtk_builder_get_object(builder, "scale"), "value-changed", G_CALLBACK(update_ui), state); 669 | g_signal_connect_swapped(gtk_builder_get_object(builder, "pos_x"), "value-changed", G_CALLBACK(update_ui), state); 670 | g_signal_connect_swapped(gtk_builder_get_object(builder, "pos_y"), "value-changed", G_CALLBACK(update_ui), state); 671 | g_signal_connect_swapped(gtk_builder_get_object(builder, "width"), "value-changed", G_CALLBACK(update_ui), state); 672 | g_signal_connect_swapped(gtk_builder_get_object(builder, "height"), "value-changed", G_CALLBACK(update_ui), state); 673 | g_signal_connect_swapped(gtk_builder_get_object(builder, "refresh"), "value-changed", G_CALLBACK(update_ui), state); 674 | g_signal_connect_swapped(gtk_builder_get_object(builder, "flipped"), "toggled", G_CALLBACK(update_ui), state); 675 | 676 | } else { 677 | form = form_iter->data; 678 | if (head != g_object_get_data(G_OBJECT(form), "head")) { 679 | g_object_set_data(G_OBJECT(form), "head", head); 680 | update_head_form(form, WD_FIELDS_ALL); 681 | } 682 | form_iter = form_iter->next; 683 | } 684 | i++; 685 | } 686 | // remove everything else 687 | for (; form_iter != NULL; form_iter = form_iter->next) { 688 | GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder")); 689 | g_object_unref(builder); 690 | gtk_container_remove(GTK_CONTAINER(state->stack), GTK_WIDGET(form_iter->data)); 691 | } 692 | update_canvas_size(state); 693 | queue_canvas_draw(state); 694 | } 695 | 696 | void wd_ui_reset_head(const struct wd_head *head, unsigned int fields) { 697 | if (head->state->stack == NULL) { 698 | return; 699 | } 700 | g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(head->state->stack)); 701 | for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { 702 | const struct wd_head *other = g_object_get_data(G_OBJECT(form_iter->data), "head"); 703 | if (head == other) { 704 | update_head_form(GTK_WIDGET(form_iter->data), fields); 705 | break; 706 | } 707 | } 708 | update_canvas_size(head->state); 709 | queue_canvas_draw(head->state); 710 | } 711 | 712 | void wd_ui_reset_all(struct wd_state *state) { 713 | wd_ui_reset_heads(state); 714 | g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); 715 | for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { 716 | update_head_form(GTK_WIDGET(form_iter->data), WD_FIELDS_ALL); 717 | } 718 | update_canvas_size(state); 719 | queue_canvas_draw(state); 720 | } 721 | 722 | void wd_ui_apply_done(struct wd_state *state, struct wl_list *outputs) { 723 | gtk_style_context_remove_class(gtk_widget_get_style_context(state->spinner), "visible"); 724 | gtk_overlay_set_overlay_pass_through(GTK_OVERLAY(state->overlay), state->spinner, TRUE); 725 | gtk_spinner_stop(GTK_SPINNER(state->spinner)); 726 | 727 | gtk_widget_set_sensitive(state->stack_switcher, TRUE); 728 | gtk_widget_set_sensitive(state->stack, TRUE); 729 | gtk_widget_set_sensitive(state->zoom_in, TRUE); 730 | gtk_widget_set_sensitive(state->zoom_reset, TRUE); 731 | gtk_widget_set_sensitive(state->zoom_out, TRUE); 732 | gtk_widget_set_sensitive(state->menu_button, TRUE); 733 | if (!state->autoapply) { 734 | show_apply(state); 735 | } 736 | g_idle_add(apply_done_reset, state); 737 | } 738 | 739 | void wd_ui_show_error(struct wd_state *state, const char *message) { 740 | gtk_label_set_text(GTK_LABEL(state->info_label), message); 741 | gtk_widget_show(state->info_bar); 742 | gtk_info_bar_set_revealed(GTK_INFO_BAR(state->info_bar), TRUE); 743 | } 744 | 745 | // BEGIN GLOBAL CALLBACKS 746 | static void cleanup(GtkWidget *window, gpointer data) { 747 | struct wd_state *state = data; 748 | g_object_unref(state->grab_cursor); 749 | g_object_unref(state->grabbing_cursor); 750 | g_object_unref(state->move_cursor); 751 | wd_state_destroy(state); 752 | } 753 | 754 | static void monitor_added(GdkDisplay *display, GdkMonitor *monitor, gpointer data) { 755 | struct wl_display *wl_display = gdk_wayland_display_get_wl_display(display); 756 | wd_add_output(data, gdk_wayland_monitor_get_wl_output(monitor), wl_display); 757 | } 758 | 759 | static void monitor_removed(GdkDisplay *display, GdkMonitor *monitor, gpointer data) { 760 | struct wl_display *wl_display = gdk_wayland_display_get_wl_display(display); 761 | wd_remove_output(data, gdk_wayland_monitor_get_wl_output(monitor), wl_display); 762 | } 763 | 764 | static void canvas_realize(GtkWidget *widget, gpointer data) { 765 | gtk_gl_area_make_current(GTK_GL_AREA(widget)); 766 | if (gtk_gl_area_get_error(GTK_GL_AREA(widget)) != NULL) { 767 | return; 768 | } 769 | 770 | struct wd_state *state = data; 771 | state->gl_data = wd_gl_setup(); 772 | } 773 | 774 | static inline bool size_changed(const struct wd_render_head_data *render) { 775 | return render->x2 - render->x1 != render->tex_width || 776 | render->y2 - render->y1 != render->tex_height; 777 | } 778 | 779 | static inline void cairo_set_source_color(cairo_t *cr, float color[4]) { 780 | cairo_set_source_rgba(cr, color[0], color[1], color[2], color[3]); 781 | } 782 | 783 | static void update_zoom(struct wd_state *state) { 784 | g_autofree gchar *zoom_percent = g_strdup_printf("%.f%%", state->zoom * 100.); 785 | gtk_button_set_label(GTK_BUTTON(state->zoom_reset), zoom_percent); 786 | gtk_widget_set_sensitive(state->zoom_in, state->zoom < MAX_ZOOM); 787 | gtk_widget_set_sensitive(state->zoom_out, state->zoom > MIN_ZOOM); 788 | 789 | update_canvas_size(state); 790 | queue_canvas_draw(state); 791 | } 792 | 793 | static void zoom_to(struct wd_state *state, double zoom) { 794 | state->zoom = zoom; 795 | state->zoom = MAX(state->zoom, MIN_ZOOM); 796 | state->zoom = MIN(state->zoom, MAX_ZOOM); 797 | update_zoom(state); 798 | } 799 | 800 | static void zoom_out(struct wd_state *state) { 801 | zoom_to(state, state->zoom * 0.75); 802 | } 803 | 804 | static void zoom_reset(struct wd_state *state) { 805 | zoom_to(state, DEFAULT_ZOOM); 806 | } 807 | 808 | static void zoom_in(struct wd_state *state) { 809 | zoom_to(state, state->zoom / 0.75); 810 | } 811 | 812 | #define TEXT_MARGIN 5 813 | 814 | static cairo_surface_t *draw_head(PangoContext *pango, 815 | struct wd_render_data *info, const char *name, 816 | unsigned width, unsigned height) { 817 | cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 818 | width, height); 819 | cairo_t *cr = cairo_create(surface); 820 | 821 | cairo_rectangle(cr, 0., 0., width, height); 822 | cairo_set_source_color(cr, info->border_color); 823 | cairo_fill(cr); 824 | 825 | PangoLayout *layout = pango_layout_new(pango); 826 | pango_layout_set_text(layout, name, -1); 827 | int text_width = pango_units_from_double(width - TEXT_MARGIN * 2); 828 | int text_height = pango_units_from_double(height - TEXT_MARGIN * 2); 829 | pango_layout_set_width(layout, MAX(text_width, 0)); 830 | pango_layout_set_height(layout, MAX(text_height, 0)); 831 | pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); 832 | pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); 833 | pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); 834 | 835 | cairo_set_source_color(cr, info->fg_color); 836 | pango_layout_get_size(layout, &text_width, &text_height); 837 | cairo_move_to(cr, TEXT_MARGIN, (height - PANGO_PIXELS(text_height)) / 2); 838 | pango_cairo_show_layout(cr, layout); 839 | g_object_unref(layout); 840 | 841 | cairo_destroy(cr); 842 | cairo_surface_flush(surface); 843 | return surface; 844 | } 845 | 846 | static void canvas_render(GtkGLArea *area, GdkGLContext *context, gpointer data) { 847 | struct wd_state *state = data; 848 | 849 | PangoContext *pango = gtk_widget_get_pango_context(state->canvas); 850 | GdkFrameClock *clock = gtk_widget_get_frame_clock(state->canvas); 851 | uint64_t tick = gdk_frame_clock_get_frame_time(clock); 852 | 853 | wd_capture_frame(state); 854 | 855 | struct wd_head *head; 856 | wl_list_for_each(head, &state->heads, link) { 857 | struct wd_render_head_data *render = head->render; 858 | struct wd_output *output = wd_find_output(state, head); 859 | struct wd_frame *frame = NULL; 860 | if (output != NULL && !wl_list_empty(&output->frames)) { 861 | frame = wl_container_of(output->frames.prev, frame, link); 862 | } 863 | if (render != NULL) { 864 | if (state->capture && frame != NULL && frame->pixels != NULL) { 865 | if (frame->tick > render->updated_at) { 866 | render->tex_stride = frame->stride; 867 | render->tex_width = frame->width; 868 | render->tex_height = frame->height; 869 | render->pixels = frame->pixels; 870 | render->preview = TRUE; 871 | render->updated_at = tick; 872 | render->y_invert = frame->y_invert; 873 | render->swap_rgb = frame->swap_rgb; 874 | } 875 | if (render->preview) { 876 | render->active.rotation = render->queued.rotation; 877 | render->active.x_invert = render->queued.x_invert; 878 | } 879 | } else if (render->preview 880 | || render->pixels == NULL || size_changed(render)) { 881 | render->tex_width = render->x2 - render->x1; 882 | render->tex_height = render->y2 - render->y1; 883 | render->preview = FALSE; 884 | if (head->surface != NULL) { 885 | cairo_surface_destroy(head->surface); 886 | } 887 | head->surface = draw_head(pango, &state->render, head->name, 888 | render->tex_width, render->tex_height); 889 | render->pixels = cairo_image_surface_get_data(head->surface); 890 | render->tex_stride = cairo_image_surface_get_stride(head->surface); 891 | render->updated_at = tick; 892 | render->active.rotation = 0; 893 | render->active.x_invert = FALSE; 894 | render->y_invert = FALSE; 895 | render->swap_rgb = FALSE; 896 | } 897 | } 898 | } 899 | 900 | wd_gl_render(state->gl_data, &state->render, tick); 901 | state->render.updated_at = tick; 902 | } 903 | 904 | static void canvas_unrealize(GtkWidget *widget, gpointer data) { 905 | gtk_gl_area_make_current(GTK_GL_AREA(widget)); 906 | if (gtk_gl_area_get_error(GTK_GL_AREA(widget)) != NULL) { 907 | return; 908 | } 909 | struct wd_state *state = data; 910 | 911 | GdkDisplay *gdk_display = gdk_display_get_default(); 912 | struct wl_display *display = gdk_wayland_display_get_wl_display(gdk_display); 913 | wd_capture_wait(state, display); 914 | 915 | wd_gl_cleanup(state->gl_data); 916 | state->gl_data = NULL; 917 | } 918 | 919 | static void set_clicked_head(struct wd_state *state, 920 | struct wd_render_head_data *clicked) { 921 | GdkFrameClock *clock = gtk_widget_get_frame_clock(state->canvas); 922 | uint64_t tick = gdk_frame_clock_get_frame_time(clock); 923 | if (clicked != state->clicked) { 924 | if (state->clicked != NULL) { 925 | state->clicked->clicked = FALSE; 926 | flip_anim(&state->clicked->click_begin, tick); 927 | } 928 | if (clicked != NULL) { 929 | clicked->clicked = TRUE; 930 | flip_anim(&clicked->click_begin, tick); 931 | } 932 | } 933 | state->clicked = clicked; 934 | } 935 | 936 | static gboolean canvas_click(GtkWidget *widget, GdkEvent *event, 937 | gpointer data) { 938 | struct wd_state *state = data; 939 | if (event->button.type == GDK_BUTTON_PRESS) { 940 | if (event->button.button == 1) { 941 | struct wd_render_head_data *render; 942 | state->clicked = NULL; 943 | wl_list_for_each(render, &state->render.heads, link) { 944 | double mouse_x = event->button.x; 945 | double mouse_y = event->button.y; 946 | if (mouse_x >= render->x1 && mouse_x < render->x2 && 947 | mouse_y >= render->y1 && mouse_y < render->y2) { 948 | set_clicked_head(state, render); 949 | state->click_offset.x = event->button.x - render->x1; 950 | state->click_offset.y = event->button.y - render->y1; 951 | break; 952 | } 953 | } 954 | if (state->clicked != NULL) { 955 | wl_list_remove(&state->clicked->link); 956 | wl_list_insert(&state->render.heads, &state->clicked->link); 957 | 958 | struct wd_render_head_data *render; 959 | wl_list_for_each(render, &state->render.heads, link) { 960 | render->updated_at = 0; 961 | render->preview = TRUE; 962 | } 963 | gtk_gl_area_queue_render(GTK_GL_AREA(state->canvas)); 964 | g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); 965 | for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { 966 | const struct wd_head *other = g_object_get_data(G_OBJECT(form_iter->data), "head"); 967 | if (state->clicked == other->render) { 968 | gtk_stack_set_visible_child(GTK_STACK(state->stack), form_iter->data); 969 | break; 970 | } 971 | } 972 | } 973 | } else if (event->button.button == 2) { 974 | state->panning = TRUE; 975 | state->pan_last.x = event->button.x; 976 | state->pan_last.y = event->button.y; 977 | } 978 | } 979 | return TRUE; 980 | } 981 | 982 | static gboolean canvas_release(GtkWidget *widget, GdkEvent *event, 983 | gpointer data) { 984 | struct wd_state *state = data; 985 | if (event->button.button == 1) { 986 | set_clicked_head(state, NULL); 987 | } 988 | if (event->button.button == 2) { 989 | state->panning = FALSE; 990 | } 991 | update_cursor(state); 992 | return TRUE; 993 | } 994 | 995 | #define SNAP_DIST 6. 996 | 997 | static gboolean canvas_motion(GtkWidget *widget, GdkEvent *event, 998 | gpointer data) { 999 | struct wd_state *state = data; 1000 | if (event->motion.state & GDK_BUTTON2_MASK) { 1001 | GtkAdjustment *xadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller)); 1002 | GtkAdjustment *yadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller)); 1003 | double delta_x = event->motion.x - state->pan_last.x; 1004 | double delta_y = event->motion.y - state->pan_last.y; 1005 | gtk_adjustment_set_value(xadj, gtk_adjustment_get_value(xadj) + delta_x); 1006 | gtk_adjustment_set_value(yadj, gtk_adjustment_get_value(yadj) + delta_y); 1007 | state->pan_last.x = event->motion.x; 1008 | state->pan_last.y = event->motion.y; 1009 | queue_canvas_draw(state); 1010 | } 1011 | if ((event->motion.state & GDK_BUTTON1_MASK) && state->clicked != NULL) { 1012 | GtkWidget *form = NULL; 1013 | g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack)); 1014 | for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { 1015 | const struct wd_head *other = g_object_get_data(G_OBJECT(form_iter->data), "head"); 1016 | if (state->clicked == other->render) { 1017 | form = form_iter->data; 1018 | break; 1019 | } 1020 | } 1021 | if (form != NULL) { 1022 | GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder")); 1023 | struct wd_point size = { 1024 | .x = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "width"))), 1025 | .y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "height"))), 1026 | }; 1027 | double scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale"))); 1028 | if (scale > 0.) { 1029 | size.x /= scale; 1030 | size.y /= scale; 1031 | } 1032 | unsigned rot = form_get_rotation(form); 1033 | if (rot & 1) { 1034 | SWAP(int, size.x, size.y); 1035 | } 1036 | struct wd_point tl = { 1037 | .x = (event->motion.x - state->click_offset.x 1038 | + state->render.x_origin + state->render.scroll_x) / state->zoom, 1039 | .y = (event->motion.y - state->click_offset.y 1040 | + state->render.y_origin + state->render.scroll_y) / state->zoom 1041 | }; 1042 | const struct wd_point br = { 1043 | .x = tl.x + size.x, 1044 | .y = tl.y + size.y 1045 | }; 1046 | struct wd_point new_pos = tl; 1047 | float snap = SNAP_DIST / state->zoom; 1048 | 1049 | for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) { 1050 | const struct wd_head *other = g_object_get_data(G_OBJECT(form_iter->data), "head"); 1051 | if (other->render != state->clicked && !(event->motion.state & GDK_SHIFT_MASK)) { 1052 | GtkBuilder *other_builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder")); 1053 | double x1 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "pos_x"))); 1054 | double y1 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "pos_y"))); 1055 | double w = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "width"))); 1056 | double h = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "height"))); 1057 | scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(other_builder, "scale"))); 1058 | if (scale > 0.) { 1059 | w /= scale; 1060 | h /= scale; 1061 | } 1062 | rot = form_get_rotation(GTK_WIDGET(form_iter->data)); 1063 | if (rot & 1) { 1064 | SWAP(int, w, h); 1065 | } 1066 | double x2 = x1 + w; 1067 | double y2 = y1 + h; 1068 | if (fabs(br.x) <= snap) 1069 | new_pos.x = -size.x; 1070 | if (fabs(br.y) <= snap) 1071 | new_pos.y = -size.y; 1072 | if (fabs(br.x - x1) <= snap) 1073 | new_pos.x = x1 - size.x; 1074 | if (fabs(br.x - x2) <= snap) 1075 | new_pos.x = x2 - size.x; 1076 | if (fabs(br.y - y1) <= snap) 1077 | new_pos.y = y1 - size.y; 1078 | if (fabs(br.y - y2) <= snap) 1079 | new_pos.y = y2 - size.y; 1080 | 1081 | if (fabs(tl.x) <= snap) 1082 | new_pos.x = 0.; 1083 | if (fabs(tl.y) <= snap) 1084 | new_pos.y = 0.; 1085 | if (fabs(tl.x - x1) <= snap) 1086 | new_pos.x = x1; 1087 | if (fabs(tl.x - x2) <= snap) 1088 | new_pos.x = x2; 1089 | if (fabs(tl.y - y1) <= snap) 1090 | new_pos.y = y1; 1091 | if (fabs(tl.y - y2) <= snap) 1092 | new_pos.y = y2; 1093 | } 1094 | } 1095 | GtkWidget *pos_x = GTK_WIDGET(gtk_builder_get_object(builder, "pos_x")); 1096 | GtkWidget *pos_y = GTK_WIDGET(gtk_builder_get_object(builder, "pos_y")); 1097 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(pos_x), new_pos.x); 1098 | gtk_spin_button_set_value(GTK_SPIN_BUTTON(pos_y), new_pos.y); 1099 | } 1100 | } 1101 | update_hovered(state); 1102 | return TRUE; 1103 | } 1104 | 1105 | static gboolean canvas_enter(GtkWidget *widget, GdkEvent *event, 1106 | gpointer data) { 1107 | struct wd_state *state = data; 1108 | if (!(event->crossing.state & GDK_BUTTON1_MASK)) { 1109 | set_clicked_head(state, NULL); 1110 | } 1111 | if (!(event->crossing.state & GDK_BUTTON2_MASK)) { 1112 | state->panning = FALSE; 1113 | } 1114 | update_cursor(state); 1115 | return TRUE; 1116 | } 1117 | 1118 | static gboolean canvas_leave(GtkWidget *widget, GdkEvent *event, 1119 | gpointer data) { 1120 | struct wd_state *state = data; 1121 | struct wd_render_head_data *render; 1122 | wl_list_for_each(render, &state->render.heads, link) { 1123 | render->hovered = FALSE; 1124 | } 1125 | update_tick_callback(state); 1126 | return TRUE; 1127 | } 1128 | 1129 | static gboolean canvas_scroll(GtkWidget *widget, GdkEvent *event, 1130 | gpointer data) { 1131 | struct wd_state *state = data; 1132 | if (event->scroll.state & GDK_CONTROL_MASK) { 1133 | switch (event->scroll.direction) { 1134 | case GDK_SCROLL_UP: 1135 | zoom_in(state); 1136 | break; 1137 | case GDK_SCROLL_DOWN: 1138 | zoom_out(state); 1139 | break; 1140 | case GDK_SCROLL_SMOOTH: 1141 | if (event->scroll.delta_y) 1142 | zoom_to(state, state->zoom * pow(0.75, event->scroll.delta_y)); 1143 | break; 1144 | default: 1145 | break; 1146 | } 1147 | } else { 1148 | GtkAdjustment *xadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller)); 1149 | GtkAdjustment *yadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller)); 1150 | double xstep = gtk_adjustment_get_step_increment(xadj); 1151 | double ystep = gtk_adjustment_get_step_increment(yadj); 1152 | switch (event->scroll.direction) { 1153 | case GDK_SCROLL_UP: 1154 | gtk_adjustment_set_value(yadj, gtk_adjustment_get_value(yadj) - ystep); 1155 | break; 1156 | case GDK_SCROLL_DOWN: 1157 | gtk_adjustment_set_value(yadj, gtk_adjustment_get_value(yadj) + ystep); 1158 | break; 1159 | case GDK_SCROLL_LEFT: 1160 | gtk_adjustment_set_value(xadj, gtk_adjustment_get_value(xadj) - xstep); 1161 | break; 1162 | case GDK_SCROLL_RIGHT: 1163 | gtk_adjustment_set_value(xadj, gtk_adjustment_get_value(xadj) + xstep); 1164 | break; 1165 | case GDK_SCROLL_SMOOTH: 1166 | if (event->scroll.delta_x) 1167 | gtk_adjustment_set_value(xadj, gtk_adjustment_get_value(xadj) + xstep * event->scroll.delta_x); 1168 | if (event->scroll.delta_y) 1169 | gtk_adjustment_set_value(yadj, gtk_adjustment_get_value(yadj) + ystep * event->scroll.delta_y); 1170 | break; 1171 | default: 1172 | break; 1173 | } 1174 | } 1175 | return FALSE; 1176 | } 1177 | 1178 | static void canvas_resize(GtkWidget *widget, GdkRectangle *allocation, 1179 | gpointer data) { 1180 | struct wd_state *state = data; 1181 | update_scroll_size(state); 1182 | } 1183 | 1184 | static void cancel_changes(GtkButton *button, gpointer data) { 1185 | struct wd_state *state = data; 1186 | gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), "title"); 1187 | wd_ui_reset_all(state); 1188 | } 1189 | 1190 | static void apply_changes(GtkButton *button, gpointer data) { 1191 | apply_state(data); 1192 | } 1193 | 1194 | static void info_response(GtkInfoBar *info_bar, gint response_id, gpointer data) { 1195 | gtk_info_bar_set_revealed(info_bar, FALSE); 1196 | } 1197 | 1198 | static void info_bar_animation_done(GObject *object, GParamSpec *pspec, gpointer data) { 1199 | gboolean done = gtk_revealer_get_child_revealed(GTK_REVEALER(object)); 1200 | if (!done) { 1201 | struct wd_state *state = data; 1202 | gtk_widget_set_visible(state->info_bar, gtk_revealer_get_reveal_child(GTK_REVEALER(object))); 1203 | } 1204 | } 1205 | 1206 | static void auto_apply_selected(GSimpleAction *action, GVariant *param, gpointer data) { 1207 | struct wd_state *state = data; 1208 | state->autoapply = !state->autoapply; 1209 | g_simple_action_set_state(action, g_variant_new_boolean(state->autoapply)); 1210 | } 1211 | 1212 | static gboolean redraw_canvas(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer data) { 1213 | struct wd_state *state = data; 1214 | if (state->capture) { 1215 | wd_capture_frame(state); 1216 | } 1217 | queue_canvas_draw(state); 1218 | return G_SOURCE_CONTINUE; 1219 | } 1220 | 1221 | static void capture_selected(GSimpleAction *action, GVariant *param, gpointer data) { 1222 | struct wd_state *state = data; 1223 | state->capture = !state->capture; 1224 | g_simple_action_set_state(action, g_variant_new_boolean(state->capture)); 1225 | update_tick_callback(state); 1226 | } 1227 | 1228 | static void overlay_selected(GSimpleAction *action, GVariant *param, gpointer data) { 1229 | struct wd_state *state = data; 1230 | state->show_overlay = !state->show_overlay; 1231 | g_simple_action_set_state(action, g_variant_new_boolean(state->show_overlay)); 1232 | 1233 | struct wd_output *output; 1234 | wl_list_for_each(output, &state->outputs, link) { 1235 | if (state->show_overlay) { 1236 | wd_create_overlay(output); 1237 | } else { 1238 | wd_destroy_overlay(output); 1239 | } 1240 | } 1241 | } 1242 | 1243 | static void activate(GtkApplication* app, gpointer user_data) { 1244 | GdkDisplay *gdk_display = gdk_display_get_default(); 1245 | if (!GDK_IS_WAYLAND_DISPLAY(gdk_display)) { 1246 | wd_fatal_error(1, "This program is only usable on Wayland sessions."); 1247 | } 1248 | 1249 | struct wd_state *state = wd_state_create(); 1250 | state->zoom = DEFAULT_ZOOM; 1251 | state->canvas_tick = -1; 1252 | 1253 | GtkCssProvider *css_provider = gtk_css_provider_new(); 1254 | gtk_css_provider_load_from_resource(css_provider, "/style.css"); 1255 | gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), GTK_STYLE_PROVIDER(css_provider), 1256 | GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); 1257 | 1258 | state->grab_cursor = gdk_cursor_new_from_name(gdk_display, "grab"); 1259 | state->grabbing_cursor = gdk_cursor_new_from_name(gdk_display, "grabbing"); 1260 | state->move_cursor = gdk_cursor_new_from_name(gdk_display, "move"); 1261 | 1262 | GtkBuilder *builder = gtk_builder_new_from_resource("/wdisplays.ui"); 1263 | GtkWidget *window = GTK_WIDGET(gtk_builder_get_object(builder, "heads_window")); 1264 | state->header_stack = GTK_WIDGET(gtk_builder_get_object(builder, "header_stack")); 1265 | state->stack_switcher = GTK_WIDGET(gtk_builder_get_object(builder, "heads_stack_switcher")); 1266 | state->stack = GTK_WIDGET(gtk_builder_get_object(builder, "heads_stack")); 1267 | state->scroller = GTK_WIDGET(gtk_builder_get_object(builder, "heads_scroll")); 1268 | state->spinner = GTK_WIDGET(gtk_builder_get_object(builder, "spinner")); 1269 | state->zoom_out = GTK_WIDGET(gtk_builder_get_object(builder, "zoom_out")); 1270 | state->zoom_reset = GTK_WIDGET(gtk_builder_get_object(builder, "zoom_reset")); 1271 | state->zoom_in = GTK_WIDGET(gtk_builder_get_object(builder, "zoom_in")); 1272 | state->overlay = GTK_WIDGET(gtk_builder_get_object(builder, "overlay")); 1273 | state->info_bar = GTK_WIDGET(gtk_builder_get_object(builder, "heads_info")); 1274 | state->info_label = GTK_WIDGET(gtk_builder_get_object(builder, "heads_info_label")); 1275 | state->menu_button = GTK_WIDGET(gtk_builder_get_object(builder, "menu_button")); 1276 | 1277 | gtk_builder_add_callback_symbol(builder, "apply_changes", G_CALLBACK(apply_changes)); 1278 | gtk_builder_add_callback_symbol(builder, "cancel_changes", G_CALLBACK(cancel_changes)); 1279 | gtk_builder_add_callback_symbol(builder, "zoom_out", G_CALLBACK(zoom_out)); 1280 | gtk_builder_add_callback_symbol(builder, "zoom_reset", G_CALLBACK(zoom_reset)); 1281 | gtk_builder_add_callback_symbol(builder, "zoom_in", G_CALLBACK(zoom_in)); 1282 | gtk_builder_add_callback_symbol(builder, "info_response", G_CALLBACK(info_response)); 1283 | gtk_builder_add_callback_symbol(builder, "destroy", G_CALLBACK(cleanup)); 1284 | gtk_builder_connect_signals(builder, state); 1285 | gtk_box_set_homogeneous(GTK_BOX(gtk_builder_get_object(builder, "zoom_box")), FALSE); 1286 | 1287 | state->canvas = wd_gl_viewport_new(); 1288 | gtk_container_add(GTK_CONTAINER(state->scroller), state->canvas); 1289 | gtk_widget_add_events(state->canvas, GDK_POINTER_MOTION_MASK 1290 | | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_SCROLL_MASK 1291 | | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK); 1292 | g_signal_connect(state->canvas, "realize", G_CALLBACK(canvas_realize), state); 1293 | g_signal_connect(state->canvas, "render", G_CALLBACK(canvas_render), state); 1294 | g_signal_connect(state->canvas, "unrealize", G_CALLBACK(canvas_unrealize), state); 1295 | g_signal_connect(state->canvas, "button-press-event", G_CALLBACK(canvas_click), state); 1296 | g_signal_connect(state->canvas, "button-release-event", G_CALLBACK(canvas_release), state); 1297 | g_signal_connect(state->canvas, "enter-notify-event", G_CALLBACK(canvas_enter), state); 1298 | g_signal_connect(state->canvas, "leave-notify-event", G_CALLBACK(canvas_leave), state); 1299 | g_signal_connect(state->canvas, "motion-notify-event", G_CALLBACK(canvas_motion), state); 1300 | g_signal_connect(state->canvas, "scroll-event", G_CALLBACK(canvas_scroll), state); 1301 | g_signal_connect(state->canvas, "size-allocate", G_CALLBACK(canvas_resize), state); 1302 | gtk_gl_area_set_use_es(GTK_GL_AREA(state->canvas), TRUE); 1303 | gtk_gl_area_set_has_alpha(GTK_GL_AREA(state->canvas), TRUE); 1304 | gtk_gl_area_set_auto_render(GTK_GL_AREA(state->canvas), state->capture); 1305 | 1306 | GtkAdjustment *scroll_x_adj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller)); 1307 | GtkAdjustment *scroll_y_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller)); 1308 | g_signal_connect_swapped(scroll_x_adj, "value-changed", G_CALLBACK(queue_canvas_draw), state); 1309 | g_signal_connect_swapped(scroll_y_adj, "value-changed", G_CALLBACK(queue_canvas_draw), state); 1310 | 1311 | update_zoom(state); 1312 | 1313 | GSimpleActionGroup *main_actions = g_simple_action_group_new(); 1314 | gtk_widget_insert_action_group(state->menu_button, APP_PREFIX, G_ACTION_GROUP(main_actions)); 1315 | g_object_unref(main_actions); 1316 | 1317 | GSimpleAction *autoapply_action = g_simple_action_new_stateful("auto-apply", NULL, 1318 | g_variant_new_boolean(state->autoapply)); 1319 | g_signal_connect(autoapply_action, "activate", G_CALLBACK(auto_apply_selected), state); 1320 | g_action_map_add_action(G_ACTION_MAP(main_actions), G_ACTION(autoapply_action)); 1321 | 1322 | GSimpleAction *capture_action = g_simple_action_new_stateful("capture-screens", NULL, 1323 | g_variant_new_boolean(state->capture)); 1324 | g_signal_connect(capture_action, "activate", G_CALLBACK(capture_selected), state); 1325 | g_action_map_add_action(G_ACTION_MAP(main_actions), G_ACTION(capture_action)); 1326 | 1327 | GSimpleAction *overlay_action = g_simple_action_new_stateful("show-overlay", NULL, 1328 | g_variant_new_boolean(state->show_overlay)); 1329 | g_signal_connect(overlay_action, "activate", G_CALLBACK(overlay_selected), state); 1330 | g_action_map_add_action(G_ACTION_MAP(main_actions), G_ACTION(overlay_action)); 1331 | 1332 | /* first child of GtkInfoBar is always GtkRevealer */ 1333 | g_autoptr(GList) info_children = gtk_container_get_children(GTK_CONTAINER(state->info_bar)); 1334 | g_signal_connect(info_children->data, "notify::child-revealed", G_CALLBACK(info_bar_animation_done), state); 1335 | 1336 | struct wl_display *display = gdk_wayland_display_get_wl_display(gdk_display); 1337 | wd_add_output_management_listener(state, display); 1338 | 1339 | if (state->output_manager == NULL) { 1340 | wd_fatal_error(1, "Compositor doesn't support wlr-output-management-unstable-v1"); 1341 | } 1342 | if (state->xdg_output_manager == NULL) { 1343 | wd_fatal_error(1, "Compositor doesn't support xdg-output-unstable-v1"); 1344 | } 1345 | if (state->copy_manager == NULL) { 1346 | state->capture = FALSE; 1347 | g_simple_action_set_state(capture_action, g_variant_new_boolean(state->capture)); 1348 | g_simple_action_set_enabled(capture_action, FALSE); 1349 | } 1350 | if (state->layer_shell == NULL) { 1351 | state->show_overlay = FALSE; 1352 | g_simple_action_set_state(overlay_action, g_variant_new_boolean(state->show_overlay)); 1353 | g_simple_action_set_enabled(overlay_action, FALSE); 1354 | } 1355 | 1356 | int n_monitors = gdk_display_get_n_monitors(gdk_display); 1357 | for (int i = 0; i < n_monitors; i++) { 1358 | GdkMonitor *monitor = gdk_display_get_monitor(gdk_display, i); 1359 | wd_add_output(state, gdk_wayland_monitor_get_wl_output(monitor), display); 1360 | } 1361 | 1362 | g_signal_connect(gdk_display, "monitor-added", G_CALLBACK(monitor_added), state); 1363 | g_signal_connect(gdk_display, "monitor-removed", G_CALLBACK(monitor_removed), state); 1364 | 1365 | gtk_application_add_window(app, GTK_WINDOW(window)); 1366 | gtk_widget_show_all(window); 1367 | g_object_unref(builder); 1368 | } 1369 | // END GLOBAL CALLBACKS 1370 | 1371 | int main(int argc, char *argv[]) { 1372 | GtkApplication *app = gtk_application_new("org.swaywm.sway-outputs", G_APPLICATION_FLAGS_NONE); 1373 | g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); 1374 | int status = g_application_run(G_APPLICATION(app), argc, argv); 1375 | g_object_unref(app); 1376 | 1377 | return status; 1378 | } 1379 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | 2 | cc = meson.get_compiler('c') 3 | m_dep = cc.find_library('m', required : false) 4 | rt_dep = cc.find_library('rt', required : false) 5 | gdk = dependency('gdk-3.0') 6 | gtk = dependency('gtk+-3.0') 7 | assert(gdk.get_pkgconfig_variable('targets').split().contains('wayland'), 'Wayland GDK backend not present') 8 | epoxy = dependency('epoxy') 9 | 10 | executable( 11 | 'wdisplays', 12 | [ 13 | 'main.c', 14 | 'outputs.c', 15 | 'render.c', 16 | 'glviewport.c', 17 | 'overlay.c', 18 | resources, 19 | ], 20 | dependencies : [ 21 | m_dep, 22 | rt_dep, 23 | wayland_client, 24 | client_protos, 25 | epoxy, 26 | gtk 27 | ], 28 | install: true 29 | ) 30 | -------------------------------------------------------------------------------- /src/outputs.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 cyclopsian 3 | * Copyright (C) 2017-2019 emersion 4 | 5 | * Permission is hereby granted, free of charge, to any person obtaining 6 | * a copy of this software and associated documentation files (the 7 | * "Software"), to deal in the Software without restriction, including 8 | * without limitation the rights to use, copy, modify, merge, publish, 9 | * distribute, sublicense, and/or sell copies of the Software, and to 10 | * permit persons to whom the Software is furnished to do so, subject to 11 | * the following conditions: 12 | 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY 20 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* 26 | * Parts of this file are taken from emersion/kanshi: 27 | * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/main.c 28 | */ 29 | 30 | #define _GNU_SOURCE 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | #include 39 | #include 40 | #include 41 | 42 | #include "wdisplays.h" 43 | 44 | #include "wlr-output-management-unstable-v1-client-protocol.h" 45 | #include "xdg-output-unstable-v1-client-protocol.h" 46 | #include "wlr-screencopy-unstable-v1-client-protocol.h" 47 | #include "wlr-layer-shell-unstable-v1-client-protocol.h" 48 | 49 | static void noop() { 50 | // This space is intentionally left blank 51 | } 52 | 53 | struct wd_pending_config { 54 | struct wd_state *state; 55 | struct wl_list *outputs; 56 | }; 57 | 58 | static void destroy_pending(struct wd_pending_config *pending) { 59 | struct wd_head_config *output, *tmp; 60 | wl_list_for_each_safe(output, tmp, pending->outputs, link) { 61 | wl_list_remove(&output->link); 62 | free(output); 63 | } 64 | free(pending->outputs); 65 | free(pending); 66 | } 67 | 68 | static void config_handle_succeeded(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, pending->outputs); 73 | destroy_pending(pending); 74 | } 75 | 76 | static void config_handle_failed(void *data, 77 | struct zwlr_output_configuration_v1 *config) { 78 | struct wd_pending_config *pending = data; 79 | zwlr_output_configuration_v1_destroy(config); 80 | wd_ui_apply_done(pending->state, NULL); 81 | wd_ui_show_error(pending->state, 82 | "The display server was not able to process your changes."); 83 | destroy_pending(pending); 84 | } 85 | 86 | static void config_handle_cancelled(void *data, 87 | struct zwlr_output_configuration_v1 *config) { 88 | struct wd_pending_config *pending = data; 89 | zwlr_output_configuration_v1_destroy(config); 90 | wd_ui_apply_done(pending->state, NULL); 91 | wd_ui_show_error(pending->state, 92 | "The display configuration was modified by the server before updates were processed. " 93 | "Please check the configuration and apply the changes again."); 94 | destroy_pending(pending); 95 | } 96 | 97 | static const struct zwlr_output_configuration_v1_listener config_listener = { 98 | .succeeded = config_handle_succeeded, 99 | .failed = config_handle_failed, 100 | .cancelled = config_handle_cancelled, 101 | }; 102 | 103 | void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs, 104 | struct wl_display *display) { 105 | struct zwlr_output_configuration_v1 *config = 106 | zwlr_output_manager_v1_create_configuration(state->output_manager, state->serial); 107 | 108 | struct wd_pending_config *pending = calloc(1, sizeof(*pending)); 109 | pending->state = state; 110 | pending->outputs = new_outputs; 111 | 112 | zwlr_output_configuration_v1_add_listener(config, &config_listener, pending); 113 | 114 | ssize_t i = -1; 115 | struct wd_head_config *output; 116 | wl_list_for_each(output, new_outputs, link) { 117 | i++; 118 | struct wd_head *head = output->head; 119 | 120 | if (!output->enabled && output->enabled != head->enabled) { 121 | zwlr_output_configuration_v1_disable_head(config, head->wlr_head); 122 | continue; 123 | } 124 | 125 | struct zwlr_output_configuration_head_v1 *config_head = zwlr_output_configuration_v1_enable_head(config, head->wlr_head); 126 | 127 | const struct wd_mode *selected_mode = NULL; 128 | const struct wd_mode *mode; 129 | wl_list_for_each(mode, &head->modes, link) { 130 | if (mode->width == output->width && mode->height == output->height && mode->refresh == output->refresh) { 131 | selected_mode = mode; 132 | break; 133 | } 134 | } 135 | if (selected_mode != NULL) { 136 | if (output->enabled != head->enabled || selected_mode != head->mode) { 137 | zwlr_output_configuration_head_v1_set_mode(config_head, selected_mode->wlr_mode); 138 | } 139 | } else if (output->enabled != head->enabled 140 | || output->width != head->custom_mode.width 141 | || output->height != head->custom_mode.height 142 | || output->refresh != head->custom_mode.refresh) { 143 | zwlr_output_configuration_head_v1_set_custom_mode(config_head, 144 | output->width, output->height, output->refresh); 145 | } 146 | if (output->enabled != head->enabled || output->x != head->x || output->y != head->y) { 147 | zwlr_output_configuration_head_v1_set_position(config_head, output->x, output->y); 148 | } 149 | if (output->enabled != head->enabled || output->scale != head->scale) { 150 | zwlr_output_configuration_head_v1_set_scale(config_head, wl_fixed_from_double(output->scale)); 151 | } 152 | if (output->enabled != head->enabled || output->transform != head->transform) { 153 | zwlr_output_configuration_head_v1_set_transform(config_head, output->transform); 154 | } 155 | } 156 | 157 | zwlr_output_configuration_v1_apply(config); 158 | 159 | wl_display_roundtrip(display); 160 | } 161 | 162 | static void wd_frame_destroy(struct wd_frame *frame) { 163 | if (frame->pixels != NULL) 164 | munmap(frame->pixels, frame->height * frame->stride); 165 | if (frame->buffer != NULL) 166 | wl_buffer_destroy(frame->buffer); 167 | if (frame->pool != NULL) 168 | wl_shm_pool_destroy(frame->pool); 169 | if (frame->capture_fd != -1) 170 | close(frame->capture_fd); 171 | if (frame->wlr_frame != NULL) 172 | zwlr_screencopy_frame_v1_destroy(frame->wlr_frame); 173 | 174 | wl_list_remove(&frame->link); 175 | free(frame); 176 | } 177 | 178 | static int create_shm_file(size_t size, const char *fmt, ...) { 179 | char *shm_name = NULL; 180 | int fd = -1; 181 | 182 | va_list vl; 183 | va_start(vl, fmt); 184 | int result = vasprintf(&shm_name, fmt, vl); 185 | va_end(vl); 186 | 187 | if (result == -1) { 188 | fprintf(stderr, "asprintf: %s\n", strerror(errno)); 189 | shm_name = NULL; 190 | return -1; 191 | } 192 | 193 | fd = shm_open(shm_name, O_CREAT | O_RDWR, 0); 194 | if (fd == -1) { 195 | fprintf(stderr, "shm_open: %s\n", strerror(errno)); 196 | free(shm_name); 197 | return -1; 198 | } 199 | shm_unlink(shm_name); 200 | free(shm_name); 201 | 202 | if (ftruncate(fd, size) == -1) { 203 | fprintf(stderr, "ftruncate: %s\n", strerror(errno)); 204 | close(fd); 205 | return -1; 206 | } 207 | return fd; 208 | } 209 | 210 | static void capture_buffer(void *data, 211 | struct zwlr_screencopy_frame_v1 *copy_frame, 212 | uint32_t format, uint32_t width, uint32_t height, uint32_t stride) { 213 | struct wd_frame *frame = data; 214 | 215 | if (format != WL_SHM_FORMAT_ARGB8888 && format != WL_SHM_FORMAT_XRGB8888 && 216 | format != WL_SHM_FORMAT_ABGR8888 && format != WL_SHM_FORMAT_XBGR8888) { 217 | goto err; 218 | } 219 | 220 | size_t size = stride * height; 221 | frame->capture_fd = create_shm_file(size, "/wd-%s", frame->output->name); 222 | if (frame->capture_fd == -1) { 223 | goto err; 224 | } 225 | 226 | frame->pool = wl_shm_create_pool(frame->output->state->shm, 227 | frame->capture_fd, size); 228 | frame->buffer = wl_shm_pool_create_buffer(frame->pool, 0, 229 | width, height, stride, format); 230 | zwlr_screencopy_frame_v1_copy(copy_frame, frame->buffer); 231 | frame->stride = stride; 232 | frame->width = width; 233 | frame->height = height; 234 | frame->swap_rgb = format == WL_SHM_FORMAT_ABGR8888 235 | || format == WL_SHM_FORMAT_XBGR8888; 236 | 237 | return; 238 | err: 239 | wd_frame_destroy(frame); 240 | } 241 | 242 | static void capture_flags(void *data, 243 | struct zwlr_screencopy_frame_v1 *wlr_frame, 244 | uint32_t flags) { 245 | struct wd_frame *frame = data; 246 | frame->y_invert = !!(flags & ZWLR_SCREENCOPY_FRAME_V1_FLAGS_Y_INVERT); 247 | } 248 | 249 | static void capture_ready(void *data, 250 | struct zwlr_screencopy_frame_v1 *wlr_frame, 251 | uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec) { 252 | struct wd_frame *frame = data; 253 | 254 | frame->pixels = mmap(NULL, frame->stride * frame->height, 255 | PROT_READ, MAP_SHARED, frame->capture_fd, 0); 256 | if (frame->pixels == MAP_FAILED) { 257 | frame->pixels = NULL; 258 | fprintf(stderr, "mmap: %d: %s\n", frame->capture_fd, strerror(errno)); 259 | wd_frame_destroy(frame); 260 | return; 261 | } else { 262 | uint64_t tv_sec = (uint64_t) tv_sec_hi << 32 | tv_sec_lo; 263 | frame->tick = (tv_sec * 1000000) + (tv_nsec / 1000); 264 | } 265 | 266 | zwlr_screencopy_frame_v1_destroy(frame->wlr_frame); 267 | frame->wlr_frame = NULL; 268 | 269 | struct wd_frame *frame_iter, *frame_tmp; 270 | wl_list_for_each_safe(frame_iter, frame_tmp, &frame->output->frames, link) { 271 | if (frame != frame_iter) { 272 | wd_frame_destroy(frame_iter); 273 | } 274 | } 275 | } 276 | 277 | static void capture_failed(void *data, 278 | struct zwlr_screencopy_frame_v1 *wlr_frame) { 279 | struct wd_frame *frame = data; 280 | wd_frame_destroy(frame); 281 | } 282 | 283 | struct zwlr_screencopy_frame_v1_listener capture_listener = { 284 | .buffer = capture_buffer, 285 | .flags = capture_flags, 286 | .ready = capture_ready, 287 | .failed = capture_failed 288 | }; 289 | 290 | static bool has_pending_captures(struct wd_state *state) { 291 | struct wd_output *output; 292 | wl_list_for_each(output, &state->outputs, link) { 293 | struct wd_frame *frame; 294 | wl_list_for_each(frame, &output->frames, link) { 295 | if (frame->pixels == NULL) { 296 | return true; 297 | } 298 | } 299 | } 300 | return false; 301 | } 302 | 303 | void wd_capture_frame(struct wd_state *state) { 304 | if (state->copy_manager == NULL || has_pending_captures(state) 305 | || !state->capture) { 306 | return; 307 | } 308 | 309 | struct wd_output *output; 310 | wl_list_for_each(output, &state->outputs, link) { 311 | struct wd_frame *frame = calloc(1, sizeof(*frame)); 312 | frame->output = output; 313 | frame->capture_fd = -1; 314 | frame->wlr_frame = 315 | zwlr_screencopy_manager_v1_capture_output(state->copy_manager, 1, 316 | output->wl_output); 317 | zwlr_screencopy_frame_v1_add_listener(frame->wlr_frame, &capture_listener, 318 | frame); 319 | wl_list_insert(&output->frames, &frame->link); 320 | } 321 | } 322 | 323 | static void wd_output_destroy(struct wd_output *output) { 324 | struct wd_frame *frame, *frame_tmp; 325 | wl_list_for_each_safe(frame, frame_tmp, &output->frames, link) { 326 | wd_frame_destroy(frame); 327 | } 328 | if (output->state->layer_shell != NULL) { 329 | wd_destroy_overlay(output); 330 | } 331 | zxdg_output_v1_destroy(output->xdg_output); 332 | free(output->name); 333 | free(output); 334 | } 335 | 336 | static void wd_mode_destroy(struct wd_mode* mode) { 337 | zwlr_output_mode_v1_destroy(mode->wlr_mode); 338 | free(mode); 339 | } 340 | 341 | static void wd_head_destroy(struct wd_head *head) { 342 | if (head->state->clicked == head->render) { 343 | head->state->clicked = NULL; 344 | } 345 | if (head->render != NULL) { 346 | wl_list_remove(&head->render->link); 347 | free(head->render); 348 | head->render = NULL; 349 | } 350 | struct wd_mode *mode, *mode_tmp; 351 | wl_list_for_each_safe(mode, mode_tmp, &head->modes, link) { 352 | zwlr_output_mode_v1_destroy(mode->wlr_mode); 353 | free(mode); 354 | } 355 | zwlr_output_head_v1_destroy(head->wlr_head); 356 | free(head->name); 357 | free(head->description); 358 | free(head); 359 | } 360 | 361 | static void mode_handle_size(void *data, struct zwlr_output_mode_v1 *wlr_mode, 362 | int32_t width, int32_t height) { 363 | struct wd_mode *mode = data; 364 | mode->width = width; 365 | mode->height = height; 366 | } 367 | 368 | static void mode_handle_refresh(void *data, 369 | struct zwlr_output_mode_v1 *wlr_mode, int32_t refresh) { 370 | struct wd_mode *mode = data; 371 | mode->refresh = refresh; 372 | } 373 | 374 | static void mode_handle_preferred(void *data, 375 | struct zwlr_output_mode_v1 *wlr_mode) { 376 | struct wd_mode *mode = data; 377 | mode->preferred = true; 378 | } 379 | 380 | static void mode_handle_finished(void *data, 381 | struct zwlr_output_mode_v1 *wlr_mode) { 382 | struct wd_mode *mode = data; 383 | wl_list_remove(&mode->link); 384 | wd_mode_destroy(mode); 385 | } 386 | 387 | static const struct zwlr_output_mode_v1_listener mode_listener = { 388 | .size = mode_handle_size, 389 | .refresh = mode_handle_refresh, 390 | .preferred = mode_handle_preferred, 391 | .finished = mode_handle_finished, 392 | }; 393 | 394 | static void head_handle_name(void *data, 395 | struct zwlr_output_head_v1 *wlr_head, const char *name) { 396 | struct wd_head *head = data; 397 | head->name = strdup(name); 398 | wd_ui_reset_head(head, WD_FIELD_NAME); 399 | } 400 | 401 | static void head_handle_description(void *data, 402 | struct zwlr_output_head_v1 *wlr_head, const char *description) { 403 | struct wd_head *head = data; 404 | head->description = strdup(description); 405 | wd_ui_reset_head(head, WD_FIELD_DESCRIPTION); 406 | } 407 | 408 | static void head_handle_physical_size(void *data, 409 | struct zwlr_output_head_v1 *wlr_head, int32_t width, int32_t height) { 410 | struct wd_head *head = data; 411 | head->phys_width = width; 412 | head->phys_height = height; 413 | wd_ui_reset_head(head, WD_FIELD_PHYSICAL_SIZE); 414 | } 415 | 416 | static void head_handle_mode(void *data, 417 | struct zwlr_output_head_v1 *wlr_head, 418 | struct zwlr_output_mode_v1 *wlr_mode) { 419 | struct wd_head *head = data; 420 | 421 | struct wd_mode *mode = calloc(1, sizeof(*mode)); 422 | mode->head = head; 423 | mode->wlr_mode = wlr_mode; 424 | wl_list_insert(head->modes.prev, &mode->link); 425 | 426 | zwlr_output_mode_v1_add_listener(wlr_mode, &mode_listener, mode); 427 | } 428 | 429 | static void head_handle_enabled(void *data, 430 | struct zwlr_output_head_v1 *wlr_head, int32_t enabled) { 431 | struct wd_head *head = data; 432 | head->enabled = !!enabled; 433 | if (!enabled) { 434 | head->output = NULL; 435 | } 436 | wd_ui_reset_head(head, WD_FIELD_ENABLED); 437 | } 438 | 439 | static void head_handle_current_mode(void *data, 440 | struct zwlr_output_head_v1 *wlr_head, 441 | struct zwlr_output_mode_v1 *wlr_mode) { 442 | struct wd_head *head = data; 443 | struct wd_mode *mode; 444 | wl_list_for_each(mode, &head->modes, link) { 445 | if (mode->wlr_mode == wlr_mode) { 446 | head->mode = mode; 447 | wd_ui_reset_head(head, WD_FIELD_MODE); 448 | return; 449 | } 450 | } 451 | fprintf(stderr, "received unknown current_mode\n"); 452 | head->mode = NULL; 453 | } 454 | 455 | static void head_handle_position(void *data, 456 | struct zwlr_output_head_v1 *wlr_head, int32_t x, int32_t y) { 457 | struct wd_head *head = data; 458 | head->x = x; 459 | head->y = y; 460 | wd_ui_reset_head(head, WD_FIELD_POSITION); 461 | } 462 | 463 | static void head_handle_transform(void *data, 464 | struct zwlr_output_head_v1 *wlr_head, int32_t transform) { 465 | struct wd_head *head = data; 466 | head->transform = transform; 467 | wd_ui_reset_head(head, WD_FIELD_TRANSFORM); 468 | } 469 | 470 | static void head_handle_scale(void *data, 471 | struct zwlr_output_head_v1 *wlr_head, wl_fixed_t scale) { 472 | struct wd_head *head = data; 473 | head->scale = wl_fixed_to_double(scale); 474 | wd_ui_reset_head(head, WD_FIELD_SCALE); 475 | } 476 | 477 | static void head_handle_finished(void *data, 478 | struct zwlr_output_head_v1 *wlr_head) { 479 | struct wd_head *head = data; 480 | struct wd_state *state = head->state; 481 | wl_list_remove(&head->link); 482 | wd_head_destroy(head); 483 | 484 | uint32_t counter = 0; 485 | wl_list_for_each(head, &state->heads, link) { 486 | if (head->id != counter) { 487 | head->id = counter; 488 | if (head->output != NULL) { 489 | wd_redraw_overlay(head->output); 490 | } 491 | } 492 | counter++; 493 | } 494 | } 495 | 496 | static const struct zwlr_output_head_v1_listener head_listener = { 497 | .name = head_handle_name, 498 | .description = head_handle_description, 499 | .physical_size = head_handle_physical_size, 500 | .mode = head_handle_mode, 501 | .enabled = head_handle_enabled, 502 | .current_mode = head_handle_current_mode, 503 | .position = head_handle_position, 504 | .transform = head_handle_transform, 505 | .scale = head_handle_scale, 506 | .finished = head_handle_finished, 507 | }; 508 | 509 | static void output_manager_handle_head(void *data, 510 | struct zwlr_output_manager_v1 *manager, 511 | struct zwlr_output_head_v1 *wlr_head) { 512 | struct wd_state *state = data; 513 | 514 | struct wd_head *head = calloc(1, sizeof(*head)); 515 | head->state = state; 516 | head->wlr_head = wlr_head; 517 | head->scale = 1.0; 518 | head->id = wl_list_length(&state->heads); 519 | wl_list_init(&head->modes); 520 | wl_list_insert(&state->heads, &head->link); 521 | 522 | zwlr_output_head_v1_add_listener(wlr_head, &head_listener, head); 523 | } 524 | 525 | static void output_manager_handle_done(void *data, 526 | struct zwlr_output_manager_v1 *manager, uint32_t serial) { 527 | struct wd_state *state = data; 528 | state->serial = serial; 529 | 530 | assert(wl_list_length(&state->heads) <= HEADS_MAX); 531 | 532 | struct wd_head *head = data; 533 | wl_list_for_each(head, &state->heads, link) { 534 | if (!head->enabled && head->mode == NULL && !wl_list_empty(&head->modes)) { 535 | struct wd_mode *mode = wl_container_of(head->modes.prev, mode, link); 536 | head->custom_mode.width = mode->width; 537 | head->custom_mode.height = mode->height; 538 | head->custom_mode.refresh = mode->refresh; 539 | } 540 | } 541 | wd_ui_reset_heads(state); 542 | } 543 | 544 | static const struct zwlr_output_manager_v1_listener output_manager_listener = { 545 | .head = output_manager_handle_head, 546 | .done = output_manager_handle_done, 547 | .finished = noop, 548 | }; 549 | static void registry_handle_global(void *data, struct wl_registry *registry, 550 | uint32_t name, const char *interface, uint32_t version) { 551 | struct wd_state *state = data; 552 | 553 | if (strcmp(interface, zwlr_output_manager_v1_interface.name) == 0) { 554 | state->output_manager = wl_registry_bind(registry, name, 555 | &zwlr_output_manager_v1_interface, version); 556 | zwlr_output_manager_v1_add_listener(state->output_manager, 557 | &output_manager_listener, state); 558 | } else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { 559 | state->xdg_output_manager = wl_registry_bind(registry, name, 560 | &zxdg_output_manager_v1_interface, version); 561 | } else if(strcmp(interface, zwlr_screencopy_manager_v1_interface.name) == 0) { 562 | state->copy_manager = wl_registry_bind(registry, name, 563 | &zwlr_screencopy_manager_v1_interface, version); 564 | } else if(strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { 565 | state->layer_shell = wl_registry_bind(registry, name, 566 | &zwlr_layer_shell_v1_interface, version); 567 | } else if(strcmp(interface, wl_shm_interface.name) == 0) { 568 | state->shm = wl_registry_bind(registry, name, &wl_shm_interface, version); 569 | } 570 | } 571 | 572 | static const struct wl_registry_listener registry_listener = { 573 | .global = registry_handle_global, 574 | .global_remove = noop, 575 | }; 576 | 577 | void wd_add_output_management_listener(struct wd_state *state, struct 578 | wl_display *display) { 579 | struct wl_registry *registry = wl_display_get_registry(display); 580 | wl_registry_add_listener(registry, ®istry_listener, state); 581 | 582 | wl_display_dispatch(display); 583 | wl_display_roundtrip(display); 584 | } 585 | 586 | struct wd_head *wd_find_head(struct wd_state *state, 587 | struct wd_output *output) { 588 | struct wd_head *head; 589 | wl_list_for_each(head, &state->heads, link) { 590 | if (output->name != NULL && strcmp(output->name, head->name) == 0) { 591 | head->output = output; 592 | return head; 593 | } 594 | } 595 | return NULL; 596 | } 597 | 598 | static void output_logical_position(void *data, struct zxdg_output_v1 *zxdg_output_v1, 599 | int32_t x, int32_t y) { 600 | struct wd_output *output = data; 601 | struct wd_head *head = wd_find_head(output->state, output); 602 | if (head != NULL) { 603 | head->x = x; 604 | head->y = y; 605 | wd_ui_reset_head(head, WD_FIELD_POSITION); 606 | } 607 | } 608 | 609 | static void output_name(void *data, struct zxdg_output_v1 *zxdg_output_v1, 610 | const char *name) { 611 | struct wd_output *output = data; 612 | if (output->name != NULL) { 613 | free(output->name); 614 | } 615 | output->name = strdup(name); 616 | struct wd_head *head = wd_find_head(output->state, output); 617 | if (head != NULL) { 618 | wd_ui_reset_head(head, WD_FIELD_NAME); 619 | } 620 | } 621 | 622 | static const struct zxdg_output_v1_listener output_listener = { 623 | .logical_position = output_logical_position, 624 | .logical_size = noop, 625 | .done = noop, 626 | .name = output_name, 627 | .description = noop 628 | }; 629 | 630 | void wd_add_output(struct wd_state *state, struct wl_output *wl_output, 631 | struct wl_display *display) { 632 | struct wd_output *output = calloc(1, sizeof(*output)); 633 | output->state = state; 634 | output->wl_output = wl_output; 635 | output->xdg_output = zxdg_output_manager_v1_get_xdg_output( 636 | state->xdg_output_manager, wl_output); 637 | wl_list_init(&output->frames); 638 | zxdg_output_v1_add_listener(output->xdg_output, &output_listener, output); 639 | wl_list_insert(output->state->outputs.prev, &output->link); 640 | if (state->layer_shell != NULL && state->show_overlay) { 641 | wl_display_roundtrip(display); 642 | wd_create_overlay(output); 643 | } 644 | } 645 | 646 | void wd_remove_output(struct wd_state *state, struct wl_output *wl_output, 647 | struct wl_display *display) { 648 | struct wd_output *output, *output_tmp; 649 | wl_list_for_each_safe(output, output_tmp, &state->outputs, link) { 650 | if (output->wl_output == wl_output) { 651 | wl_list_remove(&output->link); 652 | wd_output_destroy(output); 653 | break; 654 | } 655 | } 656 | wd_capture_wait(state, display); 657 | } 658 | 659 | struct wd_output *wd_find_output(struct wd_state *state, struct wd_head 660 | *head) { 661 | if (!head->enabled) { 662 | return NULL; 663 | } 664 | if (head->output != NULL) { 665 | return head->output; 666 | } 667 | struct wd_output *output; 668 | wl_list_for_each(output, &state->outputs, link) { 669 | if (output->name != NULL && strcmp(output->name, head->name) == 0) { 670 | head->output = output; 671 | return output; 672 | } 673 | } 674 | head->output = NULL; 675 | return NULL; 676 | } 677 | 678 | struct wd_state *wd_state_create(void) { 679 | struct wd_state *state = calloc(1, sizeof(*state)); 680 | state->zoom = 1.; 681 | state->capture = true; 682 | state->show_overlay = true; 683 | wl_list_init(&state->heads); 684 | wl_list_init(&state->outputs); 685 | wl_list_init(&state->render.heads); 686 | return state; 687 | } 688 | 689 | void wd_capture_wait(struct wd_state *state, struct wl_display *display) { 690 | wl_display_flush(display); 691 | while (has_pending_captures(state)) { 692 | if (wl_display_dispatch(display) == -1) { 693 | break; 694 | } 695 | } 696 | } 697 | 698 | void wd_state_destroy(struct wd_state *state) { 699 | struct wd_head *head, *head_tmp; 700 | wl_list_for_each_safe(head, head_tmp, &state->heads, link) { 701 | wd_head_destroy(head); 702 | } 703 | struct wd_output *output, *output_tmp; 704 | wl_list_for_each_safe(output, output_tmp, &state->outputs, link) { 705 | wd_output_destroy(output); 706 | } 707 | if (state->layer_shell != NULL) { 708 | zwlr_layer_shell_v1_destroy(state->layer_shell); 709 | } 710 | if (state->copy_manager != NULL) { 711 | zwlr_screencopy_manager_v1_destroy(state->copy_manager); 712 | } 713 | zwlr_output_manager_v1_destroy(state->output_manager); 714 | zxdg_output_manager_v1_destroy(state->xdg_output_manager); 715 | wl_shm_destroy(state->shm); 716 | free(state); 717 | } 718 | -------------------------------------------------------------------------------- /src/overlay.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 cyclopsian 3 | 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY 19 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | 24 | #define _GNU_SOURCE 25 | #include 26 | #include 27 | #include 28 | 29 | #include 30 | #include 31 | 32 | #include "wdisplays.h" 33 | 34 | #include "wlr-layer-shell-unstable-v1-client-protocol.h" 35 | 36 | #define SCREEN_MARGIN_PERCENT 0.02 37 | 38 | static void layer_surface_configure(void *data, 39 | struct zwlr_layer_surface_v1 *surface, 40 | uint32_t serial, uint32_t width, uint32_t height) { 41 | struct wd_output *output = data; 42 | gtk_widget_set_size_request(output->overlay_window, width, height); 43 | zwlr_layer_surface_v1_ack_configure(surface, serial); 44 | } 45 | 46 | static void layer_surface_closed(void *data, 47 | struct zwlr_layer_surface_v1 *surface) { 48 | } 49 | 50 | static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { 51 | .configure = layer_surface_configure, 52 | .closed = layer_surface_closed, 53 | }; 54 | 55 | static inline int min(int a, int b) { 56 | return a < b ? a : b; 57 | } 58 | 59 | static PangoLayout *create_text_layout(struct wd_head *head, 60 | PangoContext *pango, GtkStyleContext *style) { 61 | GtkStyleContext *desc_style = gtk_style_context_new(); 62 | gtk_style_context_set_screen(desc_style, 63 | gtk_style_context_get_screen(style)); 64 | GtkWidgetPath *desc_path = gtk_widget_path_copy( 65 | gtk_style_context_get_path(style)); 66 | gtk_widget_path_append_type(desc_path, G_TYPE_NONE); 67 | gtk_style_context_set_path(desc_style, desc_path); 68 | gtk_style_context_add_class(desc_style, "description"); 69 | 70 | double desc_font_size = 16.; 71 | gtk_style_context_get(desc_style, GTK_STATE_FLAG_NORMAL, 72 | "font-size", &desc_font_size, NULL); 73 | 74 | g_autofree gchar *str = g_strdup_printf("%s\n%s", 75 | head->name, (int) (desc_font_size * PANGO_SCALE), head->description); 76 | PangoLayout *layout = pango_layout_new(pango); 77 | 78 | pango_layout_set_markup(layout, str, -1); 79 | return layout; 80 | } 81 | 82 | static void resize(struct wd_output *output) { 83 | struct wd_head *head = wd_find_head(output->state, output); 84 | 85 | uint32_t screen_width = head->custom_mode.width; 86 | uint32_t screen_height = head->custom_mode.height; 87 | if (head->mode != NULL) { 88 | screen_width = head->mode->width; 89 | screen_height = head->mode->height; 90 | } 91 | uint32_t margin = min(screen_width, screen_height) * SCREEN_MARGIN_PERCENT; 92 | 93 | GdkWindow *window = gtk_widget_get_window(output->overlay_window); 94 | PangoContext *pango = gtk_widget_get_pango_context(output->overlay_window); 95 | GtkStyleContext *style_ctx = gtk_widget_get_style_context( 96 | output->overlay_window); 97 | PangoLayout *layout = create_text_layout(head, pango, style_ctx); 98 | 99 | int width; 100 | int height; 101 | pango_layout_get_pixel_size(layout, &width, &height); 102 | g_object_unref(layout); 103 | 104 | 105 | GtkBorder padding; 106 | gtk_style_context_get_padding(style_ctx, GTK_STATE_FLAG_NORMAL, &padding); 107 | 108 | width = min(width, screen_width - margin * 2) 109 | + padding.left + padding.right; 110 | height = min(height, screen_height - margin * 2) 111 | + padding.top + padding.bottom; 112 | 113 | zwlr_layer_surface_v1_set_margin(output->overlay_layer_surface, 114 | margin, margin, margin, margin); 115 | zwlr_layer_surface_v1_set_size(output->overlay_layer_surface, 116 | width, height); 117 | 118 | struct wl_surface *surface = gdk_wayland_window_get_wl_surface(window); 119 | wl_surface_commit(surface); 120 | 121 | GdkDisplay *display = gdk_window_get_display(window); 122 | wl_display_roundtrip(gdk_wayland_display_get_wl_display(display)); 123 | } 124 | 125 | void wd_redraw_overlay(struct wd_output *output) { 126 | if (output->overlay_window != NULL) { 127 | resize(output); 128 | gtk_widget_queue_draw(output->overlay_window); 129 | } 130 | } 131 | 132 | void window_realize(GtkWidget *widget, gpointer data) { 133 | GdkWindow *window = gtk_widget_get_window(widget); 134 | gdk_wayland_window_set_use_custom_surface(window); 135 | } 136 | 137 | void window_map(GtkWidget *widget, gpointer data) { 138 | struct wd_output *output = data; 139 | 140 | GdkWindow *window = gtk_widget_get_window(widget); 141 | cairo_region_t *region = cairo_region_create(); 142 | gdk_window_input_shape_combine_region(window, region, 0, 0); 143 | cairo_region_destroy(region); 144 | 145 | struct wl_surface *surface = gdk_wayland_window_get_wl_surface(window); 146 | 147 | output->overlay_layer_surface = zwlr_layer_shell_v1_get_layer_surface( 148 | output->state->layer_shell, surface, output->wl_output, 149 | ZWLR_LAYER_SHELL_V1_LAYER_TOP, "output-overlay"); 150 | 151 | zwlr_layer_surface_v1_add_listener(output->overlay_layer_surface, 152 | &layer_surface_listener, output); 153 | 154 | zwlr_layer_surface_v1_set_anchor(output->overlay_layer_surface, 155 | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | 156 | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); 157 | 158 | resize(output); 159 | } 160 | 161 | void window_unmap(GtkWidget *widget, gpointer data) { 162 | struct wd_output *output = data; 163 | zwlr_layer_surface_v1_destroy(output->overlay_layer_surface); 164 | } 165 | 166 | gboolean window_draw(GtkWidget *widget, cairo_t *cr, gpointer data) { 167 | struct wd_output *output = data; 168 | struct wd_head *head = wd_find_head(output->state, output); 169 | 170 | GtkStyleContext *style_ctx = gtk_widget_get_style_context(widget); 171 | GdkRGBA fg; 172 | gtk_style_context_get_color(style_ctx, GTK_STATE_FLAG_NORMAL, &fg); 173 | 174 | int width = gtk_widget_get_allocated_width(widget); 175 | int height = gtk_widget_get_allocated_height(widget); 176 | gtk_render_background(style_ctx, cr, 0, 0, width, height); 177 | 178 | GtkBorder padding; 179 | gtk_style_context_get_padding(style_ctx, GTK_STATE_FLAG_NORMAL, &padding); 180 | PangoContext *pango = gtk_widget_get_pango_context(widget); 181 | PangoLayout *layout = create_text_layout(head, pango, style_ctx); 182 | 183 | gdk_cairo_set_source_rgba(cr, &fg); 184 | cairo_move_to(cr, padding.left, padding.top); 185 | pango_cairo_show_layout(cr, layout); 186 | g_object_unref(layout); 187 | return TRUE; 188 | } 189 | 190 | void wd_create_overlay(struct wd_output *output) { 191 | output->overlay_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); 192 | gtk_window_set_decorated(GTK_WINDOW(output->overlay_window), FALSE); 193 | gtk_window_set_resizable(GTK_WINDOW(output->overlay_window), FALSE); 194 | gtk_widget_add_events(output->overlay_window, GDK_STRUCTURE_MASK); 195 | 196 | g_signal_connect(output->overlay_window, "realize", 197 | G_CALLBACK(window_realize), output); 198 | g_signal_connect(output->overlay_window, "map", 199 | G_CALLBACK(window_map), output); 200 | g_signal_connect(output->overlay_window, "unmap", 201 | G_CALLBACK(window_unmap), output); 202 | g_signal_connect(output->overlay_window, "draw", 203 | G_CALLBACK(window_draw), output); 204 | 205 | GtkStyleContext *style_ctx = gtk_widget_get_style_context( 206 | output->overlay_window); 207 | gtk_style_context_add_class(style_ctx, "output-overlay"); 208 | gtk_widget_show(output->overlay_window); 209 | } 210 | 211 | void wd_destroy_overlay(struct wd_output *output) { 212 | if (output->overlay_window != NULL) { 213 | gtk_widget_destroy(output->overlay_window); 214 | output->overlay_window = NULL; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/render.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 cyclopsian 3 | 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY 19 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | 24 | #include "wdisplays.h" 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #define BT_UV_VERT_SIZE (2 + 2) 33 | #define BT_UV_QUAD_SIZE (6 * BT_UV_VERT_SIZE) 34 | #define BT_UV_MAX (BT_COLOR_QUAD_SIZE * HEADS_MAX) 35 | 36 | #define BT_COLOR_VERT_SIZE (2 + 4) 37 | #define BT_COLOR_QUAD_SIZE (6 * BT_COLOR_VERT_SIZE) 38 | #define BT_COLOR_MAX (BT_COLOR_QUAD_SIZE * HEADS_MAX) 39 | 40 | #define BT_LINE_VERT_SIZE (2 + 4) 41 | #define BT_LINE_QUAD_SIZE (8 * BT_LINE_VERT_SIZE) 42 | #define BT_LINE_EXT_SIZE (24 * BT_LINE_VERT_SIZE) 43 | #define BT_LINE_MAX (BT_LINE_EXT_SIZE * (HEADS_MAX + 1)) 44 | 45 | enum gl_buffers { 46 | TEXTURE_BUFFER, 47 | COLOR_BUFFER, 48 | LINE_BUFFER, 49 | NUM_BUFFERS 50 | }; 51 | 52 | struct wd_gl_data { 53 | GLuint color_program; 54 | GLuint color_vertex_shader; 55 | GLuint color_fragment_shader; 56 | GLuint color_position_attribute; 57 | GLuint color_color_attribute; 58 | GLuint color_screen_size_uniform; 59 | 60 | GLuint texture_program; 61 | GLuint texture_vertex_shader; 62 | GLuint texture_fragment_shader; 63 | GLuint texture_position_attribute; 64 | GLuint texture_uv_attribute; 65 | GLuint texture_screen_size_uniform; 66 | GLuint texture_texture_uniform; 67 | GLuint texture_color_transform_uniform; 68 | 69 | GLuint buffers[NUM_BUFFERS]; 70 | 71 | unsigned texture_count; 72 | GLuint textures[HEADS_MAX]; 73 | 74 | float verts[BT_LINE_MAX]; 75 | }; 76 | 77 | static const char *color_vertex_shader_src = "\ 78 | attribute vec2 position;\n\ 79 | attribute vec4 color;\n\ 80 | varying vec4 color_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 | color_out = color;\n\ 86 | }"; 87 | 88 | static const char *color_fragment_shader_src = "\ 89 | varying vec4 color_out;\n\ 90 | void main(void) {\n\ 91 | gl_FragColor = color_out;\n\ 92 | }"; 93 | 94 | static const char *texture_vertex_shader_src = "\ 95 | attribute vec2 position;\n\ 96 | attribute vec2 uv;\n\ 97 | varying vec2 uv_out;\n\ 98 | uniform vec2 screen_size;\n\ 99 | void main(void) {\n\ 100 | vec2 screen_pos = (position / screen_size * 2. - 1.) * vec2(1., -1.);\n\ 101 | gl_Position = vec4(screen_pos, 0., 1.);\n\ 102 | uv_out = uv;\n\ 103 | }"; 104 | 105 | static const char *texture_fragment_shader_src = "\ 106 | varying vec2 uv_out;\n\ 107 | uniform sampler2D texture;\n\ 108 | uniform mat4 color_transform;\n\ 109 | void main(void) {\n\ 110 | gl_FragColor = texture2D(texture, uv_out) * color_transform;\n\ 111 | }"; 112 | 113 | static GLuint gl_make_shader(GLenum type, const char *src) { 114 | GLuint shader = glCreateShader(type); 115 | glShaderSource(shader, 1, &src, NULL); 116 | glCompileShader(shader); 117 | GLint status; 118 | glGetShaderiv(shader, GL_COMPILE_STATUS, &status); 119 | if (status == GL_FALSE) { 120 | GLsizei length; 121 | glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length); 122 | GLchar *log = "Failed"; 123 | if (length > 0) { 124 | log = malloc(length); 125 | glGetShaderInfoLog(shader, length, NULL, log); 126 | } 127 | fprintf(stderr, "glCompileShader: %s\n", log); 128 | if (length > 0) { 129 | free(log); 130 | } 131 | } 132 | return shader; 133 | } 134 | 135 | static void gl_link_and_validate(GLint program) { 136 | GLint status; 137 | 138 | glLinkProgram(program); 139 | glGetProgramiv(program, GL_LINK_STATUS, &status); 140 | if (status == GL_FALSE) { 141 | GLsizei length; 142 | glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length); 143 | GLchar *log = malloc(length); 144 | glGetProgramInfoLog(program, length, NULL, log); 145 | fprintf(stderr, "glLinkProgram: %s\n", log); 146 | free(log); 147 | return; 148 | } 149 | glValidateProgram(program); 150 | glGetProgramiv(program, GL_VALIDATE_STATUS, &status); 151 | if (status == GL_FALSE) { 152 | GLsizei length; 153 | glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length); 154 | GLchar *log = malloc(length); 155 | glGetProgramInfoLog(program, length, NULL, log); 156 | fprintf(stderr, "glValidateProgram: %s\n", log); 157 | free(log); 158 | } 159 | } 160 | 161 | struct wd_gl_data *wd_gl_setup(void) { 162 | struct wd_gl_data *res = calloc(1, sizeof(struct wd_gl_data)); 163 | res->color_program = glCreateProgram(); 164 | 165 | res->color_vertex_shader = gl_make_shader(GL_VERTEX_SHADER, 166 | color_vertex_shader_src); 167 | glAttachShader(res->color_program, res->color_vertex_shader); 168 | res->color_fragment_shader = gl_make_shader(GL_FRAGMENT_SHADER, 169 | color_fragment_shader_src); 170 | glAttachShader(res->color_program, res->color_fragment_shader); 171 | gl_link_and_validate(res->color_program); 172 | 173 | res->color_position_attribute = glGetAttribLocation(res->color_program, 174 | "position"); 175 | res->color_color_attribute = glGetAttribLocation(res->color_program, 176 | "color"); 177 | res->color_screen_size_uniform = glGetUniformLocation(res->color_program, 178 | "screen_size"); 179 | 180 | res->texture_program = glCreateProgram(); 181 | 182 | res->texture_vertex_shader = gl_make_shader(GL_VERTEX_SHADER, 183 | texture_vertex_shader_src); 184 | glAttachShader(res->texture_program, res->texture_vertex_shader); 185 | res->texture_fragment_shader = gl_make_shader(GL_FRAGMENT_SHADER, 186 | texture_fragment_shader_src); 187 | glAttachShader(res->texture_program, res->texture_fragment_shader); 188 | gl_link_and_validate(res->texture_program); 189 | 190 | res->texture_position_attribute = glGetAttribLocation(res->texture_program, 191 | "position"); 192 | res->texture_uv_attribute = glGetAttribLocation(res->texture_program, 193 | "uv"); 194 | res->texture_screen_size_uniform = glGetUniformLocation(res->texture_program, 195 | "screen_size"); 196 | res->texture_texture_uniform = glGetUniformLocation(res->texture_program, 197 | "texture"); 198 | res->texture_color_transform_uniform = glGetUniformLocation( 199 | res->texture_program, "color_transform"); 200 | 201 | glGenBuffers(NUM_BUFFERS, res->buffers); 202 | glBindBuffer(GL_ARRAY_BUFFER, res->buffers[TEXTURE_BUFFER]); 203 | glBufferData(GL_ARRAY_BUFFER, BT_UV_MAX * sizeof(float), 204 | NULL, GL_DYNAMIC_DRAW); 205 | 206 | glBindBuffer(GL_ARRAY_BUFFER, res->buffers[COLOR_BUFFER]); 207 | glBufferData(GL_ARRAY_BUFFER, BT_COLOR_MAX * sizeof(float), 208 | NULL, GL_DYNAMIC_DRAW); 209 | 210 | glBindBuffer(GL_ARRAY_BUFFER, res->buffers[LINE_BUFFER]); 211 | glBufferData(GL_ARRAY_BUFFER, BT_LINE_MAX * sizeof(float), 212 | NULL, GL_DYNAMIC_DRAW); 213 | 214 | return res; 215 | } 216 | 217 | static const GLfloat TRANSFORM_RGB[16] = { 218 | 1, 0, 0, 0, 219 | 0, 1, 0, 0, 220 | 0, 0, 1, 0, 221 | 0, 0, 0, 1}; 222 | 223 | static const GLfloat TRANSFORM_BGR[16] = { 224 | 0, 0, 1, 0, 225 | 0, 1, 0, 0, 226 | 1, 0, 0, 0, 227 | 0, 0, 0, 1}; 228 | 229 | #define PUSH_POINT_COLOR(_start, _a, _b, _color, _alpha) \ 230 | *((_start)++) = (_a);\ 231 | *((_start)++) = (_b);\ 232 | *((_start)++) = ((_color)[0]);\ 233 | *((_start)++) = ((_color)[1]);\ 234 | *((_start)++) = ((_color)[2]);\ 235 | *((_start)++) = (_alpha); 236 | 237 | #define PUSH_POINT_UV(_start, _a, _b, _c, _d) \ 238 | *((_start)++) = (_a);\ 239 | *((_start)++) = (_b);\ 240 | *((_start)++) = (_c);\ 241 | *((_start)++) = (_d); 242 | 243 | static inline float lerp(float x, float y, float a) { 244 | return x * (1.f - a) + y * a; 245 | } 246 | 247 | static inline void lerp_color(float out[3], float x[3], float y[3], float a) { 248 | out[0] = lerp(x[0], y[0], a); 249 | out[1] = lerp(x[1], y[1], a); 250 | out[2] = lerp(x[2], y[2], a); 251 | out[3] = lerp(x[3], y[3], a); 252 | } 253 | 254 | static inline float ease(float d) { 255 | d *= 2.f; 256 | if (d <= 1.f) { 257 | d = d * d; 258 | } else { 259 | d -= 1.f; 260 | d = d * (2.f - d) + 1.f; 261 | } 262 | d /= 2.f; 263 | return d; 264 | } 265 | 266 | void wd_gl_render(struct wd_gl_data *res, struct wd_render_data *info, 267 | uint64_t tick) { 268 | unsigned int tri_verts = 0; 269 | 270 | unsigned int head_count = wl_list_length(&info->heads); 271 | if (head_count >= HEADS_MAX) 272 | head_count = HEADS_MAX; 273 | 274 | if (head_count > res->texture_count) { 275 | glGenTextures(head_count - res->texture_count, 276 | res->textures + res->texture_count); 277 | for (int i = res->texture_count; i < head_count; i++) { 278 | glBindTexture(GL_TEXTURE_2D, res->textures[i]); 279 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, 280 | GL_LINEAR_MIPMAP_LINEAR); 281 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 282 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 283 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 284 | } 285 | glBindTexture(GL_TEXTURE_2D, 0); 286 | res->texture_count = head_count; 287 | } 288 | 289 | struct wd_render_head_data *head; 290 | int i = 0; 291 | wl_list_for_each_reverse(head, &info->heads, link) { 292 | float *tri_ptr = res->verts + i * BT_UV_QUAD_SIZE; 293 | float x1 = head->active.x_invert ? head->x2 : head->x1; 294 | float y1 = head->y_invert ? head->y2 : head->y1; 295 | float x2 = head->active.x_invert ? head->x1 : head->x2; 296 | float y2 = head->y_invert ? head->y1 : head->y2; 297 | 298 | float sa = 0.f; 299 | float sb = 1.f; 300 | float sc = sb; 301 | float sd = sa; 302 | float ta = 0.f; 303 | float tb = ta; 304 | float tc = 1.f; 305 | float td = tc; 306 | for (int i = 0; i < head->active.rotation; i++) { 307 | float tmp = sd; 308 | sd = sc; 309 | sc = sb; 310 | sb = sa; 311 | sa = tmp; 312 | 313 | tmp = td; 314 | td = tc; 315 | tc = tb; 316 | tb = ta; 317 | ta = tmp; 318 | } 319 | 320 | PUSH_POINT_UV(tri_ptr, x1, y1, sa, ta) 321 | PUSH_POINT_UV(tri_ptr, x2, y1, sb, tb) 322 | PUSH_POINT_UV(tri_ptr, x1, y2, sd, td) 323 | PUSH_POINT_UV(tri_ptr, x1, y2, sd, td) 324 | PUSH_POINT_UV(tri_ptr, x2, y1, sb, tb) 325 | PUSH_POINT_UV(tri_ptr, x2, y2, sc, tc) 326 | 327 | tri_verts += 6; 328 | i++; 329 | if (i >= HEADS_MAX) 330 | break; 331 | } 332 | 333 | glClearColor(info->bg_color[0], info->bg_color[1], info->bg_color[2], 1.f); 334 | glClear(GL_COLOR_BUFFER_BIT); 335 | 336 | float screen_size[2] = { info->viewport_width, info->viewport_height }; 337 | 338 | if (tri_verts > 0) { 339 | glUseProgram(res->texture_program); 340 | glBindBuffer(GL_ARRAY_BUFFER, res->buffers[TEXTURE_BUFFER]); 341 | glBufferSubData(GL_ARRAY_BUFFER, 0, 342 | tri_verts * BT_UV_VERT_SIZE * sizeof(float), res->verts); 343 | glEnableVertexAttribArray(res->texture_position_attribute); 344 | glEnableVertexAttribArray(res->texture_uv_attribute); 345 | glVertexAttribPointer(res->texture_position_attribute, 346 | 2, GL_FLOAT, GL_FALSE, 347 | BT_UV_VERT_SIZE * sizeof(float), (void *) (0 * sizeof(float))); 348 | glVertexAttribPointer(res->texture_uv_attribute, 2, GL_FLOAT, GL_FALSE, 349 | BT_UV_VERT_SIZE * sizeof(float), (void *) (2 * sizeof(float))); 350 | glUniform2fv(res->texture_screen_size_uniform, 1, screen_size); 351 | glUniform1i(res->texture_texture_uniform, 0); 352 | glActiveTexture(GL_TEXTURE0); 353 | 354 | i = 0; 355 | wl_list_for_each_reverse(head, &info->heads, link) { 356 | glBindTexture(GL_TEXTURE_2D, res->textures[i]); 357 | if (head->updated_at == tick) { 358 | glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, head->tex_stride / 4); 359 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 360 | head->tex_width, head->tex_height, 361 | 0, GL_RGBA, GL_UNSIGNED_BYTE, head->pixels); 362 | glPixelStorei(GL_UNPACK_ROW_LENGTH_EXT, 0); 363 | glGenerateMipmap(GL_TEXTURE_2D); 364 | } 365 | glUniformMatrix4fv(res->texture_color_transform_uniform, 1, GL_FALSE, 366 | head->swap_rgb ? TRANSFORM_RGB : TRANSFORM_BGR); 367 | glDrawArrays(GL_TRIANGLES, i * 6, 6); 368 | i++; 369 | if (i >= HEADS_MAX) 370 | break; 371 | } 372 | } 373 | 374 | tri_verts = 0; 375 | 376 | int j = 0; 377 | i = 0; 378 | bool any_clicked = false; 379 | uint64_t click_begin = 0; 380 | wl_list_for_each_reverse(head, &info->heads, link) { 381 | any_clicked = head->clicked || any_clicked; 382 | if (head->click_begin > click_begin) 383 | click_begin = head->click_begin; 384 | if (head->hovered || tick < head->hover_begin + HOVER_USECS) { 385 | float *tri_ptr = res->verts + j++ * BT_COLOR_QUAD_SIZE; 386 | float x1 = head->x1; 387 | float y1 = head->y1; 388 | float x2 = head->x2; 389 | float y2 = head->y2; 390 | 391 | float *color = info->selection_color; 392 | float d = fminf( 393 | (tick - head->hover_begin) / (double) HOVER_USECS, 1.f); 394 | if (!head->hovered) 395 | d = 1.f - d; 396 | float alpha = color[3] * ease(d) * .5f; 397 | 398 | PUSH_POINT_COLOR(tri_ptr, x1, y1, color, alpha) 399 | PUSH_POINT_COLOR(tri_ptr, x2, y1, color, alpha) 400 | PUSH_POINT_COLOR(tri_ptr, x1, y2, color, alpha) 401 | PUSH_POINT_COLOR(tri_ptr, x1, y2, color, alpha) 402 | PUSH_POINT_COLOR(tri_ptr, x2, y1, color, alpha) 403 | PUSH_POINT_COLOR(tri_ptr, x2, y2, color, alpha) 404 | 405 | tri_verts += 6; 406 | } 407 | i++; 408 | if (i >= HEADS_MAX) 409 | break; 410 | } 411 | 412 | if (tri_verts > 0) { 413 | glEnable(GL_BLEND); 414 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 415 | glUseProgram(res->color_program); 416 | glBindBuffer(GL_ARRAY_BUFFER, res->buffers[COLOR_BUFFER]); 417 | glBufferSubData(GL_ARRAY_BUFFER, 0, 418 | tri_verts * BT_COLOR_VERT_SIZE * sizeof(float), res->verts); 419 | glEnableVertexAttribArray(res->color_position_attribute); 420 | glEnableVertexAttribArray(res->color_color_attribute); 421 | glVertexAttribPointer(res->color_position_attribute, 2, GL_FLOAT, GL_FALSE, 422 | BT_COLOR_VERT_SIZE * sizeof(float), (void *) (0 * sizeof(float))); 423 | glVertexAttribPointer(res->color_color_attribute, 4, GL_FLOAT, GL_FALSE, 424 | BT_COLOR_VERT_SIZE * sizeof(float), (void *) (2 * sizeof(float))); 425 | glUniform2fv(res->color_screen_size_uniform, 1, screen_size); 426 | glDrawArrays(GL_TRIANGLES, 0, tri_verts); 427 | glDisable(GL_BLEND); 428 | } 429 | 430 | unsigned int line_verts = 0; 431 | i = 0; 432 | float *line_ptr = res->verts; 433 | if (any_clicked || (click_begin && tick < click_begin + HOVER_USECS)) { 434 | const float ox = -info->scroll_x - info->x_origin; 435 | const float oy = -info->scroll_y - info->y_origin; 436 | const float sx = screen_size[0]; 437 | const float sy = screen_size[1]; 438 | 439 | float color[4]; 440 | lerp_color(color, info->selection_color, info->fg_color, .5f); 441 | float d = fminf( 442 | (tick - click_begin) / (double) HOVER_USECS, 1.f); 443 | if (!any_clicked) 444 | d = 1.f - d; 445 | float alpha = color[3] * ease(d) * .5f; 446 | 447 | PUSH_POINT_COLOR(line_ptr, ox, oy, color, alpha) 448 | PUSH_POINT_COLOR(line_ptr, sx, oy, color, alpha) 449 | PUSH_POINT_COLOR(line_ptr, ox, oy, color, alpha) 450 | PUSH_POINT_COLOR(line_ptr, ox, sy, color, alpha) 451 | 452 | line_verts += 4; 453 | } 454 | wl_list_for_each(head, &info->heads, link) { 455 | float x1 = head->x1; 456 | float y1 = head->y1; 457 | float x2 = head->x2; 458 | float y2 = head->y2; 459 | 460 | float *color = info->fg_color; 461 | float alpha = color[3] * (head->clicked ? .5f : .25f); 462 | 463 | PUSH_POINT_COLOR(line_ptr, x1, y1, color, alpha) 464 | PUSH_POINT_COLOR(line_ptr, x2, y1, color, alpha) 465 | PUSH_POINT_COLOR(line_ptr, x2, y1, color, alpha) 466 | PUSH_POINT_COLOR(line_ptr, x2, y2, color, alpha) 467 | PUSH_POINT_COLOR(line_ptr, x2, y2, color, alpha) 468 | PUSH_POINT_COLOR(line_ptr, x1, y2, color, alpha) 469 | PUSH_POINT_COLOR(line_ptr, x1, y2, color, alpha) 470 | PUSH_POINT_COLOR(line_ptr, x1, y1, color, alpha) 471 | 472 | line_verts += 8; 473 | 474 | if (any_clicked || (click_begin && tick < click_begin + HOVER_USECS)) { 475 | float d = fminf( 476 | (tick - click_begin) / (double) HOVER_USECS, 1.f); 477 | if (!any_clicked) 478 | d = 1.f - d; 479 | alpha = color[3] * ease(d) * (head->clicked ? .15f : .075f); 480 | 481 | const float sx = screen_size[0]; 482 | const float sy = screen_size[1]; 483 | 484 | PUSH_POINT_COLOR(line_ptr, 0, y1, color, alpha) 485 | PUSH_POINT_COLOR(line_ptr, x1, y1, color, alpha) 486 | PUSH_POINT_COLOR(line_ptr, x1, 0, color, alpha) 487 | PUSH_POINT_COLOR(line_ptr, x1, y1, color, alpha) 488 | 489 | PUSH_POINT_COLOR(line_ptr, sx, y1, color, alpha) 490 | PUSH_POINT_COLOR(line_ptr, x2, y1, color, alpha) 491 | PUSH_POINT_COLOR(line_ptr, x2, 0, color, alpha) 492 | PUSH_POINT_COLOR(line_ptr, x2, y1, color, alpha) 493 | 494 | PUSH_POINT_COLOR(line_ptr, sx, y2, color, alpha) 495 | PUSH_POINT_COLOR(line_ptr, x2, y2, color, alpha) 496 | PUSH_POINT_COLOR(line_ptr, x2, sy, color, alpha) 497 | PUSH_POINT_COLOR(line_ptr, x2, y2, color, alpha) 498 | 499 | PUSH_POINT_COLOR(line_ptr, 0, y2, color, alpha) 500 | PUSH_POINT_COLOR(line_ptr, x1, y2, color, alpha) 501 | PUSH_POINT_COLOR(line_ptr, x1, sy, color, alpha) 502 | PUSH_POINT_COLOR(line_ptr, x1, y2, color, alpha) 503 | 504 | line_verts += 16; 505 | } 506 | 507 | i++; 508 | if (i >= HEADS_MAX) 509 | break; 510 | } 511 | 512 | if (line_verts > 0) { 513 | glEnable(GL_BLEND); 514 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 515 | glUseProgram(res->color_program); 516 | glBindBuffer(GL_ARRAY_BUFFER, res->buffers[LINE_BUFFER]); 517 | glBufferSubData(GL_ARRAY_BUFFER, 0, 518 | line_verts * BT_LINE_VERT_SIZE * sizeof(float), res->verts); 519 | glEnableVertexAttribArray(res->color_position_attribute); 520 | glEnableVertexAttribArray(res->color_color_attribute); 521 | glVertexAttribPointer(res->color_position_attribute, 2, GL_FLOAT, GL_FALSE, 522 | BT_LINE_VERT_SIZE * sizeof(float), (void *) (0 * sizeof(float))); 523 | glVertexAttribPointer(res->color_color_attribute, 4, GL_FLOAT, GL_FALSE, 524 | BT_LINE_VERT_SIZE * sizeof(float), (void *) (2 * sizeof(float))); 525 | glUniform2fv(res->color_screen_size_uniform, 1, screen_size); 526 | glDrawArrays(GL_LINES, 0, line_verts); 527 | glDisable(GL_BLEND); 528 | } 529 | } 530 | 531 | void wd_gl_cleanup(struct wd_gl_data *res) { 532 | glDeleteBuffers(NUM_BUFFERS, res->buffers); 533 | glDeleteShader(res->texture_fragment_shader); 534 | glDeleteShader(res->texture_vertex_shader); 535 | glDeleteProgram(res->texture_program); 536 | 537 | glDeleteShader(res->color_fragment_shader); 538 | glDeleteShader(res->color_vertex_shader); 539 | glDeleteProgram(res->color_program); 540 | 541 | free(res); 542 | } 543 | -------------------------------------------------------------------------------- /src/wdisplays.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 cyclopsian 3 | * Copyright (C) 2017-2019 emersion 4 | 5 | * Permission is hereby granted, free of charge, to any person obtaining 6 | * a copy of this software and associated documentation files (the 7 | * "Software"), to deal in the Software without restriction, including 8 | * without limitation the rights to use, copy, modify, merge, publish, 9 | * distribute, sublicense, and/or sell copies of the Software, and to 10 | * permit persons to whom the Software is furnished to do so, subject to 11 | * the following conditions: 12 | 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY 20 | * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | /* 26 | * Parts of this file are taken from emersion/kanshi: 27 | * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/include/kanshi.h 28 | * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/include/config.h 29 | */ 30 | 31 | #ifndef WDISPLAY_WDISPLAY_H 32 | #define WDISPLAY_WDISPLAY_H 33 | 34 | #define HEADS_MAX 64 35 | #define HOVER_USECS (100 * 1000) 36 | 37 | #include 38 | #include 39 | 40 | struct zxdg_output_v1; 41 | struct zxdg_output_manager_v1; 42 | struct zwlr_output_mode_v1; 43 | struct zwlr_output_head_v1; 44 | struct zwlr_output_manager_v1; 45 | struct zwlr_screencopy_manager_v1; 46 | struct zwlr_screencopy_frame_v1; 47 | struct zwlr_layer_shell_v1; 48 | struct zwlr_layer_surface_v1; 49 | 50 | struct _GtkWidget; 51 | typedef struct _GtkWidget GtkWidget; 52 | struct _GtkBuilder; 53 | typedef struct _GtkBuilder GtkBuilder; 54 | struct _GdkCursor; 55 | typedef struct _GdkCursor GdkCursor; 56 | struct _cairo_surface; 57 | typedef struct _cairo_surface cairo_surface_t; 58 | 59 | enum wd_head_fields { 60 | WD_FIELD_NAME = 1 << 0, 61 | WD_FIELD_ENABLED = 1 << 1, 62 | WD_FIELD_DESCRIPTION = 1 << 2, 63 | WD_FIELD_PHYSICAL_SIZE = 1 << 3, 64 | WD_FIELD_SCALE = 1 << 4, 65 | WD_FIELD_POSITION = 1 << 5, 66 | WD_FIELD_MODE = 1 << 6, 67 | WD_FIELD_TRANSFORM = 1 << 7, 68 | WD_FIELDS_ALL = (1 << 8) - 1 69 | }; 70 | 71 | struct wd_output { 72 | struct wd_state *state; 73 | struct zxdg_output_v1 *xdg_output; 74 | struct wl_output *wl_output; 75 | struct wl_list link; 76 | 77 | char *name; 78 | struct wl_list frames; 79 | GtkWidget *overlay_window; 80 | struct zwlr_layer_surface_v1 *overlay_layer_surface; 81 | }; 82 | 83 | struct wd_frame { 84 | struct wd_output *output; 85 | struct zwlr_screencopy_frame_v1 *wlr_frame; 86 | 87 | struct wl_list link; 88 | int capture_fd; 89 | unsigned stride; 90 | unsigned width; 91 | unsigned height; 92 | struct wl_shm_pool *pool; 93 | struct wl_buffer *buffer; 94 | uint8_t *pixels; 95 | uint64_t tick; 96 | bool y_invert; 97 | bool swap_rgb; 98 | }; 99 | 100 | struct wd_head_config { 101 | struct wl_list link; 102 | 103 | struct wd_head *head; 104 | bool enabled; 105 | int32_t width; 106 | int32_t height; 107 | int32_t refresh; // mHz 108 | int32_t x; 109 | int32_t y; 110 | double scale; 111 | enum wl_output_transform transform; 112 | }; 113 | 114 | struct wd_mode { 115 | struct wd_head *head; 116 | struct zwlr_output_mode_v1 *wlr_mode; 117 | struct wl_list link; 118 | 119 | int32_t width, height; 120 | int32_t refresh; // mHz 121 | bool preferred; 122 | }; 123 | 124 | struct wd_head { 125 | struct wd_state *state; 126 | struct zwlr_output_head_v1 *wlr_head; 127 | struct wl_list link; 128 | 129 | struct wd_output *output; 130 | struct wd_render_head_data *render; 131 | cairo_surface_t *surface; 132 | 133 | uint32_t id; 134 | char *name, *description; 135 | int32_t phys_width, phys_height; // mm 136 | struct wl_list modes; 137 | 138 | bool enabled; 139 | struct wd_mode *mode; 140 | struct { 141 | int32_t width, height; 142 | int32_t refresh; 143 | } custom_mode; 144 | int32_t x, y; 145 | enum wl_output_transform transform; 146 | double scale; 147 | }; 148 | 149 | struct wd_gl_data; 150 | 151 | struct wd_render_head_flags { 152 | uint8_t rotation; 153 | bool x_invert; 154 | }; 155 | 156 | struct wd_render_head_data { 157 | struct wl_list link; 158 | uint64_t updated_at; 159 | uint64_t hover_begin; 160 | uint64_t click_begin; 161 | 162 | float x1; 163 | float y1; 164 | float x2; 165 | float y2; 166 | 167 | struct wd_render_head_flags queued; 168 | struct wd_render_head_flags active; 169 | 170 | uint8_t *pixels; 171 | unsigned tex_stride; 172 | unsigned tex_width; 173 | unsigned tex_height; 174 | 175 | bool preview; 176 | bool y_invert; 177 | bool swap_rgb; 178 | bool hovered; 179 | bool clicked; 180 | }; 181 | 182 | struct wd_render_data { 183 | float fg_color[4]; 184 | float bg_color[4]; 185 | float border_color[4]; 186 | float selection_color[4]; 187 | unsigned int viewport_width; 188 | unsigned int viewport_height; 189 | unsigned int width; 190 | unsigned int height; 191 | int scroll_x; 192 | int scroll_y; 193 | int x_origin; 194 | int y_origin; 195 | uint64_t updated_at; 196 | 197 | struct wl_list heads; 198 | }; 199 | 200 | struct wd_point { 201 | double x; 202 | double y; 203 | }; 204 | 205 | struct wd_state { 206 | struct zxdg_output_manager_v1 *xdg_output_manager; 207 | struct zwlr_output_manager_v1 *output_manager; 208 | struct zwlr_screencopy_manager_v1 *copy_manager; 209 | struct zwlr_layer_shell_v1 *layer_shell; 210 | struct wl_shm *shm; 211 | struct wl_list heads; 212 | struct wl_list outputs; 213 | uint32_t serial; 214 | 215 | bool apply_pending; 216 | bool autoapply; 217 | bool capture; 218 | bool show_overlay; 219 | double zoom; 220 | 221 | struct wd_render_head_data *clicked; 222 | /* top left, bottom right */ 223 | struct wd_point click_offset; 224 | bool panning; 225 | struct wd_point pan_last; 226 | 227 | GtkWidget *header_stack; 228 | GtkWidget *stack_switcher; 229 | GtkWidget *stack; 230 | GtkWidget *scroller; 231 | GtkWidget *canvas; 232 | GtkWidget *spinner; 233 | GtkWidget *zoom_out; 234 | GtkWidget *zoom_reset; 235 | GtkWidget *zoom_in; 236 | GtkWidget *overlay; 237 | GtkWidget *info_bar; 238 | GtkWidget *info_label; 239 | GtkWidget *menu_button; 240 | 241 | GdkCursor *grab_cursor; 242 | GdkCursor *grabbing_cursor; 243 | GdkCursor *move_cursor; 244 | 245 | unsigned int canvas_tick; 246 | struct wd_gl_data *gl_data; 247 | struct wd_render_data render; 248 | }; 249 | 250 | 251 | /* 252 | * Creates the application state structure. 253 | */ 254 | struct wd_state *wd_state_create(void); 255 | 256 | /* 257 | * Frees the application state structure. 258 | */ 259 | void wd_state_destroy(struct wd_state *state); 260 | 261 | /* 262 | * Displays an error message and then exits the program. 263 | */ 264 | void wd_fatal_error(int status, const char *message); 265 | 266 | /* 267 | * Add an output to the list of screen captured outputs. 268 | */ 269 | void wd_add_output(struct wd_state *state, struct wl_output *wl_output, struct wl_display *display); 270 | 271 | /* 272 | * Remove an output from the list of screen captured outputs. 273 | */ 274 | void wd_remove_output(struct wd_state *state, struct wl_output *wl_output, struct wl_display *display); 275 | 276 | /* 277 | * Finds the output associated with a given head. Can return NULL if the head's 278 | * output is disabled. 279 | */ 280 | struct wd_output *wd_find_output(struct wd_state *state, struct wd_head *head); 281 | 282 | /* 283 | * Finds the head associated with a given output. 284 | */ 285 | struct wd_head *wd_find_head(struct wd_state *state, struct wd_output *output); 286 | /* 287 | * Starts listening for output management events from the compositor. 288 | */ 289 | void wd_add_output_management_listener(struct wd_state *state, struct wl_display *display); 290 | 291 | /* 292 | * Sends updated display configuration back to the compositor. 293 | */ 294 | void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs, struct wl_display *display); 295 | 296 | /* 297 | * Queues capture of the next frame of all screens. 298 | */ 299 | void wd_capture_frame(struct wd_state *state); 300 | 301 | /* 302 | * Blocks until all captures are finished. 303 | */ 304 | void wd_capture_wait(struct wd_state *state, struct wl_display *display); 305 | 306 | /* 307 | * Updates the UI stack of all heads. Does not update individual head forms. 308 | * Useful for when a display is plugged/unplugged and we want to add/remove 309 | * a page, but we don't want to wipe out user's changes on the other pages. 310 | */ 311 | void wd_ui_reset_heads(struct wd_state *state); 312 | 313 | /* 314 | * Updates the UI form for a single head. Useful for when the compositor 315 | * notifies us of updated configuration caused by another program. 316 | */ 317 | void wd_ui_reset_head(const struct wd_head *head, unsigned int fields); 318 | 319 | /* 320 | * Updates the stack and all forms to the last known server state. 321 | */ 322 | void wd_ui_reset_all(struct wd_state *state); 323 | 324 | /* 325 | * Reactivates the GUI after the display configuration updates. 326 | */ 327 | void wd_ui_apply_done(struct wd_state *state, struct wl_list *outputs); 328 | 329 | /* 330 | * Reactivates the GUI after the display configuration updates. 331 | */ 332 | void wd_ui_show_error(struct wd_state *state, const char *message); 333 | 334 | /* 335 | * Compiles the GL shaders. 336 | */ 337 | struct wd_gl_data *wd_gl_setup(void); 338 | 339 | /* 340 | * Renders the GL scene. 341 | */ 342 | void wd_gl_render(struct wd_gl_data *res, struct wd_render_data *info, uint64_t tick); 343 | 344 | /* 345 | * Destroys the GL shaders. 346 | */ 347 | void wd_gl_cleanup(struct wd_gl_data *res); 348 | 349 | /* 350 | * Create an overlay on the screen that contains a textual description of the 351 | * output. This is to help the user identify the outputs visually. 352 | */ 353 | void wd_create_overlay(struct wd_output *output); 354 | 355 | /* 356 | * Forces redrawing of the screen overlay on the given output. 357 | */ 358 | void wd_redraw_overlay(struct wd_output *output); 359 | 360 | /* 361 | * Destroys the screen overlay on the given output. 362 | */ 363 | void wd_destroy_overlay(struct wd_output *output); 364 | 365 | #endif 366 | -------------------------------------------------------------------------------- /wdisplays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelAquilina/wdisplays/b4a2f3be9603719a9b7091acc7f9fb465d940459/wdisplays.png --------------------------------------------------------------------------------