1 /*
2 Interface to XRANDR
3 
4 Copyright 2017-2021 Alexander Kulak.
5 This file is part of alttab program.
6 
7 alttab is free software: you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
11 
12 alttab is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 GNU General Public License for more details.
16 
17 You should have received a copy of the GNU General Public License
18 along with alttab.  If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 #include <X11/Xlib.h>
22 #include <X11/extensions/Xrandr.h>
23 #include <stdbool.h>
24 #include <stdio.h>
25 #include <stdlib.h>
26 #include <stdio.h>
27 #include "alttab.h"
28 #include "util.h"
29 extern Globals g;
30 extern Display *dpy;
31 extern int scr;
32 extern Window root;
33 
34 // Documentation:
35 // https://cgit.freedesktop.org/xorg/proto/randrproto/tree/randrproto.txt
36 
37 // version requirements:
38 // RRGetScreenResourcesCurrent (1.3)
39 #define MAJ_REQ 1
40 #define MIN_REQ 3
41 
42 // PRIVATE
43 
44 // current output geometry list
45 // we alloc/free them only here
46 
47 //
48 // update outs, return nout or 0
49 //
randr_update_outputs(Window w,quad ** outs)50 static int randr_update_outputs(Window w, quad ** outs)
51 {
52     XRRScreenResources *scr_res;
53     XRROutputInfo *out_info;
54     XRRCrtcInfo *crtc_info;
55     int nout;
56     int out;                    // as returned by randr; may include inactive
57 
58     scr_res = XRRGetScreenResourcesCurrent(dpy, w);
59     if (scr_res == NULL)
60         return 0;
61     nout = 0;
62     (*outs) = NULL;
63     for (out = 0; out < scr_res->noutput; out++) {
64         out_info = XRRGetOutputInfo(dpy, scr_res, scr_res->outputs[out]);
65         if (out_info == NULL
66             || out_info->connection != RR_Connected || out_info->crtc == 0) {
67             XRRFreeOutputInfo(out_info); // does it neen NULL check?
68             continue;
69         }
70         crtc_info = XRRGetCrtcInfo(dpy, scr_res, out_info->crtc);
71         if (crtc_info == NULL)
72             continue;
73         (*outs) = realloc((*outs), (nout + 1) * sizeof(quad));
74         if ((*outs) == NULL)
75             return 0; // would be nice to free Infos
76         (*outs)[nout].x = crtc_info->x;
77         (*outs)[nout].y = crtc_info->y;
78         (*outs)[nout].w = crtc_info->width;
79         (*outs)[nout].h = crtc_info->height;
80         msg(0,
81             "output %d (%d by randr) crtc: +%d+%d %dx%d noutput %d npossible %d\n",
82             nout, out,
83             crtc_info->x, crtc_info->y, crtc_info->width, crtc_info->height,
84             crtc_info->noutput, crtc_info->npossible);
85         XRRFreeCrtcInfo(crtc_info);
86         XRRFreeOutputInfo(out_info);
87         nout++;
88     }
89     XRRFreeScreenResources(scr_res);
90     return nout;
91 }
92 
93 //
94 // return currently focused window and its geometry,
95 // or focus point (depending on vp_mode).
96 // res and fw must be allocated by caller.
97 //
x_get_activity_area(quad * res,Window * fw)98 static bool x_get_activity_area(quad * res, Window * fw)
99 {
100 #define VPM  g.option_vp_mode
101     int rtr;
102     Window root_ret, child_ret;
103     int root_x, root_y, win_x, win_y;
104     unsigned int mask_ret;
105 
106     *fw = 0;
107 
108     if (VPM == VP_FOCUS) {
109         if (XGetInputFocus(dpy, fw, &rtr) == 0 || (*fw) == 0) {
110             msg(-1, "can't recognize focused window\n");
111             return false;
112         }
113         if (!get_absolute_coordinates(*fw, res)) {
114             msg(-1, "can't get window 0x%lx absolute coordinates\n", (*fw));
115             return false;
116         }
117         msg(0, "focus in window 0x%lx (%dx%d +%d+%d)\n",
118             (*fw), res->w, res->h, res->x, res->y);
119         return true;
120     }
121 
122     if (VPM == VP_POINTER) {
123         if (!XQueryPointer(dpy, root, &root_ret, &child_ret,
124                            &root_x, &root_y, &win_x, &win_y, &mask_ret)) {
125             // pointer is not on the same screen as root
126             return false;
127         }
128         res->x = win_x;
129         res->y = win_y;
130         res->w = res->h = 1;
131         *fw = root;
132         msg(0, "pointer: +%d+%d\n", res->x, res->y);
133         return true;
134     }
135     // unknown vp_mode
136     return false;
137 }
138 
139 // PUBLIC
140 
141 //
142 // check for randr at runtime
143 // assuming (for now) it's available at build time
144 //
randrAvailable()145 bool randrAvailable()
146 {
147     int maj, min;
148     bool ok;
149     Status s = XRRQueryVersion(dpy, &maj, &min);
150     if (s != 0) {
151         ok = ((maj == MAJ_REQ && min >= MIN_REQ) || (maj > MAJ_REQ)
152             );
153     } else {
154         ok = false;
155     }
156     if (g.debug > 0) {
157         if (s == 0)
158             msg(0, "randr not available\n");
159         else
160             msg(0, "randr v. %d.%d available (%ssufficient)\n",
161                 maj, min, ok ? "" : "not ");
162     }
163     return ok;
164 }
165 
166 //
167 // return best viewport from randr 'point of view'
168 // of false if not found.
169 // res must be allocated by caller.
170 //
randrGetViewport(quad * res,bool * multihead)171 bool randrGetViewport(quad * res, bool * multihead)
172 {
173     Window fw = 0;
174     quad aq;                    // 'activity area': focused window geometry or pointer point
175     quad *oq = NULL;            // outputs geometries
176     int o, no;
177     int x1, x2, y1, y2, area;
178     quad lq;                    // largest cross-section at 1st stage
179     int largest_cross_area = 0; // its square
180     int best_1_stage_output = -1;
181     int best_2_stage_output = -1;
182     int smallest_2_stage_area;
183 
184     //no = randr_update_outputs(fw != 0 ? fw : root, &oq); // no fw here
185     no = randr_update_outputs(root, &oq);
186     if (no < 1) {
187         msg(0, "randr didn't detect any output\n");
188         *multihead = false;
189         free(oq);
190         return false;
191     }
192     if (no == 1) {
193         msg(0, "using single randr output as viewport\n");
194         *res = oq[0];
195         *multihead = false;
196         free(oq);
197         return true;
198     }
199 
200     *multihead = true;
201 
202     if (!x_get_activity_area(&aq, &fw)) {
203         msg(0, "failed to detect activity area, using first randr output\n");
204         *res = oq[0];
205         free(oq);
206         return true;
207     }
208 
209     for (o = 0; o < no; o++) {
210         if (rectangles_cross(aq, oq[o])) {
211             // this output has a common cross-section with activity area,
212             // now find this cross-section.
213             x1 = aq.x > oq[o].x ? aq.x : oq[o].x;
214             x2 = (aq.x + aq.w) <
215                 (oq[o].x + oq[o].w) ? (aq.x + aq.w) : (oq[o].x + oq[o].w);
216             y1 = aq.y > oq[o].y ? aq.y : oq[o].y;
217             y2 = (aq.y + aq.h) <
218                 (oq[o].y + oq[o].h) ? (aq.y + aq.h) : (oq[o].y + oq[o].h);
219             area = (x2 - x1) * (y2 - y1);
220             msg(0, "output %d cross-section: %d\n", o, area);
221             if (area > largest_cross_area) {
222                 best_1_stage_output = o;
223                 lq.x = x1;
224                 lq.y = y1;
225                 lq.w = x2 - x1;
226                 lq.h = y2 - y1;
227                 largest_cross_area = area;
228             }
229             /*
230                // disabled feature:
231                // total area of CRTCs which have cross-section with focused window
232                if (oq[o].x < res->x) res->x = oq[o].x;
233                int right_diff = (oq[o].x + oq[o].w) - (res->x + res->w);
234                if (right_diff > 0) res->w += right_diff;
235                if (oq[o].y < res->y) res->y = oq[o].y;
236                int bot_diff = (oq[o].y + oq[o].h) - (res->y + res->h);
237                if (bot_diff > 0) res->h += bot_diff;
238              */
239         } else {
240             msg(0, "output %d doesn't cross with activity area\n", o);
241         }
242     }                           // outputs
243 
244     if (best_1_stage_output == -1) {
245         msg(0,
246             "failed to find largest cross-section, using first randr output\n");
247         *res = oq[0];
248         free(oq);
249         return true;
250     }
251     // if best cross-area is shared with some other monitor,
252     // then smallest of these monitors is choosen.
253     smallest_2_stage_area =
254         oq[best_1_stage_output].w * oq[best_1_stage_output].h;
255     for (o = 0; o < no; o++) {
256         if (o == best_1_stage_output)
257             continue;
258         if (rectangles_cross(lq, oq[o])) {
259             area = oq[o].w * oq[o].h;
260             if (area < smallest_2_stage_area) {
261                 best_2_stage_output = o;
262                 smallest_2_stage_area = area;
263             }
264         }
265     }
266 
267     if (best_2_stage_output == -1) {
268         best_2_stage_output = best_1_stage_output;
269     } else {
270         msg(0,
271             "found better (smallest output) candidate: %d\n",
272             best_2_stage_output);
273     }
274 
275     // the single result here is  best_2_stage_output
276 
277     *res = oq[best_2_stage_output];
278     msg(0,
279         "best viewport from randr: %dx%d +%d+%d\n",
280         res->w, res->h, res->x, res->y);
281     free(oq);
282     return true;
283 }
284