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/. */
7 #include "ScreenHelperGTK.h"
10 # include <gdk/gdkx.h>
13 # include <gdk/gdkwayland.h>
14 #endif /* MOZ_WAYLAND */
18 #include "gfxPlatformGtk.h"
19 #include "mozilla/dom/DOMTypes.h"
20 #include "mozilla/Logging.h"
21 #include "mozilla/WidgetUtilsGtk.h"
22 #include "nsGtkUtils.h"
29 static LazyLogModule
sScreenLog("WidgetScreen");
30 # define LOG_SCREEN(args) MOZ_LOG(sScreenLog, LogLevel::Debug, args)
32 # define LOG_SCREEN(args)
33 #endif /* MOZ_LOGGING */
35 using GdkMonitor
= struct _GdkMonitor
;
37 static UniquePtr
<ScreenGetter
> gScreenGetter
;
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();
45 static void screen_resolution_changed(GdkScreen
* aScreen
, GParamSpec
* aPspec
,
46 ScreenGetterGtk
* self
) {
47 self
->RefreshScreens();
50 static GdkFilterReturn
root_window_event_filter(GdkXEvent
* aGdkXEvent
,
54 ScreenGetterGtk
* self
= static_cast<ScreenGetterGtk
*>(aClosure
);
55 XEvent
* xevent
= static_cast<XEvent
*>(aGdkXEvent
);
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();
70 return GDK_FILTER_CONTINUE
;
73 ScreenGetterGtk::ScreenGetterGtk()
74 : mRootWindow(nullptr)
82 void ScreenGetterGtk::Init() {
83 LOG_SCREEN(("ScreenGetterGtk created"));
84 GdkScreen
* defaultScreen
= gdk_screen_get_default();
86 // Sometimes we don't initial X (e.g., xpcshell)
87 MOZ_LOG(sScreenLog
, LogLevel::Debug
,
88 ("defaultScreen is nullptr, running headless"));
91 mRootWindow
= gdk_get_default_root_window();
92 MOZ_ASSERT(mRootWindow
);
94 g_object_ref(mRootWindow
);
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
));
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
105 g_signal_connect_after(defaultScreen
, "notify::resolution",
106 G_CALLBACK(screen_resolution_changed
), this);
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
);
117 ScreenGetterGtk::~ScreenGetterGtk() {
119 g_signal_handlers_disconnect_by_data(gdk_screen_get_default(), this);
121 gdk_window_remove_filter(mRootWindow
, root_window_event_filter
, this);
122 g_object_unref(mRootWindow
);
123 mRootWindow
= nullptr;
127 static uint32_t GetGTKPixelDepth() {
128 GdkVisual
* visual
= gdk_screen_get_system_visual(gdk_screen_get_default());
129 return gdk_visual_get_depth(visual
);
132 static bool IsGNOMECompositor() {
133 const char* currentDesktop
= getenv("XDG_CURRENT_DESKTOP");
134 return currentDesktop
&& strstr(currentDesktop
, "GNOME") != nullptr;
137 static already_AddRefed
<Screen
> MakeScreenGtk(GdkScreen
* aScreen
,
139 gint gdkScaleFactor
= ScreenHelperGTK::GetGTKMonitorScaleFactor(aMonitorNum
);
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
;
150 LayoutDeviceIntRect rect
;
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
);
166 // We use Gtk workarea on Wayland as it matches our needs (Bug 1732682).
170 uint32_t pixelDepth
= GetGTKPixelDepth();
172 // Use per-monitor scaling factor in gtk/wayland, or 1.0 otherwise.
173 DesktopToLayoutDeviceScale
contentsScale(1.0);
175 if (GdkIsWaylandDisplay()) {
176 contentsScale
.scale
= gdkScaleFactor
;
180 CSSToLayoutDeviceScale
defaultCssScale(gdkScaleFactor
*
181 gfxPlatformGtk::GetFontScaleFactor());
184 gint heightMM
= gdk_screen_get_monitor_height_mm(aScreen
, aMonitorNum
);
186 dpi
= rect
.height
/ (heightMM
/ MM_PER_INCH_FLOAT
);
190 ("New monitor %d size [%d,%d -> %d x %d] depth %d scale %f CssScale %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
);
198 void ScreenGetterGtk::RefreshScreens() {
199 LOG_SCREEN(("Refreshing screens"));
200 AutoTArray
<RefPtr
<Screen
>, 4> screenList
;
202 GdkScreen
* defaultScreen
= gdk_screen_get_default();
203 gint numScreens
= gdk_screen_get_n_monitors(defaultScreen
);
204 LOG_SCREEN(("GDK reports %d screens", numScreens
));
206 for (gint i
= 0; i
< numScreens
; i
++) {
207 screenList
.AppendElement(MakeScreenGtk(defaultScreen
, i
));
210 ScreenManager::Refresh(std::move(screenList
));
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
,
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
));
224 monitor
->width_mm
= physical_width
;
225 monitor
->height_mm
= physical_height
;
228 static void output_handle_done(void* data
, struct wl_output
* wl_output
) {
229 LOG_SCREEN(("done"));
230 gScreenGetter
->RefreshScreens();
233 static void output_handle_scale(void* data
, struct wl_output
* wl_output
,
235 MonitorConfig
* monitor
= (MonitorConfig
*)data
;
236 LOG_SCREEN(("wl_output: scale %d", scale
));
237 monitor
->scale
= scale
;
240 static void output_handle_mode(void* data
, struct wl_output
* wl_output
,
241 uint32_t flags
, int width
, int height
,
243 MonitorConfig
* monitor
= (MonitorConfig
*)data
;
245 LOG_SCREEN(("wl_output: mode output size %d x %d refresh %d", width
, height
,
248 if ((flags
& WL_OUTPUT_MODE_CURRENT
) == 0) return;
250 monitor
->width
= width
;
251 monitor
->height
= height
;
254 static const struct wl_output_listener output_listener
= {
255 output_handle_geometry
,
261 static void screen_registry_handler(void* data
, wl_registry
* registry
,
262 uint32_t id
, const char* interface
,
264 ScreenGetterWayland
* getter
= static_cast<ScreenGetterWayland
*>(data
);
265 if (strcmp(interface
, "wl_output") == 0 && version
> 1) {
267 WaylandRegistryBind
<wl_output
>(registry
, id
, &wl_output_interface
, 2);
268 wl_output_add_listener(output
, &output_listener
,
269 getter
->AddMonitorConfig(id
));
273 static void screen_registry_remover(void* data
, struct wl_registry
* registry
,
275 auto* getter
= static_cast<ScreenGetterWayland
*>(data
);
276 if (getter
->RemoveMonitorConfig(id
)) {
277 getter
->RefreshScreens();
279 /* TODO: the object needs to be destroyed here, we're leaking */
282 static const struct wl_registry_listener screen_registry_listener
= {
283 screen_registry_handler
, screen_registry_remover
};
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
,
292 wl_display_roundtrip(display
);
293 wl_display_roundtrip(display
);
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]);
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
);
313 ScreenGetterWayland::~ScreenGetterWayland() {
314 g_clear_pointer(&mRegistry
, wl_registry_destroy
);
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
) {
325 s_gdk_monitor_get_workarea(monitor
, workarea
);
329 already_AddRefed
<Screen
> ScreenGetterWayland::MakeScreenWayland(gint aMonitor
) {
330 MonitorConfig monitor
= mMonitors
[aMonitor
];
332 // On GNOME/Mutter we use results from wl_output directly
333 LayoutDeviceIntRect
rect(monitor
.x
, monitor
.y
, monitor
.width
, monitor
.height
);
335 uint32_t pixelDepth
= GetGTKPixelDepth();
337 // Use per-monitor scaling factor in gtk/wayland, or 1.0 otherwise.
338 DesktopToLayoutDeviceScale
contentsScale(1.0);
339 contentsScale
.scale
= monitor
.scale
;
341 CSSToLayoutDeviceScale
defaultCssScale(monitor
.scale
*
342 gfxPlatformGtk::GetFontScaleFactor());
345 gint heightMM
= monitor
.height_mm
;
347 dpi
= rect
.height
/ (heightMM
/ MM_PER_INCH_FLOAT
);
351 ("Monitor %d [%d %d -> %d x %d depth %d content scale %f css scale %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
);
359 void ScreenGetterWayland::RefreshScreens() {
360 LOG_SCREEN(("Refreshing screens"));
361 AutoTArray
<RefPtr
<Screen
>, 4> managerScreenList
;
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
);
371 ScreenManager::Refresh(std::move(managerScreenList
));
374 int ScreenGetterWayland::GetMonitorForWindow(nsWindow
* aWindow
) {
375 LOG_SCREEN(("GetMonitorForWindow() [%p]", aWindow
));
377 static auto s_gdk_display_get_monitor_at_window
=
378 (GdkMonitor
* (*)(GdkDisplay
*, GdkWindow
*))
379 dlsym(RTLD_DEFAULT
, "gdk_display_get_monitor_at_window");
381 if (!s_gdk_display_get_monitor_at_window
) {
382 LOG_SCREEN((" failed, missing Gtk helpers"));
386 GdkWindow
* gdkWindow
= gtk_widget_get_window(aWindow
->GetGtkWidget());
388 LOG_SCREEN((" failed, can't get GdkWindow"));
392 GdkMonitor
* monitor
=
393 s_gdk_display_get_monitor_at_window(gdk_display_get_default(), gdkWindow
);
395 LOG_SCREEN((" failed, can't get monitor for GdkWindow"));
399 GdkRectangle workArea
;
400 if (!GdkMonitorGetWorkarea(monitor
, &workArea
)) {
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
));
417 RefPtr
<nsIScreen
> ScreenGetterWayland::GetScreenForWindow(nsWindow
* aWindow
) {
418 if (mScreenList
.IsEmpty()) {
422 int monitor
= GetMonitorForWindow(aWindow
);
426 return mScreenList
[monitor
];
430 RefPtr
<nsIScreen
> ScreenHelperGTK::GetScreenForWindow(nsWindow
* aWindow
) {
431 return gScreenGetter
->GetScreenForWindow(aWindow
);
434 gint
ScreenHelperGTK::GetGTKMonitorScaleFactor(gint aMonitorNum
) {
435 GdkScreen
* screen
= gdk_screen_get_default();
436 return gdk_screen_get_monitor_scale_factor(screen
, aMonitorNum
);
439 ScreenHelperGTK::ScreenHelperGTK() {
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
>();
449 if (!gScreenGetter
) {
450 gScreenGetter
= mozilla::MakeUnique
<ScreenGetterGtk
>();
452 gScreenGetter
->Init();
455 ScreenHelperGTK::~ScreenHelperGTK() { gScreenGetter
= nullptr; }
457 } // namespace widget
458 } // namespace mozilla