1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "ScreenHelperGTK.h"
8 
9 #ifdef MOZ_X11
10 #  include <gdk/gdkx.h>
11 #endif /* MOZ_X11 */
12 #ifdef MOZ_WAYLAND
13 #  include <gdk/gdkwayland.h>
14 #endif /* MOZ_WAYLAND */
15 #include <dlfcn.h>
16 #include <gtk/gtk.h>
17 
18 #include "gfxPlatformGtk.h"
19 #include "mozilla/dom/DOMTypes.h"
20 #include "mozilla/Logging.h"
21 #include "mozilla/WidgetUtilsGtk.h"
22 #include "nsGtkUtils.h"
23 #include "nsTArray.h"
24 
25 namespace mozilla {
26 namespace widget {
27 
28 #ifdef MOZ_LOGGING
29 static LazyLogModule sScreenLog("WidgetScreen");
30 #  define LOG_SCREEN(args) MOZ_LOG(sScreenLog, LogLevel::Debug, args)
31 #else
32 #  define LOG_SCREEN(args)
33 #endif /* MOZ_LOGGING */
34 
35 using GdkMonitor = struct _GdkMonitor;
36 
37 static UniquePtr<ScreenGetter> gScreenGetter;
38 
monitors_changed(GdkScreen * aScreen,gpointer aClosure)39 static void monitors_changed(GdkScreen* aScreen, gpointer aClosure) {
40   LOG_SCREEN(("Received monitors-changed event"));
41   ScreenGetterGtk* self = static_cast<ScreenGetterGtk*>(aClosure);
42   self->RefreshScreens();
43 }
44 
screen_resolution_changed(GdkScreen * aScreen,GParamSpec * aPspec,ScreenGetterGtk * self)45 static void screen_resolution_changed(GdkScreen* aScreen, GParamSpec* aPspec,
46                                       ScreenGetterGtk* self) {
47   self->RefreshScreens();
48 }
49 
root_window_event_filter(GdkXEvent * aGdkXEvent,GdkEvent * aGdkEvent,gpointer aClosure)50 static GdkFilterReturn root_window_event_filter(GdkXEvent* aGdkXEvent,
51                                                 GdkEvent* aGdkEvent,
52                                                 gpointer aClosure) {
53 #ifdef MOZ_X11
54   ScreenGetterGtk* self = static_cast<ScreenGetterGtk*>(aClosure);
55   XEvent* xevent = static_cast<XEvent*>(aGdkXEvent);
56 
57   switch (xevent->type) {
58     case PropertyNotify: {
59       XPropertyEvent* propertyEvent = &xevent->xproperty;
60       if (propertyEvent->atom == self->NetWorkareaAtom()) {
61         LOG_SCREEN(("Work area size changed"));
62         self->RefreshScreens();
63       }
64     } break;
65     default:
66       break;
67   }
68 #endif
69 
70   return GDK_FILTER_CONTINUE;
71 }
72 
ScreenGetterGtk()73 ScreenGetterGtk::ScreenGetterGtk()
74     : mRootWindow(nullptr)
75 #ifdef MOZ_X11
76       ,
77       mNetWorkareaAtom(0)
78 #endif
79 {
80 }
81 
Init()82 void ScreenGetterGtk::Init() {
83   LOG_SCREEN(("ScreenGetterGtk created"));
84   GdkScreen* defaultScreen = gdk_screen_get_default();
85   if (!defaultScreen) {
86     // Sometimes we don't initial X (e.g., xpcshell)
87     MOZ_LOG(sScreenLog, LogLevel::Debug,
88             ("defaultScreen is nullptr, running headless"));
89     return;
90   }
91   mRootWindow = gdk_get_default_root_window();
92   MOZ_ASSERT(mRootWindow);
93 
94   g_object_ref(mRootWindow);
95 
96   // GDK_PROPERTY_CHANGE_MASK ==> PropertyChangeMask, for PropertyNotify
97   gdk_window_set_events(mRootWindow,
98                         GdkEventMask(gdk_window_get_events(mRootWindow) |
99                                      GDK_PROPERTY_CHANGE_MASK));
100 
101   g_signal_connect(defaultScreen, "monitors-changed",
102                    G_CALLBACK(monitors_changed), this);
103   // Use _after to ensure this callback is run after gfxPlatformGtk.cpp's
104   // handler.
105   g_signal_connect_after(defaultScreen, "notify::resolution",
106                          G_CALLBACK(screen_resolution_changed), this);
107 #ifdef MOZ_X11
108   gdk_window_add_filter(mRootWindow, root_window_event_filter, this);
109   if (GdkIsX11Display()) {
110     mNetWorkareaAtom = XInternAtom(GDK_WINDOW_XDISPLAY(mRootWindow),
111                                    "_NET_WORKAREA", X11False);
112   }
113 #endif
114   RefreshScreens();
115 }
116 
~ScreenGetterGtk()117 ScreenGetterGtk::~ScreenGetterGtk() {
118   if (mRootWindow) {
119     g_signal_handlers_disconnect_by_data(gdk_screen_get_default(), this);
120 
121     gdk_window_remove_filter(mRootWindow, root_window_event_filter, this);
122     g_object_unref(mRootWindow);
123     mRootWindow = nullptr;
124   }
125 }
126 
GetGTKPixelDepth()127 static uint32_t GetGTKPixelDepth() {
128   GdkVisual* visual = gdk_screen_get_system_visual(gdk_screen_get_default());
129   return gdk_visual_get_depth(visual);
130 }
131 
IsGNOMECompositor()132 static bool IsGNOMECompositor() {
133   const char* currentDesktop = getenv("XDG_CURRENT_DESKTOP");
134   return currentDesktop && strstr(currentDesktop, "GNOME") != nullptr;
135 }
136 
MakeScreenGtk(GdkScreen * aScreen,gint aMonitorNum)137 static already_AddRefed<Screen> MakeScreenGtk(GdkScreen* aScreen,
138                                               gint aMonitorNum) {
139   gint gdkScaleFactor = ScreenHelperGTK::GetGTKMonitorScaleFactor(aMonitorNum);
140 
141   // gdk_screen_get_monitor_geometry / workarea returns application pixels
142   // (desktop pixels), so we need to convert it to device pixels with
143   // gdkScaleFactor on X11.
144   // GNOME/Wayland reports scales differently (Bug 1732682).
145   gint geometryScaleFactor = 1;
146   if (GdkIsX11Display() || (GdkIsWaylandDisplay() && !IsGNOMECompositor())) {
147     geometryScaleFactor = gdkScaleFactor;
148   }
149 
150   LayoutDeviceIntRect rect;
151 
152   GdkRectangle workarea;
153   gdk_screen_get_monitor_workarea(aScreen, aMonitorNum, &workarea);
154   LayoutDeviceIntRect availRect(workarea.x * geometryScaleFactor,
155                                 workarea.y * geometryScaleFactor,
156                                 workarea.width * geometryScaleFactor,
157                                 workarea.height * geometryScaleFactor);
158   if (GdkIsX11Display()) {
159     GdkRectangle monitor;
160     gdk_screen_get_monitor_geometry(aScreen, aMonitorNum, &monitor);
161     rect = LayoutDeviceIntRect(monitor.x * geometryScaleFactor,
162                                monitor.y * geometryScaleFactor,
163                                monitor.width * geometryScaleFactor,
164                                monitor.height * geometryScaleFactor);
165   } else {
166     // We use Gtk workarea on Wayland as it matches our needs (Bug 1732682).
167     rect = availRect;
168   }
169 
170   uint32_t pixelDepth = GetGTKPixelDepth();
171 
172   // Use per-monitor scaling factor in gtk/wayland, or 1.0 otherwise.
173   DesktopToLayoutDeviceScale contentsScale(1.0);
174 #ifdef MOZ_WAYLAND
175   if (GdkIsWaylandDisplay()) {
176     contentsScale.scale = gdkScaleFactor;
177   }
178 #endif
179 
180   CSSToLayoutDeviceScale defaultCssScale(gdkScaleFactor *
181                                          gfxPlatformGtk::GetFontScaleFactor());
182 
183   float dpi = 96.0f;
184   gint heightMM = gdk_screen_get_monitor_height_mm(aScreen, aMonitorNum);
185   if (heightMM > 0) {
186     dpi = rect.height / (heightMM / MM_PER_INCH_FLOAT);
187   }
188 
189   LOG_SCREEN(
190       ("New monitor %d size [%d,%d -> %d x %d] depth %d scale %f CssScale %f  "
191        "DPI %f ]",
192        aMonitorNum, rect.x, rect.y, rect.width, rect.height, pixelDepth,
193        contentsScale.scale, defaultCssScale.scale, dpi));
194   return MakeAndAddRef<Screen>(rect, availRect, pixelDepth, pixelDepth,
195                                contentsScale, defaultCssScale, dpi);
196 }
197 
RefreshScreens()198 void ScreenGetterGtk::RefreshScreens() {
199   LOG_SCREEN(("Refreshing screens"));
200   AutoTArray<RefPtr<Screen>, 4> screenList;
201 
202   GdkScreen* defaultScreen = gdk_screen_get_default();
203   gint numScreens = gdk_screen_get_n_monitors(defaultScreen);
204   LOG_SCREEN(("GDK reports %d screens", numScreens));
205 
206   for (gint i = 0; i < numScreens; i++) {
207     screenList.AppendElement(MakeScreenGtk(defaultScreen, i));
208   }
209 
210   ScreenManager::Refresh(std::move(screenList));
211 }
212 
213 #ifdef MOZ_WAYLAND
output_handle_geometry(void * data,struct wl_output * wl_output,int x,int y,int physical_width,int physical_height,int subpixel,const char * make,const char * model,int32_t transform)214 static void output_handle_geometry(void* data, struct wl_output* wl_output,
215                                    int x, int y, int physical_width,
216                                    int physical_height, int subpixel,
217                                    const char* make, const char* model,
218                                    int32_t transform) {
219   MonitorConfig* monitor = (MonitorConfig*)data;
220   LOG_SCREEN(("wl_output: geometry position %d %d physical size %d %d", x, y,
221               physical_width, physical_height));
222   monitor->x = x;
223   monitor->y = y;
224   monitor->width_mm = physical_width;
225   monitor->height_mm = physical_height;
226 }
227 
output_handle_done(void * data,struct wl_output * wl_output)228 static void output_handle_done(void* data, struct wl_output* wl_output) {
229   LOG_SCREEN(("done"));
230   gScreenGetter->RefreshScreens();
231 }
232 
output_handle_scale(void * data,struct wl_output * wl_output,int32_t scale)233 static void output_handle_scale(void* data, struct wl_output* wl_output,
234                                 int32_t scale) {
235   MonitorConfig* monitor = (MonitorConfig*)data;
236   LOG_SCREEN(("wl_output: scale %d", scale));
237   monitor->scale = scale;
238 }
239 
output_handle_mode(void * data,struct wl_output * wl_output,uint32_t flags,int width,int height,int refresh)240 static void output_handle_mode(void* data, struct wl_output* wl_output,
241                                uint32_t flags, int width, int height,
242                                int refresh) {
243   MonitorConfig* monitor = (MonitorConfig*)data;
244 
245   LOG_SCREEN(("wl_output: mode output size %d x %d refresh %d", width, height,
246               refresh));
247 
248   if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) return;
249 
250   monitor->width = width;
251   monitor->height = height;
252 }
253 
254 static const struct wl_output_listener output_listener = {
255     output_handle_geometry,
256     output_handle_mode,
257     output_handle_done,
258     output_handle_scale,
259 };
260 
screen_registry_handler(void * data,wl_registry * registry,uint32_t id,const char * interface,uint32_t version)261 static void screen_registry_handler(void* data, wl_registry* registry,
262                                     uint32_t id, const char* interface,
263                                     uint32_t version) {
264   ScreenGetterWayland* getter = static_cast<ScreenGetterWayland*>(data);
265   if (strcmp(interface, "wl_output") == 0 && version > 1) {
266     auto* output =
267         WaylandRegistryBind<wl_output>(registry, id, &wl_output_interface, 2);
268     wl_output_add_listener(output, &output_listener,
269                            getter->AddMonitorConfig(id));
270   }
271 }
272 
screen_registry_remover(void * data,struct wl_registry * registry,uint32_t id)273 static void screen_registry_remover(void* data, struct wl_registry* registry,
274                                     uint32_t id) {
275   auto* getter = static_cast<ScreenGetterWayland*>(data);
276   if (getter->RemoveMonitorConfig(id)) {
277     getter->RefreshScreens();
278   }
279   /* TODO: the object needs to be destroyed here, we're leaking */
280 }
281 
282 static const struct wl_registry_listener screen_registry_listener = {
283     screen_registry_handler, screen_registry_remover};
284 
Init()285 void ScreenGetterWayland::Init() {
286   MOZ_ASSERT(GdkIsWaylandDisplay());
287   LOG_SCREEN(("ScreenGetterWayland created"));
288   wl_display* display = WaylandDisplayGetWLDisplay();
289   mRegistry = wl_display_get_registry(display);
290   wl_registry_add_listener((wl_registry*)mRegistry, &screen_registry_listener,
291                            this);
292   wl_display_roundtrip(display);
293   wl_display_roundtrip(display);
294 }
295 
AddMonitorConfig(int aId)296 MonitorConfig* ScreenGetterWayland::AddMonitorConfig(int aId) {
297   mMonitors.EmplaceBack(aId);
298   LOG_SCREEN(("Add Monitor ID %d num %d", aId, (int)(mMonitors.Length() - 1)));
299   return &(mMonitors[mMonitors.Length() - 1]);
300 }
301 
RemoveMonitorConfig(int aId)302 bool ScreenGetterWayland::RemoveMonitorConfig(int aId) {
303   for (unsigned int i = 0; i < mMonitors.Length(); i++) {
304     if (mMonitors[i].id == aId) {
305       LOG_SCREEN(("Remove Monitor ID %d num %d", aId, i));
306       mMonitors.RemoveElementAt(i);
307       return true;
308     }
309   }
310   return false;
311 }
312 
~ScreenGetterWayland()313 ScreenGetterWayland::~ScreenGetterWayland() {
314   g_clear_pointer(&mRegistry, wl_registry_destroy);
315 }
316 
GdkMonitorGetWorkarea(GdkMonitor * monitor,GdkRectangle * workarea)317 static bool GdkMonitorGetWorkarea(GdkMonitor* monitor, GdkRectangle* workarea) {
318   static auto s_gdk_monitor_get_workarea =
319       (void (*)(GdkMonitor*, GdkRectangle*))dlsym(RTLD_DEFAULT,
320                                                   "gdk_monitor_get_workarea");
321   if (!s_gdk_monitor_get_workarea) {
322     return false;
323   }
324 
325   s_gdk_monitor_get_workarea(monitor, workarea);
326   return true;
327 }
328 
MakeScreenWayland(gint aMonitor)329 already_AddRefed<Screen> ScreenGetterWayland::MakeScreenWayland(gint aMonitor) {
330   MonitorConfig monitor = mMonitors[aMonitor];
331 
332   // On GNOME/Mutter we use results from wl_output directly
333   LayoutDeviceIntRect rect(monitor.x, monitor.y, monitor.width, monitor.height);
334 
335   uint32_t pixelDepth = GetGTKPixelDepth();
336 
337   // Use per-monitor scaling factor in gtk/wayland, or 1.0 otherwise.
338   DesktopToLayoutDeviceScale contentsScale(1.0);
339   contentsScale.scale = monitor.scale;
340 
341   CSSToLayoutDeviceScale defaultCssScale(monitor.scale *
342                                          gfxPlatformGtk::GetFontScaleFactor());
343 
344   float dpi = 96.0f;
345   gint heightMM = monitor.height_mm;
346   if (heightMM > 0) {
347     dpi = rect.height / (heightMM / MM_PER_INCH_FLOAT);
348   }
349 
350   LOG_SCREEN(
351       ("Monitor %d [%d %d -> %d x %d depth %d content scale %f css scale %f "
352        "DPI %f]",
353        aMonitor, rect.x, rect.y, rect.width, rect.height, pixelDepth,
354        contentsScale.scale, defaultCssScale.scale, dpi));
355   return MakeAndAddRef<Screen>(rect, rect, pixelDepth, pixelDepth,
356                                contentsScale, defaultCssScale, dpi);
357 }
358 
RefreshScreens()359 void ScreenGetterWayland::RefreshScreens() {
360   LOG_SCREEN(("Refreshing screens"));
361   AutoTArray<RefPtr<Screen>, 4> managerScreenList;
362 
363   mScreenList.Clear();
364   const gint numScreens = mMonitors.Length();
365   for (gint i = 0; i < numScreens; i++) {
366     RefPtr<Screen> screen = MakeScreenWayland(i);
367     mScreenList.AppendElement(screen);
368     managerScreenList.AppendElement(screen);
369   }
370 
371   ScreenManager::Refresh(std::move(managerScreenList));
372 }
373 
GetMonitorForWindow(nsWindow * aWindow)374 int ScreenGetterWayland::GetMonitorForWindow(nsWindow* aWindow) {
375   LOG_SCREEN(("GetMonitorForWindow() [%p]", aWindow));
376 
377   static auto s_gdk_display_get_monitor_at_window =
378       (GdkMonitor * (*)(GdkDisplay*, GdkWindow*))
379           dlsym(RTLD_DEFAULT, "gdk_display_get_monitor_at_window");
380 
381   if (!s_gdk_display_get_monitor_at_window) {
382     LOG_SCREEN(("  failed, missing Gtk helpers"));
383     return -1;
384   }
385 
386   GdkWindow* gdkWindow = gtk_widget_get_window(aWindow->GetGtkWidget());
387   if (!gdkWindow) {
388     LOG_SCREEN(("  failed, can't get GdkWindow"));
389     return -1;
390   }
391 
392   GdkMonitor* monitor =
393       s_gdk_display_get_monitor_at_window(gdk_display_get_default(), gdkWindow);
394   if (!monitor) {
395     LOG_SCREEN(("  failed, can't get monitor for GdkWindow"));
396     return -1;
397   }
398 
399   GdkRectangle workArea;
400   if (!GdkMonitorGetWorkarea(monitor, &workArea)) {
401     return -1;
402   }
403 
404   for (unsigned int i = 0; i < mMonitors.Length(); i++) {
405     // Although Gtk/Mutter is very creative in reporting various screens sizes
406     // we can rely on Gtk work area start position to match wl_output.
407     if (mMonitors[i].x == workArea.x && mMonitors[i].y == workArea.y) {
408       LOG_SCREEN((" monitor %d values %d %d -> %d x %d", i, mMonitors[i].x,
409                   mMonitors[i].y, mMonitors[i].width, mMonitors[i].height));
410       return i;
411     }
412   }
413 
414   return -1;
415 }
416 
GetScreenForWindow(nsWindow * aWindow)417 RefPtr<nsIScreen> ScreenGetterWayland::GetScreenForWindow(nsWindow* aWindow) {
418   if (mScreenList.IsEmpty()) {
419     return nullptr;
420   }
421 
422   int monitor = GetMonitorForWindow(aWindow);
423   if (monitor < 0) {
424     return nullptr;
425   }
426   return mScreenList[monitor];
427 }
428 #endif
429 
GetScreenForWindow(nsWindow * aWindow)430 RefPtr<nsIScreen> ScreenHelperGTK::GetScreenForWindow(nsWindow* aWindow) {
431   return gScreenGetter->GetScreenForWindow(aWindow);
432 }
433 
GetGTKMonitorScaleFactor(gint aMonitorNum)434 gint ScreenHelperGTK::GetGTKMonitorScaleFactor(gint aMonitorNum) {
435   GdkScreen* screen = gdk_screen_get_default();
436   return gdk_screen_get_monitor_scale_factor(screen, aMonitorNum);
437 }
438 
ScreenHelperGTK()439 ScreenHelperGTK::ScreenHelperGTK() {
440 #ifdef MOZ_WAYLAND
441   // Use ScreenGetterWayland on Gnome/Mutter only. It uses additional wl_output
442   // to track screen size changes (which are wrongly reported by mutter)
443   // and causes issues on Sway (Bug 1730476).
444   // https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/3941
445   if (GdkIsWaylandDisplay() && IsGNOMECompositor()) {
446     gScreenGetter = mozilla::MakeUnique<ScreenGetterWayland>();
447   }
448 #endif
449   if (!gScreenGetter) {
450     gScreenGetter = mozilla::MakeUnique<ScreenGetterGtk>();
451   }
452   gScreenGetter->Init();
453 }
454 
~ScreenHelperGTK()455 ScreenHelperGTK::~ScreenHelperGTK() { gScreenGetter = nullptr; }
456 
457 }  // namespace widget
458 }  // namespace mozilla
459