├── .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 | [](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 | 
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 |
10 |
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 |
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 |
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 |
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
--------------------------------------------------------------------------------