/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:expandtab:shiftwidth=2:tabstop=2:
 */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsWindow.h"

#include <algorithm>
#include <cstdint>
#include <dlfcn.h>
#include <gdk/gdkkeysyms.h>
#include <wchar.h>

#include "VsyncSource.h"
#include "gfx2DGlue.h"
#include "gfxContext.h"
#include "gfxImageSurface.h"
#include "gfxPlatformGtk.h"
#include "gfxUtils.h"
#include "GLContextProvider.h"
#include "GLContext.h"
#include "GtkCompositorWidget.h"
#include "gtkdrawing.h"
#include "imgIContainer.h"
#include "InputData.h"
#include "mozilla/ArrayUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/Components.h"
#include "mozilla/GRefPtr.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/WheelEventBinding.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/gfx/gfxVars.h"
#include "mozilla/gfx/GPUProcessManager.h"
#include "mozilla/gfx/HelpersCairo.h"
#include "mozilla/layers/APZThreadUtils.h"
#include "mozilla/layers/LayersTypes.h"
#include "mozilla/layers/CompositorBridgeChild.h"
#include "mozilla/layers/CompositorBridgeParent.h"
#include "mozilla/layers/CompositorThread.h"
#include "mozilla/layers/KnowsCompositor.h"
#include "mozilla/layers/WebRenderBridgeChild.h"
#include "mozilla/layers/WebRenderLayerManager.h"
#include "mozilla/layers/APZInputBridge.h"
#include "mozilla/layers/IAPZCTreeManager.h"
#include "mozilla/Likely.h"
#include "mozilla/MiscEvents.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/NativeKeyBindingsType.h"
#include "mozilla/Preferences.h"
#include "mozilla/PresShell.h"
#include "mozilla/ProfilerLabels.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/StaticPrefs_apz.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/StaticPrefs_mozilla.h"
#include "mozilla/StaticPrefs_ui.h"
#include "mozilla/StaticPrefs_widget.h"
#include "mozilla/SwipeTracker.h"
#include "mozilla/TextEventDispatcher.h"
#include "mozilla/TextEvents.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/UniquePtrExtensions.h"
#include "mozilla/WidgetUtils.h"
#include "mozilla/WritingModes.h"
#ifdef MOZ_X11
#  include "mozilla/X11Util.h"
#endif
#include "mozilla/XREAppData.h"
#include "NativeKeyBindings.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsAppRunner.h"
#include "nsDragService.h"
#include "nsGTKToolkit.h"
#include "nsGtkKeyUtils.h"
#include "nsGtkCursors.h"
#include "nsGfxCIID.h"
#include "nsGtkUtils.h"
#include "nsIFile.h"
#include "nsIGSettingsService.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsImageToPixbuf.h"
#include "nsINode.h"
#include "nsIRollupListener.h"
#include "nsIScreenManager.h"
#include "nsIUserIdleServiceInternal.h"
#include "nsIWidgetListener.h"
#include "nsLayoutUtils.h"
#include "nsMenuPopupFrame.h"
#include "nsPresContext.h"
#include "nsShmImage.h"
#include "nsString.h"
#include "nsWidgetsCID.h"
#include "nsViewManager.h"
#include "nsXPLookAndFeel.h"
#include "prlink.h"
#include "Screen.h"
#include "ScreenHelperGTK.h"
#include "SystemTimeConverter.h"
#include "WidgetUtilsGtk.h"

#ifdef ACCESSIBILITY
#  include "mozilla/a11y/LocalAccessible.h"
#  include "mozilla/a11y/Platform.h"
#  include "nsAccessibilityService.h"
#endif

#ifdef MOZ_X11
#  include <gdk/gdkkeysyms-compat.h>
#  include <X11/Xatom.h>
#  include <X11/extensions/XShm.h>
#  include <X11/extensions/shape.h>
#  include "gfxXlibSurface.h"
#  include "GLContextGLX.h"  // for GLContextGLX::FindVisual()
#  include "GLContextEGL.h"  // for GLContextEGL::FindVisual()
#  include "WindowSurfaceX11Image.h"
#  include "WindowSurfaceX11SHM.h"
#endif
#ifdef MOZ_WAYLAND
#  include <gdk/gdkkeysyms-compat.h>
#  include "nsIClipboard.h"
#  include "nsView.h"
#endif

using namespace mozilla;
using namespace mozilla::gfx;
using namespace mozilla::layers;
using namespace mozilla::widget;
#ifdef MOZ_X11
using mozilla::gl::GLContextEGL;
using mozilla::gl::GLContextGLX;
#endif

// Don't put more than this many rects in the dirty region, just fluff
// out to the bounding-box if there are more
#define MAX_RECTS_IN_REGION 100

#if !GTK_CHECK_VERSION(3, 18, 0)

struct _GdkEventTouchpadPinch {
  GdkEventType type;
  GdkWindow* window;
  gint8 send_event;
  gint8 phase;
  gint8 n_fingers;
  guint32 time;
  gdouble x;
  gdouble y;
  gdouble dx;
  gdouble dy;
  gdouble angle_delta;
  gdouble scale;
  gdouble x_root, y_root;
  guint state;
};

typedef enum {
  GDK_TOUCHPAD_GESTURE_PHASE_BEGIN,
  GDK_TOUCHPAD_GESTURE_PHASE_UPDATE,
  GDK_TOUCHPAD_GESTURE_PHASE_END,
  GDK_TOUCHPAD_GESTURE_PHASE_CANCEL
} GdkTouchpadGesturePhase;

gint GDK_TOUCHPAD_GESTURE_MASK = 1 << 24;
GdkEventType GDK_TOUCHPAD_PINCH = static_cast<GdkEventType>(42);

#endif

const gint kEvents = GDK_TOUCHPAD_GESTURE_MASK | GDK_EXPOSURE_MASK |
                     GDK_STRUCTURE_MASK | GDK_VISIBILITY_NOTIFY_MASK |
                     GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK |
                     GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
                     GDK_SMOOTH_SCROLL_MASK | GDK_TOUCH_MASK | GDK_SCROLL_MASK |
                     GDK_POINTER_MOTION_MASK | GDK_PROPERTY_CHANGE_MASK;

/* utility functions */
static bool is_mouse_in_window(GdkWindow* aWindow, gdouble aMouseX,
                               gdouble aMouseY);
static nsWindow* get_window_for_gtk_widget(GtkWidget* widget);
static nsWindow* get_window_for_gdk_window(GdkWindow* window);
static GtkWidget* get_gtk_widget_for_gdk_window(GdkWindow* window);
static GdkCursor* get_gtk_cursor(nsCursor aCursor);
static GdkWindow* get_inner_gdk_window(GdkWindow* aWindow, gint x, gint y,
                                       gint* retx, gint* rety);

/* callbacks from widgets */
static gboolean expose_event_cb(GtkWidget* widget, cairo_t* cr);
static gboolean configure_event_cb(GtkWidget* widget, GdkEventConfigure* event);
static void widget_map_cb(GtkWidget* widget);
static void widget_unmap_cb(GtkWidget* widget);
static void widget_unrealize_cb(GtkWidget* widget);
static void size_allocate_cb(GtkWidget* widget, GtkAllocation* allocation);
static void toplevel_window_size_allocate_cb(GtkWidget* widget,
                                             GtkAllocation* allocation);
static gboolean delete_event_cb(GtkWidget* widget, GdkEventAny* event);
static gboolean enter_notify_event_cb(GtkWidget* widget,
                                      GdkEventCrossing* event);
static gboolean leave_notify_event_cb(GtkWidget* widget,
                                      GdkEventCrossing* event);
static gboolean motion_notify_event_cb(GtkWidget* widget,
                                       GdkEventMotion* event);
MOZ_CAN_RUN_SCRIPT static gboolean button_press_event_cb(GtkWidget* widget,
                                                         GdkEventButton* event);
static gboolean button_release_event_cb(GtkWidget* widget,
                                        GdkEventButton* event);
static gboolean focus_in_event_cb(GtkWidget* widget, GdkEventFocus* event);
static gboolean focus_out_event_cb(GtkWidget* widget, GdkEventFocus* event);
static gboolean key_press_event_cb(GtkWidget* widget, GdkEventKey* event);
static gboolean key_release_event_cb(GtkWidget* widget, GdkEventKey* event);
static gboolean property_notify_event_cb(GtkWidget* widget,
                                         GdkEventProperty* event);
static gboolean scroll_event_cb(GtkWidget* widget, GdkEventScroll* event);

static void hierarchy_changed_cb(GtkWidget* widget,
                                 GtkWidget* previous_toplevel);
static gboolean window_state_event_cb(GtkWidget* widget,
                                      GdkEventWindowState* event);
static void settings_xft_dpi_changed_cb(GtkSettings* settings,
                                        GParamSpec* pspec, nsWindow* data);
static void check_resize_cb(GtkContainer* container, gpointer user_data);
static void screen_composited_changed_cb(GdkScreen* screen, gpointer user_data);
static void widget_composited_changed_cb(GtkWidget* widget, gpointer user_data);

static void scale_changed_cb(GtkWidget* widget, GParamSpec* aPSpec,
                             gpointer aPointer);
static gboolean touch_event_cb(GtkWidget* aWidget, GdkEventTouch* aEvent);
static gboolean generic_event_cb(GtkWidget* widget, GdkEvent* aEvent);

static nsWindow* GetFirstNSWindowForGDKWindow(GdkWindow* aGdkWindow);

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
#ifdef MOZ_X11
static GdkFilterReturn popup_take_focus_filter(GdkXEvent* gdk_xevent,
                                               GdkEvent* event, gpointer data);
#endif /* MOZ_X11 */
#ifdef __cplusplus
}
#endif /* __cplusplus */

static gboolean drag_motion_event_cb(GtkWidget* aWidget,
                                     GdkDragContext* aDragContext, gint aX,
                                     gint aY, guint aTime, gpointer aData);
static void drag_leave_event_cb(GtkWidget* aWidget,
                                GdkDragContext* aDragContext, guint aTime,
                                gpointer aData);
static gboolean drag_drop_event_cb(GtkWidget* aWidget,
                                   GdkDragContext* aDragContext, gint aX,
                                   gint aY, guint aTime, gpointer aData);
static void drag_data_received_event_cb(GtkWidget* aWidget,
                                        GdkDragContext* aDragContext, gint aX,
                                        gint aY,
                                        GtkSelectionData* aSelectionData,
                                        guint aInfo, guint32 aTime,
                                        gpointer aData);

/* initialization static functions */
static nsresult initialize_prefs(void);

static guint32 sLastUserInputTime = GDK_CURRENT_TIME;

static SystemTimeConverter<guint32>& TimeConverter() {
  static SystemTimeConverter<guint32> sTimeConverterSingleton;
  return sTimeConverterSingleton;
}

bool nsWindow::sTransparentMainWindow = false;

// forward declare from mozgtk
extern "C" MOZ_EXPORT void mozgtk_linker_holder();

namespace mozilla {

#ifdef MOZ_X11
class CurrentX11TimeGetter {
 public:
  explicit CurrentX11TimeGetter(GdkWindow* aWindow)
      : mWindow(aWindow), mAsyncUpdateStart() {}

  guint32 GetCurrentTime() const { return gdk_x11_get_server_time(mWindow); }

  void GetTimeAsyncForPossibleBackwardsSkew(const TimeStamp& aNow) {
    // Check for in-flight request
    if (!mAsyncUpdateStart.IsNull()) {
      return;
    }
    mAsyncUpdateStart = aNow;

    Display* xDisplay = GDK_WINDOW_XDISPLAY(mWindow);
    Window xWindow = GDK_WINDOW_XID(mWindow);
    unsigned char c = 'a';
    Atom timeStampPropAtom = TimeStampPropAtom();
    XChangeProperty(xDisplay, xWindow, timeStampPropAtom, timeStampPropAtom, 8,
                    PropModeReplace, &c, 1);
    XFlush(xDisplay);
  }

  gboolean PropertyNotifyHandler(GtkWidget* aWidget, GdkEventProperty* aEvent) {
    if (aEvent->atom != gdk_x11_xatom_to_atom(TimeStampPropAtom())) {
      return FALSE;
    }

    guint32 eventTime = aEvent->time;
    TimeStamp lowerBound = mAsyncUpdateStart;

    TimeConverter().CompensateForBackwardsSkew(eventTime, lowerBound);
    mAsyncUpdateStart = TimeStamp();
    return TRUE;
  }

 private:
  static Atom TimeStampPropAtom() {
    return gdk_x11_get_xatom_by_name_for_display(gdk_display_get_default(),
                                                 "GDK_TIMESTAMP_PROP");
  }

  // This is safe because this class is stored as a member of mWindow and
  // won't outlive it.
  GdkWindow* mWindow;
  TimeStamp mAsyncUpdateStart;
};
#endif

}  // namespace mozilla

// The window from which the focus manager asks us to dispatch key events.
static nsWindow* gFocusWindow = nullptr;
static bool gBlockActivateEvent = false;
static bool gGlobalsInitialized = false;
static bool gUseAspectRatio = true;
static uint32_t gLastTouchID = 0;
// See Bug 1777269 for details. We don't know if the suspected leave notify
// event is a correct one when we get it.
// Store it and issue it later from enter notify event if it's correct,
// throw it away otherwise.
static GUniquePtr<GdkEventCrossing> sStoredLeaveNotifyEvent;

#define NS_WINDOW_TITLE_MAX_LENGTH 4095

// cursor cache
static GdkCursor* gCursorCache[eCursorCount];

// Sometimes this actually also includes the state of the modifier keys, but
// only the button state bits are used.
static guint gButtonState;

static inline int32_t GetBitmapStride(int32_t width) {
#if defined(MOZ_X11)
  return (width + 7) / 8;
#else
  return cairo_format_stride_for_width(CAIRO_FORMAT_A1, width);
#endif
}

static inline bool TimestampIsNewerThan(guint32 a, guint32 b) {
  // Timestamps are just the least significant bits of a monotonically
  // increasing function, and so the use of unsigned overflow arithmetic.
  return a - b <= G_MAXUINT32 / 2;
}

static void UpdateLastInputEventTime(void* aGdkEvent) {
  nsCOMPtr<nsIUserIdleServiceInternal> idleService =
      do_GetService("@mozilla.org/widget/useridleservice;1");
  if (idleService) {
    idleService->ResetIdleTimeOut(0);
  }

  guint timestamp = gdk_event_get_time(static_cast<GdkEvent*>(aGdkEvent));
  if (timestamp == GDK_CURRENT_TIME) return;

  sLastUserInputTime = timestamp;
}

// Don't set parent (transient for) if nothing changes.
// gtk_window_set_transient_for() blows up wl_subsurfaces used by aWindow
// even if aParent is the same.
static void GtkWindowSetTransientFor(GtkWindow* aWindow, GtkWindow* aParent) {
  GtkWindow* parent = gtk_window_get_transient_for(aWindow);
  if (parent != aParent) {
    gtk_window_set_transient_for(aWindow, aParent);
  }
}

#define gtk_window_set_transient_for(a, b)                         \
  {                                                                \
    MOZ_ASSERT_UNREACHABLE(                                        \
        "gtk_window_set_transient_for() can't be used directly."); \
  }

nsWindow::nsWindow()
    : mDestroyMutex("nsWindow::mDestroyMutex"),
      mIsDestroyed(false),
      mIsShown(false),
      mNeedsShow(false),
      mIsMapped(false),
      mEnabled(true),
      mCreated(false),
      mHandleTouchEvent(false),
      mIsDragPopup(false),
      mCompositedScreen(gdk_screen_is_composited(gdk_screen_get_default())),
      mIsAccelerated(false),
      mWindowShouldStartDragging(false),
      mHasMappedToplevel(false),
      mRetryPointerGrab(false),
      mPanInProgress(false),
      mDrawToContainer(false),
      mTitlebarBackdropState(false),
      mIsWaylandPanelWindow(false),
      mIsChildWindow(false),
      mAlwaysOnTop(false),
      mNoAutoHide(false),
      mIsTransparent(false),
      mHasReceivedSizeAllocate(false),
      mWidgetCursorLocked(false),
      mPopupTrackInHierarchy(false),
      mPopupTrackInHierarchyConfigured(false),
      mHiddenPopupPositioned(false),
      mTransparencyBitmapForTitlebar(false),
      mHasAlphaVisual(false),
      mPopupAnchored(false),
      mPopupContextMenu(false),
      mPopupMatchesLayout(false),
      mPopupChanged(false),
      mPopupTemporaryHidden(false),
      mPopupClosed(false),
      mPopupUseMoveToRect(false),
      mWaitingForMoveToRectCallback(false),
      mMovedAfterMoveToRect(false),
      mResizedAfterMoveToRect(false),
      mConfiguredClearColor(false),
      mGotNonBlankPaint(false),
      mNeedsToRetryCapturingMouse(false) {
  mWindowType = WindowType::Child;
  mSizeConstraints.mMaxSize = GetSafeWindowSize(mSizeConstraints.mMaxSize);

  if (!gGlobalsInitialized) {
    gGlobalsInitialized = true;

    // It's OK if either of these fail, but it may not be one day.
    initialize_prefs();

#ifdef MOZ_WAYLAND
    // Wayland provides clipboard data to application on focus-in event
    // so we need to init our clipboard hooks before we create window
    // and get focus.
    if (GdkIsWaylandDisplay()) {
      nsCOMPtr<nsIClipboard> clipboard =
          do_GetService("@mozilla.org/widget/clipboard;1");
      NS_ASSERTION(clipboard, "Failed to init clipboard!");
    }
#endif
  }
  // Dummy call to mozgtk to prevent the linker from removing
  // the dependency with --as-needed.
  // see toolkit/library/moz.build for details.
  mozgtk_linker_holder();
}

nsWindow::~nsWindow() {
  LOG("nsWindow::~nsWindow()");
  Destroy();
}

/* static */
void nsWindow::ReleaseGlobals() {
  for (auto& cursor : gCursorCache) {
    if (cursor) {
      g_object_unref(cursor);
      cursor = nullptr;
    }
  }
}

void nsWindow::DispatchActivateEvent(void) {
  NS_ASSERTION(mContainer || mIsDestroyed,
               "DispatchActivateEvent only intended for container windows");

#ifdef ACCESSIBILITY
  DispatchActivateEventAccessible();
#endif  // ACCESSIBILITY

  if (mWidgetListener) mWidgetListener->WindowActivated();
}

void nsWindow::DispatchDeactivateEvent() {
  if (mWidgetListener) {
    mWidgetListener->WindowDeactivated();
  }

#ifdef ACCESSIBILITY
  DispatchDeactivateEventAccessible();
#endif  // ACCESSIBILITY
}

void nsWindow::DispatchResized() {
  LOG("nsWindow::DispatchResized() size [%d, %d]", (int)(mBounds.width),
      (int)(mBounds.height));

  mNeedsDispatchSize = LayoutDeviceIntSize(-1, -1);
  if (mWidgetListener) {
    mWidgetListener->WindowResized(this, mBounds.width, mBounds.height);
  }
  if (mAttachedWidgetListener) {
    mAttachedWidgetListener->WindowResized(this, mBounds.width, mBounds.height);
  }
}

void nsWindow::MaybeDispatchResized() {
  if (mNeedsDispatchSize != LayoutDeviceIntSize(-1, -1) && !mIsDestroyed) {
    mBounds.SizeTo(mNeedsDispatchSize);
    // Check mBounds size
    if (mCompositorSession &&
        !wr::WindowSizeSanityCheck(mBounds.width, mBounds.height)) {
      gfxCriticalNoteOnce << "Invalid mBounds in MaybeDispatchResized "
                          << mBounds << " size state " << mSizeMode;
    }

    if (IsTopLevelWindowType()) {
      UpdateTopLevelOpaqueRegion();
    }

    // Notify the GtkCompositorWidget of a ClientSizeChange
    if (mCompositorWidgetDelegate) {
      mCompositorWidgetDelegate->NotifyClientSizeChanged(GetClientSize());
    }

    DispatchResized();
  }
}

nsIWidgetListener* nsWindow::GetListener() {
  return mAttachedWidgetListener ? mAttachedWidgetListener : mWidgetListener;
}

nsresult nsWindow::DispatchEvent(WidgetGUIEvent* aEvent,
                                 nsEventStatus& aStatus) {
#ifdef DEBUG
  debug_DumpEvent(stdout, aEvent->mWidget, aEvent, "something", 0);
#endif
  aStatus = nsEventStatus_eIgnore;
  nsIWidgetListener* listener = GetListener();
  if (listener) {
    aStatus = listener->HandleEvent(aEvent, mUseAttachedEvents);
  }

  return NS_OK;
}

void nsWindow::OnDestroy(void) {
  if (mOnDestroyCalled) return;

  mOnDestroyCalled = true;

  // Prevent deletion.
  nsCOMPtr<nsIWidget> kungFuDeathGrip = this;

  // release references to children, device context, toolkit + app shell
  nsBaseWidget::OnDestroy();

  // Remove association between this object and its parent and siblings.
  nsBaseWidget::Destroy();
  mParent = nullptr;

  NotifyWindowDestroyed();
}

bool nsWindow::AreBoundsSane() {
  // Check requested size, as mBounds might not have been updated.
  return !mLastSizeRequest.IsEmpty();
}

// Walk the list of child windows and call destroy on them.
void nsWindow::DestroyChildWindows() {
  LOG("nsWindow::DestroyChildWindows()");
  if (!mGdkWindow) {
    return;
  }
  while (GList* children = gdk_window_peek_children(mGdkWindow)) {
    GdkWindow* child = GDK_WINDOW(children->data);
    nsWindow* kid = get_window_for_gdk_window(child);
    if (kid) {
      kid->Destroy();
    }
  }
}

void nsWindow::Destroy() {
  MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());

  if (mIsDestroyed || !mCreated) {
    return;
  }

  LOG("nsWindow::Destroy\n");

  MutexAutoLock lock(mDestroyMutex);
  mIsDestroyed = true;
  mCreated = false;

  MozClearHandleID(mCompositorPauseTimeoutID, g_source_remove);

  ClearTransparencyBitmap();

#ifdef MOZ_WAYLAND
  // Shut down our local vsync source
  if (mWaylandVsyncSource) {
    mWaylandVsyncSource->Shutdown();
    mWaylandVsyncSource = nullptr;
  }
  mWaylandVsyncDispatcher = nullptr;
#endif

  /** Need to clean our LayerManager up while still alive */
  DestroyLayerManager();

  // Ensure any resources assigned to the window get cleaned up first
  // to avoid double-freeing.
  mSurfaceProvider.CleanupResources();

  g_signal_handlers_disconnect_by_data(gtk_settings_get_default(), this);

  nsIRollupListener* rollupListener = nsBaseWidget::GetActiveRollupListener();
  if (rollupListener) {
    nsCOMPtr<nsIWidget> rollupWidget = rollupListener->GetRollupWidget();
    if (static_cast<nsIWidget*>(this) == rollupWidget) {
      rollupListener->Rollup({});
    }
  }

  // dragService will be null after shutdown of the service manager.
  RefPtr<nsDragService> dragService = nsDragService::GetInstance();
  if (dragService && this == dragService->GetMostRecentDestWindow()) {
    dragService->ScheduleLeaveEvent();
  }

  NativeShow(false);

  if (mIMContext) {
    mIMContext->OnDestroyWindow(this);
  }

  // make sure that we remove ourself as the focus window
  if (gFocusWindow == this) {
    LOG("automatically losing focus...\n");
    gFocusWindow = nullptr;
  }

  if (sStoredLeaveNotifyEvent) {
    nsWindow* window =
        get_window_for_gdk_window(sStoredLeaveNotifyEvent->window);
    if (window == this) {
      sStoredLeaveNotifyEvent = nullptr;
    }
  }

  // We need to detach accessible object here because mContainer is a custom
  // widget and doesn't call gtk_widget_real_destroy() from destroy handler
  // as regular widgets.
  if (AtkObject* ac = gtk_widget_get_accessible(GTK_WIDGET(mContainer))) {
    gtk_accessible_set_widget(GTK_ACCESSIBLE(ac), nullptr);
  }

  gtk_widget_destroy(mShell);
  mShell = nullptr;
  mContainer = nullptr;

  MOZ_ASSERT(!mGdkWindow,
             "mGdkWindow should be NULL when mContainer is destroyed");

#ifdef ACCESSIBILITY
  if (mRootAccessible) {
    mRootAccessible = nullptr;
  }
#endif

  // Save until last because OnDestroy() may cause us to be deleted.
  OnDestroy();
}

nsIWidget* nsWindow::GetParent() { return mParent; }

float nsWindow::GetDPI() {
  float dpi = 96.0f;
  nsCOMPtr<nsIScreen> screen = GetWidgetScreen();
  if (screen) {
    screen->GetDpi(&dpi);
  }
  return dpi;
}

double nsWindow::GetDefaultScaleInternal() { return FractionalScaleFactor(); }

DesktopToLayoutDeviceScale nsWindow::GetDesktopToDeviceScale() {
#ifdef MOZ_WAYLAND
  if (GdkIsWaylandDisplay()) {
    return DesktopToLayoutDeviceScale(FractionalScaleFactor());
  }
#endif

  // In Gtk/X11, we manage windows using device pixels.
  return DesktopToLayoutDeviceScale(1.0);
}

DesktopToLayoutDeviceScale nsWindow::GetDesktopToDeviceScaleByScreen() {
#ifdef MOZ_WAYLAND
  // In Wayland there's no way to get absolute position of the window and use it
  // to determine the screen factor of the monitor on which the window is
  // placed. The window is notified of the current scale factor but not at this
  // point, so the GdkScaleFactor can return wrong value which can lead to wrong
  // popup placement. We need to use parent's window scale factor for the new
  // one.
  if (GdkIsWaylandDisplay()) {
    nsView* view = nsView::GetViewFor(this);
    if (view) {
      nsView* parentView = view->GetParent();
      if (parentView) {
        nsIWidget* parentWidget = parentView->GetNearestWidget(nullptr);
        if (parentWidget) {
          return DesktopToLayoutDeviceScale(
              parentWidget->RoundsWidgetCoordinatesTo());
        }
        NS_WARNING("Widget has no parent");
      }
    } else {
      NS_WARNING("Cannot find widget view");
    }
  }
#endif
  return nsBaseWidget::GetDesktopToDeviceScale();
}

// Reparent a child window to a new parent.
void nsWindow::SetParent(nsIWidget* aNewParent) {
  LOG("nsWindow::SetParent() new parent %p", aNewParent);
  if (!mIsChildWindow) {
    NS_WARNING("Used by child widgets only");
    return;
  }

  nsCOMPtr<nsIWidget> kungFuDeathGrip = this;
  if (mParent) {
    mParent->RemoveChild(this);
  }
  mParent = aNewParent;

  // We're already deleted, quit.
  if (!mGdkWindow || mIsDestroyed || !aNewParent) {
    return;
  }
  aNewParent->AddChild(this);

  auto* newParent = static_cast<nsWindow*>(aNewParent);

  // New parent is deleted, quit.
  if (newParent->mIsDestroyed) {
    Destroy();
    return;
  }

  GdkWindow* window = GetToplevelGdkWindow();
  GdkWindow* parentWindow = newParent->GetToplevelGdkWindow();
  LOG("  child GdkWindow %p set parent GdkWindow %p", window, parentWindow);
  gdk_window_reparent(window, parentWindow, 0, 0);
  SetHasMappedToplevel(newParent && newParent->mHasMappedToplevel);
}

bool nsWindow::WidgetTypeSupportsAcceleration() {
  if (mWindowType == WindowType::Invisible) {
    return false;
  }

  if (IsSmallPopup()) {
    return false;
  }
  // Workaround for Bug 1479135
  // We draw transparent popups on non-compositing screens by SW as we don't
  // implement X shape masks in WebRender.
  if (mWindowType == WindowType::Popup) {
    return HasRemoteContent() && mCompositedScreen;
  }

  return true;
}

void nsWindow::ReparentNativeWidget(nsIWidget* aNewParent) {
  MOZ_ASSERT(aNewParent, "null widget");
  MOZ_ASSERT(!mIsDestroyed, "");
  MOZ_ASSERT(!static_cast<nsWindow*>(aNewParent)->mIsDestroyed, "");
  MOZ_ASSERT(
      !mParent,
      "nsWindow::ReparentNativeWidget() works on toplevel windows only.");

  auto* newParent = static_cast<nsWindow*>(aNewParent);
  GtkWindow* newParentWidget = GTK_WINDOW(newParent->GetGtkWidget());

  LOG("nsWindow::ReparentNativeWidget new parent %p\n", newParent);
  GtkWindowSetTransientFor(GTK_WINDOW(mShell), newParentWidget);
}

void nsWindow::SetModal(bool aModal) {
  LOG("nsWindow::SetModal %d\n", aModal);
  if (mIsDestroyed) return;
  gtk_window_set_modal(GTK_WINDOW(mShell), aModal ? TRUE : FALSE);
}

// nsIWidget method, which means IsShown.
bool nsWindow::IsVisible() const { return mIsShown; }

bool nsWindow::IsMapped() const {
  // TODO: Enable for X11 when Mozilla testsuite is moved to new
  // testing environment from Ubuntu 18.04 which is broken.
  return GdkIsWaylandDisplay() ? mIsMapped : true;
}

void nsWindow::RegisterTouchWindow() {
  mHandleTouchEvent = true;
  mTouches.Clear();
}

LayoutDeviceIntPoint nsWindow::GetScreenEdgeSlop() {
  if (DrawsToCSDTitlebar()) {
    return GetClientOffset();
  }
  return {};
}

void nsWindow::ConstrainPosition(DesktopIntPoint& aPoint) {
  if (!mShell || GdkIsWaylandDisplay()) {
    return;
  }

  double dpiScale = GetDefaultScale().scale;

  // we need to use the window size in logical screen pixels
  int32_t logWidth = std::max(NSToIntRound(mBounds.width / dpiScale), 1);
  int32_t logHeight = std::max(NSToIntRound(mBounds.height / dpiScale), 1);

  /* get our playing field. use the current screen, or failing that
    for any reason, use device caps for the default screen. */
  nsCOMPtr<nsIScreenManager> screenmgr =
      do_GetService("@mozilla.org/gfx/screenmanager;1");
  if (!screenmgr) {
    return;
  }
  nsCOMPtr<nsIScreen> screen;
  screenmgr->ScreenForRect(aPoint.x, aPoint.y, logWidth, logHeight,
                           getter_AddRefs(screen));
  // We don't have any screen so leave the coordinates as is
  if (!screen) {
    return;
  }

  // For normalized windows, use the desktop work area.
  // For full screen windows, use the desktop.
  DesktopIntRect screenRect = mSizeMode == nsSizeMode_Fullscreen
                                  ? screen->GetRectDisplayPix()
                                  : screen->GetAvailRectDisplayPix();

  // Expand for the decoration size if needed.
  auto slop =
      DesktopIntPoint::Round(GetScreenEdgeSlop() / GetDesktopToDeviceScale());
  screenRect.Inflate(slop.x, slop.y);

  if (aPoint.x < screenRect.x) {
    aPoint.x = screenRect.x;
  } else if (aPoint.x >= screenRect.XMost() - logWidth) {
    aPoint.x = screenRect.XMost() - logWidth;
  }

  if (aPoint.y < screenRect.y) {
    aPoint.y = screenRect.y;
  } else if (aPoint.y >= screenRect.YMost() - logHeight) {
    aPoint.y = screenRect.YMost() - logHeight;
  }
}

void nsWindow::SetSizeConstraints(const SizeConstraints& aConstraints) {
  mSizeConstraints.mMinSize = GetSafeWindowSize(aConstraints.mMinSize);
  mSizeConstraints.mMaxSize = GetSafeWindowSize(aConstraints.mMaxSize);

  ApplySizeConstraints();
}

bool nsWindow::DrawsToCSDTitlebar() const {
  return mSizeMode == nsSizeMode_Normal &&
         mGtkWindowDecoration == GTK_DECORATION_CLIENT && mDrawInTitlebar;
}

void nsWindow::AddCSDDecorationSize(int* aWidth, int* aHeight) {
  if (!DrawsToCSDTitlebar()) {
    return;
  }
  GtkBorder decorationSize = GetCSDDecorationSize(IsPopup());
  *aWidth += decorationSize.left + decorationSize.right;
  *aHeight += decorationSize.top + decorationSize.bottom;
}

#ifdef MOZ_WAYLAND
bool nsWindow::GetCSDDecorationOffset(int* aDx, int* aDy) {
  if (!DrawsToCSDTitlebar()) {
    return false;
  }
  GtkBorder decorationSize = GetCSDDecorationSize(IsPopup());
  *aDx = decorationSize.left;
  *aDy = decorationSize.top;
  return true;
}
#endif

void nsWindow::ApplySizeConstraints() {
  if (mShell) {
    GdkGeometry geometry;
    geometry.min_width =
        DevicePixelsToGdkCoordRoundUp(mSizeConstraints.mMinSize.width);
    geometry.min_height =
        DevicePixelsToGdkCoordRoundUp(mSizeConstraints.mMinSize.height);
    geometry.max_width =
        DevicePixelsToGdkCoordRoundDown(mSizeConstraints.mMaxSize.width);
    geometry.max_height =
        DevicePixelsToGdkCoordRoundDown(mSizeConstraints.mMaxSize.height);

    uint32_t hints = 0;
    if (mSizeConstraints.mMinSize != LayoutDeviceIntSize()) {
      if (GdkIsWaylandDisplay()) {
        gtk_widget_set_size_request(GTK_WIDGET(mContainer), geometry.min_width,
                                    geometry.min_height);
      }
      AddCSDDecorationSize(&geometry.min_width, &geometry.min_height);
      hints |= GDK_HINT_MIN_SIZE;
    }
    if (mSizeConstraints.mMaxSize !=
        LayoutDeviceIntSize(NS_MAXSIZE, NS_MAXSIZE)) {
      AddCSDDecorationSize(&geometry.max_width, &geometry.max_height);
      hints |= GDK_HINT_MAX_SIZE;
    }

    if (mAspectRatio != 0.0f) {
      geometry.min_aspect = mAspectRatio;
      geometry.max_aspect = mAspectRatio;
      hints |= GDK_HINT_ASPECT;
    }

    gtk_window_set_geometry_hints(GTK_WINDOW(mShell), nullptr, &geometry,
                                  GdkWindowHints(hints));
  }
}

void nsWindow::Show(bool aState) {
  if (aState == mIsShown) return;

  mIsShown = aState;

#ifdef MOZ_LOGGING
  LOG("nsWindow::Show state %d frame %s\n", aState, GetFrameTag().get());
  if (!aState && mSourceDragContext && GdkIsWaylandDisplay()) {
    LOG("  closing Drag&Drop source window, D&D will be canceled!");
  }
#endif

  // Ok, someone called show on a window that isn't sized to a sane
  // value.  Mark this window as needing to have Show() called on it
  // and return.
  if ((aState && !AreBoundsSane()) || !mCreated) {
    LOG("\tbounds are insane or window hasn't been created yet\n");
    mNeedsShow = true;
    return;
  }

  // If someone is hiding this widget, clear any needing show flag.
  if (!aState) mNeedsShow = false;

#ifdef ACCESSIBILITY
  if (aState && a11y::ShouldA11yBeEnabled()) CreateRootAccessible();
#endif

  NativeShow(aState);
}

void nsWindow::ResizeInt(const Maybe<LayoutDeviceIntPoint>& aMove,
                         LayoutDeviceIntSize aSize) {
  LOG("nsWindow::ResizeInt w:%d h:%d\n", aSize.width, aSize.height);
  const bool moved = aMove && *aMove != mBounds.TopLeft();
  if (moved) {
    mBounds.MoveTo(*aMove);
    LOG("  with move to left:%d top:%d", aMove->x.value, aMove->y.value);
  }

  ConstrainSize(&aSize.width, &aSize.height);
  LOG("  ConstrainSize: w:%d h;%d\n", aSize.width, aSize.height);

  const bool resized = aSize != mLastSizeRequest || mBounds.Size() != aSize;
#if MOZ_LOGGING
  LOG("  resized %d aSize [%d, %d] mLastSizeRequest [%d, %d] mBounds [%d, %d]",
      resized, aSize.width, aSize.height, mLastSizeRequest.width,
      mLastSizeRequest.height, mBounds.width, mBounds.height);
#endif

  // For top-level windows, aSize should possibly be
  // interpreted as frame bounds, but NativeMoveResize treats these as window
  // bounds (Bug 581866).
  mLastSizeRequest = aSize;
  // Check size
  if (mCompositorSession &&
      !wr::WindowSizeSanityCheck(aSize.width, aSize.height)) {
    gfxCriticalNoteOnce << "Invalid aSize in ResizeInt " << aSize
                        << " size state " << mSizeMode;
  }

  // Recalculate aspect ratio when resized from DOM
  if (mAspectRatio != 0.0) {
    LockAspectRatio(true);
  }

  if (!mCreated) {
    return;
  }

  if (!moved && !resized) {
    LOG("  not moved or resized, quit");
    return;
  }

  NativeMoveResize(moved, resized);

  // We optimistically assume size changes immediately in two cases:
  // 1. Override-redirect window: Size is controlled by only us.
  // 2. Managed window that has not not yet received a size-allocate event:
  //    Resize() Callers expect initial sizes to be applied synchronously.
  //    If the size request is not honored, then we'll correct in
  //    OnSizeAllocate().
  //
  // When a managed window has already received a size-allocate, we cannot
  // assume we'll always get a notification if our request does not get
  // honored: "If the configure request has not changed, we don't ever resend
  // it, because it could mean fighting the user or window manager."
  // https://gitlab.gnome.org/GNOME/gtk/-/blob/3.24.31/gtk/gtkwindow.c#L9782
  // So we don't update mBounds until OnSizeAllocate() when we know the
  // request is granted.
  bool isOrWillBeVisible = mHasReceivedSizeAllocate || mNeedsShow || mIsShown;
  if (!isOrWillBeVisible ||
      gtk_window_get_window_type(GTK_WINDOW(mShell)) == GTK_WINDOW_POPUP) {
    mBounds.SizeTo(aSize);
    if (mCompositorWidgetDelegate) {
      mCompositorWidgetDelegate->NotifyClientSizeChanged(aSize);
    }
    DispatchResized();
  }
}

void nsWindow::Resize(double aWidth, double aHeight, bool aRepaint) {
  LOG("nsWindow::Resize %f %f\n", aWidth, aHeight);

  double scale =
      BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0;
  auto size = LayoutDeviceIntSize::Round(scale * aWidth, scale * aHeight);

  ResizeInt(Nothing(), size);
}

void nsWindow::Resize(double aX, double aY, double aWidth, double aHeight,
                      bool aRepaint) {
  LOG("nsWindow::Resize [%f,%f] -> [%f x %f] repaint %d\n", aX, aY, aWidth,
      aHeight, aRepaint);

  double scale =
      BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0;
  auto size = LayoutDeviceIntSize::Round(scale * aWidth, scale * aHeight);
  auto topLeft = LayoutDeviceIntPoint::Round(scale * aX, scale * aY);

  ResizeInt(Some(topLeft), size);
}

void nsWindow::Enable(bool aState) { mEnabled = aState; }

bool nsWindow::IsEnabled() const { return mEnabled; }

void nsWindow::Move(double aX, double aY) {
  double scale =
      BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0;
  int32_t x = NSToIntRound(aX * scale);
  int32_t y = NSToIntRound(aY * scale);

  LOG("nsWindow::Move to %d x %d\n", x, y);

  if (mSizeMode != nsSizeMode_Normal && IsTopLevelWindowType()) {
    LOG("  size state is not normal, bailing");
    return;
  }

  // Since a popup window's x/y coordinates are in relation to to
  // the parent, the parent might have moved so we always move a
  // popup window.
  LOG("  bounds %d x %d\n", mBounds.x, mBounds.y);
  if (x == mBounds.x && y == mBounds.y && mWindowType != WindowType::Popup) {
    LOG("  position is the same, return\n");
    return;
  }

  // XXX Should we do some AreBoundsSane check here?

  mBounds.x = x;
  mBounds.y = y;

  if (!mCreated) {
    LOG("  is not created, return.\n");
    return;
  }

  NativeMoveResize(/* move */ true, /* resize */ false);
}

bool nsWindow::IsPopup() const { return mWindowType == WindowType::Popup; }

bool nsWindow::IsWaylandPopup() const {
  return GdkIsWaylandDisplay() && IsPopup();
}

static nsMenuPopupFrame* GetMenuPopupFrame(nsIFrame* aFrame) {
  return do_QueryFrame(aFrame);
}

void nsWindow::AppendPopupToHierarchyList(nsWindow* aToplevelWindow) {
  mWaylandToplevel = aToplevelWindow;

  nsWindow* popup = aToplevelWindow;
  while (popup && popup->mWaylandPopupNext) {
    popup = popup->mWaylandPopupNext;
  }
  popup->mWaylandPopupNext = this;

  mWaylandPopupPrev = popup;
  mWaylandPopupNext = nullptr;
  mPopupChanged = true;
  mPopupClosed = false;
}

void nsWindow::RemovePopupFromHierarchyList() {
  // We're already removed from the popup hierarchy
  if (!IsInPopupHierarchy()) {
    return;
  }
  mWaylandPopupPrev->mWaylandPopupNext = mWaylandPopupNext;
  if (mWaylandPopupNext) {
    mWaylandPopupNext->mWaylandPopupPrev = mWaylandPopupPrev;
    mWaylandPopupNext->mPopupChanged = true;
  }
  mWaylandPopupNext = mWaylandPopupPrev = nullptr;
}

// Gtk refuses to map popup window with x < 0 && y < 0 relative coordinates
// see https://gitlab.gnome.org/GNOME/gtk/-/issues/4071
// as a workaround just fool around and place the popup temporary to 0,0.
bool nsWindow::WaylandPopupRemoveNegativePosition(int* aX, int* aY) {
  // https://gitlab.gnome.org/GNOME/gtk/-/issues/4071 applies to temporary
  // windows only
  GdkWindow* window = GetToplevelGdkWindow();
  if (!window || gdk_window_get_window_type(window) != GDK_WINDOW_TEMP) {
    return false;
  }

  LOG("nsWindow::WaylandPopupRemoveNegativePosition()");

  int x, y;
  gtk_window_get_position(GTK_WINDOW(mShell), &x, &y);
  bool moveBack = (x < 0 && y < 0);
  if (moveBack) {
    gtk_window_move(GTK_WINDOW(mShell), 0, 0);
    if (aX) {
      *aX = x;
    }
    if (aY) {
      *aY = y;
    }
  }

  gdk_window_get_geometry(window, &x, &y, nullptr, nullptr);
  if (x < 0 && y < 0) {
    gdk_window_move(window, 0, 0);
  }

  return moveBack;
}

void nsWindow::ShowWaylandPopupWindow() {
  LOG("nsWindow::ShowWaylandPopupWindow. Expected to see visible.");
  MOZ_ASSERT(IsWaylandPopup());

  if (!mPopupTrackInHierarchy) {
    LOG("  popup is not tracked in popup hierarchy, show it now");
    gtk_widget_show(mShell);
    return;
  }

  // Popup position was checked before gdk_window_move_to_rect() callback
  // so just show it.
  if (mPopupUseMoveToRect && mWaitingForMoveToRectCallback) {
    LOG("  active move-to-rect callback, show it as is");
    gtk_widget_show(mShell);
    return;
  }

  if (gtk_widget_is_visible(mShell)) {
    LOG("  is already visible, quit");
    return;
  }

  int x, y;
  bool moved = WaylandPopupRemoveNegativePosition(&x, &y);
  gtk_widget_show(mShell);
  if (moved) {
    LOG("  move back to (%d, %d) and show", x, y);
    gtk_window_move(GTK_WINDOW(mShell), x, y);
  }
}

void nsWindow::WaylandPopupMarkAsClosed() {
  LOG("nsWindow::WaylandPopupMarkAsClosed: [%p]\n", this);
  mPopupClosed = true;
  // If we have any child popup window notify it about
  // parent switch.
  if (mWaylandPopupNext) {
    mWaylandPopupNext->mPopupChanged = true;
  }
}

nsWindow* nsWindow::WaylandPopupFindLast(nsWindow* aPopup) {
  while (aPopup && aPopup->mWaylandPopupNext) {
    aPopup = aPopup->mWaylandPopupNext;
  }
  return aPopup;
}

// Hide and potentially removes popup from popup hierarchy.
void nsWindow::HideWaylandPopupWindow(bool aTemporaryHide,
                                      bool aRemoveFromPopupList) {
  LOG("nsWindow::HideWaylandPopupWindow: remove from list %d\n",
      aRemoveFromPopupList);
  if (aRemoveFromPopupList) {
    RemovePopupFromHierarchyList();
  }

  if (!mPopupClosed) {
    mPopupClosed = !aTemporaryHide;
  }

  bool visible = gtk_widget_is_visible(mShell);
  LOG("  gtk_widget_is_visible() = %d\n", visible);

  // Restore only popups which are really visible
  mPopupTemporaryHidden = aTemporaryHide && visible;

  // Hide only visible popups or popups closed pernamently.
  if (visible) {
    gtk_widget_hide(mShell);

    // If there's pending Move-To-Rect callback and we hide the popup
    // the callback won't be called any more.
    mWaitingForMoveToRectCallback = false;
  }

  if (mPopupClosed) {
    LOG("  Clearing mMoveToRectPopupSize\n");
    mMoveToRectPopupSize = {};
#ifdef MOZ_WAYLAND
    if (moz_container_wayland_is_waiting_to_show(mContainer)) {
      // We need to clear rendering queue, see Bug 1782948.
      LOG("  popup failed to show by Wayland compositor, clear rendering "
          "queue.");
      moz_container_wayland_clear_waiting_to_show_flag(mContainer);
      ClearRenderingQueue();
    }
#endif
  }
}

void nsWindow::HideWaylandToplevelWindow() {
  LOG("nsWindow::HideWaylandToplevelWindow: [%p]\n", this);
  if (mWaylandPopupNext) {
    nsWindow* popup = WaylandPopupFindLast(mWaylandPopupNext);
    while (popup->mWaylandToplevel != nullptr) {
      nsWindow* prev = popup->mWaylandPopupPrev;
      popup->HideWaylandPopupWindow(/* aTemporaryHide */ false,
                                    /* aRemoveFromPopupList */ true);
      popup = prev;
    }
  }
  WaylandStopVsync();
  gtk_widget_hide(mShell);
}

void nsWindow::ShowWaylandToplevelWindow() {
  MOZ_ASSERT(!IsWaylandPopup());
  LOG("nsWindow::ShowWaylandToplevelWindow");
  gtk_widget_show(mShell);
}

void nsWindow::WaylandPopupRemoveClosedPopups() {
  LOG("nsWindow::WaylandPopupRemoveClosedPopups()");
  nsWindow* popup = this;
  while (popup) {
    nsWindow* next = popup->mWaylandPopupNext;
    if (popup->mPopupClosed) {
      popup->HideWaylandPopupWindow(/* aTemporaryHide */ false,
                                    /* aRemoveFromPopupList */ true);
    }
    popup = next;
  }
}

// Hide all tooltips except the latest one.
void nsWindow::WaylandPopupHideTooltips() {
  LOG("nsWindow::WaylandPopupHideTooltips");
  MOZ_ASSERT(mWaylandToplevel == nullptr, "Should be called on toplevel only!");

  nsWindow* popup = mWaylandPopupNext;
  while (popup && popup->mWaylandPopupNext) {
    if (popup->mPopupType == PopupType::Tooltip) {
      LOG("  hidding tooltip [%p]", popup);
      popup->WaylandPopupMarkAsClosed();
    }
    popup = popup->mWaylandPopupNext;
  }
}

void nsWindow::WaylandPopupCloseOrphanedPopups() {
#ifdef MOZ_WAYLAND
  LOG("nsWindow::WaylandPopupCloseOrphanedPopups");
  MOZ_ASSERT(mWaylandToplevel == nullptr, "Should be called on toplevel only!");

  nsWindow* popup = mWaylandPopupNext;
  bool dangling = false;
  while (popup) {
    if (!dangling &&
        moz_container_wayland_is_waiting_to_show(popup->GetMozContainer())) {
      LOG("  popup [%p] is waiting to show, close all child popups", popup);
      dangling = true;
    } else if (dangling) {
      popup->WaylandPopupMarkAsClosed();
    }
    popup = popup->mWaylandPopupNext;
  }
#endif
}

// We can't show popups with remote content or overflow popups
// on top of regular ones.
// If there's any remote popup opened, close all parent popups of it.
void nsWindow::CloseAllPopupsBeforeRemotePopup() {
  LOG("nsWindow::CloseAllPopupsBeforeRemotePopup");
  MOZ_ASSERT(mWaylandToplevel == nullptr, "Should be called on toplevel only!");

  // Don't waste time when there's only one popup opened.
  if (!mWaylandPopupNext || mWaylandPopupNext->mWaylandPopupNext == nullptr) {
    return;
  }

  // Find the first opened remote content popup
  nsWindow* remotePopup = mWaylandPopupNext;
  while (remotePopup) {
    if (remotePopup->HasRemoteContent() ||
        remotePopup->IsWidgetOverflowWindow()) {
      LOG("  remote popup [%p]", remotePopup);
      break;
    }
    remotePopup = remotePopup->mWaylandPopupNext;
  }

  if (!remotePopup) {
    return;
  }

  // ...hide opened popups before the remote one.
  nsWindow* popup = mWaylandPopupNext;
  while (popup && popup != remotePopup) {
    LOG("  hidding popup [%p]", popup);
    popup->WaylandPopupMarkAsClosed();
    popup = popup->mWaylandPopupNext;
  }
}

static void GetLayoutPopupWidgetChain(
    nsTArray<nsIWidget*>* aLayoutWidgetHierarchy) {
  nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
  pm->GetSubmenuWidgetChain(aLayoutWidgetHierarchy);
  aLayoutWidgetHierarchy->Reverse();
}

// Compare 'this' popup position in Wayland widget hierarchy
// (mWaylandPopupPrev/mWaylandPopupNext) with
// 'this' popup position in layout hierarchy.
//
// When aMustMatchParent is true we also request
// 'this' parents match, i.e. 'this' has the same parent in
// both layout and widget hierarchy.
bool nsWindow::IsPopupInLayoutPopupChain(
    nsTArray<nsIWidget*>* aLayoutWidgetHierarchy, bool aMustMatchParent) {
  int len = (int)aLayoutWidgetHierarchy->Length();
  for (int i = 0; i < len; i++) {
    if (this == (*aLayoutWidgetHierarchy)[i]) {
      if (!aMustMatchParent) {
        return true;
      }

      // Find correct parent popup for 'this' according to widget
      // hierarchy. That means we need to skip closed popups.
      nsWindow* parentPopup = nullptr;
      if (mWaylandPopupPrev != mWaylandToplevel) {
        parentPopup = mWaylandPopupPrev;
        while (parentPopup != mWaylandToplevel && parentPopup->mPopupClosed) {
          parentPopup = parentPopup->mWaylandPopupPrev;
        }
      }

      if (i == 0) {
        // We found 'this' popups as a first popup in layout hierarchy.
        // It matches layout hierarchy if it's first widget also in
        // wayland widget hierarchy (i.e. parent is null).
        return parentPopup == nullptr;
      }

      return parentPopup == (*aLayoutWidgetHierarchy)[i - 1];
    }
  }
  return false;
}

// Hide popups which are not in popup chain.
void nsWindow::WaylandPopupHierarchyHideByLayout(
    nsTArray<nsIWidget*>* aLayoutWidgetHierarchy) {
  LOG("nsWindow::WaylandPopupHierarchyHideByLayout");
  MOZ_ASSERT(mWaylandToplevel == nullptr, "Should be called on toplevel only!");

  // Hide all popups which are not in layout popup chain
  nsWindow* popup = mWaylandPopupNext;
  while (popup) {
    // Don't check closed popups and drag source popups and tooltips.
    if (!popup->mPopupClosed && popup->mPopupType != PopupType::Tooltip &&
        !popup->mSourceDragContext) {
      if (!popup->IsPopupInLayoutPopupChain(aLayoutWidgetHierarchy,
                                            /* aMustMatchParent */ false)) {
        LOG("  hidding popup [%p]", popup);
        popup->WaylandPopupMarkAsClosed();
      }
    }
    popup = popup->mWaylandPopupNext;
  }
}

// Mark popups outside of layout hierarchy
void nsWindow::WaylandPopupHierarchyValidateByLayout(
    nsTArray<nsIWidget*>* aLayoutWidgetHierarchy) {
  LOG("nsWindow::WaylandPopupHierarchyValidateByLayout");
  nsWindow* popup = mWaylandPopupNext;
  while (popup) {
    if (popup->mPopupType == PopupType::Tooltip) {
      popup->mPopupMatchesLayout = true;
    } else if (!popup->mPopupClosed) {
      popup->mPopupMatchesLayout = popup->IsPopupInLayoutPopupChain(
          aLayoutWidgetHierarchy, /* aMustMatchParent */ true);
      LOG("  popup [%p] parent window [%p] matches layout %d\n", (void*)popup,
          (void*)popup->mWaylandPopupPrev, popup->mPopupMatchesLayout);
    }
    popup = popup->mWaylandPopupNext;
  }
}

void nsWindow::WaylandPopupHierarchyHideTemporary() {
  LOG("nsWindow::WaylandPopupHierarchyHideTemporary()");
  nsWindow* popup = WaylandPopupFindLast(this);
  while (popup && popup != this) {
    LOG("  temporary hidding popup [%p]", popup);
    nsWindow* prev = popup->mWaylandPopupPrev;
    popup->HideWaylandPopupWindow(/* aTemporaryHide */ true,
                                  /* aRemoveFromPopupList */ false);
    popup = prev;
  }
}

void nsWindow::WaylandPopupHierarchyShowTemporaryHidden() {
  LOG("nsWindow::WaylandPopupHierarchyShowTemporaryHidden()");
  nsWindow* popup = this;
  while (popup) {
    if (popup->mPopupTemporaryHidden) {
      popup->mPopupTemporaryHidden = false;
      LOG("  showing temporary hidden popup [%p]", popup);
      popup->ShowWaylandPopupWindow();
    }
    popup = popup->mWaylandPopupNext;
  }
}

void nsWindow::WaylandPopupHierarchyCalculatePositions() {
  LOG("nsWindow::WaylandPopupHierarchyCalculatePositions()");

  // Set widget hierarchy in Gtk
  nsWindow* popup = mWaylandToplevel->mWaylandPopupNext;
  while (popup) {
    LOG("  popup [%p] set parent window [%p]", (void*)popup,
        (void*)popup->mWaylandPopupPrev);
    GtkWindowSetTransientFor(GTK_WINDOW(popup->mShell),
                             GTK_WINDOW(popup->mWaylandPopupPrev->mShell));
    popup = popup->mWaylandPopupNext;
  }

  popup = this;
  while (popup) {
    // Anchored window has mPopupPosition already calculated against
    // its parent, no need to recalculate.
    LOG("  popup [%p] bounds [%d, %d] -> [%d x %d]", popup,
        (int)(popup->mBounds.x / FractionalScaleFactor()),
        (int)(popup->mBounds.y / FractionalScaleFactor()),
        (int)(popup->mBounds.width / FractionalScaleFactor()),
        (int)(popup->mBounds.height / FractionalScaleFactor()));
#ifdef MOZ_LOGGING
    if (LOG_ENABLED()) {
      if (nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame())) {
        auto r = LayoutDeviceRect::FromAppUnitsRounded(
            popupFrame->GetRect(),
            popupFrame->PresContext()->AppUnitsPerDevPixel());
        LOG("  popup [%p] layout [%d, %d] -> [%d x %d]", popup, r.x, r.y,
            r.width, r.height);
      }
    }
#endif
    if (popup->WaylandPopupIsFirst()) {
      LOG("  popup [%p] has toplevel as parent", popup);
      popup->mRelativePopupPosition = popup->mPopupPosition;
    } else {
      if (popup->mPopupAnchored) {
        LOG("  popup [%p] is anchored", popup);
        if (!popup->mPopupMatchesLayout) {
          NS_WARNING("Anchored popup does not match layout!");
        }
      }
      GdkPoint parent = popup->WaylandGetParentPosition();

      LOG("  popup [%p] uses transformed coordinates\n", popup);
      LOG("    parent position [%d, %d]\n", parent.x, parent.y);
      LOG("    popup position [%d, %d]\n", popup->mPopupPosition.x,
          popup->mPopupPosition.y);

      popup->mRelativePopupPosition.x = popup->mPopupPosition.x - parent.x;
      popup->mRelativePopupPosition.y = popup->mPopupPosition.y - parent.y;
    }
    LOG("  popup [%p] transformed popup coordinates from [%d, %d] to [%d, %d]",
        popup, popup->mPopupPosition.x, popup->mPopupPosition.y,
        popup->mRelativePopupPosition.x, popup->mRelativePopupPosition.y);
    popup = popup->mWaylandPopupNext;
  }
}

// The MenuList popups are used as dropdown menus for example in WebRTC
// microphone/camera chooser or autocomplete widgets.
bool nsWindow::WaylandPopupIsMenu() {
  nsMenuPopupFrame* menuPopupFrame = GetMenuPopupFrame(GetFrame());
  if (menuPopupFrame) {
    return mPopupType == PopupType::Menu && !menuPopupFrame->IsMenuList();
  }
  return false;
}

bool nsWindow::WaylandPopupIsContextMenu() {
  nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
  if (!popupFrame) {
    return false;
  }
  return popupFrame->IsContextMenu();
}

bool nsWindow::WaylandPopupIsPermanent() {
  nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
  if (!popupFrame) {
    // We can always hide popups without frames.
    return false;
  }
  return popupFrame->IsNoAutoHide();
}

bool nsWindow::WaylandPopupIsAnchored() {
  nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
  if (!popupFrame) {
    // We can always hide popups without frames.
    return false;
  }
  return !!popupFrame->GetAnchor();
}

bool nsWindow::IsWidgetOverflowWindow() {
  if (this->GetFrame() && this->GetFrame()->GetContent()->GetID()) {
    nsCString nodeId;
    this->GetFrame()->GetContent()->GetID()->ToUTF8String(nodeId);
    return nodeId.Equals("widget-overflow");
  }
  return false;
}

bool nsWindow::WaylandPopupIsFirst() {
  return !mWaylandPopupPrev || !mWaylandPopupPrev->mWaylandToplevel;
}

nsWindow* nsWindow::GetEffectiveParent() {
  GtkWindow* parentGtkWindow = gtk_window_get_transient_for(GTK_WINDOW(mShell));
  if (!parentGtkWindow || !GTK_IS_WIDGET(parentGtkWindow)) {
    return nullptr;
  }
  return get_window_for_gtk_widget(GTK_WIDGET(parentGtkWindow));
}

GdkPoint nsWindow::WaylandGetParentPosition() {
  GdkPoint topLeft = {0, 0};
  nsWindow* window = GetEffectiveParent();
  if (window->IsPopup()) {
    topLeft = DevicePixelsToGdkPointRoundDown(window->mBounds.TopLeft());
  }
  LOG("nsWindow::WaylandGetParentPosition() [%d, %d]\n", topLeft.x, topLeft.y);
  return topLeft;
}

#ifdef MOZ_LOGGING
void nsWindow::LogPopupHierarchy() {
  if (!LOG_ENABLED()) {
    return;
  }

  LOG("Widget Popup Hierarchy:\n");
  if (!mWaylandToplevel->mWaylandPopupNext) {
    LOG("    Empty\n");
  } else {
    int indent = 4;
    nsWindow* popup = mWaylandToplevel->mWaylandPopupNext;
    while (popup) {
      nsPrintfCString indentString("%*s", indent, " ");
      LOG("%s %s %s nsWindow [%p] Menu %d Permanent %d ContextMenu %d "
          "Anchored %d Visible %d MovedByRect %d\n",
          indentString.get(), popup->GetFrameTag().get(),
          popup->GetPopupTypeName().get(), popup, popup->WaylandPopupIsMenu(),
          popup->WaylandPopupIsPermanent(), popup->mPopupContextMenu,
          popup->mPopupAnchored, gtk_widget_is_visible(popup->mShell),
          popup->mPopupUseMoveToRect);
      indent += 4;
      popup = popup->mWaylandPopupNext;
    }
  }

  LOG("Layout Popup Hierarchy:\n");
  AutoTArray<nsIWidget*, 5> widgetChain;
  GetLayoutPopupWidgetChain(&widgetChain);
  if (widgetChain.Length() == 0) {
    LOG("    Empty\n");
  } else {
    for (unsigned long i = 0; i < widgetChain.Length(); i++) {
      nsWindow* window = static_cast<nsWindow*>(widgetChain[i]);
      nsPrintfCString indentString("%*s", (int)(i + 1) * 4, " ");
      if (window) {
        LOG("%s %s %s nsWindow [%p] Menu %d Permanent %d ContextMenu %d "
            "Anchored %d Visible %d MovedByRect %d\n",
            indentString.get(), window->GetFrameTag().get(),
            window->GetPopupTypeName().get(), window,
            window->WaylandPopupIsMenu(), window->WaylandPopupIsPermanent(),
            window->mPopupContextMenu, window->mPopupAnchored,
            gtk_widget_is_visible(window->mShell), window->mPopupUseMoveToRect);
      } else {
        LOG("%s null window\n", indentString.get());
      }
    }
  }
}
#endif

nsWindow* nsWindow::GetTopmostWindow() {
  if (nsView* view = nsView::GetViewFor(this)) {
    if (nsView* parentView = view->GetParent()) {
      if (nsIWidget* parentWidget = parentView->GetNearestWidget(nullptr)) {
        return static_cast<nsWindow*>(parentWidget);
      }
    }
  }
  return nullptr;
}

// Configure Wayland popup. If true is returned we need to track popup
// in popup hierarchy. Otherwise we just show it as is.
bool nsWindow::WaylandPopupConfigure() {
  if (mIsDragPopup) {
    return false;
  }

  // Don't track popups without frame
  nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
  if (!popupFrame) {
    return false;
  }

  // Popup state can be changed, see Bug 1728952.
  bool permanentStateMatches =
      mPopupTrackInHierarchy == !WaylandPopupIsPermanent();

  // Popup permanent state (noautohide attribute) can change during popup life.
  if (mPopupTrackInHierarchyConfigured && permanentStateMatches) {
    return mPopupTrackInHierarchy;
  }

  // Configure persistent popup params only once.
  // WaylandPopupIsAnchored() can give it wrong value after
  // nsMenuPopupFrame::MoveTo() call which we use in move-to-rect callback
  // to position popup after wayland position change.
  if (!mPopupTrackInHierarchyConfigured) {
    mPopupAnchored = WaylandPopupIsAnchored();
    mPopupContextMenu = WaylandPopupIsContextMenu();
  }

  LOG("nsWindow::WaylandPopupConfigure tracked %d anchored %d hint %d\n",
      mPopupTrackInHierarchy, mPopupAnchored, int(mPopupHint));

  // Permanent state changed and popup is mapped.
  // We need to switch popup type but that's done when popup is mapped
  // by Gtk so we need to unmap the popup here.
  // It will be mapped again by gtk_widget_show().
  if (!permanentStateMatches && mIsMapped) {
    LOG("  permanent state change from %d to %d, unmapping",
        mPopupTrackInHierarchy, !WaylandPopupIsPermanent());
    gtk_widget_unmap(mShell);
  }

  mPopupTrackInHierarchy = !WaylandPopupIsPermanent();
  LOG("  tracked in hierarchy %d\n", mPopupTrackInHierarchy);

  // See gdkwindow-wayland.c and
  // should_map_as_popup()/should_map_as_subsurface()
  GdkWindowTypeHint gtkTypeHint;
  switch (mPopupHint) {
    case PopupType::Menu:
      // GDK_WINDOW_TYPE_HINT_POPUP_MENU is mapped as xdg_popup by default.
      // We use this type for all menu popups.
      gtkTypeHint = GDK_WINDOW_TYPE_HINT_POPUP_MENU;
      LOG("  popup type Menu");
      break;
    case PopupType::Tooltip:
      gtkTypeHint = GDK_WINDOW_TYPE_HINT_TOOLTIP;
      LOG("  popup type Tooltip");
      break;
    default:
      gtkTypeHint = GDK_WINDOW_TYPE_HINT_UTILITY;
      LOG("  popup type Utility");
      break;
  }

  if (!mPopupTrackInHierarchy) {
    // GDK_WINDOW_TYPE_HINT_UTILITY is mapped as wl_subsurface
    // by default.
    LOG("  not tracked in popup hierarchy, switch to Utility");
    gtkTypeHint = GDK_WINDOW_TYPE_HINT_UTILITY;
  }
  gtk_window_set_type_hint(GTK_WINDOW(mShell), gtkTypeHint);

  mPopupTrackInHierarchyConfigured = true;
  return mPopupTrackInHierarchy;
}

bool nsWindow::IsInPopupHierarchy() {
  return mPopupTrackInHierarchy && mWaylandToplevel && mWaylandPopupPrev;
}

void nsWindow::AddWindowToPopupHierarchy() {
  LOG("nsWindow::AddWindowToPopupHierarchy\n");
  if (!GetFrame()) {
    LOG("  Window without frame cannot be added as popup!\n");
    return;
  }

  // Check if we're already in the hierarchy
  if (!IsInPopupHierarchy()) {
    mWaylandToplevel = GetTopmostWindow();
    AppendPopupToHierarchyList(mWaylandToplevel);
  }
}

// Wayland keeps strong popup window hierarchy. We need to track active
// (visible) popup windows and make sure we hide popup on the same level
// before we open another one on that level. It means that every open
// popup needs to have an unique parent.
void nsWindow::UpdateWaylandPopupHierarchy() {
  LOG("nsWindow::UpdateWaylandPopupHierarchy\n");

  // This popup hasn't been added to popup hierarchy yet so no need to
  // do any configurations.
  if (!IsInPopupHierarchy()) {
    LOG("  popup isn't in hierarchy\n");
    return;
  }

#ifdef MOZ_LOGGING
  LogPopupHierarchy();
  auto printPopupHierarchy = MakeScopeExit([&] { LogPopupHierarchy(); });
#endif

  // Hide all tooltips without the last one. Tooltip can't be popup parent.
  mWaylandToplevel->WaylandPopupHideTooltips();

  // See Bug 1709254 / https://gitlab.gnome.org/GNOME/gtk/-/issues/5092
  // It's possible that Wayland compositor refuses to show
  // a popup although Gtk claims it's visible.
  // We don't know if the popup is shown or not.
  // To avoid application crash refuse to create any child of such invisible
  // popup and close any child of it now.
  mWaylandToplevel->WaylandPopupCloseOrphanedPopups();

  // Check if we have any remote content / overflow window in hierarchy.
  // We can't attach such widget on top of other popup.
  mWaylandToplevel->CloseAllPopupsBeforeRemotePopup();

  // Check if your popup hierarchy matches layout hierarchy.
  // For instance we should not connect hamburger menu on top
  // of context menu.
  // Close all popups from different layout chains if possible.
  AutoTArray<nsIWidget*, 5> layoutPopupWidgetChain;
  GetLayoutPopupWidgetChain(&layoutPopupWidgetChain);

  mWaylandToplevel->WaylandPopupHierarchyHideByLayout(&layoutPopupWidgetChain);
  mWaylandToplevel->WaylandPopupHierarchyValidateByLayout(
      &layoutPopupWidgetChain);

  // Now we have Popup hierarchy complete.
  // Find first unchanged (and still open) popup to start with hierarchy
  // changes.
  nsWindow* changedPopup = mWaylandToplevel->mWaylandPopupNext;
  while (changedPopup) {
    // Stop when parent of this popup was changed and we need to recalc
    // popup position.
    if (changedPopup->mPopupChanged) {
      break;
    }
    // Stop when this popup is closed.
    if (changedPopup->mPopupClosed) {
      break;
    }
    changedPopup = changedPopup->mWaylandPopupNext;
  }

  // We don't need to recompute popup positions, quit now.
  if (!changedPopup) {
    LOG("  changed Popup is null, quit.\n");
    return;
  }

  LOG("  first changed popup [%p]\n", (void*)changedPopup);

  // Hide parent popups if necessary (there are layout discontinuity)
  // reposition the popup and show them again.
  changedPopup->WaylandPopupHierarchyHideTemporary();

  nsWindow* parentOfchangedPopup = nullptr;
  if (changedPopup->mPopupClosed) {
    parentOfchangedPopup = changedPopup->mWaylandPopupPrev;
  }
  changedPopup->WaylandPopupRemoveClosedPopups();

  // It's possible that changedPopup was removed from widget hierarchy,
  // in such case use child popup of the removed one if there's any.
  if (!changedPopup->IsInPopupHierarchy()) {
    if (!parentOfchangedPopup || !parentOfchangedPopup->mWaylandPopupNext) {
      LOG("  last popup was removed, quit.\n");
      return;
    }
    changedPopup = parentOfchangedPopup->mWaylandPopupNext;
  }

  GetLayoutPopupWidgetChain(&layoutPopupWidgetChain);
  mWaylandToplevel->WaylandPopupHierarchyValidateByLayout(
      &layoutPopupWidgetChain);

  changedPopup->WaylandPopupHierarchyCalculatePositions();

  nsWindow* popup = changedPopup;
  while (popup) {
    const bool useMoveToRect = [&] {
      if (!StaticPrefs::widget_wayland_use_move_to_rect_AtStartup()) {
        return false;  // Not available.
      }
      if (!popup->mPopupMatchesLayout) {
        // We can use move_to_rect only when popups in popup hierarchy matches
        // layout hierarchy as move_to_rect request that parent/child
        // popups are adjacent.
        return false;
      }
      if (popup->mPopupHint == PopupType::Panel &&
          popup->WaylandPopupIsFirst() &&
          popup->WaylandPopupFitsToplevelWindow(/* aMove */ true)) {
        // Workaround for https://gitlab.gnome.org/GNOME/gtk/-/issues/1986
        //
        // PopupType::Panel types are used for extension popups which may be
        // resized. If such popup uses move-to-rect, we need to hide it before
        // resize and show it again. That leads to massive flickering
        // so use plain move if possible to avoid it.
        //
        // Bug 1760276 - don't use move-to-rect when popup is inside main
        // Firefox window.
        //
        // Use it for first popups only due to another mutter bug
        // https://gitlab.gnome.org/GNOME/gtk/-/issues/5089
        // https://bugzilla.mozilla.org/show_bug.cgi?id=1784873
        return false;
      }
      if (!popup->WaylandPopupIsFirst() &&
          !popup->mWaylandPopupPrev->WaylandPopupIsFirst() &&
          !popup->mWaylandPopupPrev->mPopupUseMoveToRect) {
        // We can't use move-to-rect if there are more parents of
        // wl_subsurface popups types.
        //
        // It's because wl_subsurface is ignored by xgd_popup
        // (created by move-to-rect) so our popup scenario:
        //
        // toplevel -> xgd_popup(1) -> wl_subsurface(2) -> xgd_popup(3)
        //
        // looks for Wayland compositor as:
        //
        // toplevel -> xgd_popup(1) -> xgd_popup(3)
        //
        // If xgd_popup(1) and xgd_popup(3) are not connected
        // move-to-rect applied to xgd_popup(3) fails and we get missing popup.
        return false;
      }
      return true;
    }();

    LOG("  popup [%p] matches layout [%d] anchored [%d] first popup [%d] use "
        "move-to-rect %d\n",
        popup, popup->mPopupMatchesLayout, popup->mPopupAnchored,
        popup->WaylandPopupIsFirst(), useMoveToRect);

    popup->mPopupUseMoveToRect = useMoveToRect;
    popup->WaylandPopupMoveImpl();
    popup->mPopupChanged = false;
    popup = popup->mWaylandPopupNext;
  }

  changedPopup->WaylandPopupHierarchyShowTemporaryHidden();
}

static void NativeMoveResizeCallback(GdkWindow* window,
                                     const GdkRectangle* flipped_rect,
                                     const GdkRectangle* final_rect,
                                     gboolean flipped_x, gboolean flipped_y,
                                     void* aWindow) {
  LOG_POPUP("[%p] NativeMoveResizeCallback flipped_x %d flipped_y %d\n",
            aWindow, flipped_x, flipped_y);
  LOG_POPUP("[%p]    new position [%d, %d] -> [%d x %d]", aWindow,
            final_rect->x, final_rect->y, final_rect->width,
            final_rect->height);
  nsWindow* wnd = get_window_for_gdk_window(window);

  wnd->NativeMoveResizeWaylandPopupCallback(final_rect, flipped_x, flipped_y);
}

// When popup is repositioned by widget code, we need to notify
// layout about it. It's because we control popup placement
// on widget on Wayland so layout may have old popup size/coordinates.
void nsWindow::WaylandPopupPropagateChangesToLayout(bool aMove, bool aResize) {
  LOG("nsWindow::WaylandPopupPropagateChangesToLayout()");

  if (aResize) {
    LOG("  needSizeUpdate\n");
    if (nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame())) {
      RefPtr<PresShell> presShell = popupFrame->PresShell();
      presShell->FrameNeedsReflow(popupFrame, IntrinsicDirty::None,
                                  NS_FRAME_IS_DIRTY);
    }
  }
  if (aMove) {
    LOG("  needPositionUpdate, bounds [%d, %d]", mBounds.x, mBounds.y);
    NotifyWindowMoved(mBounds.x, mBounds.y, ByMoveToRect::Yes);
  }
}

void nsWindow::NativeMoveResizeWaylandPopupCallback(
    const GdkRectangle* aFinalSize, bool aFlippedX, bool aFlippedY) {
  // We're getting move-to-rect callback without move-to-rect call.
  // That indicates a compositor bug. It happens when a window is hidden and
  // shown again before move-to-rect callback is fired.
  // It may lead to incorrect popup placement as we may call
  // gtk_window_move() between hide & show.
  // See Bug 1777919, 1789581.
#if MOZ_LOGGING
  if (!mWaitingForMoveToRectCallback) {
    LOG("  Bogus move-to-rect callback! Expect wrong popup coordinates.");
  }
#endif

  mWaitingForMoveToRectCallback = false;

  bool movedByLayout = mMovedAfterMoveToRect;
  bool resizedByLayout = mResizedAfterMoveToRect;

  // Popup was moved between move-to-rect call and move-to-rect callback
  // and the coordinates from move-to-rect callback are outdated.
  if (movedByLayout || resizedByLayout) {
    LOG("  Another move/resize called during waiting for callback\n");
    mMovedAfterMoveToRect = false;
    mResizedAfterMoveToRect = false;
    // Fire another round of move/resize to reflect latest request
    // from layout.
    NativeMoveResize(movedByLayout, resizedByLayout);
    return;
  }

  LOG("  orig mBounds [%d, %d] -> [%d x %d]\n", mBounds.x, mBounds.y,
      mBounds.width, mBounds.height);

  LayoutDeviceIntRect newBounds = [&] {
    GdkRectangle finalRect = *aFinalSize;
    GdkPoint parent = WaylandGetParentPosition();
    finalRect.x += parent.x;
    finalRect.y += parent.y;
    return GdkRectToDevicePixels(finalRect);
  }();

  LOG("  new mBounds [%d, %d] -> [%d x %d]", newBounds.x, newBounds.y,
      newBounds.width, newBounds.height);

  bool needsPositionUpdate = newBounds.TopLeft() != mBounds.TopLeft();
  bool needsSizeUpdate = newBounds.Size() != mLastSizeRequest;

  if (needsSizeUpdate) {
    // Wayland compositor changed popup size request from layout.
    // Set the constraints to use them in nsMenuPopupFrame::SetPopupPosition().
    // Beware that gtk_window_resize() requests sizes asynchronously and so
    // newBounds might not have the size from the most recent
    // gtk_window_resize().
    if (newBounds.width < mLastSizeRequest.width) {
      mMoveToRectPopupSize.width = newBounds.width;
    }
    if (newBounds.height < mLastSizeRequest.height) {
      mMoveToRectPopupSize.height = newBounds.height;
    }
    LOG("  mMoveToRectPopupSize set to [%d, %d]", mMoveToRectPopupSize.width,
        mMoveToRectPopupSize.height);
  }
  mBounds = newBounds;
  // Check mBounds size
  if (mCompositorSession &&
      !wr::WindowSizeSanityCheck(mBounds.width, mBounds.height)) {
    gfxCriticalNoteOnce << "Invalid mBounds in PopupCallback " << mBounds
                        << " size state " << mSizeMode;
  }
  WaylandPopupPropagateChangesToLayout(needsPositionUpdate, needsSizeUpdate);
}

static GdkGravity PopupAlignmentToGdkGravity(int8_t aAlignment) {
  switch (aAlignment) {
    case POPUPALIGNMENT_NONE:
      return GDK_GRAVITY_NORTH_WEST;
    case POPUPALIGNMENT_TOPLEFT:
      return GDK_GRAVITY_NORTH_WEST;
    case POPUPALIGNMENT_TOPRIGHT:
      return GDK_GRAVITY_NORTH_EAST;
    case POPUPALIGNMENT_BOTTOMLEFT:
      return GDK_GRAVITY_SOUTH_WEST;
    case POPUPALIGNMENT_BOTTOMRIGHT:
      return GDK_GRAVITY_SOUTH_EAST;
    case POPUPALIGNMENT_LEFTCENTER:
      return GDK_GRAVITY_WEST;
    case POPUPALIGNMENT_RIGHTCENTER:
      return GDK_GRAVITY_EAST;
    case POPUPALIGNMENT_TOPCENTER:
      return GDK_GRAVITY_NORTH;
    case POPUPALIGNMENT_BOTTOMCENTER:
      return GDK_GRAVITY_SOUTH;
  }
  return GDK_GRAVITY_STATIC;
}

bool nsWindow::IsPopupDirectionRTL() {
  nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
  return popupFrame && popupFrame->IsDirectionRTL();
}

// Position the popup directly by gtk_window_move() and try to keep it
// on screen by just moving it in scope of it's parent window.
//
// It's used when we position noautihode popup and we don't use xdg_positioner.
// See Bug 1718867
void nsWindow::WaylandPopupSetDirectPosition() {
  GdkPoint topLeft = DevicePixelsToGdkPointRoundDown(mBounds.TopLeft());
  GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mLastSizeRequest);

  LOG("nsWindow::WaylandPopupSetDirectPosition %d,%d -> %d x %d\n", topLeft.x,
      topLeft.y, size.width, size.height);

  mPopupPosition = {topLeft.x, topLeft.y};

  if (mIsDragPopup) {
    gtk_window_move(GTK_WINDOW(mShell), topLeft.x, topLeft.y);
    gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height);
    // DND window is placed inside container so we need to make hard size
    // request to ensure parent container is resized too.
    gtk_widget_set_size_request(GTK_WIDGET(mShell), size.width, size.height);
    return;
  }

  GtkWindow* parentGtkWindow = gtk_window_get_transient_for(GTK_WINDOW(mShell));
  nsWindow* window = get_window_for_gtk_widget(GTK_WIDGET(parentGtkWindow));
  if (!window) {
    return;
  }
  GdkWindow* gdkWindow =
      gtk_widget_get_window(GTK_WIDGET(window->GetMozContainer()));

  int parentWidth = gdk_window_get_width(gdkWindow);
  int popupWidth = size.width;

  int x;
  gdk_window_get_position(gdkWindow, &x, nullptr);

  // If popup is bigger than main window just center it.
  if (popupWidth > parentWidth) {
    mPopupPosition.x = -(parentWidth - popupWidth) / 2 + x;
  } else {
    if (IsPopupDirectionRTL()) {
      // Stick with right window edge
      if (mPopupPosition.x < x) {
        mPopupPosition.x = x;
      }
    } else {
      // Stick with left window edge
      if (mPopupPosition.x + popupWidth > parentWidth + x) {
        mPopupPosition.x = parentWidth + x - popupWidth;
      }
    }
  }

  LOG("  set position [%d, %d]\n", mPopupPosition.x, mPopupPosition.y);
  gtk_window_move(GTK_WINDOW(mShell), mPopupPosition.x, mPopupPosition.y);

  LOG("  set size [%d, %d]\n", size.width, size.height);
  gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height);

  if (mPopupPosition.x != topLeft.x) {
    mBounds.MoveTo(GdkPointToDevicePixels(mPopupPosition));
    LOG("  setting new bounds [%d, %d]\n", mBounds.x, mBounds.y);
    WaylandPopupPropagateChangesToLayout(/* move */ true, /* resize */ false);
  }
}

bool nsWindow::WaylandPopupFitsToplevelWindow(bool aMove) {
  LOG("nsWindow::WaylandPopupFitsToplevelWindow() move %d", aMove);

  GtkWindow* parent = gtk_window_get_transient_for(GTK_WINDOW(mShell));
  GtkWindow* tmp = parent;
  while ((tmp = gtk_window_get_transient_for(GTK_WINDOW(parent)))) {
    parent = tmp;
  }
  GdkWindow* toplevelGdkWindow = gtk_widget_get_window(GTK_WIDGET(parent));

  if (!toplevelGdkWindow) {
    NS_WARNING("Toplevel widget without GdkWindow?");
    return false;
  }

  int parentWidth = gdk_window_get_width(toplevelGdkWindow);
  int parentHeight = gdk_window_get_height(toplevelGdkWindow);
  LOG("  parent size %d x %d", parentWidth, parentHeight);

  GdkPoint topLeft = aMove ? mPopupPosition
                           : DevicePixelsToGdkPointRoundDown(mBounds.TopLeft());
  GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mLastSizeRequest);
  LOG("  popup topleft %d, %d size %d x %d", topLeft.x, topLeft.y, size.width,
      size.height);
  int fits = topLeft.x >= 0 && topLeft.y >= 0 &&
             topLeft.x + size.width <= parentWidth &&
             topLeft.y + size.height <= parentHeight;

  LOG("  fits %d", fits);
  return fits;
}

void nsWindow::NativeMoveResizeWaylandPopup(bool aMove, bool aResize) {
  GdkPoint topLeft = DevicePixelsToGdkPointRoundDown(mBounds.TopLeft());
  GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mLastSizeRequest);

  LOG("nsWindow::NativeMoveResizeWaylandPopup Bounds %d,%d -> %d x %d move %d "
      "resize %d\n",
      topLeft.x, topLeft.y, size.width, size.height, aMove, aResize);

  // Compositor may be confused by windows with width/height = 0
  // and positioning such windows leads to Bug 1555866.
  if (!AreBoundsSane()) {
    LOG("  Bounds are not sane (width: %d height: %d)\n",
        mLastSizeRequest.width, mLastSizeRequest.height);
    return;
  }

  if (mWaitingForMoveToRectCallback) {
    LOG("  waiting for move to rect, scheduling");
    // mBounds position must not be overwritten before it is applied.
    // OnConfigureEvent() will not set mBounds to an old position for
    // GTK_WINDOW_POPUP.
    MOZ_ASSERT(gtk_window_get_window_type(GTK_WINDOW(mShell)) ==
               GTK_WINDOW_POPUP);
    mMovedAfterMoveToRect = aMove;
    mResizedAfterMoveToRect = aResize;
    return;
  }

  mMovedAfterMoveToRect = false;
  mResizedAfterMoveToRect = false;

  bool trackedInHierarchy = WaylandPopupConfigure();

  // Read popup position from layout if it was moved or newly created.
  // This position is used by move-to-rect method as we need anchor and other
  // info to place popup correctly.
  // We need WaylandPopupConfigure() to be called before to have all needed
  // popup info in place (mainly the anchored flag).
  if (aMove) {
    mPopupMoveToRectParams = WaylandPopupGetPositionFromLayout();
  }

  if (!trackedInHierarchy) {
    WaylandPopupSetDirectPosition();
    return;
  }

  if (aResize) {
    LOG("  set size [%d, %d]\n", size.width, size.height);
    gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height);
  }

  if (!aMove && WaylandPopupFitsToplevelWindow(aMove)) {
    // Popup position has not been changed and its position/size fits
    // parent window so no need to reposition the window.
    LOG("  fits parent window size, just resize\n");
    return;
  }

  // Mark popup as changed as we're updating position/size.
  mPopupChanged = true;

  // Save popup position for former re-calculations when popup hierarchy
  // is changed.
  LOG("  popup position changed from [%d, %d] to [%d, %d]\n", mPopupPosition.x,
      mPopupPosition.y, topLeft.x, topLeft.y);
  mPopupPosition = {topLeft.x, topLeft.y};

  UpdateWaylandPopupHierarchy();
}

struct PopupSides {
  Maybe<Side> mVertical;
  Maybe<Side> mHorizontal;
};

static PopupSides SidesForPopupAlignment(int8_t aAlignment) {
  switch (aAlignment) {
    case POPUPALIGNMENT_NONE:
      break;
    case POPUPALIGNMENT_TOPLEFT:
      return {Some(eSideTop), Some(eSideLeft)};
    case POPUPALIGNMENT_TOPRIGHT:
      return {Some(eSideTop), Some(eSideRight)};
    case POPUPALIGNMENT_BOTTOMLEFT:
      return {Some(eSideBottom), Some(eSideLeft)};
    case POPUPALIGNMENT_BOTTOMRIGHT:
      return {Some(eSideBottom), Some(eSideRight)};
    case POPUPALIGNMENT_LEFTCENTER:
      return {Nothing(), Some(eSideLeft)};
    case POPUPALIGNMENT_RIGHTCENTER:
      return {Nothing(), Some(eSideRight)};
    case POPUPALIGNMENT_TOPCENTER:
      return {Some(eSideTop), Nothing()};
    case POPUPALIGNMENT_BOTTOMCENTER:
      return {Some(eSideBottom), Nothing()};
  }
  return {};
}

// We want to apply margins based on popup alignment (which would generally be
// just an offset to apply to the popup). However, to deal with flipping
// correctly, we apply the margin to the anchor when possible.
struct ResolvedPopupMargin {
  // A margin to be applied to the anchor.
  nsMargin mAnchorMargin;
  // An offset in app units to be applied to the popup for when we need to tell
  // GTK to center inside the anchor precisely (so we can't really do better in
  // presence of flips).
  nsPoint mPopupOffset;
};

static ResolvedPopupMargin ResolveMargin(nsMenuPopupFrame* aFrame,
                                         int8_t aPopupAlign,
                                         int8_t aAnchorAlign,
                                         bool aAnchoredToPoint,
                                         bool aIsContextMenu) {
  nsMargin margin = aFrame->GetMargin();
  nsPoint offset;

  if (aAnchoredToPoint) {
    // Since GTK doesn't allow us to specify margins itself, when anchored to a
    // point we can just assume we'll be aligned correctly... This is kind of
    // annoying but alas.
    //
    // This calculation must match the relevant unanchored popup calculation in
    // nsMenuPopupFrame::SetPopupPosition(), which should itself be the inverse
    // inverse of nsMenuPopupFrame::MoveTo().
    if (aIsContextMenu && aFrame->IsDirectionRTL()) {
      offset.x = -margin.right;
    } else {
      offset.x = margin.left;
    }
    offset.y = margin.top;
    return {nsMargin(), offset};
  }

  auto popupSides = SidesForPopupAlignment(aPopupAlign);
  auto anchorSides = SidesForPopupAlignment(aAnchorAlign);
  // Matched sides: Invert the margin, so that we pull in the right direction.
  // Popup not aligned to any anchor side: We give up and use the offset,
  // applying the margin from the popup side.
  // Mismatched sides: We swap the margins so that we pull in the right
  // direction, e.g. margin-left: -10px should shrink 10px the _right_ of the
  // box, not the left of the box.
  if (popupSides.mHorizontal == anchorSides.mHorizontal) {
    margin.left = -margin.left;
    margin.right = -margin.right;
  } else if (!anchorSides.mHorizontal) {
    auto popupSide = *popupSides.mHorizontal;
    offset.x += popupSide == eSideRight ? -margin.Side(popupSide)
                                        : margin.Side(popupSide);
    margin.left = margin.right = 0;
  } else {
    std::swap(margin.left, margin.right);
  }

  // Same logic as above, but in the vertical direction.
  if (popupSides.mVertical == anchorSides.mVertical) {
    margin.top = -margin.top;
    margin.bottom = -margin.bottom;
  } else if (!anchorSides.mVertical) {
    auto popupSide = *popupSides.mVertical;
    offset.y += popupSide == eSideBottom ? -margin.Side(popupSide)
                                         : margin.Side(popupSide);
    margin.top = margin.bottom = 0;
  } else {
    std::swap(margin.top, margin.bottom);
  }

  return {margin, offset};
}

#ifdef MOZ_LOGGING
void nsWindow::LogPopupAnchorHints(int aHints) {
  static struct hints_ {
    int hint;
    char name[100];
  } hints[] = {
      {GDK_ANCHOR_FLIP_X, "GDK_ANCHOR_FLIP_X"},
      {GDK_ANCHOR_FLIP_Y, "GDK_ANCHOR_FLIP_Y"},
      {GDK_ANCHOR_SLIDE_X, "GDK_ANCHOR_SLIDE_X"},
      {GDK_ANCHOR_SLIDE_Y, "GDK_ANCHOR_SLIDE_Y"},
      {GDK_ANCHOR_RESIZE_X, "GDK_ANCHOR_RESIZE_X"},
      {GDK_ANCHOR_RESIZE_Y, "GDK_ANCHOR_RESIZE_X"},
  };

  LOG("  PopupAnchorHints");
  for (const auto& hint : hints) {
    if (hint.hint & aHints) {
      LOG("    %s", hint.name);
    }
  }
}

void nsWindow::LogPopupGravity(GdkGravity aGravity) {
  static char gravity[][100]{"NONE",
                             "GDK_GRAVITY_NORTH_WEST",
                             "GDK_GRAVITY_NORTH",
                             "GDK_GRAVITY_NORTH_EAST",
                             "GDK_GRAVITY_WEST",
                             "GDK_GRAVITY_CENTER",
                             "GDK_GRAVITY_EAST",
                             "GDK_GRAVITY_SOUTH_WEST",
                             "GDK_GRAVITY_SOUTH",
                             "GDK_GRAVITY_SOUTH_EAST",
                             "GDK_GRAVITY_STATIC"};
  LOG("    %s", gravity[aGravity]);
}
#endif

const nsWindow::WaylandPopupMoveToRectParams
nsWindow::WaylandPopupGetPositionFromLayout() {
  LOG("nsWindow::WaylandPopupGetPositionFromLayout\n");

  nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());

  const bool isTopContextMenu = mPopupContextMenu && !mPopupAnchored;
  const bool isRTL = IsPopupDirectionRTL();
  const bool anchored = popupFrame->IsAnchored();
  int8_t popupAlign = POPUPALIGNMENT_TOPLEFT;
  int8_t anchorAlign = POPUPALIGNMENT_BOTTOMRIGHT;
  if (anchored) {
    // See nsMenuPopupFrame::AdjustPositionForAnchorAlign.
    popupAlign = popupFrame->GetPopupAlignment();
    anchorAlign = popupFrame->GetPopupAnchor();
  }
  if (isRTL && (anchored || isTopContextMenu)) {
    popupAlign = -popupAlign;
    anchorAlign = -anchorAlign;
  }

  // Although we have mPopupPosition / mRelativePopupPosition here
  // we can't use it. move-to-rect needs anchor rectangle to position a popup
  // but we have only a point from Resize().
  //
  // So we need to extract popup position from nsMenuPopupFrame() and duplicate
  // the layout work here.
  LayoutDeviceIntRect anchorRect;
  ResolvedPopupMargin popupMargin;
  {
    nsRect anchorRectAppUnits = popupFrame->GetUntransformedAnchorRect();
    // This is a somewhat hacky way of applying the popup margin. We don't know
    // if GTK will end up flipping the popup, in which case the offset we
    // compute is just wrong / applied to the wrong side.
    //
    // Instead, we tell it to anchor us at a smaller or bigger rect depending on
    // the margin, which achieves the same result if the popup is positioned
    // correctly, but doesn't misposition the popup when flipped across the
    // anchor.
    popupMargin = ResolveMargin(popupFrame, popupAlign, anchorAlign,
                                anchorRectAppUnits.IsEmpty(), isTopContextMenu);
    LOG("  layout popup CSS anchor (%d, %d) %s, margin %s offset %s\n",
        popupAlign, anchorAlign, ToString(anchorRectAppUnits).c_str(),
        ToString(popupMargin.mAnchorMargin).c_str(),
        ToString(popupMargin.mPopupOffset).c_str());
    anchorRectAppUnits.Inflate(popupMargin.mAnchorMargin);
    LOG("    after margins %s\n", ToString(anchorRectAppUnits).c_str());
    nscoord auPerDev = popupFrame->PresContext()->AppUnitsPerDevPixel();
    anchorRect = LayoutDeviceIntRect::FromAppUnitsToNearest(anchorRectAppUnits,
                                                            auPerDev);
    if (anchorRect.width < 0) {
      auto w = -anchorRect.width;
      anchorRect.width += w + 1;
      anchorRect.x += w;
    }
    LOG("    final %s\n", ToString(anchorRect).c_str());
  }

  LOG("  relative popup rect position [%d, %d] -> [%d x %d]\n", anchorRect.x,
      anchorRect.y, anchorRect.width, anchorRect.height);

  // Get gravity and flip type
  GdkGravity rectAnchor = PopupAlignmentToGdkGravity(anchorAlign);
  GdkGravity menuAnchor = PopupAlignmentToGdkGravity(popupAlign);

  LOG("  parentRect gravity: %d anchor gravity: %d\n", rectAnchor, menuAnchor);

  // Gtk default is: GDK_ANCHOR_FLIP | GDK_ANCHOR_SLIDE | GDK_ANCHOR_RESIZE.
  // We want to SLIDE_X menu on the dual monitor setup rather than resize it
  // on the other monitor.
  GdkAnchorHints hints =
      GdkAnchorHints(GDK_ANCHOR_FLIP | GDK_ANCHOR_SLIDE_X | GDK_ANCHOR_RESIZE);

  // slideHorizontal from nsMenuPopupFrame::SetPopupPosition
  int8_t position = popupFrame->GetAlignmentPosition();
  if (position >= POPUPPOSITION_BEFORESTART &&
      position <= POPUPPOSITION_AFTEREND) {
    hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE_X);
  }
  // slideVertical from nsMenuPopupFrame::SetPopupPosition
  if (position >= POPUPPOSITION_STARTBEFORE &&
      position <= POPUPPOSITION_ENDAFTER) {
    hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE_Y);
  }

  FlipType flipType = popupFrame->GetFlipType();
  if (rectAnchor == GDK_GRAVITY_CENTER && menuAnchor == GDK_GRAVITY_CENTER) {
    // only slide
    hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE);
  } else {
    switch (flipType) {
      case FlipType_Both:
        hints = GdkAnchorHints(hints | GDK_ANCHOR_FLIP);
        break;
      case FlipType_Slide:
        hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE);
        break;
      case FlipType_Default:
        hints = GdkAnchorHints(hints | GDK_ANCHOR_FLIP);
        break;
      default:
        break;
    }
  }
  if (!WaylandPopupIsMenu()) {
    // we don't want to slide menus to fit the screen rather resize them
    hints = GdkAnchorHints(hints | GDK_ANCHOR_SLIDE);
  }

  // We want tooltips to flip verticaly or slide only.
  // See nsMenuPopupFrame::SetPopupPosition().
  // https://searchfox.org/mozilla-central/rev/d0f5bc50aff3462c9d1546b88d60c5cb020eb15c/layout/xul/nsMenuPopupFrame.cpp#1603
  if (mPopupType == PopupType::Tooltip) {
    hints = GdkAnchorHints(GDK_ANCHOR_FLIP_Y | GDK_ANCHOR_SLIDE);
  }

  return {
      anchorRect,
      rectAnchor,
      menuAnchor,
      hints,
      DevicePixelsToGdkPointRoundDown(LayoutDevicePoint::FromAppUnitsToNearest(
          popupMargin.mPopupOffset,
          popupFrame->PresContext()->AppUnitsPerDevPixel())),
      true};
}

bool nsWindow::WaylandPopupAnchorAdjustForParentPopup(
    GdkRectangle* aPopupAnchor, GdkPoint* aOffset) {
  LOG("nsWindow::WaylandPopupAnchorAdjustForParentPopup");

  GtkWindow* parentGtkWindow = gtk_window_get_transient_for(GTK_WINDOW(mShell));
  if (!parentGtkWindow || !GTK_IS_WIDGET(parentGtkWindow)) {
    NS_WARNING("Popup has no parent!");
    return false;
  }
  GdkWindow* window = gtk_widget_get_window(GTK_WIDGET(parentGtkWindow));
  if (!window) {
    NS_WARNING("Popup parrent is not mapped!");
    return false;
  }

  GdkRectangle parentWindowRect = {0, 0, gdk_window_get_width(window),
                                   gdk_window_get_height(window)};
  LOG("  parent window size %d x %d", parentWindowRect.width,
      parentWindowRect.height);

  // We can't have rectangle anchor with zero width/height.
  if (!aPopupAnchor->width) {
    aPopupAnchor->width = 1;
  }
  if (!aPopupAnchor->height) {
    aPopupAnchor->height = 1;
  }

  GdkRectangle finalRect;
  if (!gdk_rectangle_intersect(aPopupAnchor, &parentWindowRect, &finalRect)) {
    return false;
  }
  *aPopupAnchor = finalRect;
  LOG("  anchor is correct %d,%d -> %d x %d", finalRect.x, finalRect.y,
      finalRect.width, finalRect.height);

  *aOffset = mPopupMoveToRectParams.mOffset;
  LOG("  anchor offset %d, %d", aOffset->x, aOffset->y);
  return true;
}

bool nsWindow::WaylandPopupCheckAndGetAnchor(GdkRectangle* aPopupAnchor,
                                             GdkPoint* aOffset) {
  LOG("nsWindow::WaylandPopupCheckAndGetAnchor");

  GdkWindow* gdkWindow = GetToplevelGdkWindow();
  nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame());
  if (!gdkWindow || !popupFrame) {
    LOG("  can't use move-to-rect due missing gdkWindow or popupFrame");
    return false;
  }

  if (popupFrame->IsConstrainedByLayout()) {
    LOG("  can't use move-to-rect, flipped / constrained by layout");
    return false;
  }

  if (!mPopupMoveToRectParams.mAnchorSet) {
    LOG("  can't use move-to-rect due missing anchor");
    return false;
  }
  // Update popup layout coordinates from layout by recent popup hierarchy
  // (calculate correct position according to parent window)
  // and convert to Gtk coordinates.
  LayoutDeviceIntRect anchorRect = mPopupMoveToRectParams.mAnchorRect;
  if (!WaylandPopupIsFirst()) {
    GdkPoint parent = WaylandGetParentPosition();
    LOG("  subtract parent position from anchor [%d, %d]\n", parent.x,
        parent.y);
    anchorRect.MoveBy(-GdkPointToDevicePixels(parent));
  }

  *aPopupAnchor = DevicePixelsToGdkRectRoundOut(anchorRect);
  LOG("  anchored to rectangle [%d, %d] -> [%d x %d]", aPopupAnchor->x,
      aPopupAnchor->y, aPopupAnchor->width, aPopupAnchor->height);

  if (!WaylandPopupAnchorAdjustForParentPopup(aPopupAnchor, aOffset)) {
    LOG("  can't use move-to-rect, anchor is not placed inside of parent "
        "window");
    return false;
  }

  return true;
}

void nsWindow::WaylandPopupPrepareForMove() {
  LOG("nsWindow::WaylandPopupPrepareForMove()");

  if (mPopupHint == PopupType::Tooltip) {
    // Don't fiddle with tooltips type, just hide it before move-to-rect
    if (mPopupUseMoveToRect && gtk_widget_is_visible(mShell)) {
      HideWaylandPopupWindow(/* aTemporaryHide */ true,
                             /* aRemoveFromPopupList */ false);
    }
    LOG("  it's tooltip, quit");
    return;
  }

  // See https://bugzilla.mozilla.org/show_bug.cgi?id=1785185#c8
  // gtk_window_move() needs GDK_WINDOW_TYPE_HINT_UTILITY popup type.
  // move-to-rect requires GDK_WINDOW_TYPE_HINT_POPUP_MENU popups type.
  // We need to set it before map event when popup is hidden.
  const GdkWindowTypeHint currentType =
      gtk_window_get_type_hint(GTK_WINDOW(mShell));
  const GdkWindowTypeHint requiredType = mPopupUseMoveToRect
                                             ? GDK_WINDOW_TYPE_HINT_POPUP_MENU
                                             : GDK_WINDOW_TYPE_HINT_UTILITY;

  if (!mPopupUseMoveToRect && currentType == requiredType) {
    LOG("  type matches and we're not forced to hide it, quit.");
    return;
  }

  if (gtk_widget_is_visible(mShell)) {
    HideWaylandPopupWindow(/* aTemporaryHide */ true,
                           /* aRemoveFromPopupList */ false);
  }

  if (currentType != requiredType) {
    LOG("  set type %s",
        requiredType == GDK_WINDOW_TYPE_HINT_POPUP_MENU ? "MENU" : "UTILITY");
    gtk_window_set_type_hint(GTK_WINDOW(mShell), requiredType);
  }
}

// Plain popup move on Wayland - simply place popup on given location.
// We can't just call gtk_window_move() as it's not effective on visible
// popups.
void nsWindow::WaylandPopupMovePlain(int aX, int aY) {
  LOG("nsWindow::WaylandPopupMovePlain(%d, %d)", aX, aY);

  // We can directly move only popups based on wl_subsurface type.
  MOZ_DIAGNOSTIC_ASSERT(gtk_window_get_type_hint(GTK_WINDOW(mShell)) ==
                            GDK_WINDOW_TYPE_HINT_UTILITY ||
                        gtk_window_get_type_hint(GTK_WINDOW(mShell)) ==
                            GDK_WINDOW_TYPE_HINT_TOOLTIP);

  gtk_window_move(GTK_WINDOW(mShell), aX, aY);

  // gtk_window_move() can trick us. When widget is hidden gtk_window_move()
  // does not move the widget but sets new widget coordinates when widget
  // is mapped again.
  //
  // If popup used move-to-rect before
  // (GdkWindow has POSITION_METHOD_MOVE_TO_RECT set), popup will use
  // move-to-rect again when it's mapped and we'll get bogus move-to-rect
  // callback.
  //
  // gdk_window_move() sets position_method to POSITION_METHOD_MOVE_RESIZE
  // so we'll use plain move when popup is shown.
  if (!gtk_widget_get_mapped(mShell)) {
    if (GdkWindow* window = GetToplevelGdkWindow()) {
      gdk_window_move(window, aX, aY);
    }
  }
}

void nsWindow::WaylandPopupMoveImpl() {
  // Available as of GTK 3.24+
  static auto sGdkWindowMoveToRect = (void (*)(
      GdkWindow*, const GdkRectangle*, GdkGravity, GdkGravity, GdkAnchorHints,
      gint, gint))dlsym(RTLD_DEFAULT, "gdk_window_move_to_rect");

  if (mPopupUseMoveToRect && !sGdkWindowMoveToRect) {
    LOG("can't use move-to-rect due missing gdk_window_move_to_rect()");
    mPopupUseMoveToRect = false;
  }

  GdkRectangle gtkAnchorRect;
  GdkPoint offset;
  if (mPopupUseMoveToRect) {
    mPopupUseMoveToRect =
        WaylandPopupCheckAndGetAnchor(&gtkAnchorRect, &offset);
  }

  LOG("nsWindow::WaylandPopupMove");
  LOG("  original widget popup position [%d, %d]\n", mPopupPosition.x,
      mPopupPosition.y);
  LOG("  relative widget popup position [%d, %d]\n", mRelativePopupPosition.x,
      mRelativePopupPosition.y);
  LOG("  popup use move to rect %d", mPopupUseMoveToRect);

  WaylandPopupPrepareForMove();

  if (!mPopupUseMoveToRect) {
    WaylandPopupMovePlain(mRelativePopupPosition.x, mRelativePopupPosition.y);
    // Layout already should be aware of our bounds, since we didn't change it
    // from the widget side for flipping or so.
    return;
  }

  // Correct popup position now. It will be updated by gdk_window_move_to_rect()
  // anyway but we need to set it now to avoid a race condition here.
  WaylandPopupRemoveNegativePosition();

  GdkWindow* gdkWindow = GetToplevelGdkWindow();
  if (!g_signal_handler_find(gdkWindow, G_SIGNAL_MATCH_FUNC, 0, 0, nullptr,
                             FuncToGpointer(NativeMoveResizeCallback), this)) {
    g_signal_connect(gdkWindow, "moved-to-rect",
                     G_CALLBACK(NativeMoveResizeCallback), this);
  }
  mWaitingForMoveToRectCallback = true;

#ifdef MOZ_LOGGING
  if (LOG_ENABLED()) {
    LOG("  Call move-to-rect");
    LOG("  Anchor rect [%d, %d] -> [%d x %d]", gtkAnchorRect.x, gtkAnchorRect.y,
        gtkAnchorRect.width, gtkAnchorRect.height);
    LOG("  Offset [%d, %d]", offset.x, offset.y);
    LOG("  AnchorType");
    LogPopupGravity(mPopupMoveToRectParams.mAnchorRectType);
    LOG("  PopupAnchorType");
    LogPopupGravity(mPopupMoveToRectParams.mPopupAnchorType);
    LogPopupAnchorHints(mPopupMoveToRectParams.mHints);
  }
#endif

  sGdkWindowMoveToRect(gdkWindow, &gtkAnchorRect,
                       mPopupMoveToRectParams.mAnchorRectType,
                       mPopupMoveToRectParams.mPopupAnchorType,
                       mPopupMoveToRectParams.mHints, offset.x, offset.y);
}

void nsWindow::SetZIndex(int32_t aZIndex) {
  nsIWidget* oldPrev = GetPrevSibling();

  nsBaseWidget::SetZIndex(aZIndex);

  if (GetPrevSibling() == oldPrev) {
    return;
  }

  NS_ASSERTION(!mContainer, "Expected Mozilla child widget");

  // We skip the nsWindows that don't have mGdkWindows.
  // These are probably in the process of being destroyed.
  if (!mGdkWindow) {
    return;
  }

  if (!GetNextSibling()) {
    // We're to be on top.
    if (mGdkWindow) {
      gdk_window_raise(mGdkWindow);
    }
  } else {
    // All the siblings before us need to be below our widget.
    for (nsWindow* w = this; w;
         w = static_cast<nsWindow*>(w->GetPrevSibling())) {
      if (w->mGdkWindow) {
        gdk_window_lower(w->mGdkWindow);
      }
    }
  }
}

void nsWindow::SetSizeMode(nsSizeMode aMode) {
  LOG("nsWindow::SetSizeMode %d\n", aMode);

  // Return if there's no shell or our current state is the same as the mode we
  // were just set to.
  if (!mShell) {
    LOG("    no shell");
    return;
  }

  if (mSizeMode == aMode && mLastSizeModeRequest == aMode) {
    LOG("    already set");
    return;
  }

  // It is tempting to try to optimize calls below based only on current
  // mSizeMode, but that wouldn't work if there's a size-request in flight
  // (specially before show). See bug 1789823.
  const auto SizeModeMightBe = [&](nsSizeMode aModeToTest) {
    if (mSizeMode != mLastSizeModeRequest) {
      // Arbitrary size mode requests might be ongoing.
      return true;
    }
    return mSizeMode == aModeToTest;
  };

  if (aMode != nsSizeMode_Fullscreen && aMode != nsSizeMode_Minimized) {
    // Fullscreen and minimized are compatible.
    if (SizeModeMightBe(nsSizeMode_Fullscreen)) {
      MakeFullScreen(false);
    }
  }

  switch (aMode) {
    case nsSizeMode_Maximized:
      LOG("    set maximized");
      gtk_window_maximize(GTK_WINDOW(mShell));
      break;
    case nsSizeMode_Minimized:
      LOG("    set minimized");
      gtk_window_iconify(GTK_WINDOW(mShell));
      break;
    case nsSizeMode_Fullscreen:
      LOG("    set fullscreen");
      MakeFullScreen(true);
      break;
    default:
      MOZ_FALLTHROUGH_ASSERT("Unknown size mode");
    case nsSizeMode_Normal:
      LOG("    set normal");
      if (SizeModeMightBe(nsSizeMode_Maximized)) {
        gtk_window_unmaximize(GTK_WINDOW(mShell));
      }
      if (SizeModeMightBe(nsSizeMode_Minimized)) {
        gtk_window_deiconify(GTK_WINDOW(mShell));
        // We need this for actual deiconification on mutter.
        gtk_window_present(GTK_WINDOW(mShell));
      }
      break;
  }
  mLastSizeModeRequest = aMode;
}

#define kDesktopMutterSchema "org.gnome.mutter"_ns
#define kDesktopDynamicWorkspacesKey "dynamic-workspaces"_ns

static bool WorkspaceManagementDisabled(GdkScreen* screen) {
  if (Preferences::GetBool("widget.disable-workspace-management", false)) {
    return true;
  }
  if (Preferences::HasUserValue("widget.workspace-management")) {
    return Preferences::GetBool("widget.workspace-management");
  }

  if (IsGnomeDesktopEnvironment()) {
    // Gnome uses dynamic workspaces by default so disable workspace management
    // in that case.
    bool usesDynamicWorkspaces = true;
    nsCOMPtr<nsIGSettingsService> gsettings =
        do_GetService(NS_GSETTINGSSERVICE_CONTRACTID);
    if (gsettings) {
      nsCOMPtr<nsIGSettingsCollection> mutterSettings;
      gsettings->GetCollectionForSchema(kDesktopMutterSchema,
                                        getter_AddRefs(mutterSettings));
      if (mutterSettings) {
        mutterSettings->GetBoolean(kDesktopDynamicWorkspacesKey,
                                   &usesDynamicWorkspaces);
      }
    }
    return usesDynamicWorkspaces;
  }

  const auto& desktop = GetDesktopEnvironmentIdentifier();
  return desktop.EqualsLiteral("bspwm") || desktop.EqualsLiteral("i3");
}

void nsWindow::GetWorkspaceID(nsAString& workspaceID) {
  workspaceID.Truncate();

  if (!GdkIsX11Display() || !mShell) {
    return;
  }

#ifdef MOZ_X11
  LOG("nsWindow::GetWorkspaceID()\n");

  // Get the gdk window for this widget.
  GdkWindow* gdk_window = GetToplevelGdkWindow();
  if (!gdk_window) {
    LOG("  missing Gdk window, quit.");
    return;
  }

  if (WorkspaceManagementDisabled(gdk_window_get_screen(gdk_window))) {
    LOG("  WorkspaceManagementDisabled, quit.");
    return;
  }

  GdkAtom cardinal_atom = gdk_x11_xatom_to_atom(XA_CARDINAL);
  GdkAtom type_returned;
  int format_returned;
  int length_returned;
  long* wm_desktop;

  if (!gdk_property_get(gdk_window, gdk_atom_intern("_NET_WM_DESKTOP", FALSE),
                        cardinal_atom,
                        0,          // offset
                        INT32_MAX,  // length
                        FALSE,      // delete
                        &type_returned, &format_returned, &length_returned,
                        (guchar**)&wm_desktop)) {
    LOG("  gdk_property_get() failed, quit.");
    return;
  }

  LOG("  got workspace ID %d", (int32_t)wm_desktop[0]);
  workspaceID.AppendInt((int32_t)wm_desktop[0]);
  g_free(wm_desktop);
#endif
}

void nsWindow::MoveToWorkspace(const nsAString& workspaceIDStr) {
  nsresult rv = NS_OK;
  int32_t workspaceID = workspaceIDStr.ToInteger(&rv);

  LOG("nsWindow::MoveToWorkspace() ID %d", workspaceID);
  if (NS_FAILED(rv) || !workspaceID || !GdkIsX11Display() || !mShell) {
    LOG("  MoveToWorkspace disabled, quit");
    return;
  }

#ifdef MOZ_X11
  // Get the gdk window for this widget.
  GdkWindow* gdk_window = GetToplevelGdkWindow();
  if (!gdk_window) {
    LOG("  failed to get GdkWindow, quit.");
    return;
  }

  // This code is inspired by some found in the 'gxtuner' project.
  // https://github.com/brummer10/gxtuner/blob/792d453da0f3a599408008f0f1107823939d730d/deskpager.cpp#L50
  XEvent xevent;
  Display* xdisplay = gdk_x11_get_default_xdisplay();
  GdkScreen* screen = gdk_window_get_screen(gdk_window);
  Window root_win = GDK_WINDOW_XID(gdk_screen_get_root_window(screen));
  GdkDisplay* display = gdk_window_get_display(gdk_window);
  Atom type = gdk_x11_get_xatom_by_name_for_display(display, "_NET_WM_DESKTOP");

  xevent.type = ClientMessage;
  xevent.xclient.type = ClientMessage;
  xevent.xclient.serial = 0;
  xevent.xclient.send_event = TRUE;
  xevent.xclient.display = xdisplay;
  xevent.xclient.window = GDK_WINDOW_XID(gdk_window);
  xevent.xclient.message_type = type;
  xevent.xclient.format = 32;
  xevent.xclient.data.l[0] = workspaceID;
  xevent.xclient.data.l[1] = X11CurrentTime;
  xevent.xclient.data.l[2] = 0;
  xevent.xclient.data.l[3] = 0;
  xevent.xclient.data.l[4] = 0;

  XSendEvent(xdisplay, root_win, FALSE,
             SubstructureNotifyMask | SubstructureRedirectMask, &xevent);

  XFlush(xdisplay);
  LOG("  moved to workspace");
#endif
}

void nsWindow::SetUserTimeAndStartupTokenForActivatedWindow() {
  nsGTKToolkit* toolkit = nsGTKToolkit::GetToolkit();
  if (!toolkit || MOZ_UNLIKELY(mWindowType == WindowType::Invisible)) {
    return;
  }

  mWindowActivationTokenFromEnv = toolkit->GetStartupToken();
  if (!mWindowActivationTokenFromEnv.IsEmpty()) {
    if (!GdkIsWaylandDisplay()) {
      gtk_window_set_startup_id(GTK_WINDOW(mShell),
                                mWindowActivationTokenFromEnv.get());
      // In the case of X11, the above call is all we need. For wayland we need
      // to keep the token around until we take it in
      // TransferFocusToWaylandWindow.
      mWindowActivationTokenFromEnv.Truncate();
    }
  } else if (uint32_t timestamp = toolkit->GetFocusTimestamp()) {
    // We don't have the data we need. Fall back to an
    // approximation ... using the timestamp of the remote command
    // being received as a guess for the timestamp of the user event
    // that triggered it.
    gdk_window_focus(GetToplevelGdkWindow(), timestamp);
  }

  // If we used the startup ID, that already contains the focus timestamp;
  // we don't want to reuse the timestamp next time we raise the window
  toolkit->SetFocusTimestamp(0);
  toolkit->SetStartupToken(""_ns);
}

/* static */
guint32 nsWindow::GetLastUserInputTime() {
  // gdk_x11_display_get_user_time/gtk_get_current_event_time tracks
  // button and key presses, DESKTOP_STARTUP_ID used to start the app,
  // drop events from external drags,
  // WM_DELETE_WINDOW delete events, but not usually mouse motion nor
  // button and key releases.  Therefore use the most recent of
  // gdk_x11_display_get_user_time and the last time that we have seen.
#ifdef MOZ_X11
  GdkDisplay* gdkDisplay = gdk_display_get_default();
  guint32 timestamp = GdkIsX11Display(gdkDisplay)
                          ? gdk_x11_display_get_user_time(gdkDisplay)
                          : gtk_get_current_event_time();
#else
  guint32 timestamp = gtk_get_current_event_time();
#endif

  if (sLastUserInputTime != GDK_CURRENT_TIME &&
      TimestampIsNewerThan(sLastUserInputTime, timestamp)) {
    return sLastUserInputTime;
  }

  return timestamp;
}

#ifdef MOZ_WAYLAND
void nsWindow::FocusWaylandWindow(const char* aTokenID) {
  MOZ_DIAGNOSTIC_ASSERT(aTokenID);

  LOG("nsWindow::FocusWaylandWindow(%s)", aTokenID);
  if (IsDestroyed()) {
    LOG("  already destroyed, quit.");
    return;
  }
  wl_surface* surface =
      mGdkWindow ? gdk_wayland_window_get_wl_surface(mGdkWindow) : nullptr;
  if (!surface) {
    LOG("  mGdkWindow is not visible, quit.");
    return;
  }

  LOG("  requesting xdg-activation, surface ID %d",
      wl_proxy_get_id((struct wl_proxy*)surface));
  xdg_activation_v1* xdg_activation = WaylandDisplayGet()->GetXdgActivation();
  if (!xdg_activation) {
    return;
  }
  xdg_activation_v1_activate(xdg_activation, aTokenID, surface);
}

// Transfer focus from gFocusWindow to aWindow and use xdg_activation
// protocol for it.
void nsWindow::TransferFocusToWaylandWindow(nsWindow* aWindow) {
  LOGW("nsWindow::TransferFocusToWaylandWindow(%p) gFocusWindow %p", aWindow,
       gFocusWindow);
  auto promise = mozilla::widget::RequestWaylandFocusPromise();
  if (NS_WARN_IF(!promise)) {
    LOGW("  quit, failed to create TransferFocusToWaylandWindow [%p]", aWindow);
    return;
  }
  promise->Then(
      GetMainThreadSerialEventTarget(), __func__,
      /* resolve */
      [window = RefPtr{aWindow}](nsCString token) {
        window->FocusWaylandWindow(token.get());
      },
      /* reject */
      [window = RefPtr{aWindow}](bool state) {
        LOGW("TransferFocusToWaylandWindow [%p] failed", window.get());
      });
}
#endif

// Request activation of this window or give focus to this widget.
// aRaise means whether we should request activation of this widget's
// toplevel window.
//
// nsWindow::SetFocus(Raise::Yes) - Raise and give focus to toplevel window.
// nsWindow::SetFocus(Raise::No) - Give focus to this window.
void nsWindow::SetFocus(Raise aRaise, mozilla::dom::CallerType aCallerType) {
  LOG("nsWindow::SetFocus Raise %d\n", aRaise == Raise::Yes);

  // Raise the window if someone passed in true and the prefs are
  // set properly.
  GtkWidget* toplevelWidget = gtk_widget_get_toplevel(GTK_WIDGET(mContainer));

  LOG("  gFocusWindow [%p]\n", gFocusWindow);
  LOG("  mContainer [%p]\n", GTK_WIDGET(mContainer));
  LOG("  Toplevel widget [%p]\n", toplevelWidget);

  // Make sure that our owning widget has focus.  If it doesn't try to
  // grab it.  Note that we don't set our focus flag in this case.
  if (StaticPrefs::mozilla_widget_raise_on_setfocus_AtStartup() &&
      aRaise == Raise::Yes && toplevelWidget &&
      !gtk_widget_has_focus(toplevelWidget)) {
    if (gtk_widget_get_visible(mShell)) {
      LOG("  toplevel is not focused");
      gdk_window_show_unraised(GetToplevelGdkWindow());
      // Unset the urgency hint if possible.
      SetUrgencyHint(mShell, false);
    }
  }

  RefPtr<nsWindow> toplevelWindow = get_window_for_gtk_widget(toplevelWidget);
  if (!toplevelWindow) {
    LOG("  missing toplevel nsWindow, quit\n");
    return;
  }

  if (aRaise == Raise::Yes) {
    // means request toplevel activation.

    // This is asynchronous. If and when the window manager accepts the request,
    // then the focus widget will get a focus-in-event signal.
    if (StaticPrefs::mozilla_widget_raise_on_setfocus_AtStartup() &&
        toplevelWindow->mIsShown && toplevelWindow->mShell &&
        !gtk_window_is_active(GTK_WINDOW(toplevelWindow->mShell))) {
      LOG("  toplevel is visible but not active, requesting activation [%p]",
          toplevelWindow.get());

      // Take the time here explicitly for the call below.
      const uint32_t timestamp = [&] {
        if (nsGTKToolkit* toolkit = nsGTKToolkit::GetToolkit()) {
          if (uint32_t t = toolkit->GetFocusTimestamp()) {
            toolkit->SetFocusTimestamp(0);
            return t;
          }
        }
        return GetLastUserInputTime();
      }();

      toplevelWindow->SetUserTimeAndStartupTokenForActivatedWindow();
      gtk_window_present_with_time(GTK_WINDOW(toplevelWindow->mShell),
                                   timestamp);

#ifdef MOZ_WAYLAND
      if (GdkIsWaylandDisplay()) {
        auto existingToken =
            std::move(toplevelWindow->mWindowActivationTokenFromEnv);
        if (!existingToken.IsEmpty()) {
          LOG("  has existing activation token.");
          toplevelWindow->FocusWaylandWindow(existingToken.get());
        } else {
          LOG("  missing activation token, try to transfer from focused "
              "window");
          TransferFocusToWaylandWindow(toplevelWindow);
        }
      }
#endif
    }
    return;
  }

  // aRaise == No means that keyboard events should be dispatched from this
  // widget.

  // Ensure GTK_WIDGET(mContainer) is the focused GtkWidget within its toplevel
  // window.
  //
  // For WindowType::Popup, this GtkWidget may not actually be the one that
  // receives the key events as it may be the parent window that is active.
  if (!gtk_widget_is_focus(GTK_WIDGET(mContainer))) {
    // This is synchronous.  It takes focus from a plugin or from a widget
    // in an embedder.  The focus manager already knows that this window
    // is active so gBlockActivateEvent avoids another (unnecessary)
    // activate notification.
    gBlockActivateEvent = true;
    gtk_widget_grab_focus(GTK_WIDGET(mContainer));
    gBlockActivateEvent = false;
  }

  // If this is the widget that already has focus, return.
  if (gFocusWindow == this) {
    LOG("  already have focus");
    return;
  }

  // Set this window to be the focused child window
  gFocusWindow = this;

  if (mIMContext) {
    mIMContext->OnFocusWindow(this);
  }

  LOG("  widget now has focus in SetFocus()");
}

LayoutDeviceIntRect nsWindow::GetScreenBounds() {
  const LayoutDeviceIntPoint origin = [&] {
    // XXX Can't we use mGdkWindow here?
    //
    // Use the point including window decorations. Don't do this for popups,
    // because we get wrong coordinates for gtk for override-redirect windows in
    // HiDPI screens, and those don't have window decorations anyways.
    //
    // See https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/4820
    if (mContainer && mWindowType != WindowType::Popup) {
      gint x, y;
      gdk_window_get_root_origin(gtk_widget_get_window(GTK_WIDGET(mContainer)),
                                 &x, &y);
      return GdkPointToDevicePixels({x, y});
    }
    return WidgetToScreenOffset();
  }();

  // mBounds.Size() is the window bounds, not the window-manager frame
  // bounds (bug 581863).  gdk_window_get_frame_extents would give the
  // frame bounds, but mBounds.Size() is returned here for consistency
  // with Resize.
  const LayoutDeviceIntRect rect(origin, mBounds.Size());
#if MOZ_LOGGING
  if (MOZ_LOG_TEST(IsPopup() ? gWidgetPopupLog : gWidgetLog,
                   LogLevel::Verbose)) {
    gint scale = GdkCeiledScaleFactor();
    if (mLastLoggedScale != scale || !(mLastLoggedBoundSize == rect)) {
      mLastLoggedScale = scale;
      mLastLoggedBoundSize = rect;
      LOG("GetScreenBounds %d,%d -> %d x %d, unscaled %d,%d -> %d x %d\n",
          rect.x, rect.y, rect.width, rect.height, rect.x / scale,
          rect.y / scale, rect.width / scale, rect.height / scale);
    }
  }
#endif
  return rect;
}

LayoutDeviceIntSize nsWindow::GetClientSize() {
  return LayoutDeviceIntSize(mBounds.width, mBounds.height);
}

LayoutDeviceIntRect nsWindow::GetClientBounds() {
  // GetBounds returns a rect whose top left represents the top left of the
  // outer bounds, but whose width/height represent the size of the inner
  // bounds (which is messed up).
  LayoutDeviceIntRect rect = GetBounds();
  rect.MoveBy(GetClientOffset());
  return rect;
}

void nsWindow::RecomputeClientOffset(bool aNotify) {
  if (!IsTopLevelWindowType()) {
    return;
  }

  auto oldOffset = mClientOffset;

  mClientOffset = WidgetToScreenOffset() - mBounds.TopLeft();

  if (aNotify && mClientOffset != oldOffset) {
    // Send a WindowMoved notification. This ensures that BrowserParent picks up
    // the new client offset and sends it to the child process if appropriate.
    NotifyWindowMoved(mBounds.x, mBounds.y);
  }
}

gboolean nsWindow::OnPropertyNotifyEvent(GtkWidget* aWidget,
                                         GdkEventProperty* aEvent) {
  if (aEvent->atom == gdk_atom_intern("_NET_FRAME_EXTENTS", FALSE)) {
    RecomputeClientOffset(/* aNotify = */ true);
    return FALSE;
  }
  if (!mGdkWindow) {
    return FALSE;
  }
#ifdef MOZ_X11
  if (GetCurrentTimeGetter()->PropertyNotifyHandler(aWidget, aEvent)) {
    return TRUE;
  }
#endif
  return FALSE;
}

static GdkCursor* GetCursorForImage(const nsIWidget::Cursor& aCursor,
                                    int32_t aWidgetScaleFactor) {
  if (!aCursor.IsCustom()) {
    return nullptr;
  }
  nsIntSize size = nsIWidget::CustomCursorSize(aCursor);

  // NOTE: GTK only allows integer scale factors, so we ceil to the larger scale
  // factor and then tell gtk to scale it down. We ensure to scale at least to
  // the GDK scale factor, so that cursors aren't downsized in HiDPI on wayland,
  // see bug 1707533.
  int32_t gtkScale = std::max(
      aWidgetScaleFactor, int32_t(std::ceil(std::max(aCursor.mResolution.mX,
                                                     aCursor.mResolution.mY))));

  // Reject cursors greater than 128 pixels in some direction, to prevent
  // spoofing.
  // XXX ideally we should rescale. Also, we could modify the API to
  // allow trusted content to set larger cursors.
  //
  // TODO(emilio, bug 1445844): Unify the solution for this with other
  // platforms.
  if (size.width > 128 || size.height > 128) {
    return nullptr;
  }

  nsIntSize rasterSize = size * gtkScale;
  RefPtr<GdkPixbuf> pixbuf =
      nsImageToPixbuf::ImageToPixbuf(aCursor.mContainer, Some(rasterSize));
  if (!pixbuf) {
    return nullptr;
  }

  // Looks like all cursors need an alpha channel (tested on Gtk 2.4.4). This
  // is of course not documented anywhere...
  // So add one if there isn't one yet
  if (!gdk_pixbuf_get_has_alpha(pixbuf)) {
    RefPtr<GdkPixbuf> alphaBuf =
        dont_AddRef(gdk_pixbuf_add_alpha(pixbuf, FALSE, 0, 0, 0));
    pixbuf = std::move(alphaBuf);
    if (!pixbuf) {
      return nullptr;
    }
  }

  cairo_surface_t* surface =
      gdk_cairo_surface_create_from_pixbuf(pixbuf, gtkScale, nullptr);
  if (!surface) {
    return nullptr;
  }

  auto CleanupSurface =
      MakeScopeExit([&]() { cairo_surface_destroy(surface); });

  return gdk_cursor_new_from_surface(gdk_display_get_default(), surface,
                                     aCursor.mHotspotX, aCursor.mHotspotY);
}

void nsWindow::SetCursor(const Cursor& aCursor) {
  // if we're not the toplevel window pass up the cursor request to
  // the toplevel window to handle it.
  if (!mContainer && mGdkWindow) {
    if (nsWindow* window = GetContainerWindow()) {
      window->SetCursor(aCursor);
    }
    return;
  }

  if (mWidgetCursorLocked) {
    return;
  }

  // Only change cursor if it's actually been changed
  if (!mUpdateCursor && mCursor == aCursor) {
    return;
  }

  mUpdateCursor = false;
  mCursor = aCursor;

  // Try to set the cursor image first, and fall back to the numeric cursor.
  bool fromImage = true;
  GdkCursor* newCursor = GetCursorForImage(aCursor, GdkCeiledScaleFactor());
  if (!newCursor) {
    fromImage = false;
    newCursor = get_gtk_cursor(aCursor.mDefaultCursor);
  }

  auto CleanupCursor = mozilla::MakeScopeExit([&]() {
    // get_gtk_cursor returns a weak reference, which we shouldn't unref.
    if (fromImage) {
      g_object_unref(newCursor);
    }
  });

  if (!newCursor || !mContainer) {
    return;
  }

  gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(mContainer)),
                        newCursor);
}

void nsWindow::Invalidate(const LayoutDeviceIntRect& aRect) {
  if (!mGdkWindow) return;

  GdkRectangle rect = DevicePixelsToGdkRectRoundOut(aRect);
  gdk_window_invalidate_rect(mGdkWindow, &rect, FALSE);

  LOG("Invalidate (rect): %d %d %d %d\n", rect.x, rect.y, rect.width,
      rect.height);
}

void* nsWindow::GetNativeData(uint32_t aDataType) {
  switch (aDataType) {
    case NS_NATIVE_WINDOW:
    case NS_NATIVE_WIDGET: {
      return mGdkWindow;
    }

    case NS_NATIVE_SHELLWIDGET:
      return GetToplevelWidget();

    case NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID:
#ifdef MOZ_X11
      if (GdkIsX11Display()) {
        return (void*)GDK_WINDOW_XID(gdk_window_get_toplevel(mGdkWindow));
      }
#endif
      NS_WARNING(
          "nsWindow::GetNativeData(): NS_NATIVE_WINDOW_WEBRTC_DEVICE_ID is not "
          "handled on Wayland!");
      return nullptr;
    case NS_RAW_NATIVE_IME_CONTEXT: {
      void* pseudoIMEContext = GetPseudoIMEContext();
      if (pseudoIMEContext) {
        return pseudoIMEContext;
      }
      // If IME context isn't available on this widget, we should set |this|
      // instead of nullptr.
      if (!mIMContext) {
        return this;
      }
      return mIMContext.get();
    }
    case NS_NATIVE_OPENGL_CONTEXT:
      return nullptr;
    case NS_NATIVE_EGL_WINDOW: {
      void* eglWindow = nullptr;

      // We can't block on mutex here as it leads to a deadlock:
      // 1) mutex is taken at nsWindow::Destroy()
      // 2) NS_NATIVE_EGL_WINDOW is called from compositor/rendering thread,
      //    blocking on mutex.
      // 3) DestroyCompositor() is called by nsWindow::Destroy(). As a sync
      //    call it waits to compositor/rendering threads,
      //    but they're blocked at 2).
      //    It's fine if we return null EGL window during DestroyCompositor(),
      //    in such case compositor painting is skipped.
      if (mDestroyMutex.TryLock()) {
        if (mGdkWindow && !mIsDestroyed) {
#ifdef MOZ_X11
          if (GdkIsX11Display()) {
            eglWindow = (void*)GDK_WINDOW_XID(mGdkWindow);
          }
#endif
#ifdef MOZ_WAYLAND
          if (GdkIsWaylandDisplay()) {
            bool hiddenWindow =
                mCompositorWidgetDelegate &&
                mCompositorWidgetDelegate->AsGtkCompositorWidget() &&
                mCompositorWidgetDelegate->AsGtkCompositorWidget()->IsHidden();
            if (!hiddenWindow) {
              eglWindow = moz_container_wayland_get_egl_window(
                  mContainer, FractionalScaleFactor());
            }
          }
#endif
        }
        mDestroyMutex.Unlock();
      }

      LOG("Get NS_NATIVE_EGL_WINDOW mGdkWindow %p returned eglWindow %p",
          mGdkWindow, eglWindow);
      return eglWindow;
    }
    default:
      NS_WARNING("nsWindow::GetNativeData called with bad value");
      return nullptr;
  }
}

nsresult nsWindow::SetTitle(const nsAString& aTitle) {
  if (!mShell) return NS_OK;

    // convert the string into utf8 and set the title.
#define UTF8_FOLLOWBYTE(ch) (((ch)&0xC0) == 0x80)
  NS_ConvertUTF16toUTF8 titleUTF8(aTitle);
  if (titleUTF8.Length() > NS_WINDOW_TITLE_MAX_LENGTH) {
    // Truncate overlong titles (bug 167315). Make sure we chop after a
    // complete sequence by making sure the next char isn't a follow-byte.
    uint32_t len = NS_WINDOW_TITLE_MAX_LENGTH;
    while (UTF8_FOLLOWBYTE(titleUTF8[len])) --len;
    titleUTF8.Truncate(len);
  }
  gtk_window_set_title(GTK_WINDOW(mShell), (const char*)titleUTF8.get());

  return NS_OK;
}

void nsWindow::SetIcon(const nsAString& aIconSpec) {
  if (!mShell) return;

  nsAutoCString iconName;

  if (aIconSpec.EqualsLiteral("default")) {
    nsAutoString brandName;
    WidgetUtils::GetBrandShortName(brandName);
    if (brandName.IsEmpty()) {
      brandName.AssignLiteral(u"Mozilla");
    }
    AppendUTF16toUTF8(brandName, iconName);
    ToLowerCase(iconName);
  } else {
    AppendUTF16toUTF8(aIconSpec, iconName);
  }

  nsCOMPtr<nsIFile> iconFile;
  nsAutoCString path;

  gint* iconSizes = gtk_icon_theme_get_icon_sizes(gtk_icon_theme_get_default(),
                                                  iconName.get());
  bool foundIcon = (iconSizes[0] != 0);
  g_free(iconSizes);

  if (!foundIcon) {
    // Look for icons with the following suffixes appended to the base name
    // The last two entries (for the old XPM format) will be ignored unless
    // no icons are found using other suffixes. XPM icons are deprecated.

    const char16_t extensions[9][8] = {u".png",    u"16.png", u"32.png",
                                       u"48.png",  u"64.png", u"128.png",
                                       u"256.png", u".xpm",   u"16.xpm"};

    for (uint32_t i = 0; i < ArrayLength(extensions); i++) {
      // Don't bother looking for XPM versions if we found a PNG.
      if (i == ArrayLength(extensions) - 2 && foundIcon) break;

      ResolveIconName(aIconSpec, nsDependentString(extensions[i]),
                      getter_AddRefs(iconFile));
      if (iconFile) {
        iconFile->GetNativePath(path);
        GdkPixbuf* icon = gdk_pixbuf_new_from_file(path.get(), nullptr);
        if (icon) {
          gtk_icon_theme_add_builtin_icon(iconName.get(),
                                          gdk_pixbuf_get_height(icon), icon);
          g_object_unref(icon);
          foundIcon = true;
        }
      }
    }
  }

  // leave the default icon intact if no matching icons were found
  if (foundIcon) {
    gtk_window_set_icon_name(GTK_WINDOW(mShell), iconName.get());
  }
}

/* TODO(bug 1655924): gdk_window_get_origin is can block waiting for the X
   server for a long time, we would like to use the implementation below
   instead. However, removing the synchronous x server queries causes a race
   condition to surface, causing issues such as bug 1652743 and bug 1653711.


   This code can be used instead of gdk_window_get_origin() but it cuases
   such issues:

    *aX = 0;
    *aY = 0;
    if (!aWindow) {
      return;
    }

    GdkWindow* current = aWindow;
    while (GdkWindow* parent = gdk_window_get_parent(current)) {
      if (parent == current) {
        break;
      }

      int x = 0;
      int y = 0;
      gdk_window_get_position(current, &x, &y);
      *aX += x;
      *aY += y;

      current = parent;
    }
*/
LayoutDeviceIntPoint nsWindow::WidgetToScreenOffset() {
  // Don't use gdk_window_get_origin() on wl_subsurface Wayland popups
  // https://gitlab.gnome.org/GNOME/gtk/-/issues/5287
  if (IsWaylandPopup() && !mPopupUseMoveToRect) {
    return mBounds.TopLeft();
  }
  nsIntPoint origin(0, 0);
  if (mGdkWindow) {
    gdk_window_get_origin(mGdkWindow, &origin.x.value, &origin.y.value);
  }
  return GdkPointToDevicePixels({origin.x, origin.y});
}

void nsWindow::CaptureRollupEvents(bool aDoCapture) {
  LOG("CaptureRollupEvents(%d)\n", aDoCapture);
  if (mIsDestroyed) {
    return;
  }

  static constexpr auto kCaptureEventsMask =
      GdkEventMask(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
                   GDK_POINTER_MOTION_MASK | GDK_TOUCH_MASK);

  static bool sSystemNeedsPointerGrab = [&] {
    if (GdkIsWaylandDisplay()) {
      return false;
    }
    // We only need to grab the pointer for X servers that move the focus with
    // the pointer (like twm, sawfish...). Since we roll up popups on focus out,
    // not grabbing the pointer triggers rollup when the mouse enters the popup
    // and leaves the main window, see bug 1807482.
    //
    // FVWM is also affected but less severely: the pointer can enter the
    // popup, but if it briefly moves out of the popup and over the main window
    // then we see a focus change and roll up the popup.
    //
    // We don't do it for most common desktops, if only because it causes X11
    // crashes like bug 1607713.
    const auto& desktop = GetDesktopEnvironmentIdentifier();
    return desktop.EqualsLiteral("twm") || desktop.EqualsLiteral("sawfish") ||
           StringBeginsWith(desktop, "fvwm"_ns);
  }();

  const bool grabPointer = [] {
    switch (StaticPrefs::widget_gtk_grab_pointer()) {
      case 0:
        return false;
      case 1:
        return true;
      default:
        return sSystemNeedsPointerGrab;
    }
  }();

  if (!grabPointer) {
    return;
  }

  mNeedsToRetryCapturingMouse = false;
  if (aDoCapture) {
    if (mIsDragPopup || DragInProgress()) {
      // Don't add a grab if a drag is in progress, or if the widget is a drag
      // feedback popup. (panels with type="drag").
      return;
    }

    if (!mHasMappedToplevel) {
      // On X, capturing an unmapped window is pointless (returns
      // GDK_GRAB_NOT_VIEWABLE). Avoid the X server round-trip and just retry
      // when we're mapped.
      mNeedsToRetryCapturingMouse = true;
      return;
    }

    // gdk_pointer_grab is deprecated in favor of gdk_device_grab, but that
    // causes a strange bug on X11, most obviously with nested popup menus:
    // we somehow take the pointer position relative to the top left of the
    // outer menu and use it as if it were relative to the submenu.  This
    // doesn't happen with gdk_pointer_grab even though the code is very
    // similar.  See the video attached to bug 1750721 for a demonstration,
    // and see also bug 1820542 for when the same thing happened with
    // another attempt to use gdk_device_grab.
    //
    // (gdk_device_grab is deprecated in favor of gdk_seat_grab as of 3.20,
    // but at the time of this writing we still support older versions of
    // GTK 3.)
    GdkGrabStatus status =
        gdk_pointer_grab(GetToplevelGdkWindow(),
                         /* owner_events = */ true, kCaptureEventsMask,
                         /* confine_to = */ nullptr,
                         /* cursor = */ nullptr, GetLastUserInputTime());
    Unused << NS_WARN_IF(status != GDK_GRAB_SUCCESS);
    LOG(" > pointer grab with status %d", int(status));
    gtk_grab_add(GTK_WIDGET(mContainer));
  } else {
    // There may not have been a drag in process when aDoCapture was set,
    // so make sure to remove any added grab.  This is a no-op if the grab
    // was not added to this widget.
    gtk_grab_remove(GTK_WIDGET(mContainer));
    gdk_pointer_ungrab(GetLastUserInputTime());
  }
}

nsresult nsWindow::GetAttention(int32_t aCycleCount) {
  LOG("nsWindow::GetAttention");

  GtkWidget* top_window = GetToplevelWidget();
  GtkWidget* top_focused_window =
      gFocusWindow ? gFocusWindow->GetToplevelWidget() : nullptr;

  // Don't get attention if the window is focused anyway.
  if (top_window && (gtk_widget_get_visible(top_window)) &&
      top_window != top_focused_window) {
    SetUrgencyHint(top_window, true);
  }

  return NS_OK;
}

bool nsWindow::HasPendingInputEvent() {
  // This sucks, but gtk/gdk has no way to answer the question we want while
  // excluding paint events, and there's no X API that will let us peek
  // without blocking or removing.  To prevent event reordering, peek
  // anything except expose events.  Reordering expose and others should be
  // ok, hopefully.
  bool haveEvent = false;
#ifdef MOZ_X11
  XEvent ev;
  if (GdkIsX11Display()) {
    Display* display = GDK_DISPLAY_XDISPLAY(gdk_display_get_default());
    haveEvent = XCheckMaskEvent(
        display,
        KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask |
            EnterWindowMask | LeaveWindowMask | PointerMotionMask |
            PointerMotionHintMask | Button1MotionMask | Button2MotionMask |
            Button3MotionMask | Button4MotionMask | Button5MotionMask |
            ButtonMotionMask | KeymapStateMask | VisibilityChangeMask |
            StructureNotifyMask | ResizeRedirectMask | SubstructureNotifyMask |
            SubstructureRedirectMask | FocusChangeMask | PropertyChangeMask |
            ColormapChangeMask | OwnerGrabButtonMask,
        &ev);
    if (haveEvent) {
      XPutBackEvent(display, &ev);
    }
  }
#endif
  return haveEvent;
}

#ifdef cairo_copy_clip_rectangle_list
#  error "Looks like we're including Mozilla's cairo instead of system cairo"
#endif
static bool ExtractExposeRegion(LayoutDeviceIntRegion& aRegion, cairo_t* cr) {
  cairo_rectangle_list_t* rects = cairo_copy_clip_rectangle_list(cr);
  if (rects->status != CAIRO_STATUS_SUCCESS) {
    NS_WARNING("Failed to obtain cairo rectangle list.");
    return false;
  }

  for (int i = 0; i < rects->num_rectangles; i++) {
    const cairo_rectangle_t& r = rects->rectangles[i];
    aRegion.Or(aRegion,
               LayoutDeviceIntRect::Truncate((float)r.x, (float)r.y,
                                             (float)r.width, (float)r.height));
  }

  cairo_rectangle_list_destroy(rects);
  return true;
}

#ifdef MOZ_WAYLAND
void nsWindow::CreateCompositorVsyncDispatcher() {
  LOG_VSYNC("nsWindow::CreateCompositorVsyncDispatcher()");
  if (!mWaylandVsyncSource) {
    LOG_VSYNC(
        "  mWaylandVsyncSource is missing, create "
        "nsBaseWidget::CompositorVsyncDispatcher()");
    nsBaseWidget::CreateCompositorVsyncDispatcher();
    return;
  }
  if (!mCompositorVsyncDispatcherLock) {
    mCompositorVsyncDispatcherLock =
        MakeUnique<Mutex>("mCompositorVsyncDispatcherLock");
  }
  MutexAutoLock lock(*mCompositorVsyncDispatcherLock);
  if (!mCompositorVsyncDispatcher) {
    LOG_VSYNC("  create CompositorVsyncDispatcher()");
    mCompositorVsyncDispatcher =
        new CompositorVsyncDispatcher(mWaylandVsyncDispatcher);
  }
}
#endif

gboolean nsWindow::OnExposeEvent(cairo_t* cr) {
  // This might destroy us.
  NotifyOcclusionState(OcclusionState::VISIBLE);
  if (mIsDestroyed) {
    return FALSE;
  }

  // Send any pending resize events so that layout can update.
  // May run event loop and destroy us.
  MaybeDispatchResized();
  if (mIsDestroyed) {
    return FALSE;
  }

  // Windows that are not visible will be painted after they become visible.
  if (!mGdkWindow || !mHasMappedToplevel) {
    return FALSE;
  }
#ifdef MOZ_WAYLAND
  if (GdkIsWaylandDisplay() && !moz_container_wayland_can_draw(mContainer)) {
    return FALSE;
  }
#endif

  nsIWidgetListener* listener = GetListener();
  if (!listener) return FALSE;

  LOG("nsWindow::OnExposeEvent GdkWindow [%p] XWindow [0x%lx]", mGdkWindow,
      GetX11Window());

  LayoutDeviceIntRegion exposeRegion;
  if (!ExtractExposeRegion(exposeRegion, cr)) {
    LOG("  no rects, quit");
    return FALSE;
  }

  gint scale = GdkCeiledScaleFactor();
  LayoutDeviceIntRegion region = exposeRegion;
  region.ScaleRoundOut(scale, scale);

  WindowRenderer* renderer = GetWindowRenderer();
  WebRenderLayerManager* layerManager = renderer->AsWebRender();
  KnowsCompositor* knowsCompositor = renderer->AsKnowsCompositor();

  if (knowsCompositor && layerManager && mCompositorSession) {
    if (!mConfiguredClearColor && !IsPopup()) {
      layerManager->WrBridge()->SendSetDefaultClearColor(LookAndFeel::Color(
          LookAndFeel::ColorID::Window, LookAndFeel::ColorSchemeForChrome(),
          LookAndFeel::UseStandins::No));
      mConfiguredClearColor = true;
    }

    // We need to paint to the screen even if nothing changed, since if we
    // don't have a compositing window manager, our pixels could be stale.
    layerManager->SetNeedsComposite(true);
    layerManager->SendInvalidRegion(region.ToUnknownRegion());
  }

  RefPtr<nsWindow> strongThis(this);

  // Dispatch WillPaintWindow notification to allow scripts etc. to run
  // before we paint
  {
    listener->WillPaintWindow(this);

    // If the window has been destroyed during the will paint notification,
    // there is nothing left to do.
    if (!mGdkWindow) return TRUE;

    // Re-get the listener since the will paint notification might have
    // killed it.
    listener = GetListener();
    if (!listener) return FALSE;
  }

  if (knowsCompositor && layerManager && layerManager->NeedsComposite()) {
    layerManager->ScheduleComposite(wr::RenderReasons::WIDGET);
    layerManager->SetNeedsComposite(false);
  }

  // Our bounds may have changed after calling WillPaintWindow.  Clip
  // to the new bounds here.  The region is relative to this
  // window.
  region.And(region, LayoutDeviceIntRect(0, 0, mBounds.width, mBounds.height));

  bool shaped = false;
  if (TransparencyMode::Transparent == GetTransparencyMode()) {
    auto* window = static_cast<nsWindow*>(GetTopLevelWidget());
    if (mTransparencyBitmapForTitlebar) {
      if (mSizeMode == nsSizeMode_Normal) {
        window->UpdateTitlebarTransparencyBitmap();
      } else {
        window->ClearTransparencyBitmap();
      }
    } else {
      if (mHasAlphaVisual) {
        // Remove possible shape mask from when window manger was not
        // previously compositing.
        window->ClearTransparencyBitmap();
      } else {
        shaped = true;
      }
    }
  }

  if (region.IsEmpty()) {
    return TRUE;
  }

  // If this widget uses OMTC...
  if (renderer->GetBackendType() == LayersBackend::LAYERS_WR) {
    listener->PaintWindow(this, region);

    // Re-get the listener since the will paint notification might have
    // killed it.
    listener = GetListener();
    if (!listener) return TRUE;

    listener->DidPaintWindow();
    return TRUE;
  }

  BufferMode layerBuffering = BufferMode::BUFFERED;
  RefPtr<DrawTarget> dt = StartRemoteDrawingInRegion(region, &layerBuffering);
  if (!dt || !dt->IsValid()) {
    return FALSE;
  }
  Maybe<gfxContext> ctx;
  IntRect boundsRect = region.GetBounds().ToUnknownRect();
  IntPoint offset(0, 0);
  if (dt->GetSize() == boundsRect.Size()) {
    offset = boundsRect.TopLeft();
    dt->SetTransform(Matrix::Translation(-offset));
  }

#ifdef MOZ_X11
  if (shaped) {
    // Collapse update area to the bounding box. This is so we only have to
    // call UpdateTranslucentWindowAlpha once. After we have dropped
    // support for non-Thebes graphics, UpdateTranslucentWindowAlpha will be
    // our private interface so we can rework things to avoid this.
    dt->PushClipRect(Rect(boundsRect));

    // The double buffering is done here to extract the shape mask.
    // (The shape mask won't be necessary when a visual with an alpha
    // channel is used on compositing window managers.)
    layerBuffering = BufferMode::BUFFER_NONE;
    RefPtr<DrawTarget> destDT =
        dt->CreateSimilarDrawTarget(boundsRect.Size(), SurfaceFormat::B8G8R8A8);
    if (!destDT || !destDT->IsValid()) {
      return FALSE;
    }
    destDT->SetTransform(Matrix::Translation(-boundsRect.TopLeft()));
    ctx.emplace(destDT, /* aPreserveTransform */ true);
  } else {
    gfxUtils::ClipToRegion(dt, region.ToUnknownRegion());
    ctx.emplace(dt, /* aPreserveTransform */ true);
  }

#  if 0
    // NOTE: Paint flashing region would be wrong for cairo, since
    // cairo inflates the update region, etc.  So don't paint flash
    // for cairo.
#    ifdef DEBUG
    // XXX aEvent->region may refer to a newly-invalid area.  FIXME
    if (0 && WANT_PAINT_FLASHING && gtk_widget_get_window(aEvent))
        gdk_window_flash(mGdkWindow, 1, 100, aEvent->region);
#    endif
#  endif

#endif  // MOZ_X11

  bool painted = false;
  {
    if (renderer->GetBackendType() == LayersBackend::LAYERS_NONE) {
      if (GetTransparencyMode() == TransparencyMode::Transparent &&
          layerBuffering == BufferMode::BUFFER_NONE && mHasAlphaVisual) {
        // If our draw target is unbuffered and we use an alpha channel,
        // clear the image beforehand to ensure we don't get artifacts from a
        // reused SHM image. See bug 1258086.
        dt->ClearRect(Rect(boundsRect));
      }
      AutoLayerManagerSetup setupLayerManager(
          this, ctx.isNothing() ? nullptr : &ctx.ref(), layerBuffering);
      painted = listener->PaintWindow(this, region);

      // Re-get the listener since the will paint notification might have
      // killed it.
      listener = GetListener();
      if (!listener) return TRUE;
    }
  }

#ifdef MOZ_X11
  // PaintWindow can Destroy us (bug 378273), avoid doing any paint
  // operations below if that happened - it will lead to XError and exit().
  if (shaped) {
    if (MOZ_LIKELY(!mIsDestroyed)) {
      if (painted) {
        RefPtr<SourceSurface> surf = ctx->GetDrawTarget()->Snapshot();

        UpdateAlpha(surf, boundsRect);

        dt->DrawSurface(surf, Rect(boundsRect),
                        Rect(0, 0, boundsRect.width, boundsRect.height),
                        DrawSurfaceOptions(SamplingFilter::POINT),
                        DrawOptions(1.0f, CompositionOp::OP_SOURCE));
      }
    }
  }

  ctx.reset();
  dt->PopClip();

#endif  // MOZ_X11

  EndRemoteDrawingInRegion(dt, region);

  listener->DidPaintWindow();

  // Synchronously flush any new dirty areas
  cairo_region_t* dirtyArea = gdk_window_get_update_area(mGdkWindow);

  if (dirtyArea) {
    gdk_window_invalidate_region(mGdkWindow, dirtyArea, false);
    cairo_region_destroy(dirtyArea);
    gdk_window_process_updates(mGdkWindow, false);
  }

  // check the return value!
  return TRUE;
}

void nsWindow::UpdateAlpha(SourceSurface* aSourceSurface,
                           nsIntRect aBoundsRect) {
  // We need to create our own buffer to force the stride to match the
  // expected stride.
  int32_t stride =
      GetAlignedStride<4>(aBoundsRect.width, BytesPerPixel(SurfaceFormat::A8));
  if (stride == 0) {
    return;
  }
  int32_t bufferSize = stride * aBoundsRect.height;
  auto imageBuffer = MakeUniqueFallible<uint8_t[]>(bufferSize);
  {
    RefPtr<DrawTarget> drawTarget = gfxPlatform::CreateDrawTargetForData(
        imageBuffer.get(), aBoundsRect.Size(), stride, SurfaceFormat::A8);

    if (drawTarget) {
      drawTarget->DrawSurface(aSourceSurface,
                              Rect(0, 0, aBoundsRect.width, aBoundsRect.height),
                              Rect(0, 0, aSourceSurface->GetSize().width,
                                   aSourceSurface->GetSize().height),
                              DrawSurfaceOptions(SamplingFilter::POINT),
                              DrawOptions(1.0f, CompositionOp::OP_SOURCE));
    }
  }
  UpdateTranslucentWindowAlphaInternal(aBoundsRect, imageBuffer.get(), stride);
}

gboolean nsWindow::OnConfigureEvent(GtkWidget* aWidget,
                                    GdkEventConfigure* aEvent) {
  // These events are only received on toplevel windows.
  //
  // GDK ensures that the coordinates are the client window top-left wrt the
  // root window.
  //
  //   GDK calculates the cordinates for real ConfigureNotify events on
  //   managed windows (that would normally be relative to the parent
  //   window).
  //
  //   Synthetic ConfigureNotify events are from the window manager and
  //   already relative to the root window.  GDK creates all X windows with
  //   border_width = 0, so synthetic events also indicate the top-left of
  //   the client window.
  //
  //   Override-redirect windows are children of the root window so parent
  //   coordinates are root coordinates.

#ifdef MOZ_LOGGING
  int scale = mGdkWindow ? gdk_window_get_scale_factor(mGdkWindow) : -1;
  LOG("configure event %d,%d -> %d x %d direct mGdkWindow scale %d (scaled "
      "size %d x %d)\n",
      aEvent->x, aEvent->y, aEvent->width, aEvent->height, scale,
      aEvent->width * scale, aEvent->height * scale);
#endif

  if (mPendingConfigures > 0) {
    mPendingConfigures--;
  }

  // Don't fire configure event for scale changes, we handle that
  // OnScaleChanged event. Skip that for toplevel windows only.
  if (mGdkWindow && IsTopLevelWindowType()) {
    if (mCeiledScaleFactor != gdk_window_get_scale_factor(mGdkWindow)) {
      LOG("  scale factor changed to %d,return early",
          gdk_window_get_scale_factor(mGdkWindow));
      return FALSE;
    }
  }

  LayoutDeviceIntRect screenBounds = GetScreenBounds();

  if (IsTopLevelWindowType()) {
    // This check avoids unwanted rollup on spurious configure events from
    // Cygwin/X (bug 672103).
    if (mBounds.x != screenBounds.x || mBounds.y != screenBounds.y) {
      RollupAllMenus();
    }
  }

  NS_ASSERTION(GTK_IS_WINDOW(aWidget),
               "Configure event on widget that is not a GtkWindow");
  if (mGdkWindow &&
      gtk_window_get_window_type(GTK_WINDOW(aWidget)) == GTK_WINDOW_POPUP) {
    // Override-redirect window
    //
    // These windows should not be moved by the window manager, and so any
    // change in position is a result of our direction.  mBounds has
    // already been set in std::move() or Resize(), and that is more
    // up-to-date than the position in the ConfigureNotify event if the
    // event is from an earlier window move.
    //
    // Skipping the WindowMoved call saves context menus from an infinite
    // loop when nsXULPopupManager::PopupMoved moves the window to the new
    // position and nsMenuPopupFrame::SetPopupPosition adds
    // offsetForContextMenu on each iteration.

    // Our back buffer might have been invalidated while we drew the last
    // frame, and its contents might be incorrect. See bug 1280653 comment 7
    // and comment 10. Specifically we must ensure we recomposite the frame
    // as soon as possible to avoid the corrupted frame being displayed.
    GetWindowRenderer()->FlushRendering(wr::RenderReasons::WIDGET);
    return FALSE;
  }

  mBounds.MoveTo(screenBounds.TopLeft());
  RecomputeClientOffset(/* aNotify = */ false);

  // XXX mozilla will invalidate the entire window after this move
  // complete.  wtf?
  NotifyWindowMoved(mBounds.x, mBounds.y);

  return FALSE;
}

void nsWindow::OnMap() {
  LOG("nsWindow::OnMap");
  // Gtk mapped widget to screen. Configure underlying GdkWindow properly
  // as our rendering target.
  // This call means we have X11 (or Wayland) window we can render to by GL
  // so we need to notify compositor about it.
  mIsMapped = true;
  ConfigureGdkWindow();
}

void nsWindow::OnUnmap() {
  LOG("nsWindow::OnUnmap");

  // Mark window as unmapped. It can be still used for rendering on X11
  // untill OnUnrealize is called.
  mIsMapped = false;

  if (mSourceDragContext) {
    static auto sGtkDragCancel =
        (void (*)(GdkDragContext*))dlsym(RTLD_DEFAULT, "gtk_drag_cancel");
    if (sGtkDragCancel) {
      sGtkDragCancel(mSourceDragContext);
      mSourceDragContext = nullptr;
    }
  }

#ifdef MOZ_WAYLAND
  // wl_surface owned by mContainer is going to be deleted.
  // Make sure we don't paint to it on Wayland.
  if (GdkIsWaylandDisplay()) {
    if (mCompositorWidgetDelegate) {
      mCompositorWidgetDelegate->DisableRendering();
    }
    if (moz_container_wayland_has_egl_window(mContainer)) {
      // Widget is backed by OpenGL EGLSurface created over wl_surface
      // owned by mContainer.
      // RenderCompositorEGL::Resume() deletes recent EGLSurface based on
      // wl_surface owned by mContainer and creates a new fallback EGLSurface.
      // Then we can delete wl_surface in moz_container_wayland_unmap().
      // We don't want to pause compositor as it may lead to whole
      // browser freeze (Bug 1777664).
      if (CompositorBridgeChild* remoteRenderer = GetRemoteRenderer()) {
        remoteRenderer->SendResume();
      }
    }
    if (GdkIsWaylandDisplay()) {
      moz_container_wayland_unmap(GTK_WIDGET(mContainer));
    }
  }
#endif
  moz_container_unmap(GTK_WIDGET(mContainer));
}

void nsWindow::OnUnrealize() {
  // The GdkWindows are about to be destroyed (but not deleted), so remove
  // their references back to their container widget while the GdkWindow
  // hierarchy is still available.
  // This call means we *don't* have X11 (or Wayland) window we can render to.
  LOG("nsWindow::OnUnrealize GdkWindow %p", mGdkWindow);
  ReleaseGdkWindow();
}

void nsWindow::OnSizeAllocate(GtkAllocation* aAllocation) {
  LOG("nsWindow::OnSizeAllocate %d,%d -> %d x %d\n", aAllocation->x,
      aAllocation->y, aAllocation->width, aAllocation->height);

  // Client offset are updated by _NET_FRAME_EXTENTS on X11 when system titlebar
  // is enabled. In either cases (Wayland or system titlebar is off on X11)
  // we don't get _NET_FRAME_EXTENTS X11 property notification so we derive
  // it from mContainer position.
  RecomputeClientOffset(/* aNotify = */ true);

  mHasReceivedSizeAllocate = true;

  LayoutDeviceIntSize size = GdkRectToDevicePixels(*aAllocation).Size();

  // Sometimes the window manager gives us garbage sizes (way past the maximum
  // texture size) causing crashes if we don't enforce size constraints again
  // here.
  ConstrainSize(&size.width, &size.height);

  if (mBounds.Size() == size) {
    LOG("  Already the same size");
    // mBounds was set at Create() or Resize().
    if (mNeedsDispatchSize != LayoutDeviceIntSize(-1, -1)) {
      LOG("  No longer needs to dispatch %dx%d", mNeedsDispatchSize.width,
          mNeedsDispatchSize.height);
      mNeedsDispatchSize = LayoutDeviceIntSize(-1, -1);
    }
    return;
  }

  // Invalidate the new part of the window now for the pending paint to
  // minimize background flashes (GDK does not do this for external resizes
  // of toplevels.)
  if (mGdkWindow) {
    if (mBounds.width < size.width) {
      GdkRectangle rect = DevicePixelsToGdkRectRoundOut(LayoutDeviceIntRect(
          mBounds.width, 0, size.width - mBounds.width, size.height));
      gdk_window_invalidate_rect(mGdkWindow, &rect, FALSE);
    }
    if (mBounds.height < size.height) {
      GdkRectangle rect = DevicePixelsToGdkRectRoundOut(LayoutDeviceIntRect(
          0, mBounds.height, size.width, size.height - mBounds.height));
      gdk_window_invalidate_rect(mGdkWindow, &rect, FALSE);
    }
  }

  // If we update mBounds here, then inner/outerHeight are out of sync until
  // we call WindowResized.
  mNeedsDispatchSize = size;

  // Gecko permits running nested event loops during processing of events,
  // GtkWindow callers of gtk_widget_size_allocate expect the signal
  // handlers to return sometime in the near future.
  NS_DispatchToCurrentThread(NewRunnableMethod(
      "nsWindow::MaybeDispatchResized", this, &nsWindow::MaybeDispatchResized));
}

void nsWindow::OnDeleteEvent() {
  if (mWidgetListener) mWidgetListener->RequestWindowClose(this);
}

void nsWindow::OnEnterNotifyEvent(GdkEventCrossing* aEvent) {
  LOG("enter notify (win=%p, sub=%p): %f, %f mode %d, detail %d\n",
      aEvent->window, aEvent->subwindow, aEvent->x, aEvent->y, aEvent->mode,
      aEvent->detail);
  // This skips NotifyVirtual and NotifyNonlinearVirtual enter notify events
  // when the pointer enters a child window.  If the destination window is a
  // Gecko window then we'll catch the corresponding event on that window,
  // but we won't notice when the pointer directly enters a foreign (plugin)
  // child window without passing over a visible portion of a Gecko window.
  if (aEvent->subwindow) {
    return;
  }

  // Check before checking for ungrab as the button state may have
  // changed while a non-Gecko ancestor window had a pointer grab.
  DispatchMissedButtonReleases(aEvent);

  WidgetMouseEvent event(true, eMouseEnterIntoWidget, this,
                         WidgetMouseEvent::eReal);

  event.mRefPoint = GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y);
  event.AssignEventTime(GetWidgetEventTime(aEvent->time));

  LOG("OnEnterNotify");

  DispatchInputEvent(&event);
}

// Some window managers send a bogus top-level leave-notify event on every
// click. That confuses our event handling code in ways that can break websites,
// see bug 1805939 for details.
//
// Make sure to only check this on bogus environments, since for environments
// with CSD, gdk_device_get_window_at_position could return the window even when
// the pointer is in the decoration area.
static bool IsBogusLeaveNotifyEvent(GdkWindow* aWindow,
                                    GdkEventCrossing* aEvent) {
  static bool sBogusWm = [] {
    if (GdkIsWaylandDisplay()) {
      return false;
    }
    const auto& desktopEnv = GetDesktopEnvironmentIdentifier();
    return desktopEnv.EqualsLiteral("fluxbox") ||   // Bug 1805939 comment 0.
           desktopEnv.EqualsLiteral("blackbox") ||  // Bug 1805939 comment 32.
           desktopEnv.EqualsLiteral("lg3d") ||      // Bug 1820405.
           desktopEnv.EqualsLiteral("pekwm") ||     // Bug 1822911.
           StringBeginsWith(desktopEnv, "fvwm"_ns);
  }();

  const bool shouldCheck = [] {
    switch (StaticPrefs::widget_gtk_ignore_bogus_leave_notify()) {
      case 0:
        return false;
      case 1:
        return true;
      default:
        return sBogusWm;
    }
  }();

  if (!shouldCheck) {
    return false;
  }
  GdkDevice* pointer = GdkGetPointer();
  GdkWindow* winAtPt =
      gdk_device_get_window_at_position(pointer, nullptr, nullptr);
  if (!winAtPt) {
    return false;
  }
  // We're still in the same top level window, ignore this leave notify event.
  GdkWindow* topLevelAtPt = gdk_window_get_toplevel(winAtPt);
  GdkWindow* topLevelWidget = gdk_window_get_toplevel(aWindow);
  return topLevelAtPt == topLevelWidget;
}

void nsWindow::OnLeaveNotifyEvent(GdkEventCrossing* aEvent) {
  LOG("leave notify (win=%p, sub=%p): %f, %f mode %d, detail %d\n",
      aEvent->window, aEvent->subwindow, aEvent->x, aEvent->y, aEvent->mode,
      aEvent->detail);

  // This ignores NotifyVirtual and NotifyNonlinearVirtual leave notify
  // events when the pointer leaves a child window.  If the destination
  // window is a Gecko window then we'll catch the corresponding event on
  // that window.
  //
  // XXXkt However, we will miss toplevel exits when the pointer directly
  // leaves a foreign (plugin) child window without passing over a visible
  // portion of a Gecko window.
  if (aEvent->subwindow) {
    return;
  }

  // The filter out for subwindows should make sure that this is targeted to
  // this nsWindow.
  const bool leavingTopLevel = IsTopLevelWindowType();
  if (leavingTopLevel && IsBogusLeaveNotifyEvent(mGdkWindow, aEvent)) {
    return;
  }

  WidgetMouseEvent event(true, eMouseExitFromWidget, this,
                         WidgetMouseEvent::eReal);

  event.mRefPoint = GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y);
  event.AssignEventTime(GetWidgetEventTime(aEvent->time));
  event.mExitFrom = Some(leavingTopLevel ? WidgetMouseEvent::ePlatformTopLevel
                                         : WidgetMouseEvent::ePlatformChild);

  LOG("OnLeaveNotify");

  DispatchInputEvent(&event);
}

Maybe<GdkWindowEdge> nsWindow::CheckResizerEdge(
    const LayoutDeviceIntPoint& aPoint) {
  const bool canResize = [&] {
    // Don't allow resizing maximized/fullscreen windows.
    if (mSizeMode != nsSizeMode_Normal) {
      return false;
    }
    if (mIsPIPWindow) {
      // Note that since we do show resizers on left/right sides on PIP windows,
      // we still want the resizers there, even when tiled.
      return true;
    }
    if (!mDrawInTitlebar) {
      return false;
    }
    // On KDE, allow for 1 extra pixel at the top of regular windows when
    // drawing to the titlebar. This matches the native titlebar behavior on
    // that environment. See bug 1813554.
    //
    // Don't do that on GNOME (see bug 1822764). If we wanted to do this on
    // GNOME we'd need an extra check for mIsTiled, since the window is "stuck"
    // to the top and bottom.
    //
    // Other DEs are untested.
    return mDrawInTitlebar && IsKdeDesktopEnvironment();
  }();

  if (!canResize) {
    return {};
  }

  // If we're not in a PiP window, allow 1px resizer edge from the top edge,
  // and nothing else.
  // This is to allow resizes of tiled windows on KDE, see bug 1813554.
  const int resizerSize = (mIsPIPWindow ? 15 : 1) * GdkCeiledScaleFactor();

  const int topDist = aPoint.y;
  const int leftDist = aPoint.x;
  const int rightDist = mBounds.width - aPoint.x;
  const int bottomDist = mBounds.height - aPoint.y;

  if (topDist <= resizerSize) {
    if (rightDist <= resizerSize) {
      return Some(GDK_WINDOW_EDGE_NORTH_EAST);
    }
    if (leftDist <= resizerSize) {
      return Some(GDK_WINDOW_EDGE_NORTH_WEST);
    }
    return Some(GDK_WINDOW_EDGE_NORTH);
  }

  if (!mIsPIPWindow) {
    return {};
  }

  if (bottomDist <= resizerSize) {
    if (leftDist <= resizerSize) {
      return Some(GDK_WINDOW_EDGE_SOUTH_WEST);
    }
    if (rightDist <= resizerSize) {
      return Some(GDK_WINDOW_EDGE_SOUTH_EAST);
    }
    return Some(GDK_WINDOW_EDGE_SOUTH);
  }

  if (leftDist <= resizerSize) {
    return Some(GDK_WINDOW_EDGE_WEST);
  }
  if (rightDist <= resizerSize) {
    return Some(GDK_WINDOW_EDGE_EAST);
  }
  return {};
}

template <typename Event>
static LayoutDeviceIntPoint GetRefPoint(nsWindow* aWindow, Event* aEvent) {
  if (aEvent->window == aWindow->GetGdkWindow()) {
    // we are the window that the event happened on so no need for expensive
    // WidgetToScreenOffset
    return aWindow->GdkEventCoordsToDevicePixels(aEvent->x, aEvent->y);
  }
  // XXX we're never quite sure which GdkWindow the event came from due to our
  // custom bubbling in scroll_event_cb(), so use ScreenToWidget to translate
  // the screen root coordinates into coordinates relative to this widget.
  return aWindow->GdkEventCoordsToDevicePixels(aEvent->x_root, aEvent->y_root) -
         aWindow->WidgetToScreenOffset();
}

void nsWindow::OnMotionNotifyEvent(GdkEventMotion* aEvent) {
  if (!mGdkWindow) {
    return;
  }

  if (mWindowShouldStartDragging) {
    mWindowShouldStartDragging = false;
    // find the top-level window
    GdkWindow* gdk_window = gdk_window_get_toplevel(mGdkWindow);
    MOZ_ASSERT(gdk_window, "gdk_window_get_toplevel should not return null");

    bool canDrag = true;
#ifdef MOZ_X11
    if (GdkIsX11Display()) {
      // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=789054
      // To avoid crashes disable double-click on WM without _NET_WM_MOVERESIZE.
      // See _should_perform_ewmh_drag() at gdkwindow-x11.c
      GdkScreen* screen = gdk_window_get_screen(gdk_window);
      GdkAtom atom = gdk_atom_intern("_NET_WM_MOVERESIZE", FALSE);
      if (!gdk_x11_screen_supports_net_wm_hint(screen, atom)) {
        canDrag = false;
      }
    }
#endif

    if (canDrag) {
      gdk_window_begin_move_drag(gdk_window, 1, aEvent->x_root, aEvent->y_root,
                                 aEvent->time);
      return;
    }
  }

  mWidgetCursorLocked = false;
  const auto refPoint = GetRefPoint(this, aEvent);
  if (auto edge = CheckResizerEdge(refPoint)) {
    nsCursor cursor = eCursor_none;
    switch (*edge) {
      case GDK_WINDOW_EDGE_SOUTH:
      case GDK_WINDOW_EDGE_NORTH:
        cursor = eCursor_ns_resize;
        break;
      case GDK_WINDOW_EDGE_WEST:
      case GDK_WINDOW_EDGE_EAST:
        cursor = eCursor_ew_resize;
        break;
      case GDK_WINDOW_EDGE_NORTH_WEST:
      case GDK_WINDOW_EDGE_SOUTH_EAST:
        cursor = eCursor_nwse_resize;
        break;
      case GDK_WINDOW_EDGE_NORTH_EAST:
      case GDK_WINDOW_EDGE_SOUTH_WEST:
        cursor = eCursor_nesw_resize;
        break;
    }
    SetCursor(Cursor{cursor});
    // If we set resize cursor on widget level keep it locked and prevent layout
    // to switch it back to default (by synthetic mouse events for instance)
    // until resize is finished.
    mWidgetCursorLocked = true;
    return;
  }

  WidgetMouseEvent event(true, eMouseMove, this, WidgetMouseEvent::eReal);

  gdouble pressure = 0;
  gdk_event_get_axis((GdkEvent*)aEvent, GDK_AXIS_PRESSURE, &pressure);
  // Sometime gdk generate 0 pressure value between normal values
  // We have to ignore that and use last valid value
  if (pressure) {
    mLastMotionPressure = pressure;
  }
  event.mPressure = mLastMotionPressure;
  event.mRefPoint = refPoint;
  event.AssignEventTime(GetWidgetEventTime(aEvent->time));

  KeymapWrapper::InitInputEvent(event, aEvent->state);

  DispatchInputEvent(&event);
}

// If the automatic pointer grab on ButtonPress has deactivated before
// ButtonRelease, and the mouse button is released while the pointer is not
// over any a Gecko window, then the ButtonRelease event will not be received.
// (A similar situation exists when the pointer is grabbed with owner_events
// True as the ButtonRelease may be received on a foreign [plugin] window).
// Use this method to check for released buttons when the pointer returns to a
// Gecko window.
void nsWindow::DispatchMissedButtonReleases(GdkEventCrossing* aGdkEvent) {
  guint changed = aGdkEvent->state ^ gButtonState;
  // Only consider button releases.
  // (Ignore button presses that occurred outside Gecko.)
  guint released = changed & gButtonState;
  gButtonState = aGdkEvent->state;

  // Loop over each button, excluding mouse wheel buttons 4 and 5 for which
  // GDK ignores releases.
  for (guint buttonMask = GDK_BUTTON1_MASK; buttonMask <= GDK_BUTTON3_MASK;
       buttonMask <<= 1) {
    if (released & buttonMask) {
      int16_t buttonType;
      switch (buttonMask) {
        case GDK_BUTTON1_MASK:
          buttonType = MouseButton::ePrimary;
          break;
        case GDK_BUTTON2_MASK:
          buttonType = MouseButton::eMiddle;
          break;
        default:
          NS_ASSERTION(buttonMask == GDK_BUTTON3_MASK,
                       "Unexpected button mask");
          buttonType = MouseButton::eSecondary;
      }

      LOG("Synthesized button %u release", guint(buttonType + 1));

      // Dispatch a synthesized button up event to tell Gecko about the
      // change in state.  This event is marked as synthesized so that
      // it is not dispatched as a DOM event, because we don't know the
      // position, widget, modifiers, or time/order.
      WidgetMouseEvent synthEvent(true, eMouseUp, this,
                                  WidgetMouseEvent::eSynthesized);
      synthEvent.mButton = buttonType;
      DispatchInputEvent(&synthEvent);
    }
  }
}

void nsWindow::InitButtonEvent(WidgetMouseEvent& aEvent,
                               GdkEventButton* aGdkEvent,
                               const LayoutDeviceIntPoint& aRefPoint) {
  aEvent.mRefPoint = aRefPoint;

  guint modifierState = aGdkEvent->state;
  // aEvent's state includes the button state from immediately before this
  // event.  If aEvent is a mousedown or mouseup event, we need to update
  // the button state.
  guint buttonMask = 0;
  switch (aGdkEvent->button) {
    case 1:
      buttonMask = GDK_BUTTON1_MASK;
      break;
    case 2:
      buttonMask = GDK_BUTTON2_MASK;
      break;
    case 3:
      buttonMask = GDK_BUTTON3_MASK;
      break;
  }
  if (aGdkEvent->type == GDK_BUTTON_RELEASE) {
    modifierState &= ~buttonMask;
  } else {
    modifierState |= buttonMask;
  }

  KeymapWrapper::InitInputEvent(aEvent, modifierState);

  aEvent.AssignEventTime(GetWidgetEventTime(aGdkEvent->time));

  switch (aGdkEvent->type) {
    case GDK_2BUTTON_PRESS:
      aEvent.mClickCount = 2;
      break;
    case GDK_3BUTTON_PRESS:
      aEvent.mClickCount = 3;
      break;
      // default is one click
    default:
      aEvent.mClickCount = 1;
  }
}

static guint ButtonMaskFromGDKButton(guint button) {
  return GDK_BUTTON1_MASK << (button - 1);
}

void nsWindow::DispatchContextMenuEventFromMouseEvent(
    uint16_t domButton, GdkEventButton* aEvent,
    const LayoutDeviceIntPoint& aRefPoint) {
  if (domButton == MouseButton::eSecondary && MOZ_LIKELY(!mIsDestroyed)) {
    WidgetMouseEvent contextMenuEvent(true, eContextMenu, this,
                                      WidgetMouseEvent::eReal);
    InitButtonEvent(contextMenuEvent, aEvent, aRefPoint);
    contextMenuEvent.mPressure = mLastMotionPressure;
    DispatchInputEvent(&contextMenuEvent);
  }
}

void nsWindow::TryToShowNativeWindowMenu(GdkEventButton* aEvent) {
  if (!gdk_window_show_window_menu(GetToplevelGdkWindow(), (GdkEvent*)aEvent)) {
    NS_WARNING("Native context menu wasn't shown");
  }
}

void nsWindow::OnButtonPressEvent(GdkEventButton* aEvent) {
  LOG("Button %u press\n", aEvent->button);

  SetLastMousePressEvent((GdkEvent*)aEvent);

  // If you double click in GDK, it will actually generate a second
  // GDK_BUTTON_PRESS before sending the GDK_2BUTTON_PRESS, and this is
  // different than the DOM spec.  GDK puts this in the queue
  // programatically, so it's safe to assume that if there's a
  // double click in the queue, it was generated so we can just drop
  // this click.
  GUniquePtr<GdkEvent> peekedEvent(gdk_event_peek());
  if (peekedEvent) {
    GdkEventType type = peekedEvent->any.type;
    if (type == GDK_2BUTTON_PRESS || type == GDK_3BUTTON_PRESS) {
      return;
    }
  }

  nsWindow* containerWindow = GetContainerWindow();
  if (!gFocusWindow && containerWindow) {
    containerWindow->DispatchActivateEvent();
  }

  const auto refPoint = GetRefPoint(this, aEvent);

  // check to see if we should rollup
  if (CheckForRollup(aEvent->x_root, aEvent->y_root, false, false)) {
    if (aEvent->button == 3 && mDraggableRegion.Contains(refPoint)) {
      GUniquePtr<GdkEvent> eventCopy;
      if (aEvent->type != GDK_BUTTON_PRESS) {
        // If the user double-clicks too fast we'll get a 2BUTTON_PRESS event
        // instead, and that isn't handled by open_window_menu, so coerce it
        // into a regular press.
        eventCopy.reset(gdk_event_copy((GdkEvent*)aEvent));
        eventCopy->type = GDK_BUTTON_PRESS;
      }
      TryToShowNativeWindowMenu(eventCopy ? &eventCopy->button : aEvent);
    }
    return;
  }

  // Check to see if the event is within our window's resize region
  if (auto edge = CheckResizerEdge(refPoint)) {
    gdk_window_begin_resize_drag(GetToplevelGdkWindow(), *edge, aEvent->button,
                                 aEvent->x_root, aEvent->y_root, aEvent->time);
    return;
  }

  gdouble pressure = 0;
  gdk_event_get_axis((GdkEvent*)aEvent, GDK_AXIS_PRESSURE, &pressure);
  mLastMotionPressure = pressure;

  uint16_t domButton;
  switch (aEvent->button) {
    case 1:
      domButton = MouseButton::ePrimary;
      break;
    case 2:
      domButton = MouseButton::eMiddle;
      break;
    case 3:
      domButton = MouseButton::eSecondary;
      break;
    // These are mapped to horizontal scroll
    case 6:
    case 7:
      NS_WARNING("We're not supporting legacy horizontal scroll event");
      return;
    // Map buttons 8-9 to back/forward
    case 8:
      if (!Preferences::GetBool("mousebutton.4th.enabled", true)) {
        return;
      }
      DispatchCommandEvent(nsGkAtoms::Back);
      return;
    case 9:
      if (!Preferences::GetBool("mousebutton.5th.enabled", true)) {
        return;
      }
      DispatchCommandEvent(nsGkAtoms::Forward);
      return;
    default:
      return;
  }

  gButtonState |= ButtonMaskFromGDKButton(aEvent->button);

  WidgetMouseEvent event(true, eMouseDown, this, WidgetMouseEvent::eReal);
  event.mButton = domButton;
  InitButtonEvent(event, aEvent, refPoint);
  event.mPressure = mLastMotionPressure;

  nsIWidget::ContentAndAPZEventStatus eventStatus = DispatchInputEvent(&event);

  if ((mIsWaylandPanelWindow || mDraggableRegion.Contains(refPoint)) &&
      domButton == MouseButton::ePrimary &&
      eventStatus.mContentStatus != nsEventStatus_eConsumeNoDefault) {
    mWindowShouldStartDragging = true;
  }

  // right menu click on linux should also pop up a context menu
  if (!StaticPrefs::ui_context_menus_after_mouseup() &&
      eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) {
    DispatchContextMenuEventFromMouseEvent(domButton, aEvent, refPoint);
  }
}

void nsWindow::OnButtonReleaseEvent(GdkEventButton* aEvent) {
  LOG("Button %u release\n", aEvent->button);

  SetLastMousePressEvent(nullptr);

  if (!mGdkWindow) {
    return;
  }

  if (mWindowShouldStartDragging) {
    mWindowShouldStartDragging = false;
  }

  uint16_t domButton;
  switch (aEvent->button) {
    case 1:
      domButton = MouseButton::ePrimary;
      break;
    case 2:
      domButton = MouseButton::eMiddle;
      break;
    case 3:
      domButton = MouseButton::eSecondary;
      break;
    default:
      return;
  }

  gButtonState &= ~ButtonMaskFromGDKButton(aEvent->button);

  const auto refPoint = GetRefPoint(this, aEvent);

  WidgetMouseEvent event(true, eMouseUp, this, WidgetMouseEvent::eReal);
  event.mButton = domButton;
  InitButtonEvent(event, aEvent, refPoint);
  gdouble pressure = 0;
  gdk_event_get_axis((GdkEvent*)aEvent, GDK_AXIS_PRESSURE, &pressure);
  event.mPressure = pressure ? (float)pressure : (float)mLastMotionPressure;

  // The mRefPoint is manipulated in DispatchInputEvent, we're saving it
  // to use it for the doubleclick position check.
  const LayoutDeviceIntPoint pos = event.mRefPoint;

  nsIWidget::ContentAndAPZEventStatus eventStatus = DispatchInputEvent(&event);

  const bool defaultPrevented =
      eventStatus.mContentStatus == nsEventStatus_eConsumeNoDefault;
  // Check if mouse position in titlebar and doubleclick happened to
  // trigger restore/maximize.
  if (!defaultPrevented && mDrawInTitlebar &&
      event.mButton == MouseButton::ePrimary && event.mClickCount == 2 &&
      mDraggableRegion.Contains(pos)) {
    if (mSizeMode == nsSizeMode_Maximized) {
      SetSizeMode(nsSizeMode_Normal);
    } else if (mSizeMode == nsSizeMode_Normal) {
      SetSizeMode(nsSizeMode_Maximized);
    }
  }
  mLastMotionPressure = pressure;

  // right menu click on linux should also pop up a context menu
  if (StaticPrefs::ui_context_menus_after_mouseup() &&
      eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) {
    DispatchContextMenuEventFromMouseEvent(domButton, aEvent, refPoint);
  }

  // Open window manager menu on PIP window to allow user
  // to place it on top / all workspaces.
  if (mAlwaysOnTop && aEvent->button == 3) {
    TryToShowNativeWindowMenu(aEvent);
  }
}

void nsWindow::OnContainerFocusInEvent(GdkEventFocus* aEvent) {
  LOG("OnContainerFocusInEvent");

  // Unset the urgency hint, if possible
  GtkWidget* top_window = GetToplevelWidget();
  if (top_window && (gtk_widget_get_visible(top_window))) {
    SetUrgencyHint(top_window, false);
  }

  // Return if being called within SetFocus because the focus manager
  // already knows that the window is active.
  if (gBlockActivateEvent) {
    LOG("activated notification is blocked");
    return;
  }

  // If keyboard input will be accepted, the focus manager will call
  // SetFocus to set the correct window.
  gFocusWindow = nullptr;

  DispatchActivateEvent();

  if (!gFocusWindow) {
    // We don't really have a window for dispatching key events, but
    // setting a non-nullptr value here prevents OnButtonPressEvent() from
    // dispatching an activation notification if the widget is already
    // active.
    gFocusWindow = this;
  }

  LOG("Events sent from focus in event");
}

void nsWindow::OnContainerFocusOutEvent(GdkEventFocus* aEvent) {
  LOG("OnContainerFocusOutEvent");

  if (IsTopLevelWindowType()) {
    // Rollup menus when a window is focused out unless a drag is occurring.
    // This check is because drags grab the keyboard and cause a focus out on
    // versions of GTK before 2.18.
    const bool shouldRollupMenus = [&] {
      nsCOMPtr<nsIDragService> dragService =
          do_GetService("@mozilla.org/widget/dragservice;1");
      nsCOMPtr<nsIDragSession> dragSession;
      dragService->GetCurrentSession(getter_AddRefs(dragSession));
      if (!dragSession) {
        return true;
      }
      // We also roll up when a drag is from a different application
      nsCOMPtr<nsINode> sourceNode;
      dragSession->GetSourceNode(getter_AddRefs(sourceNode));
      return !sourceNode;
    }();

    if (shouldRollupMenus) {
      RollupAllMenus();
    }

    if (RefPtr pm = nsXULPopupManager::GetInstance()) {
      pm->RollupTooltips();
    }
  }

  if (gFocusWindow) {
    RefPtr<nsWindow> kungFuDeathGrip = gFocusWindow;
    if (gFocusWindow->mIMContext) {
      gFocusWindow->mIMContext->OnBlurWindow(gFocusWindow);
    }
    gFocusWindow = nullptr;
  }

  DispatchDeactivateEvent();

  if (IsChromeWindowTitlebar()) {
    // DispatchDeactivateEvent() ultimately results in a call to
    // BrowsingContext::SetIsActiveBrowserWindow(), which resets
    // the state.  We call UpdateMozWindowActive() to keep it in
    // sync with GDK_WINDOW_STATE_FOCUSED.
    UpdateMozWindowActive();
  }

  LOG("Done with container focus out");
}

bool nsWindow::DispatchCommandEvent(nsAtom* aCommand) {
  nsEventStatus status;
  WidgetCommandEvent appCommandEvent(true, aCommand, this);
  DispatchEvent(&appCommandEvent, status);
  return TRUE;
}

bool nsWindow::DispatchContentCommandEvent(EventMessage aMsg) {
  nsEventStatus status;
  WidgetContentCommandEvent event(true, aMsg, this);
  DispatchEvent(&event, status);
  return TRUE;
}

WidgetEventTime nsWindow::GetWidgetEventTime(guint32 aEventTime) {
  return WidgetEventTime(GetEventTimeStamp(aEventTime));
}

TimeStamp nsWindow::GetEventTimeStamp(guint32 aEventTime) {
  if (MOZ_UNLIKELY(!mGdkWindow)) {
    // nsWindow has been Destroy()ed.
    return TimeStamp::Now();
  }
  if (aEventTime == 0) {
    // Some X11 and GDK events may be received with a time of 0 to indicate
    // that they are synthetic events. Some input method editors do this.
    // In this case too, just return the current timestamp.
    return TimeStamp::Now();
  }

  TimeStamp eventTimeStamp;

  if (GdkIsWaylandDisplay()) {
    // Wayland compositors use monotonic time to set timestamps.
    int64_t timestampTime = g_get_monotonic_time() / 1000;
    guint32 refTimeTruncated = guint32(timestampTime);

    timestampTime -= refTimeTruncated - aEventTime;
    int64_t tick =
        BaseTimeDurationPlatformUtils::TicksFromMilliseconds(timestampTime);
    eventTimeStamp = TimeStamp::FromSystemTime(tick);
  } else {
#ifdef MOZ_X11
    CurrentX11TimeGetter* getCurrentTime = GetCurrentTimeGetter();
    MOZ_ASSERT(getCurrentTime,
               "Null current time getter despite having a window");
    eventTimeStamp =
        TimeConverter().GetTimeStampFromSystemTime(aEventTime, *getCurrentTime);
#endif
  }
  return eventTimeStamp;
}

#ifdef MOZ_X11
mozilla::CurrentX11TimeGetter* nsWindow::GetCurrentTimeGetter() {
  MOZ_ASSERT(mGdkWindow, "Expected mGdkWindow to be set");
  if (MOZ_UNLIKELY(!mCurrentTimeGetter)) {
    mCurrentTimeGetter = MakeUnique<CurrentX11TimeGetter>(mGdkWindow);
  }
  return mCurrentTimeGetter.get();
}
#endif

gboolean nsWindow::OnKeyPressEvent(GdkEventKey* aEvent) {
  LOG("OnKeyPressEvent");

  KeymapWrapper::HandleKeyPressEvent(this, aEvent);
  return TRUE;
}

gboolean nsWindow::OnKeyReleaseEvent(GdkEventKey* aEvent) {
  LOG("OnKeyReleaseEvent");
  if (NS_WARN_IF(!KeymapWrapper::HandleKeyReleaseEvent(this, aEvent))) {
    return FALSE;
  }
  return TRUE;
}

void nsWindow::OnScrollEvent(GdkEventScroll* aEvent) {
  // check to see if we should rollup
  if (CheckForRollup(aEvent->x_root, aEvent->y_root, true, false)) {
    return;
  }

  // check for duplicate legacy scroll event, see GNOME bug 726878
  if (aEvent->direction != GDK_SCROLL_SMOOTH &&
      mLastScrollEventTime == aEvent->time) {
    LOG("[%d] duplicate legacy scroll event %d\n", aEvent->time,
        aEvent->direction);
    return;
  }
  WidgetWheelEvent wheelEvent(true, eWheel, this);
  wheelEvent.mDeltaMode = dom::WheelEvent_Binding::DOM_DELTA_LINE;
  switch (aEvent->direction) {
    case GDK_SCROLL_SMOOTH: {
      // As of GTK 3.4, all directional scroll events are provided by
      // the GDK_SCROLL_SMOOTH direction on XInput2 and Wayland devices.
      mLastScrollEventTime = aEvent->time;

      // Special handling for touchpads to support flings
      // (also known as kinetic/inertial/momentum scrolling)
      GdkDevice* device = gdk_event_get_source_device((GdkEvent*)aEvent);
      GdkInputSource source = gdk_device_get_source(device);
      if (source == GDK_SOURCE_TOUCHSCREEN || source == GDK_SOURCE_TOUCHPAD ||
          mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase.isSome()) {
        if (StaticPrefs::apz_gtk_pangesture_enabled() &&
            gtk_check_version(3, 20, 0) == nullptr) {
          static auto sGdkEventIsScrollStopEvent =
              (gboolean(*)(const GdkEvent*))dlsym(
                  RTLD_DEFAULT, "gdk_event_is_scroll_stop_event");

          LOG("[%d] pan smooth event dx=%f dy=%f inprogress=%d\n", aEvent->time,
              aEvent->delta_x, aEvent->delta_y, mPanInProgress);
          auto eventType = PanGestureInput::PANGESTURE_PAN;
          if (sGdkEventIsScrollStopEvent((GdkEvent*)aEvent)) {
            eventType = PanGestureInput::PANGESTURE_END;
            mPanInProgress = false;
          } else if (!mPanInProgress) {
            eventType = PanGestureInput::PANGESTURE_START;
            mPanInProgress = true;
          } else if (mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase
                         .isSome()) {
            switch (*mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase) {
              case PHASE_BEGIN:
                // we should never hit this because we'll hit the above case
                // before this.
                MOZ_ASSERT_UNREACHABLE();
                eventType = PanGestureInput::PANGESTURE_START;
                mPanInProgress = true;
                break;
              case PHASE_UPDATE:
                // nothing to do here, eventtype should already be set
                MOZ_ASSERT(mPanInProgress);
                MOZ_ASSERT(eventType == PanGestureInput::PANGESTURE_PAN);
                eventType = PanGestureInput::PANGESTURE_PAN;
                break;
              case PHASE_END:
                MOZ_ASSERT(mPanInProgress);
                eventType = PanGestureInput::PANGESTURE_END;
                mPanInProgress = false;
                break;
              default:
                MOZ_ASSERT_UNREACHABLE();
                break;
            }
          }

          mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase.reset();

          const bool isPageMode =
#ifdef NIGHTLY_BUILD
              StaticPrefs::apz_gtk_pangesture_delta_mode() == 1;
#else
              StaticPrefs::apz_gtk_pangesture_delta_mode() != 2;
#endif
          const double multiplier =
              isPageMode
                  ? StaticPrefs::apz_gtk_pangesture_page_delta_mode_multiplier()
                  : StaticPrefs::
                            apz_gtk_pangesture_pixel_delta_mode_multiplier() *
                        FractionalScaleFactor();

          ScreenPoint deltas(float(aEvent->delta_x * multiplier),
                             float(aEvent->delta_y * multiplier));

          LayoutDeviceIntPoint touchPoint = GetRefPoint(this, aEvent);
          PanGestureInput panEvent(
              eventType, GetEventTimeStamp(aEvent->time),
              ScreenPoint(touchPoint.x, touchPoint.y), deltas,
              KeymapWrapper::ComputeKeyModifiers(aEvent->state));
          panEvent.mDeltaType = isPageMode ? PanGestureInput::PANDELTA_PAGE
                                           : PanGestureInput::PANDELTA_PIXEL;
          panEvent.mSimulateMomentum =
              StaticPrefs::apz_gtk_kinetic_scroll_enabled();

          DispatchPanGesture(panEvent);

          if (mCurrentSynthesizedTouchpadPan.mSavedObserver != 0) {
            mozilla::widget::AutoObserverNotifier::NotifySavedObserver(
                mCurrentSynthesizedTouchpadPan.mSavedObserver,
                "touchpadpanevent");
            mCurrentSynthesizedTouchpadPan.mSavedObserver = 0;
          }

          return;
        }

        // Older GTK doesn't support stop events, so we can't support fling
        wheelEvent.mScrollType = WidgetWheelEvent::SCROLL_ASYNCHRONOUSLY;
      }

      // TODO - use a more appropriate scrolling unit than lines.
      // Multiply event deltas by 3 to emulate legacy behaviour.
      wheelEvent.mDeltaX = aEvent->delta_x * 3;
      wheelEvent.mDeltaY = aEvent->delta_y * 3;
      wheelEvent.mWheelTicksX = aEvent->delta_x;
      wheelEvent.mWheelTicksY = aEvent->delta_y;
      wheelEvent.mIsNoLineOrPageDelta = true;

      break;
    }
    case GDK_SCROLL_UP:
      wheelEvent.mDeltaY = wheelEvent.mLineOrPageDeltaY = -3;
      wheelEvent.mWheelTicksY = -1;
      break;
    case GDK_SCROLL_DOWN:
      wheelEvent.mDeltaY = wheelEvent.mLineOrPageDeltaY = 3;
      wheelEvent.mWheelTicksY = 1;
      break;
    case GDK_SCROLL_LEFT:
      wheelEvent.mDeltaX = wheelEvent.mLineOrPageDeltaX = -1;
      wheelEvent.mWheelTicksX = -1;
      break;
    case GDK_SCROLL_RIGHT:
      wheelEvent.mDeltaX = wheelEvent.mLineOrPageDeltaX = 1;
      wheelEvent.mWheelTicksX = 1;
      break;
  }

  wheelEvent.mRefPoint = GetRefPoint(this, aEvent);

  KeymapWrapper::InitInputEvent(wheelEvent, aEvent->state);

  wheelEvent.AssignEventTime(GetWidgetEventTime(aEvent->time));

  DispatchInputEvent(&wheelEvent);
}

void nsWindow::DispatchPanGesture(PanGestureInput& aPanInput) {
  MOZ_ASSERT(NS_IsMainThread());

  if (mSwipeTracker) {
    // Give the swipe tracker a first pass at the event. If a new pan gesture
    // has been started since the beginning of the swipe, the swipe tracker
    // will know to ignore the event.
    nsEventStatus status = mSwipeTracker->ProcessEvent(aPanInput);
    if (status == nsEventStatus_eConsumeNoDefault) {
      return;
    }
  }

  APZEventResult result;
  if (mAPZC) {
    MOZ_ASSERT(APZThreadUtils::IsControllerThread());

    result = mAPZC->InputBridge()->ReceiveInputEvent(aPanInput);
    if (result.GetStatus() == nsEventStatus_eConsumeNoDefault) {
      return;
    }
  }

  WidgetWheelEvent event = aPanInput.ToWidgetEvent(this);
  if (!mAPZC) {
    if (MayStartSwipeForNonAPZ(aPanInput)) {
      return;
    }
  } else {
    event = MayStartSwipeForAPZ(aPanInput, result);
  }

  ProcessUntransformedAPZEvent(&event, result);
}

void nsWindow::OnWindowStateEvent(GtkWidget* aWidget,
                                  GdkEventWindowState* aEvent) {
  LOG("nsWindow::OnWindowStateEvent for %p changed 0x%x new_window_state "
      "0x%x\n",
      aWidget, aEvent->changed_mask, aEvent->new_window_state);

  if (IS_MOZ_CONTAINER(aWidget)) {
    // This event is notifying the container widget of changes to the
    // toplevel window.  Just detect changes affecting whether windows are
    // viewable.
    //
    // (A visibility notify event is sent to each window that becomes
    // viewable when the toplevel is mapped, but we can't rely on that for
    // setting mHasMappedToplevel because these toplevel window state
    // events are asynchronous.  The windows in the hierarchy now may not
    // be the same windows as when the toplevel was mapped, so they may
    // not get VisibilityNotify events.)
    bool mapped = !(aEvent->new_window_state &
                    (GDK_WINDOW_STATE_ICONIFIED | GDK_WINDOW_STATE_WITHDRAWN));
    SetHasMappedToplevel(mapped);
    LOG("\tquick return because IS_MOZ_CONTAINER(aWidget) is true\n");
    return;
  }
  // else the widget is a shell widget.

  // The block below is a bit evil.
  //
  // When a window is resized before it is shown, gtk_window_resize() delays
  // resizes until the window is shown.  If gtk_window_state_event() sees a
  // GDK_WINDOW_STATE_MAXIMIZED change [1] before the window is shown, then
  // gtk_window_compute_configure_request_size() ignores the values from the
  // resize [2].  See bug 1449166 for an example of how this could happen.
  //
  // [1] https://gitlab.gnome.org/GNOME/gtk/blob/3.22.30/gtk/gtkwindow.c#L7967
  // [2] https://gitlab.gnome.org/GNOME/gtk/blob/3.22.30/gtk/gtkwindow.c#L9377
  //
  // In order to provide a sensible size for the window when the user exits
  // maximized state, we hide the GDK_WINDOW_STATE_MAXIMIZED change from
  // gtk_window_state_event() so as to trick GTK into using the values from
  // gtk_window_resize() in its configure request.
  //
  // We instead notify gtk_window_state_event() of the maximized state change
  // once the window is shown.
  //
  // See https://gitlab.gnome.org/GNOME/gtk/issues/1044
  //
  // This may be fixed in Gtk 3.24+ but it's still live and kicking
  // (Bug 1791779).
  if (!mIsShown) {
    aEvent->changed_mask = static_cast<GdkWindowState>(
        aEvent->changed_mask & ~GDK_WINDOW_STATE_MAXIMIZED);
  } else if (aEvent->changed_mask & GDK_WINDOW_STATE_WITHDRAWN &&
             aEvent->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) {
    aEvent->changed_mask = static_cast<GdkWindowState>(
        aEvent->changed_mask | GDK_WINDOW_STATE_MAXIMIZED);
  }

  // This is a workaround for https://gitlab.gnome.org/GNOME/gtk/issues/1395
  // Gtk+ controls window active appearance by window-state-event signal.
  if (IsChromeWindowTitlebar() &&
      (aEvent->changed_mask & GDK_WINDOW_STATE_FOCUSED)) {
    // Emulate what Gtk+ does at gtk_window_state_event().
    // We can't check GTK_STATE_FLAG_BACKDROP directly as it's set by Gtk+
    // *after* this window-state-event handler.
    mTitlebarBackdropState =
        !(aEvent->new_window_state & GDK_WINDOW_STATE_FOCUSED);

    // keep IsActiveBrowserWindow in sync with GDK_WINDOW_STATE_FOCUSED
    UpdateMozWindowActive();

    ForceTitlebarRedraw();
  }

  // We don't care about anything but changes in the maximized/icon/fullscreen
  // states but we need a workaround for bug in Wayland:
  // https://gitlab.gnome.org/GNOME/gtk/issues/67
  // Under wayland the gtk_window_iconify implementation does NOT synthetize
  // window_state_event where the GDK_WINDOW_STATE_ICONIFIED is set.
  // During restore we  won't get aEvent->changed_mask with
  // the GDK_WINDOW_STATE_ICONIFIED so to detect that change we use the stored
  // mSizeMode and obtaining a focus.
  bool waylandWasIconified =
      (GdkIsWaylandDisplay() &&
       aEvent->changed_mask & GDK_WINDOW_STATE_FOCUSED &&
       aEvent->new_window_state & GDK_WINDOW_STATE_FOCUSED &&
       mSizeMode == nsSizeMode_Minimized);
  if (!waylandWasIconified &&
      (aEvent->changed_mask &
       (GDK_WINDOW_STATE_ICONIFIED | GDK_WINDOW_STATE_MAXIMIZED |
        GDK_WINDOW_STATE_TILED | GDK_WINDOW_STATE_FULLSCREEN)) == 0) {
    LOG("\tearly return because no interesting bits changed\n");
    return;
  }

  auto oldSizeMode = mSizeMode;
  if (aEvent->new_window_state & GDK_WINDOW_STATE_ICONIFIED) {
    LOG("\tIconified\n");
    mSizeMode = nsSizeMode_Minimized;
#ifdef ACCESSIBILITY
    DispatchMinimizeEventAccessible();
#endif  // ACCESSIBILITY
  } else if (aEvent->new_window_state & GDK_WINDOW_STATE_FULLSCREEN) {
    LOG("\tFullscreen\n");
    mSizeMode = nsSizeMode_Fullscreen;
  } else if (aEvent->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) {
    LOG("\tMaximized\n");
    mSizeMode = nsSizeMode_Maximized;
#ifdef ACCESSIBILITY
    DispatchMaximizeEventAccessible();
#endif  // ACCESSIBILITY
  } else {
    LOG("\tNormal\n");
    mSizeMode = nsSizeMode_Normal;
#ifdef ACCESSIBILITY
    DispatchRestoreEventAccessible();
#endif  // ACCESSIBILITY
  }

  mIsTiled = aEvent->new_window_state & GDK_WINDOW_STATE_TILED;
  LOG("\tTiled: %d\n", int(mIsTiled));

  if (mWidgetListener && mSizeMode != oldSizeMode) {
    mWidgetListener->SizeModeChanged(mSizeMode);
  }

  if (mDrawInTitlebar && mTransparencyBitmapForTitlebar) {
    if (mSizeMode == nsSizeMode_Normal && !mIsTiled) {
      UpdateTitlebarTransparencyBitmap();
    } else {
      ClearTransparencyBitmap();
    }
  }
}

void nsWindow::OnDPIChanged() {
  // Update menu's font size etc.
  // This affects style / layout because it affects system font sizes.
  if (mWidgetListener) {
    if (PresShell* presShell = mWidgetListener->GetPresShell()) {
      presShell->BackingScaleFactorChanged();
    }
    mWidgetListener->UIResolutionChanged();
  }
}

void nsWindow::OnCheckResize() { mPendingConfigures++; }

void nsWindow::OnCompositedChanged() {
  // Update CSD after the change in alpha visibility. This only affects
  // system metrics, not other theme shenanigans.
  NotifyThemeChanged(ThemeChangeKind::MediaQueriesOnly);
  mCompositedScreen = gdk_screen_is_composited(gdk_screen_get_default());
}

void nsWindow::OnScaleChanged(bool aNotify) {
  if (!IsTopLevelWindowType()) {
    return;
  }
  if (!mGdkWindow) {
    return;  // We'll get there again when we configure the window.
  }
  gint newCeiled = gdk_window_get_scale_factor(mGdkWindow);
  double newFractional = [&] {
#ifdef MOZ_WAYLAND
    if (GdkIsWaylandDisplay()) {
      return moz_container_wayland_get_fractional_scale(mContainer);
    }
#endif
    return 0.0;
  }();

  if (mCeiledScaleFactor == newCeiled &&
      mFractionalScaleFactor == newFractional) {
    return;
  }

  LOG("OnScaleChanged %d, %f -> %d, %f\n", int(mCeiledScaleFactor),
      mFractionalScaleFactor, newCeiled, newFractional);

  mCeiledScaleFactor = newCeiled;
  mFractionalScaleFactor = newFractional;

  if (!aNotify) {
    return;
  }

  // We pause compositor to avoid rendering of obsoleted remote content which
  // produces flickering.
  // Re-enable compositor again when remote content is updated or timeout
  // happens.
  PauseCompositorFlickering();

  GtkAllocation allocation;
  gtk_widget_get_allocation(GTK_WIDGET(mContainer), &allocation);
  LayoutDeviceIntSize size = GdkRectToDevicePixels(allocation).Size();
  mBounds.SizeTo(size);
  // Check mBounds size
  if (mCompositorSession &&
      !wr::WindowSizeSanityCheck(mBounds.width, mBounds.height)) {
    gfxCriticalNoteOnce << "Invalid mBounds in OnScaleChanged " << mBounds;
  }

  if (mWidgetListener) {
    if (PresShell* presShell = mWidgetListener->GetPresShell()) {
      presShell->BackingScaleFactorChanged();
    }
  }

  DispatchResized();

  if (mCompositorWidgetDelegate) {
    mCompositorWidgetDelegate->NotifyClientSizeChanged(GetClientSize());
  }

  if (mCursor.IsCustom()) {
    mUpdateCursor = true;
    SetCursor(Cursor{mCursor});
  }
}

void nsWindow::DispatchDragEvent(EventMessage aMsg,
                                 const LayoutDeviceIntPoint& aRefPoint,
                                 guint aTime) {
  LOGDRAG("nsWindow::DispatchDragEvent");
  WidgetDragEvent event(true, aMsg, this);

  InitDragEvent(event);

  event.mRefPoint = aRefPoint;
  event.AssignEventTime(GetWidgetEventTime(aTime));

  DispatchInputEvent(&event);
}

void nsWindow::OnDragDataReceivedEvent(GtkWidget* aWidget,
                                       GdkDragContext* aDragContext, gint aX,
                                       gint aY,
                                       GtkSelectionData* aSelectionData,
                                       guint aInfo, guint aTime,
                                       gpointer aData) {
  LOGDRAG("nsWindow::OnDragDataReceived");

  RefPtr<nsDragService> dragService = nsDragService::GetInstance();
  nsDragService::AutoEventLoop loop(dragService);
  dragService->TargetDataReceived(aWidget, aDragContext, aX, aY, aSelectionData,
                                  aInfo, aTime);
}

nsWindow* nsWindow::GetTransientForWindowIfPopup() {
  if (mWindowType != WindowType::Popup) {
    return nullptr;
  }
  GtkWindow* toplevel = gtk_window_get_transient_for(GTK_WINDOW(mShell));
  if (toplevel) {
    return get_window_for_gtk_widget(GTK_WIDGET(toplevel));
  }
  return nullptr;
}

bool nsWindow::IsHandlingTouchSequence(GdkEventSequence* aSequence) {
  return mHandleTouchEvent && mTouches.Contains(aSequence);
}

gboolean nsWindow::OnTouchpadPinchEvent(GdkEventTouchpadPinch* aEvent) {
  if (!StaticPrefs::apz_gtk_touchpad_pinch_enabled()) {
    return TRUE;
  }
  // Do not respond to pinch gestures involving more than two fingers
  // unless specifically preffed on. These are sometimes hooked up to other
  // actions at the desktop environment level and having the browser also
  // pinch can be undesirable.
  if (aEvent->n_fingers > 2 &&
      !StaticPrefs::apz_gtk_touchpad_pinch_three_fingers_enabled()) {
    return FALSE;
  }
  auto pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE;
  ScreenCoord currentSpan;
  ScreenCoord previousSpan;

  switch (aEvent->phase) {
    case GDK_TOUCHPAD_GESTURE_PHASE_BEGIN:
      pinchGestureType = PinchGestureInput::PINCHGESTURE_START;
      currentSpan = aEvent->scale;
      mCurrentTouchpadFocus = ViewAs<ScreenPixel>(
          GetRefPoint(this, aEvent),
          PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);

      // Assign PreviousSpan --> 0.999 to make mDeltaY field of the
      // WidgetWheelEvent that this PinchGestureInput event will be converted
      // to not equal Zero as our discussion because we observed that the
      // scale of the PHASE_BEGIN event is 1.
      previousSpan = 0.999;
      break;

    case GDK_TOUCHPAD_GESTURE_PHASE_UPDATE:
      pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE;
      mCurrentTouchpadFocus += ScreenPoint(aEvent->dx, aEvent->dy);
      if (aEvent->scale == mLastPinchEventSpan) {
        return FALSE;
      }
      currentSpan = aEvent->scale;
      previousSpan = mLastPinchEventSpan;
      break;

    case GDK_TOUCHPAD_GESTURE_PHASE_END:
      pinchGestureType = PinchGestureInput::PINCHGESTURE_END;
      currentSpan = aEvent->scale;
      previousSpan = mLastPinchEventSpan;
      break;

    default:
      return FALSE;
  }

  PinchGestureInput event(
      pinchGestureType, PinchGestureInput::TRACKPAD,
      GetEventTimeStamp(aEvent->time), ExternalPoint(0, 0),
      mCurrentTouchpadFocus,
      100.0 * ((aEvent->phase == GDK_TOUCHPAD_GESTURE_PHASE_END)
                   ? ScreenCoord(1.f)
                   : currentSpan),
      100.0 * ((aEvent->phase == GDK_TOUCHPAD_GESTURE_PHASE_END)
                   ? ScreenCoord(1.f)
                   : previousSpan),
      KeymapWrapper::ComputeKeyModifiers(aEvent->state));

  if (!event.SetLineOrPageDeltaY(this)) {
    return FALSE;
  }

  mLastPinchEventSpan = aEvent->scale;
  DispatchPinchGestureInput(event);
  return TRUE;
}

gboolean nsWindow::OnTouchEvent(GdkEventTouch* aEvent) {
  LOG("OnTouchEvent: x=%f y=%f type=%d\n", aEvent->x, aEvent->y, aEvent->type);
  if (!mHandleTouchEvent) {
    // If a popup window was spawned (e.g. as the result of a long-press)
    // and touch events got diverted to that window within a touch sequence,
    // ensure the touch event gets sent to the original window instead. We
    // keep the checks here very conservative so that we only redirect
    // events in this specific scenario.
    nsWindow* targetWindow = GetTransientForWindowIfPopup();
    if (targetWindow &&
        targetWindow->IsHandlingTouchSequence(aEvent->sequence)) {
      return targetWindow->OnTouchEvent(aEvent);
    }

    return FALSE;
  }

  EventMessage msg;
  switch (aEvent->type) {
    case GDK_TOUCH_BEGIN:
      // check to see if we should rollup
      if (CheckForRollup(aEvent->x_root, aEvent->y_root, false, false)) {
        return FALSE;
      }
      msg = eTouchStart;
      break;
    case GDK_TOUCH_UPDATE:
      msg = eTouchMove;
      // Start dragging when motion events happens in the dragging area
      if (mWindowShouldStartDragging) {
        mWindowShouldStartDragging = false;
        GdkWindow* gdk_window = gdk_window_get_toplevel(mGdkWindow);
        MOZ_ASSERT(gdk_window,
                   "gdk_window_get_toplevel should not return null");

        LOG("  start window dragging window\n");
        gdk_window_begin_move_drag(gdk_window, 1, aEvent->x_root,
                                   aEvent->y_root, aEvent->time);

        // Cancel the event sequence. gdk will steal all subsequent events
        // (including TOUCH_END).
        msg = eTouchCancel;
      }
      break;
    case GDK_TOUCH_END:
      msg = eTouchEnd;
      if (mWindowShouldStartDragging) {
        LOG("  end of window dragging window\n");
        mWindowShouldStartDragging = false;
      }
      break;
    case GDK_TOUCH_CANCEL:
      msg = eTouchCancel;
      break;
    default:
      return FALSE;
  }

  const LayoutDeviceIntPoint touchPoint = GetRefPoint(this, aEvent);

  int32_t id;
  RefPtr<dom::Touch> touch;
  if (mTouches.Remove(aEvent->sequence, getter_AddRefs(touch))) {
    id = touch->mIdentifier;
  } else {
    id = ++gLastTouchID & 0x7FFFFFFF;
  }

  touch =
      new dom::Touch(id, touchPoint, LayoutDeviceIntPoint(1, 1), 0.0f, 0.0f);

  WidgetTouchEvent event(true, msg, this);
  KeymapWrapper::InitInputEvent(event, aEvent->state);

  if (msg == eTouchStart || msg == eTouchMove) {
    mTouches.InsertOrUpdate(aEvent->sequence, std::move(touch));
    // add all touch points to event object
    for (const auto& data : mTouches.Values()) {
      event.mTouches.AppendElement(new dom::Touch(*data));
    }
  } else if (msg == eTouchEnd || msg == eTouchCancel) {
    *event.mTouches.AppendElement() = std::move(touch);
  }

  nsIWidget::ContentAndAPZEventStatus eventStatus = DispatchInputEvent(&event);

  // There's a chance that we are in drag area and the event is not consumed
  // by something on it.
  if (msg == eTouchStart && mDraggableRegion.Contains(touchPoint) &&
      eventStatus.mApzStatus != nsEventStatus_eConsumeNoDefault) {
    mWindowShouldStartDragging = true;
  }
  return TRUE;
}

// Return true if toplevel window is transparent.
// It's transparent when we're running on composited screens
// and we can draw main window without system titlebar.
bool nsWindow::IsToplevelWindowTransparent() {
  static bool transparencyConfigured = false;

  if (!transparencyConfigured) {
    if (gdk_screen_is_composited(gdk_screen_get_default())) {
      // Some Gtk+ themes use non-rectangular toplevel windows. To fully
      // support such themes we need to make toplevel window transparent
      // with ARGB visual.
      // It may cause performanance issue so make it configurable
      // and enable it by default for selected window managers.
      if (Preferences::HasUserValue("mozilla.widget.use-argb-visuals")) {
        // argb visual is explicitly required so use it
        sTransparentMainWindow =
            Preferences::GetBool("mozilla.widget.use-argb-visuals");
      } else {
        // Enable transparent toplevel window if we can draw main window
        // without system titlebar as Gtk+ themes use titlebar round corners.
        sTransparentMainWindow =
            GetSystemGtkWindowDecoration() != GTK_DECORATION_NONE;
      }
    }
    transparencyConfigured = true;
  }

  return sTransparentMainWindow;
}

#ifdef MOZ_X11
// Configure GL visual on X11.
bool nsWindow::ConfigureX11GLVisual() {
  auto* screen = gtk_widget_get_screen(mShell);
  int visualId = 0;
  bool haveVisual = false;

  if (gfxVars::UseEGL()) {
    haveVisual = GLContextEGL::FindVisual(&visualId);
  }

  // We are on GLX or use it as a fallback on Mesa, see
  // https://gitlab.freedesktop.org/mesa/mesa/-/issues/149
  if (!haveVisual) {
    auto* display = GDK_DISPLAY_XDISPLAY(gtk_widget_get_display(mShell));
    int screenNumber = GDK_SCREEN_XNUMBER(screen);
    haveVisual = GLContextGLX::FindVisual(display, screenNumber, &visualId);
  }

  GdkVisual* gdkVisual = nullptr;
  if (haveVisual) {
    // If we're using CSD, rendering will go through mContainer, but
    // it will inherit this visual as it is a child of mShell.
    gdkVisual = gdk_x11_screen_lookup_visual(screen, visualId);
  }
  if (!gdkVisual) {
    NS_WARNING("We're missing X11 Visual!");
    // We try to use a fallback alpha visual
    GdkScreen* screen = gtk_widget_get_screen(mShell);
    gdkVisual = gdk_screen_get_rgba_visual(screen);
  }
  if (gdkVisual) {
    gtk_widget_set_visual(mShell, gdkVisual);
    mHasAlphaVisual = true;
    return true;
  }

  return false;
}
#endif

nsAutoCString nsWindow::GetFrameTag() const {
  if (nsIFrame* frame = GetFrame()) {
#ifdef DEBUG_FRAME_DUMP
    return frame->ListTag();
#else
    nsAutoCString buf;
    buf.AppendPrintf("Frame(%p)", frame);
    if (nsIContent* content = frame->GetContent()) {
      buf.Append(' ');
      AppendUTF16toUTF8(content->NodeName(), buf);
    }
    return buf;
#endif
  }
  return nsAutoCString("(no frame)");
}

nsCString nsWindow::GetPopupTypeName() {
  switch (mPopupHint) {
    case PopupType::Menu:
      return nsCString("Menu");
    case PopupType::Tooltip:
      return nsCString("Tooltip");
    case PopupType::Panel:
      return nsCString("Panel/Utility");
    default:
      return nsCString("Unknown");
  }
}

// Disables all rendering of GtkWidget from Gtk side.
// We do our best to persuade Gtk/Gdk to ignore all painting
// to the widget.
static void GtkWidgetDisableUpdates(GtkWidget* aWidget) {
  // Clear exposure mask - it disabled synthesized events.
  GdkWindow* window = gtk_widget_get_window(aWidget);
  gdk_window_set_events(window, (GdkEventMask)(gdk_window_get_events(window) &
                                               (~GDK_EXPOSURE_MASK)));

  // Remove before/after paint handles from frame clock.
  // It disables widget content updates.
  GdkFrameClock* frame_clock = gdk_window_get_frame_clock(window);
  g_signal_handlers_disconnect_by_data(frame_clock, window);
}

Window nsWindow::GetX11Window() {
#ifdef MOZ_X11
  if (GdkIsX11Display()) {
    return mGdkWindow ? gdk_x11_window_get_xid(mGdkWindow) : X11None;
  }
#endif
  return (Window) nullptr;
}

void nsWindow::EnsureGdkWindow() {
  if (!mGdkWindow) {
    mGdkWindow = gtk_widget_get_window(mDrawToContainer ? GTK_WIDGET(mContainer)
                                                        : mShell);
    g_object_set_data(G_OBJECT(mGdkWindow), "nsWindow", this);
  }
}

bool nsWindow::GetShapedState() {
  return mIsTransparent && !mHasAlphaVisual && !mTransparencyBitmapForTitlebar;
}

void nsWindow::ConfigureCompositor() {
  MOZ_DIAGNOSTIC_ASSERT(mCompositorState == COMPOSITOR_ENABLED);
  MOZ_DIAGNOSTIC_ASSERT(mIsMapped);

  LOG("nsWindow::ConfigureCompositor()");
  auto startCompositing = [self = RefPtr{this}, this]() -> void {
    LOG("  moz_container_wayland_add_or_fire_initial_draw_callback "
        "ConfigureCompositor");

    // too late
    if (mIsDestroyed || !mIsMapped) {
      LOG("  quit, mIsDestroyed = %d mIsMapped = %d", !!mIsDestroyed,
          mIsMapped);
      return;
    }
    // Compositor will be resumed later by ResumeCompositorFlickering().
    if (mCompositorState == COMPOSITOR_PAUSED_FLICKERING) {
      LOG("  quit, will be resumed by ResumeCompositorFlickering.");
      return;
    }
    // Compositor will be resumed at nsWindow::SetCompositorWidgetDelegate().
    if (!mCompositorWidgetDelegate) {
      LOG("  quit, missing mCompositorWidgetDelegate");
      return;
    }

    ResumeCompositorImpl();
  };

  if (GdkIsWaylandDisplay()) {
#ifdef MOZ_WAYLAND
    moz_container_wayland_add_or_fire_initial_draw_callback(mContainer,
                                                            startCompositing);
#endif
  } else {
    startCompositing();
  }
}

void nsWindow::ConfigureGdkWindow() {
  LOG("nsWindow::ConfigureGdkWindow()");

  EnsureGdkWindow();
  OnScaleChanged(/* aNotify = */ false);

#ifdef MOZ_X11
  if (GdkIsX11Display()) {
    GdkVisual* gdkVisual = gdk_window_get_visual(mGdkWindow);
    Visual* visual = gdk_x11_visual_get_xvisual(gdkVisual);
    int depth = gdk_visual_get_depth(gdkVisual);
    mSurfaceProvider.Initialize(GetX11Window(), visual, depth,
                                GetShapedState());

    // Set window manager hint to keep fullscreen windows composited.
    //
    // If the window were to get unredirected, there could be visible
    // tearing because Gecko does not align its framebuffer updates with
    // vblank.
    SetCompositorHint(GTK_WIDGET_COMPOSIDED_ENABLED);
  }
#endif
#ifdef MOZ_WAYLAND
  if (GdkIsWaylandDisplay()) {
    mSurfaceProvider.Initialize(this);
  }
#endif

  if (mIsDragPopup) {
    if (GdkIsWaylandDisplay()) {
      // Disable painting to the widget on Wayland as we paint directly to the
      // widget. Wayland compositors does not paint wl_subsurface
      // of D&D widget.
      if (GtkWidget* parent = gtk_widget_get_parent(mShell)) {
        GtkWidgetDisableUpdates(parent);
      }
      GtkWidgetDisableUpdates(mShell);
      GtkWidgetDisableUpdates(GTK_WIDGET(mContainer));
    } else {
      // Disable rendering of parent container on X11 to avoid flickering.
      if (GtkWidget* parent = gtk_widget_get_parent(mShell)) {
        gtk_widget_set_opacity(parent, 0.0);
      }
    }
  }

  if (mWindowType == WindowType::Popup) {
    if (mNoAutoHide) {
      gint wmd = ConvertBorderStyles(mBorderStyle);
      if (wmd != -1) {
        gdk_window_set_decorations(mGdkWindow, (GdkWMDecoration)wmd);
      }
    }
    // If the popup ignores mouse events, set an empty input shape.
    SetInputRegion(mInputRegion);
  }

  RefreshWindowClass();

  // We're not mapped yet but we have already created compositor.
  if (mCompositorWidgetDelegate) {
    ConfigureCompositor();
  }

  LOG("  finished, new GdkWindow %p XID 0x%lx\n", mGdkWindow, GetX11Window());
}

void nsWindow::ReleaseGdkWindow() {
  LOG("nsWindow::ReleaseGdkWindow()");

  DestroyChildWindows();

  if (mCompositorWidgetDelegate) {
    mCompositorWidgetDelegate->DisableRendering();
  }

  if (mGdkWindow) {
    g_object_set_data(G_OBJECT(mGdkWindow), "nsWindow", nullptr);
    mGdkWindow = nullptr;
  }

  mSurfaceProvider.CleanupResources();
}

nsresult nsWindow::Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
                          const LayoutDeviceIntRect& aRect,
                          widget::InitData* aInitData) {
  LOG("nsWindow::Create\n");

  // only set the base parent if we're going to be a dialog or a
  // toplevel
  nsIWidget* baseParent =
      aInitData && (aInitData->mWindowType == WindowType::Dialog ||
                    aInitData->mWindowType == WindowType::TopLevel ||
                    aInitData->mWindowType == WindowType::Invisible)
          ? nullptr
          : aParent;

#ifdef ACCESSIBILITY
  // Send a DBus message to check whether a11y is enabled
  a11y::PreInit();
#endif

#ifdef MOZ_WAYLAND
  // Ensure that KeymapWrapper is created on Wayland as we need it for
  // keyboard focus tracking.
  if (GdkIsWaylandDisplay()) {
    KeymapWrapper::EnsureInstance();
  }
#endif

  // Ensure that the toolkit is created.
  nsGTKToolkit::GetToolkit();

  // initialize all the common bits of this class
  BaseCreate(baseParent, aInitData);

  // and do our common creation
  mParent = aParent;
  // save our bounds
  mBounds = aRect;
  LOG("  mBounds: x:%d y:%d w:%d h:%d\n", mBounds.x, mBounds.y, mBounds.width,
      mBounds.height);

  ConstrainSize(&mBounds.width, &mBounds.height);
  mLastSizeRequest = mBounds.Size();

  GtkWidget* eventWidget = nullptr;
  bool popupNeedsAlphaVisual = mWindowType == WindowType::Popup &&
                               (aInitData && aInitData->mTransparencyMode ==
                                                 TransparencyMode::Transparent);

  // Figure out our parent window - only used for WindowType::Child
  nsWindow* parentnsWindow = nullptr;

  if (aParent) {
    parentnsWindow = static_cast<nsWindow*>(aParent);
  } else if (aNativeParent && GDK_IS_WINDOW(aNativeParent)) {
    parentnsWindow = get_window_for_gdk_window(GDK_WINDOW(aNativeParent));
    if (!parentnsWindow) {
      return NS_ERROR_FAILURE;
    }
  }

  if (mWindowType == WindowType::Child) {
    // We don't support WindowType::Child directly but emulate it by popup
    // windows.
    mWindowType = WindowType::Popup;
    if (!parentnsWindow) {
      if (aNativeParent && GTK_IS_CONTAINER(aNativeParent)) {
        parentnsWindow = get_window_for_gtk_widget(GTK_WIDGET(aNativeParent));
      }
    }
    mIsChildWindow = true;
    LOG("  child widget, switch to popup. parent nsWindow %p", parentnsWindow);
  }

  if (mWindowType == WindowType::Popup && !parentnsWindow) {
    LOG("  popup window without parent!");
    if (GdkIsWaylandDisplay()) {
      LOG("  switch to toplevel on Wayland.");
      // Wayland does not allow to create popup without parent so switch to
      // toplevel and mark as wayland panel.
      mIsWaylandPanelWindow = true;
      mWindowType = WindowType::TopLevel;
    }
  }

  if (mWindowType != WindowType::Dialog && mWindowType != WindowType::Popup &&
      mWindowType != WindowType::TopLevel &&
      mWindowType != WindowType::Invisible) {
    MOZ_ASSERT_UNREACHABLE("Unexpected eWindowType");
    return NS_ERROR_FAILURE;
  }

  mAlwaysOnTop = aInitData && aInitData->mAlwaysOnTop;
  // mNoAutoHide seems to be always false here.
  // The mNoAutoHide state is set later on nsMenuPopupFrame level
  // and can be changed so we use WaylandPopupIsPermanent() to get
  // recent popup config (Bug 1728952).
  mNoAutoHide = aInitData && aInitData->mNoAutoHide;

  // Popups that are not noautohide are only temporary. The are used
  // for menus and the like and disappear when another window is used.
  // For most popups, use the standard GtkWindowType GTK_WINDOW_POPUP,
  // which will use a Window with the override-redirect attribute
  // (for temporary windows).
  // For long-lived windows, their stacking order is managed by the
  // window manager, as indicated by GTK_WINDOW_TOPLEVEL.
  // For Wayland we have to always use GTK_WINDOW_POPUP to control
  // popup window position.
  GtkWindowType type = GTK_WINDOW_TOPLEVEL;
  if (mWindowType == WindowType::Popup) {
    MOZ_ASSERT(aInitData);
    type = GTK_WINDOW_POPUP;
    if (GdkIsX11Display() && mNoAutoHide) {
      type = GTK_WINDOW_TOPLEVEL;
    }
  }
  mShell = gtk_window_new(type);

  // Ensure gfxPlatform is initialized, since that is what initializes
  // gfxVars, used below.
  Unused << gfxPlatform::GetPlatform();

  if (IsTopLevelWindowType()) {
    mGtkWindowDecoration = GetSystemGtkWindowDecoration();
    // Inherit initial scale from our parent, or use the default monitor scale
    // otherwise.
    mCeiledScaleFactor = parentnsWindow
                             ? int32_t(parentnsWindow->mCeiledScaleFactor)
                             : ScreenHelperGTK::GetGTKMonitorScaleFactor();
  }

  // Don't use transparency for PictureInPicture windows.
  bool toplevelNeedsAlphaVisual = false;
  if (mWindowType == WindowType::TopLevel && !mIsPIPWindow) {
    toplevelNeedsAlphaVisual = IsToplevelWindowTransparent();
  }

  bool isGLVisualSet = false;
  mIsAccelerated = ComputeShouldAccelerate();
#ifdef MOZ_X11
  if (GdkIsX11Display() && mIsAccelerated) {
    isGLVisualSet = ConfigureX11GLVisual();
  }
#endif
  if (!isGLVisualSet && (popupNeedsAlphaVisual || toplevelNeedsAlphaVisual)) {
    // We're running on composited screen so we can use alpha visual
    // for both toplevel and popups.
    if (mCompositedScreen) {
      GdkVisual* visual =
          gdk_screen_get_rgba_visual(gtk_widget_get_screen(mShell));
      if (visual) {
        gtk_widget_set_visual(mShell, visual);
        mHasAlphaVisual = true;
      }
    }
  }

  // Use X shape mask to draw round corners of Firefox titlebar.
  // We don't use shape masks any more as we switched to ARGB visual
  // by default and non-compositing screens use solid-csd decorations
  // without round corners.
  // Leave the shape mask code here as it can be used to draw round
  // corners on EGL (https://gitlab.freedesktop.org/mesa/mesa/-/issues/149)
  // or when custom titlebar theme is used.
  mTransparencyBitmapForTitlebar = TitlebarUseShapeMask();

  // We have a toplevel window with transparency.
  // Calls to UpdateTitlebarTransparencyBitmap() from OnExposeEvent()
  // occur before SetTransparencyMode() receives TransparencyMode::Transparent
  // from layout, so set mIsTransparent here.
  if (mWindowType == WindowType::TopLevel &&
      (mHasAlphaVisual || mTransparencyBitmapForTitlebar)) {
    mIsTransparent = true;
  }

  // We only move a general managed toplevel window if someone has
  // actually placed the window somewhere.  If no placement has taken
  // place, we just let the window manager Do The Right Thing.
  if (AreBoundsSane()) {
    GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mBounds.Size());
    LOG("nsWindow::Create() Initial resize to %d x %d\n", size.width,
        size.height);
    gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height);
  }
  if (mIsPIPWindow) {
    LOG("    Is PIP Window\n");
    gtk_window_set_type_hint(GTK_WINDOW(mShell), GDK_WINDOW_TYPE_HINT_UTILITY);
  } else if (mWindowType == WindowType::Dialog) {
    mGtkWindowRoleName = "Dialog";

    SetDefaultIcon();
    gtk_window_set_type_hint(GTK_WINDOW(mShell), GDK_WINDOW_TYPE_HINT_DIALOG);
    LOG("nsWindow::Create(): dialog");
    if (parentnsWindow) {
      GtkWindowSetTransientFor(GTK_WINDOW(mShell),
                               GTK_WINDOW(parentnsWindow->GetGtkWidget()));
      LOG("    set parent window [%p]\n", parentnsWindow);
    }
  } else if (mWindowType == WindowType::Popup) {
    MOZ_ASSERT(aInitData);
    mGtkWindowRoleName = "Popup";
    mPopupHint = aInitData->mPopupHint;

    LOG("nsWindow::Create() Popup");

    if (mNoAutoHide) {
      // ... but the window manager does not decorate this window,
      // nor provide a separate taskbar icon.
      if (mBorderStyle == BorderStyle::Default) {
        gtk_window_set_decorated(GTK_WINDOW(mShell), FALSE);
      } else {
        bool decorate = bool(mBorderStyle & BorderStyle::Title);
        gtk_window_set_decorated(GTK_WINDOW(mShell), decorate);
        if (decorate) {
          gtk_window_set_deletable(GTK_WINDOW(mShell),
                                   bool(mBorderStyle & BorderStyle::Close));
        }
      }
      gtk_window_set_skip_taskbar_hint(GTK_WINDOW(mShell), TRUE);
      // Element focus is managed by the parent window so the
      // WM_HINTS input field is set to False to tell the window
      // manager not to set input focus to this window ...
      gtk_window_set_accept_focus(GTK_WINDOW(mShell), FALSE);
#ifdef MOZ_X11
      // ... but when the window manager offers focus through
      // WM_TAKE_FOCUS, focus is requested on the parent window.
      if (GdkIsX11Display()) {
        gtk_widget_realize(mShell);
        gdk_window_add_filter(GetToplevelGdkWindow(), popup_take_focus_filter,
                              nullptr);
      }
#endif
    }

    if (aInitData->mIsDragPopup) {
      gtk_window_set_type_hint(GTK_WINDOW(mShell), GDK_WINDOW_TYPE_HINT_DND);
      mIsDragPopup = true;
      LOG("nsWindow::Create() Drag popup\n");
    } else if (GdkIsX11Display()) {
      // Set the window hints on X11 only. Wayland popups are configured
      // at WaylandPopupConfigure().
      GdkWindowTypeHint gtkTypeHint;
      switch (mPopupHint) {
        case PopupType::Menu:
          gtkTypeHint = GDK_WINDOW_TYPE_HINT_POPUP_MENU;
          break;
        case PopupType::Tooltip:
          gtkTypeHint = GDK_WINDOW_TYPE_HINT_TOOLTIP;
          break;
        default:
          gtkTypeHint = GDK_WINDOW_TYPE_HINT_UTILITY;
          break;
      }
      gtk_window_set_type_hint(GTK_WINDOW(mShell), gtkTypeHint);
      LOG("nsWindow::Create() popup type %s", GetPopupTypeName().get());
    }
    if (parentnsWindow) {
      LOG("    set parent window [%p] %s", parentnsWindow,
          parentnsWindow->mGtkWindowRoleName.get());
      GtkWindow* parentWidget = GTK_WINDOW(parentnsWindow->GetGtkWidget());
      GtkWindowSetTransientFor(GTK_WINDOW(mShell), parentWidget);

      // If popup parent is modal, we need to make popup modal too.
      if (mPopupHint != PopupType::Tooltip &&
          gtk_window_get_modal(parentWidget)) {
        gtk_window_set_modal(GTK_WINDOW(mShell), true);
      }
    }

    // We need realized mShell at NativeMoveResize().
    gtk_widget_realize(mShell);

    // With popup windows, we want to set their position.
    // Place them immediately on X11 and save initial popup position
    // on Wayland as we place Wayland popup on show.
    if (GdkIsX11Display()) {
      NativeMoveResize(/* move */ true, /* resize */ false);
    } else if (AreBoundsSane()) {
      GdkRectangle rect = DevicePixelsToGdkRectRoundOut(mBounds);
      mPopupPosition = {rect.x, rect.y};
    }
  } else {  // must be WindowType::TopLevel
    mGtkWindowRoleName = "Toplevel";
    SetDefaultIcon();

    LOG("nsWindow::Create() Toplevel\n");

    // each toplevel window gets its own window group
    GtkWindowGroup* group = gtk_window_group_new();
    gtk_window_group_add_window(group, GTK_WINDOW(mShell));
    g_object_unref(group);
  }

  if (mAlwaysOnTop) {
    gtk_window_set_keep_above(GTK_WINDOW(mShell), TRUE);
  }

  // It is important that this happens before the realize() call below, so that
  // we don't get bogus CSD margins on Wayland, see bug 1794577.
  if (IsAlwaysUndecoratedWindow()) {
    LOG("    Is undecorated Window\n");
    gtk_window_set_titlebar(GTK_WINDOW(mShell), gtk_fixed_new());
    gtk_window_set_decorated(GTK_WINDOW(mShell), false);
  }

  // Create a container to hold child windows and child GtkWidgets.
  GtkWidget* container = moz_container_new();
  mContainer = MOZ_CONTAINER(container);

  // "csd" style is set when widget is realized so we need to call
  // it explicitly now.
  gtk_widget_realize(mShell);

  /* There are several cases here:
   *
   * 1) We're running on Gtk+ without client side decorations.
   *    Content is rendered to mShell window and we listen
   *    to the Gtk+ events on mShell
   * 2) We're running on Gtk+ and client side decorations
   *    are drawn by Gtk+ to mShell. Content is rendered to mContainer
   *    and we listen to the Gtk+ events on mContainer.
   * 3) We're running on Wayland. All gecko content is rendered
   *    to mContainer and we listen to the Gtk+ events on mContainer.
   */
  GtkStyleContext* style = gtk_widget_get_style_context(mShell);
  mDrawToContainer = GdkIsWaylandDisplay() ||
                     mGtkWindowDecoration == GTK_DECORATION_CLIENT ||
                     gtk_style_context_has_class(style, "csd");
  eventWidget = mDrawToContainer ? container : mShell;

  // Prevent GtkWindow from painting a background to avoid flickering.
  gtk_widget_set_app_paintable(
      eventWidget, StaticPrefs::widget_transparent_windows_AtStartup());

  gtk_widget_add_events(eventWidget, kEvents);

  if (mDrawToContainer) {
    gtk_widget_add_events(mShell, GDK_PROPERTY_CHANGE_MASK);
    gtk_widget_set_app_paintable(
        mShell, StaticPrefs::widget_transparent_windows_AtStartup());
  }
  if (mTransparencyBitmapForTitlebar) {
    moz_container_force_default_visual(mContainer);
  }

  // If we draw to mContainer window then configure it now because
  // gtk_container_add() realizes the child widget.
  gtk_widget_set_has_window(container, mDrawToContainer);

  gtk_container_add(GTK_CONTAINER(mShell), container);

  // alwaysontop windows are generally used for peripheral indicators,
  // so we don't focus them by default.
  if (mAlwaysOnTop) {
    gtk_window_set_focus_on_map(GTK_WINDOW(mShell), FALSE);
  }

  gtk_widget_realize(container);

  // make sure this is the focus widget in the container
  gtk_widget_show(container);

  if (!mAlwaysOnTop) {
    gtk_widget_grab_focus(container);
  }

#ifdef MOZ_WAYLAND
  if (mIsDragPopup && GdkIsWaylandDisplay()) {
    LOG("  set commit to parent");
    moz_container_wayland_set_commit_to_parent(mContainer);
  }
#endif

  if (mWindowType == WindowType::TopLevel && gKioskMode) {
    if (gKioskMonitor != -1) {
      mKioskMonitor = Some(gKioskMonitor);
      LOG("  set kiosk mode monitor %d", mKioskMonitor.value());
    } else {
      LOG("  set kiosk mode");
    }
    // Kiosk mode always use fullscreen.
    MakeFullScreen(/* aFullScreen */ true);
  }

  if (mWindowType == WindowType::Popup) {
    MOZ_ASSERT(aInitData);
    // gdk does not automatically set the cursor for "temporary"
    // windows, which are what gtk uses for popups.

    // force SetCursor to actually set the cursor, even though our internal
    // state indicates that we already have the standard cursor.
    mUpdateCursor = true;
    SetCursor(Cursor{eCursor_standard});
  }

  if (mIsChildWindow && parentnsWindow) {
    GdkWindow* window = GetToplevelGdkWindow();
    GdkWindow* parentWindow = parentnsWindow->GetToplevelGdkWindow();
    LOG("  child GdkWindow %p set parent GdkWindow %p", window, parentWindow);
    gdk_window_reparent(window, parentWindow,
                        DevicePixelsToGdkCoordRoundDown(mBounds.x),
                        DevicePixelsToGdkCoordRoundDown(mBounds.y));
  }

  if (mDrawToContainer) {
    // Also label mShell toplevel window,
    // property_notify_event_cb callback also needs to find its way home
    g_object_set_data(G_OBJECT(GetToplevelGdkWindow()), "nsWindow", this);
  }

  g_object_set_data(G_OBJECT(mContainer), "nsWindow", this);
  g_object_set_data(G_OBJECT(mShell), "nsWindow", this);

  // attach listeners for events
  g_signal_connect(mShell, "configure_event", G_CALLBACK(configure_event_cb),
                   nullptr);
  g_signal_connect(mShell, "delete_event", G_CALLBACK(delete_event_cb),
                   nullptr);
  g_signal_connect(mShell, "window_state_event",
                   G_CALLBACK(window_state_event_cb), nullptr);
  g_signal_connect(mShell, "check-resize", G_CALLBACK(check_resize_cb),
                   nullptr);
  g_signal_connect(mShell, "composited-changed",
                   G_CALLBACK(widget_composited_changed_cb), nullptr);
  g_signal_connect(mShell, "property-notify-event",
                   G_CALLBACK(property_notify_event_cb), nullptr);

  if (mWindowType == WindowType::TopLevel) {
    g_signal_connect_after(mShell, "size_allocate",
                           G_CALLBACK(toplevel_window_size_allocate_cb),
                           nullptr);
  }

  GdkScreen* screen = gtk_widget_get_screen(mShell);
  if (!g_signal_handler_find(screen, G_SIGNAL_MATCH_FUNC, 0, 0, nullptr,
                             FuncToGpointer(screen_composited_changed_cb),
                             nullptr)) {
    g_signal_connect(screen, "composited-changed",
                     G_CALLBACK(screen_composited_changed_cb), nullptr);
  }

  gtk_drag_dest_set((GtkWidget*)mShell, (GtkDestDefaults)0, nullptr, 0,
                    (GdkDragAction)0);
  g_signal_connect(mShell, "drag_motion", G_CALLBACK(drag_motion_event_cb),
                   nullptr);
  g_signal_connect(mShell, "drag_leave", G_CALLBACK(drag_leave_event_cb),
                   nullptr);
  g_signal_connect(mShell, "drag_drop", G_CALLBACK(drag_drop_event_cb),
                   nullptr);
  g_signal_connect(mShell, "drag_data_received",
                   G_CALLBACK(drag_data_received_event_cb), nullptr);

  GtkSettings* default_settings = gtk_settings_get_default();
  g_signal_connect_after(default_settings, "notify::gtk-xft-dpi",
                         G_CALLBACK(settings_xft_dpi_changed_cb), this);

  // Widget signals
  g_signal_connect(mContainer, "map", G_CALLBACK(widget_map_cb), nullptr);
  g_signal_connect(mContainer, "unmap", G_CALLBACK(widget_unmap_cb), nullptr);
  g_signal_connect(mContainer, "unrealize", G_CALLBACK(widget_unrealize_cb),
                   nullptr);
  g_signal_connect_after(mContainer, "size_allocate",
                         G_CALLBACK(size_allocate_cb), nullptr);
  g_signal_connect(mContainer, "hierarchy-changed",
                   G_CALLBACK(hierarchy_changed_cb), nullptr);
  g_signal_connect(mContainer, "notify::scale-factor",
                   G_CALLBACK(scale_changed_cb), nullptr);
  // Initialize mHasMappedToplevel.
  hierarchy_changed_cb(GTK_WIDGET(mContainer), nullptr);
  // Expose, focus, key, and drag events are sent even to GTK_NO_WINDOW
  // widgets.
  g_signal_connect(G_OBJECT(mContainer), "draw", G_CALLBACK(expose_event_cb),
                   nullptr);
  g_signal_connect(mContainer, "focus_in_event", G_CALLBACK(focus_in_event_cb),
                   nullptr);
  g_signal_connect(mContainer, "focus_out_event",
                   G_CALLBACK(focus_out_event_cb), nullptr);
  g_signal_connect(mContainer, "key_press_event",
                   G_CALLBACK(key_press_event_cb), nullptr);
  g_signal_connect(mContainer, "key_release_event",
                   G_CALLBACK(key_release_event_cb), nullptr);

#ifdef MOZ_X11
  if (GdkIsX11Display()) {
    GtkWidget* widgets[] = {GTK_WIDGET(mContainer),
                            !mDrawToContainer ? mShell : nullptr};
    for (size_t i = 0; i < ArrayLength(widgets) && widgets[i]; ++i) {
      // Double buffering is controlled by the window's owning
      // widget. Disable double buffering for painting directly to the
      // X Window.
      gtk_widget_set_double_buffered(widgets[i], FALSE);
    }
  }
#endif
#ifdef MOZ_WAYLAND
  // Initialize the window specific VsyncSource early in order to avoid races
  // with BrowserParent::UpdateVsyncParentVsyncDispatcher().
  // Only use for toplevel windows for now, see bug 1619246.
  if (GdkIsWaylandDisplay() &&
      StaticPrefs::widget_wayland_vsync_enabled_AtStartup() &&
      IsTopLevelWindowType()) {
    mWaylandVsyncSource = new WaylandVsyncSource(this);
    mWaylandVsyncDispatcher = new VsyncDispatcher(mWaylandVsyncSource);
    LOG_VSYNC("  created WaylandVsyncSource");
  }
#endif

  // We create input contexts for all containers, except for
  // toplevel popup windows
  if (mWindowType != WindowType::Popup) {
    mIMContext = new IMContextWrapper(this);
  }

  // These events are sent to the owning widget of the relevant window
  // and propagate up to the first widget that handles the events, so we
  // need only connect on mShell, if it exists, to catch events on its
  // window and windows of mContainer.
  g_signal_connect(eventWidget, "enter-notify-event",
                   G_CALLBACK(enter_notify_event_cb), nullptr);
  g_signal_connect(eventWidget, "leave-notify-event",
                   G_CALLBACK(leave_notify_event_cb), nullptr);
  g_signal_connect(eventWidget, "motion-notify-event",
                   G_CALLBACK(motion_notify_event_cb), nullptr);
  g_signal_connect(eventWidget, "button-press-event",
                   G_CALLBACK(button_press_event_cb), nullptr);
  g_signal_connect(eventWidget, "button-release-event",
                   G_CALLBACK(button_release_event_cb), nullptr);
  g_signal_connect(eventWidget, "scroll-event", G_CALLBACK(scroll_event_cb),
                   nullptr);
  if (gtk_check_version(3, 18, 0) == nullptr) {
    g_signal_connect(eventWidget, "event", G_CALLBACK(generic_event_cb),
                     nullptr);
  }
  g_signal_connect(eventWidget, "touch-event", G_CALLBACK(touch_event_cb),
                   nullptr);

  LOG("  nsWindow type %d %s\n", int(mWindowType),
      mIsPIPWindow ? "PIP window" : "");
  LOG("  mShell %p mContainer %p mGdkWindow %p XID 0x%lx\n", mShell, mContainer,
      mGdkWindow, GetX11Window());

  // Set default application name when it's empty.
  if (mGtkWindowAppName.IsEmpty()) {
    mGtkWindowAppName = gAppData->name;
  }

  mCreated = true;
  return NS_OK;
}

void nsWindow::RefreshWindowClass(void) {
  GdkWindow* gdkWindow = GetToplevelGdkWindow();
  if (!gdkWindow) {
    return;
  }

  if (!mGtkWindowRoleName.IsEmpty()) {
    gdk_window_set_role(gdkWindow, mGtkWindowRoleName.get());
  }

#ifdef MOZ_X11
  if (GdkIsX11Display()) {
    XClassHint* class_hint = XAllocClassHint();
    if (!class_hint) return;

    const char* res_name =
        !mGtkWindowAppName.IsEmpty() ? mGtkWindowAppName.get() : gAppData->name;

    const char* res_class = !mGtkWindowAppClass.IsEmpty()
                                ? mGtkWindowAppClass.get()
                                : gdk_get_program_class();

    if (!res_name || !res_class) {
      XFree(class_hint);
      return;
    }

    class_hint->res_name = const_cast<char*>(res_name);
    class_hint->res_class = const_cast<char*>(res_class);

    // Can't use gtk_window_set_wmclass() for this; it prints
    // a warning & refuses to make the change.
    GdkDisplay* display = gdk_display_get_default();
    XSetClassHint(GDK_DISPLAY_XDISPLAY(display),
                  gdk_x11_window_get_xid(gdkWindow), class_hint);
    XFree(class_hint);
  }
#endif /* MOZ_X11 */
}

void nsWindow::SetWindowClass(const nsAString& xulWinType,
                              const nsAString& xulWinClass,
                              const nsAString& xulWinName) {
  if (!mShell) return;

  // If window type attribute is set, parse it into name and role
  if (!xulWinType.IsEmpty()) {
    char* res_name = ToNewCString(xulWinType, mozilla::fallible);
    const char* role = nullptr;

    if (res_name) {
      // Parse res_name into a name and role. Characters other than
      // [A-Za-z0-9_-] are converted to '_'. Anything after the first
      // colon is assigned to role; if there's no colon, assign the
      // whole thing to both role and res_name.
      for (char* c = res_name; *c; c++) {
        if (':' == *c) {
          *c = 0;
          role = c + 1;
        } else if (!isascii(*c) ||
                   (!isalnum(*c) && ('_' != *c) && ('-' != *c))) {
          *c = '_';
        }
      }
      res_name[0] = (char)toupper(res_name[0]);
      if (!role) role = res_name;

      mGtkWindowAppName = res_name;
      mGtkWindowRoleName = role;
      free(res_name);
    }
  }

  // If window class attribute is set, store it as app class
  // If this attribute is not set, reset app class to default
  if (!xulWinClass.IsEmpty()) {
    CopyUTF16toUTF8(xulWinClass, mGtkWindowAppClass);
  } else {
    mGtkWindowAppClass = nullptr;
  }

  // If window class attribute is set, store it as app name
  // If both name and type are not set, reset app name to default
  if (!xulWinName.IsEmpty()) {
    CopyUTF16toUTF8(xulWinName, mGtkWindowAppName);
  } else if (xulWinType.IsEmpty()) {
    mGtkWindowAppClass = nullptr;
  }

  RefreshWindowClass();
}

nsAutoCString nsWindow::GetDebugTag() const {
  nsAutoCString tag;
  tag.AppendPrintf("[%p]", this);
  return tag;
}

void nsWindow::NativeMoveResize(bool aMoved, bool aResized) {
  GdkPoint topLeft = [&] {
    auto target = mBounds.TopLeft();
    // gtk_window_move will undo the csd offset, but nothing else, so only add
    // the client offset if drawing to the csd titlebar.
    if (DrawsToCSDTitlebar()) {
      target += mClientOffset;
    }
    return DevicePixelsToGdkPointRoundDown(target);
  }();
  GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mLastSizeRequest);

  LOG("nsWindow::NativeMoveResize move %d resize %d to %d,%d -> %d x %d\n",
      aMoved, aResized, topLeft.x, topLeft.y, size.width, size.height);

  if (aResized && !AreBoundsSane()) {
    LOG("  bounds are insane, hidding the window");
    // We have been resized but to incorrect size.
    // If someone has set this so that the needs show flag is false
    // and it needs to be hidden, update the flag and hide the
    // window.  This flag will be cleared the next time someone
    // hides the window or shows it.  It also prevents us from
    // calling NativeShow(false) excessively on the window which
    // causes unneeded X traffic.
    if (!mNeedsShow && mIsShown) {
      mNeedsShow = true;
      NativeShow(false);
    }
    if (aMoved) {
      LOG("  moving to %d x %d", topLeft.x, topLeft.y);
      gtk_window_move(GTK_WINDOW(mShell), topLeft.x, topLeft.y);
    }
    return;
  }

  // Set position to hidden window on X11 may fail, so save the position
  // and move it when it's shown.
  if (aMoved && GdkIsX11Display() && IsPopup() &&
      !gtk_widget_get_visible(GTK_WIDGET(mShell))) {
    LOG("  store position of hidden popup window");
    mHiddenPopupPositioned = true;
    mPopupPosition = {topLeft.x, topLeft.y};
  }

  if (IsWaylandPopup()) {
    NativeMoveResizeWaylandPopup(aMoved, aResized);
  } else {
    // x and y give the position of the window manager frame top-left.
    if (aMoved) {
      gtk_window_move(GTK_WINDOW(mShell), topLeft.x, topLeft.y);
    }
    if (aResized) {
      gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height);
      if (mIsDragPopup) {
        // DND window is placed inside container so we need to make hard size
        // request to ensure parent container is resized too.
        gtk_widget_set_size_request(GTK_WIDGET(mShell), size.width,
                                    size.height);
      }
    }
  }

  if (aResized) {
    // Recompute the input region, in case the window grew or shrunk.
    SetInputRegion(mInputRegion);
  }

  // Does it need to be shown because bounds were previously insane?
  if (mNeedsShow && mIsShown && aResized) {
    NativeShow(true);
  }
}

// We pause compositor to avoid rendering of obsoleted remote content which
// produces flickering.
// Re-enable compositor again when remote content is updated or
// timeout happens.

// Define maximal compositor pause when it's paused to avoid flickering,
// in milliseconds.
#define COMPOSITOR_PAUSE_TIMEOUT (1000)

void nsWindow::PauseCompositorFlickering() {
  bool pauseCompositor = IsTopLevelWindowType() &&
                         mCompositorState == COMPOSITOR_ENABLED &&
                         mCompositorWidgetDelegate && !mIsDestroyed;
  if (!pauseCompositor) {
    return;
  }

  LOG("nsWindow::PauseCompositorFlickering()");

  MozClearHandleID(mCompositorPauseTimeoutID, g_source_remove);

  CompositorBridgeChild* remoteRenderer = GetRemoteRenderer();
  if (remoteRenderer) {
    remoteRenderer->SendPause();
    mCompositorState = COMPOSITOR_PAUSED_FLICKERING;
    mCompositorPauseTimeoutID = (int)g_timeout_add(
        COMPOSITOR_PAUSE_TIMEOUT,
        [](void* data) -> gint {
          nsWindow* window = static_cast<nsWindow*>(data);
          if (!window->IsDestroyed()) {
            window->ResumeCompositorFlickering();
          }
          return true;
        },
        this);
  }
}

bool nsWindow::IsWaitingForCompositorResume() {
  return mCompositorState == COMPOSITOR_PAUSED_FLICKERING;
}

void nsWindow::ResumeCompositorFlickering() {
  MOZ_RELEASE_ASSERT(NS_IsMainThread());

  LOG("nsWindow::ResumeCompositorFlickering()\n");

  if (mIsDestroyed || !IsWaitingForCompositorResume()) {
    LOG("  early quit\n");
    return;
  }

  MozClearHandleID(mCompositorPauseTimeoutID, g_source_remove);

  ResumeCompositorImpl();
}

void nsWindow::ResumeCompositorFromCompositorThread() {
  nsCOMPtr<nsIRunnable> event =
      NewRunnableMethod("nsWindow::ResumeCompositorFlickering", this,
                        &nsWindow::ResumeCompositorFlickering);
  NS_DispatchToMainThread(event.forget());
}

void nsWindow::ResumeCompositorImpl() {
  MOZ_RELEASE_ASSERT(NS_IsMainThread());

  LOG("nsWindow::ResumeCompositorImpl()\n");

  MOZ_DIAGNOSTIC_ASSERT(mCompositorWidgetDelegate);
  mCompositorWidgetDelegate->EnableRendering(GetX11Window(), GetShapedState());

  // As WaylandStartVsync needs mCompositorWidgetDelegate this is the right
  // time to start it.
  WaylandStartVsync();

  CompositorBridgeChild* remoteRenderer = GetRemoteRenderer();
  MOZ_RELEASE_ASSERT(remoteRenderer);
  remoteRenderer->SendResume();
  remoteRenderer->SendForcePresent(wr::RenderReasons::WIDGET);
  mCompositorState = COMPOSITOR_ENABLED;
}

void nsWindow::WaylandStartVsync() {
#ifdef MOZ_WAYLAND
  if (!mWaylandVsyncSource) {
    return;
  }

  LOG_VSYNC("nsWindow::WaylandStartVsync");

  MOZ_DIAGNOSTIC_ASSERT(mCompositorWidgetDelegate);
  if (mCompositorWidgetDelegate->AsGtkCompositorWidget() &&
      mCompositorWidgetDelegate->AsGtkCompositorWidget()
          ->GetNativeLayerRoot()) {
    LOG_VSYNC("  use source NativeLayerRootWayland");
    mWaylandVsyncSource->MaybeUpdateSource(
        mCompositorWidgetDelegate->AsGtkCompositorWidget()
            ->GetNativeLayerRoot()
            ->AsNativeLayerRootWayland());
  } else {
    LOG_VSYNC("  use source mContainer");
    mWaylandVsyncSource->MaybeUpdateSource(mContainer);
  }

  mWaylandVsyncSource->EnableMonitor();
#endif
}

void nsWindow::WaylandStopVsync() {
#ifdef MOZ_WAYLAND
  if (!mWaylandVsyncSource) {
    return;
  }

  LOG_VSYNC("nsWindow::WaylandStopVsync");

  // The widget is going to be hidden, so clear the surface of our
  // vsync source.
  mWaylandVsyncSource->DisableMonitor();
  mWaylandVsyncSource->MaybeUpdateSource(nullptr);
#endif
}

void nsWindow::NativeShow(bool aAction) {
  if (aAction) {
    // unset our flag now that our window has been shown
    mNeedsShow = true;
    auto removeShow = MakeScopeExit([&] { mNeedsShow = false; });

    LOG("nsWindow::NativeShow show\n");

    if (IsWaylandPopup()) {
      mPopupClosed = false;
      if (WaylandPopupConfigure()) {
        AddWindowToPopupHierarchy();
        UpdateWaylandPopupHierarchy();
        if (mPopupClosed) {
          return;
        }
      }
    }
    // Set up usertime/startupID metadata for the created window.
    // On X11 we use gtk_window_set_startup_id() so we need to call it
    // before show.
    if (GdkIsX11Display()) {
      SetUserTimeAndStartupTokenForActivatedWindow();
    }
    if (GdkIsWaylandDisplay()) {
      if (IsWaylandPopup()) {
        ShowWaylandPopupWindow();
      } else {
        ShowWaylandToplevelWindow();
      }
    } else {
      LOG("  calling gtk_widget_show(mShell)\n");
      gtk_widget_show(mShell);
    }
    if (GdkIsWaylandDisplay()) {
      SetUserTimeAndStartupTokenForActivatedWindow();
#ifdef MOZ_WAYLAND
      auto token = std::move(mWindowActivationTokenFromEnv);
      if (!token.IsEmpty()) {
        FocusWaylandWindow(token.get());
      }
#endif
    }
    if (mHiddenPopupPositioned && IsPopup()) {
      LOG("  re-position hidden popup window");
      gtk_window_move(GTK_WINDOW(mShell), mPopupPosition.x, mPopupPosition.y);
      mHiddenPopupPositioned = false;
    }
  } else {
    LOG("nsWindow::NativeShow hide\n");
    if (GdkIsWaylandDisplay()) {
      if (IsWaylandPopup()) {
        // We can't close tracked popups directly as they may have visible
        // child popups. Just mark is as closed and let
        // UpdateWaylandPopupHierarchy() do the job.
        if (IsInPopupHierarchy()) {
          WaylandPopupMarkAsClosed();
          UpdateWaylandPopupHierarchy();
        } else {
          // Close untracked popups directly.
          HideWaylandPopupWindow(/* aTemporaryHide */ false,
                                 /* aRemoveFromPopupList */ true);
        }
      } else {
        HideWaylandToplevelWindow();
      }
    } else {
      // Workaround window freezes on GTK versions before 3.21.2 by
      // ensuring that configure events get dispatched to windows before
      // they are unmapped. See bug 1225044.
      if (gtk_check_version(3, 21, 2) != nullptr && mPendingConfigures > 0) {
        GtkAllocation allocation;
        gtk_widget_get_allocation(GTK_WIDGET(mShell), &allocation);

        GdkEventConfigure event;
        PodZero(&event);
        event.type = GDK_CONFIGURE;
        event.window = mGdkWindow;
        event.send_event = TRUE;
        event.x = allocation.x;
        event.y = allocation.y;
        event.width = allocation.width;
        event.height = allocation.height;

        auto* shellClass = GTK_WIDGET_GET_CLASS(mShell);
        for (unsigned int i = 0; i < mPendingConfigures; i++) {
          Unused << shellClass->configure_event(mShell, &event);
        }
        mPendingConfigures = 0;
      }
      gtk_widget_hide(mShell);

      ClearTransparencyBitmap();  // Release some resources
    }
  }
}

void nsWindow::SetHasMappedToplevel(bool aState) {
  LOG("nsWindow::SetHasMappedToplevel(%d)", aState);
  if (aState == mHasMappedToplevel) {
    return;
  }
  // Even when aState == mHasMappedToplevel (as when this method is called
  // from Show()), child windows need to have their state checked, so don't
  // return early.
  mHasMappedToplevel = aState;
  if (aState && mNeedsToRetryCapturingMouse) {
    CaptureRollupEvents(true);
    MOZ_ASSERT(!mNeedsToRetryCapturingMouse);
  }
}

LayoutDeviceIntSize nsWindow::GetSafeWindowSize(LayoutDeviceIntSize aSize) {
  // The X protocol uses CARD32 for window sizes, but the server (1.11.3)
  // reads it as CARD16.  Sizes of pixmaps, used for drawing, are (unsigned)
  // CARD16 in the protocol, but the server's ProcCreatePixmap returns
  // BadAlloc if dimensions cannot be represented by signed shorts.
  // Because we are creating Cairo surfaces to represent window buffers,
  // we also must ensure that the window can fit in a Cairo surface.
  LayoutDeviceIntSize result = aSize;
  int32_t maxSize = 32767;
  if (mWindowRenderer && mWindowRenderer->AsKnowsCompositor()) {
    maxSize = std::min(
        maxSize, mWindowRenderer->AsKnowsCompositor()->GetMaxTextureSize());
  }
  if (result.width > maxSize) {
    result.width = maxSize;
  }
  if (result.height > maxSize) {
    result.height = maxSize;
  }
  return result;
}

void nsWindow::SetTransparencyMode(TransparencyMode aMode) {
  bool isTransparent = aMode == TransparencyMode::Transparent;

  if (mIsTransparent == isTransparent) {
    return;
  }

  if (mWindowType != WindowType::Popup) {
    // https://bugzilla.mozilla.org/show_bug.cgi?id=1344839 reported
    // problems cleaning the layer manager for toplevel windows.
    // Ignore the request so as to workaround that.
    // mIsTransparent is set in Create() if transparency may be required.
    if (isTransparent) {
      NS_WARNING("Transparent mode not supported on non-popup windows.");
    }
    return;
  }

  if (!isTransparent) {
    ClearTransparencyBitmap();
  }  // else the new default alpha values are "all 1", so we don't
  // need to change anything yet

  mIsTransparent = isTransparent;

  if (!mHasAlphaVisual) {
    // The choice of layer manager depends on
    // GtkCompositorWidgetInitData::Shaped(), which will need to change, so
    // clean out the old layer manager.
    DestroyLayerManager();
  }
}

TransparencyMode nsWindow::GetTransparencyMode() {
  return mIsTransparent ? TransparencyMode::Transparent
                        : TransparencyMode::Opaque;
}

gint nsWindow::GetInputRegionMarginInGdkCoords() {
  return DevicePixelsToGdkCoordRoundDown(mInputRegion.mMargin);
}

void nsWindow::SetInputRegion(const InputRegion& aInputRegion) {
  mInputRegion = aInputRegion;

  GdkWindow* window = mDrawToContainer ? GetToplevelGdkWindow() : mGdkWindow;
  if (!window) {
    return;
  }

  LOG("nsWindow::SetInputRegion(%d, %d)", aInputRegion.mFullyTransparent,
      int(aInputRegion.mMargin));

  cairo_rectangle_int_t rect = {0, 0, 0, 0};
  cairo_region_t* region = nullptr;
  auto releaseRegion = MakeScopeExit([&] {
    if (region) {
      cairo_region_destroy(region);
    }
  });

  if (aInputRegion.mFullyTransparent) {
    region = cairo_region_create_rectangle(&rect);
  } else if (aInputRegion.mMargin != 0) {
    LayoutDeviceIntRect inputRegion(LayoutDeviceIntPoint(), mLastSizeRequest);
    inputRegion.Deflate(aInputRegion.mMargin);
    GdkRectangle gdkRect = DevicePixelsToGdkRectRoundOut(inputRegion);
    rect = {gdkRect.x, gdkRect.y, gdkRect.width, gdkRect.height};
    region = cairo_region_create_rectangle(&rect);
  }

  gdk_window_input_shape_combine_region(window, region, 0, 0);

  // On Wayland gdk_window_input_shape_combine_region() call is cached and
  // applied to underlying wl_surface when GdkWindow is repainted.
  // Force repaint of GdkWindow to apply the change immediately.
  if (GdkIsWaylandDisplay()) {
    gdk_window_invalidate_rect(window, nullptr, false);
  }
}

// For setting the draggable titlebar region from CSS
// with -moz-window-dragging: drag.
void nsWindow::UpdateWindowDraggingRegion(
    const LayoutDeviceIntRegion& aRegion) {
  if (mDraggableRegion != aRegion) {
    mDraggableRegion = aRegion;
  }
}

LayoutDeviceIntCoord nsWindow::GetTitlebarRadius() {
  MOZ_RELEASE_ASSERT(NS_IsMainThread());
  int32_t cssCoord = LookAndFeel::GetInt(LookAndFeel::IntID::TitlebarRadius);
  return GdkCoordToDevicePixels(cssCoord);
}

// See subtract_corners_from_region() at gtk/gtkwindow.c
// We need to subtract corners from toplevel window opaque region
// to draw transparent corners of default Gtk titlebar.
// Both implementations (cairo_region_t and wl_region) needs to be synced.
static void SubtractTitlebarCorners(cairo_region_t* aRegion, int aX, int aY,
                                    int aWindowWidth, int aWindowHeight,
                                    int aTitlebarRadius) {
  if (!aTitlebarRadius) {
    return;
  }
  cairo_rectangle_int_t rect = {aX, aY, aTitlebarRadius, aTitlebarRadius};
  cairo_region_subtract_rectangle(aRegion, &rect);
  rect = {
      aX + aWindowWidth - aTitlebarRadius,
      aY,
      aTitlebarRadius,
      aTitlebarRadius,
  };
  cairo_region_subtract_rectangle(aRegion, &rect);
  rect = {
      aX,
      aY + aWindowHeight - aTitlebarRadius,
      aTitlebarRadius,
      aTitlebarRadius,
  };
  cairo_region_subtract_rectangle(aRegion, &rect);
  rect = {
      aX + aWindowWidth - aTitlebarRadius,
      aY + aWindowHeight - aTitlebarRadius,
      aTitlebarRadius,
      aTitlebarRadius,
  };
  cairo_region_subtract_rectangle(aRegion, &rect);
}

void nsWindow::UpdateTopLevelOpaqueRegion() {
  if (!mCompositedScreen) {
    return;
  }

  GdkWindow* window = mDrawToContainer ? GetToplevelGdkWindow() : mGdkWindow;
  if (!window) {
    return;
  }
  MOZ_ASSERT(gdk_window_get_window_type(window) == GDK_WINDOW_TOPLEVEL);

  int x = 0;
  int y = 0;

  if (mDrawToContainer) {
    gdk_window_get_position(mGdkWindow, &x, &y);
  }

  int width = DevicePixelsToGdkCoordRoundDown(mBounds.width);
  int height = DevicePixelsToGdkCoordRoundDown(mBounds.height);

  cairo_region_t* region = cairo_region_create();
  cairo_rectangle_int_t rect = {x, y, width, height};
  cairo_region_union_rectangle(region, &rect);

  // TODO: We actually could get a proper opaque region from layout, see
  // nsIWidget::UpdateOpaqueRegion. This could simplify titlebar drawing.
  int radius = DoDrawTilebarCorners() ? int(GetTitlebarRadius()) : 0;
  SubtractTitlebarCorners(region, x, y, width, height, radius);

  gdk_window_set_opaque_region(window, region);

  cairo_region_destroy(region);

#ifdef MOZ_WAYLAND
  if (GdkIsWaylandDisplay()) {
    moz_container_wayland_update_opaque_region(mContainer, radius);
  }
#endif
}

bool nsWindow::IsChromeWindowTitlebar() {
  return mDrawInTitlebar && !mIsPIPWindow &&
         mWindowType == WindowType::TopLevel;
}

bool nsWindow::DoDrawTilebarCorners() {
  return IsChromeWindowTitlebar() && mSizeMode == nsSizeMode_Normal &&
         !mIsTiled;
}

void nsWindow::ResizeTransparencyBitmap() {
  if (!mTransparencyBitmap) return;

  if (mBounds.width == mTransparencyBitmapWidth &&
      mBounds.height == mTransparencyBitmapHeight) {
    return;
  }

  int32_t newRowBytes = GetBitmapStride(mBounds.width);
  int32_t newSize = newRowBytes * mBounds.height;
  auto* newBits = new gchar[newSize];
  // fill new mask with "transparent", first
  memset(newBits, 0, newSize);

  // Now copy the intersection of the old and new areas into the new mask
  int32_t copyWidth = std::min(mBounds.width, mTransparencyBitmapWidth);
  int32_t copyHeight = std::min(mBounds.height, mTransparencyBitmapHeight);
  int32_t oldRowBytes = GetBitmapStride(mTransparencyBitmapWidth);
  int32_t copyBytes = GetBitmapStride(copyWidth);

  int32_t i;
  gchar* fromPtr = mTransparencyBitmap;
  gchar* toPtr = newBits;
  for (i = 0; i < copyHeight; i++) {
    memcpy(toPtr, fromPtr, copyBytes);
    fromPtr += oldRowBytes;
    toPtr += newRowBytes;
  }

  delete[] mTransparencyBitmap;
  mTransparencyBitmap = newBits;
  mTransparencyBitmapWidth = mBounds.width;
  mTransparencyBitmapHeight = mBounds.height;
}

static bool ChangedMaskBits(gchar* aMaskBits, int32_t aMaskWidth,
                            int32_t aMaskHeight, const nsIntRect& aRect,
                            uint8_t* aAlphas, int32_t aStride) {
  int32_t x, y, xMax = aRect.XMost(), yMax = aRect.YMost();
  int32_t maskBytesPerRow = GetBitmapStride(aMaskWidth);
  for (y = aRect.y; y < yMax; y++) {
    gchar* maskBytes = aMaskBits + y * maskBytesPerRow;
    uint8_t* alphas = aAlphas;
    for (x = aRect.x; x < xMax; x++) {
      bool newBit = *alphas > 0x7f;
      alphas++;

      gchar maskByte = maskBytes[x >> 3];
      bool maskBit = (maskByte & (1 << (x & 7))) != 0;

      if (maskBit != newBit) {
        return true;
      }
    }
    aAlphas += aStride;
  }

  return false;
}

static void UpdateMaskBits(gchar* aMaskBits, int32_t aMaskWidth,
                           int32_t aMaskHeight, const nsIntRect& aRect,
                           uint8_t* aAlphas, int32_t aStride) {
  int32_t x, y, xMax = aRect.XMost(), yMax = aRect.YMost();
  int32_t maskBytesPerRow = GetBitmapStride(aMaskWidth);
  for (y = aRect.y; y < yMax; y++) {
    gchar* maskBytes = aMaskBits + y * maskBytesPerRow;
    uint8_t* alphas = aAlphas;
    for (x = aRect.x; x < xMax; x++) {
      bool newBit = *alphas > 0x7f;
      alphas++;

      gchar mask = 1 << (x & 7);
      gchar maskByte = maskBytes[x >> 3];
      // Note: '-newBit' turns 0 into 00...00 and 1 into 11...11
      maskBytes[x >> 3] = (maskByte & ~mask) | (-newBit & mask);
    }
    aAlphas += aStride;
  }
}

void nsWindow::ApplyTransparencyBitmap() {
#ifdef MOZ_X11
  // We use X11 calls where possible, because GDK handles expose events
  // for shaped windows in a way that's incompatible with us (Bug 635903).
  // It doesn't occur when the shapes are set through X.
  Display* xDisplay = GDK_WINDOW_XDISPLAY(mGdkWindow);
  Window xDrawable = GDK_WINDOW_XID(mGdkWindow);
  Pixmap maskPixmap = XCreateBitmapFromData(
      xDisplay, xDrawable, mTransparencyBitmap, mTransparencyBitmapWidth,
      mTransparencyBitmapHeight);
  XShapeCombineMask(xDisplay, xDrawable, ShapeBounding, 0, 0, maskPixmap,
                    ShapeSet);
  XFreePixmap(xDisplay, maskPixmap);
#endif  // MOZ_X11
}

void nsWindow::ClearTransparencyBitmap() {
  if (!mTransparencyBitmap) return;

  delete[] mTransparencyBitmap;
  mTransparencyBitmap = nullptr;
  mTransparencyBitmapWidth = 0;
  mTransparencyBitmapHeight = 0;

  if (!mShell) return;

#ifdef MOZ_X11
  if (MOZ_UNLIKELY(!mGdkWindow)) {
    return;
  }

  Display* xDisplay = GDK_WINDOW_XDISPLAY(mGdkWindow);
  Window xWindow = gdk_x11_window_get_xid(mGdkWindow);

  XShapeCombineMask(xDisplay, xWindow, ShapeBounding, 0, 0, X11None, ShapeSet);
#endif
}

nsresult nsWindow::UpdateTranslucentWindowAlphaInternal(const nsIntRect& aRect,
                                                        uint8_t* aAlphas,
                                                        int32_t aStride) {
  NS_ASSERTION(mIsTransparent, "Window is not transparent");
  NS_ASSERTION(!mTransparencyBitmapForTitlebar,
               "Transparency bitmap is already used for titlebar rendering");

  if (mTransparencyBitmap == nullptr) {
    int32_t size = GetBitmapStride(mBounds.width) * mBounds.height;
    mTransparencyBitmap = new gchar[size];
    memset(mTransparencyBitmap, 255, size);
    mTransparencyBitmapWidth = mBounds.width;
    mTransparencyBitmapHeight = mBounds.height;
  } else {
    ResizeTransparencyBitmap();
  }

  nsIntRect rect;
  rect.IntersectRect(aRect, nsIntRect(0, 0, mBounds.width, mBounds.height));

  if (!ChangedMaskBits(mTransparencyBitmap, mBounds.width, mBounds.height, rect,
                       aAlphas, aStride)) {
    // skip the expensive stuff if the mask bits haven't changed; hopefully
    // this is the common case
    return NS_OK;
  }

  UpdateMaskBits(mTransparencyBitmap, mBounds.width, mBounds.height, rect,
                 aAlphas, aStride);

  if (!mNeedsShow) {
    ApplyTransparencyBitmap();
  }
  return NS_OK;
}

#define TITLEBAR_HEIGHT 10

LayoutDeviceIntRect nsWindow::GetTitlebarRect() {
  // See NS_NATIVE_EGL_WINDOW why we can't block here.
  auto ret = LayoutDeviceIntRect();

  if (mDestroyMutex.TryLock()) {
    if (mGdkWindow && mDrawInTitlebar) {
      int height = 0;
      if (DoDrawTilebarCorners()) {
        height = GdkCeiledScaleFactor() * TITLEBAR_HEIGHT;
      }
      ret = LayoutDeviceIntRect(0, 0, mBounds.width, height);
    }
    mDestroyMutex.Unlock();
  }

  return ret;
}

void nsWindow::UpdateTitlebarTransparencyBitmap() {
  NS_ASSERTION(mTransparencyBitmapForTitlebar,
               "Transparency bitmap is already used to draw window shape");

  if (!mGdkWindow || !mDrawInTitlebar ||
      (mBounds.width == mTransparencyBitmapWidth &&
       mBounds.height == mTransparencyBitmapHeight)) {
    return;
  }

  bool maskCreate =
      !mTransparencyBitmap || mBounds.width > mTransparencyBitmapWidth;

  bool maskUpdate =
      !mTransparencyBitmap || mBounds.width != mTransparencyBitmapWidth;

  LayoutDeviceIntCoord radius = GetTitlebarRadius();
  if (maskCreate) {
    delete[] mTransparencyBitmap;
    int32_t size = GetBitmapStride(mBounds.width) * radius;
    mTransparencyBitmap = new gchar[size];
    mTransparencyBitmapWidth = mBounds.width;
  } else {
    mTransparencyBitmapWidth = mBounds.width;
  }
  mTransparencyBitmapHeight = mBounds.height;

  if (maskUpdate) {
    cairo_surface_t* surface = cairo_image_surface_create(
        CAIRO_FORMAT_A8, mTransparencyBitmapWidth, radius);
    if (!surface) return;

    cairo_t* cr = cairo_create(surface);

    GtkWidgetState state;
    memset((void*)&state, 0, sizeof(state));
    GdkRectangle rect = {0, 0, mTransparencyBitmapWidth, radius};

    moz_gtk_widget_paint(MOZ_GTK_HEADER_BAR, cr, &rect, &state, 0,
                         GTK_TEXT_DIR_NONE);

    cairo_destroy(cr);
    cairo_surface_mark_dirty(surface);
    cairo_surface_flush(surface);

    UpdateMaskBits(mTransparencyBitmap, mTransparencyBitmapWidth, radius,
                   nsIntRect(0, 0, mTransparencyBitmapWidth, radius),
                   cairo_image_surface_get_data(surface),
                   cairo_format_stride_for_width(CAIRO_FORMAT_A8,
                                                 mTransparencyBitmapWidth));

    cairo_surface_destroy(surface);
  }

#ifdef MOZ_X11
  if (!mNeedsShow) {
    Display* xDisplay = GDK_WINDOW_XDISPLAY(mGdkWindow);
    Window xDrawable = GDK_WINDOW_XID(mGdkWindow);

    Pixmap maskPixmap =
        XCreateBitmapFromData(xDisplay, xDrawable, mTransparencyBitmap,
                              mTransparencyBitmapWidth, radius);

    XShapeCombineMask(xDisplay, xDrawable, ShapeBounding, 0, 0, maskPixmap,
                      ShapeSet);

    if (mTransparencyBitmapHeight > radius) {
      XRectangle rect = {0, 0, (unsigned short)mTransparencyBitmapWidth,
                         (unsigned short)(mTransparencyBitmapHeight - radius)};
      XShapeCombineRectangles(xDisplay, xDrawable, ShapeBounding, 0, radius,
                              &rect, 1, ShapeUnion, 0);
    }

    XFreePixmap(xDisplay, maskPixmap);
  }
#endif
}

GtkWidget* nsWindow::GetToplevelWidget() const { return mShell; }

GdkWindow* nsWindow::GetToplevelGdkWindow() const {
  return gtk_widget_get_window(mShell);
}

nsWindow* nsWindow::GetContainerWindow() const {
  GtkWidget* owningWidget = GTK_WIDGET(mContainer);
  if (!owningWidget) {
    return nullptr;
  }

  nsWindow* window = get_window_for_gtk_widget(owningWidget);
  NS_ASSERTION(window, "No nsWindow for container widget");
  return window;
}

void nsWindow::SetUrgencyHint(GtkWidget* top_window, bool state) {
  LOG("  nsWindow::SetUrgencyHint widget %p\n", top_window);

  if (!top_window) return;

  // TODO: Use xdg-activation on Wayland?
  gdk_window_set_urgency_hint(gtk_widget_get_window(top_window), state);
}

void nsWindow::SetDefaultIcon(void) { SetIcon(u"default"_ns); }

gint nsWindow::ConvertBorderStyles(BorderStyle aStyle) {
  gint w = 0;

  if (aStyle == BorderStyle::Default) return -1;

  // note that we don't handle BorderStyle::Close yet
  if (aStyle & BorderStyle::All) w |= GDK_DECOR_ALL;
  if (aStyle & BorderStyle::Border) w |= GDK_DECOR_BORDER;
  if (aStyle & BorderStyle::ResizeH) w |= GDK_DECOR_RESIZEH;
  if (aStyle & BorderStyle::Title) w |= GDK_DECOR_TITLE;
  if (aStyle & BorderStyle::Menu) w |= GDK_DECOR_MENU;
  if (aStyle & BorderStyle::Minimize) w |= GDK_DECOR_MINIMIZE;
  if (aStyle & BorderStyle::Maximize) w |= GDK_DECOR_MAXIMIZE;

  return w;
}

class FullscreenTransitionWindow final : public nsISupports {
 public:
  NS_DECL_ISUPPORTS

  explicit FullscreenTransitionWindow(GtkWidget* aWidget);

  GtkWidget* mWindow;

 private:
  ~FullscreenTransitionWindow();
};

NS_IMPL_ISUPPORTS0(FullscreenTransitionWindow)

FullscreenTransitionWindow::FullscreenTransitionWindow(GtkWidget* aWidget) {
  mWindow = gtk_window_new(GTK_WINDOW_POPUP);
  GtkWindow* gtkWin = GTK_WINDOW(mWindow);

  gtk_window_set_type_hint(gtkWin, GDK_WINDOW_TYPE_HINT_SPLASHSCREEN);
  GtkWindowSetTransientFor(gtkWin, GTK_WINDOW(aWidget));
  gtk_window_set_decorated(gtkWin, false);

  GdkWindow* gdkWin = gtk_widget_get_window(aWidget);
  GdkScreen* screen = gtk_widget_get_screen(aWidget);
  gint monitorNum = gdk_screen_get_monitor_at_window(screen, gdkWin);
  GdkRectangle monitorRect;
  gdk_screen_get_monitor_geometry(screen, monitorNum, &monitorRect);
  gtk_window_set_screen(gtkWin, screen);
  gtk_window_move(gtkWin, monitorRect.x, monitorRect.y);
  MOZ_ASSERT(monitorRect.width > 0 && monitorRect.height > 0,
             "Can't resize window smaller than 1x1.");
  gtk_window_resize(gtkWin, monitorRect.width, monitorRect.height);

  GdkRGBA bgColor;
  bgColor.red = bgColor.green = bgColor.blue = 0.0;
  bgColor.alpha = 1.0;
  gtk_widget_override_background_color(mWindow, GTK_STATE_FLAG_NORMAL,
                                       &bgColor);

  gtk_widget_set_opacity(mWindow, 0.0);
  gtk_widget_show(mWindow);
}

FullscreenTransitionWindow::~FullscreenTransitionWindow() {
  gtk_widget_destroy(mWindow);
}

class FullscreenTransitionData {
 public:
  FullscreenTransitionData(nsIWidget::FullscreenTransitionStage aStage,
                           uint16_t aDuration, nsIRunnable* aCallback,
                           FullscreenTransitionWindow* aWindow)
      : mStage(aStage),
        mStartTime(TimeStamp::Now()),
        mDuration(TimeDuration::FromMilliseconds(aDuration)),
        mCallback(aCallback),
        mWindow(aWindow) {}

  static const guint sInterval = 1000 / 30;  // 30fps
  static gboolean TimeoutCallback(gpointer aData);

 private:
  nsIWidget::FullscreenTransitionStage mStage;
  TimeStamp mStartTime;
  TimeDuration mDuration;
  nsCOMPtr<nsIRunnable> mCallback;
  RefPtr<FullscreenTransitionWindow> mWindow;
};

/* static */
gboolean FullscreenTransitionData::TimeoutCallback(gpointer aData) {
  bool finishing = false;
  auto* data = static_cast<FullscreenTransitionData*>(aData);
  gdouble opacity = (TimeStamp::Now() - data->mStartTime) / data->mDuration;
  if (opacity >= 1.0) {
    opacity = 1.0;
    finishing = true;
  }
  if (data->mStage == nsIWidget::eAfterFullscreenToggle) {
    opacity = 1.0 - opacity;
  }
  gtk_widget_set_opacity(data->mWindow->mWindow, opacity);

  if (!finishing) {
    return TRUE;
  }
  NS_DispatchToMainThread(data->mCallback.forget());
  delete data;
  return FALSE;
}

/* virtual */
bool nsWindow::PrepareForFullscreenTransition(nsISupports** aData) {
  if (!mCompositedScreen) {
    return false;
  }
  *aData = do_AddRef(new FullscreenTransitionWindow(mShell)).take();
  return true;
}

/* virtual */
void nsWindow::PerformFullscreenTransition(FullscreenTransitionStage aStage,
                                           uint16_t aDuration,
                                           nsISupports* aData,
                                           nsIRunnable* aCallback) {
  auto* data = static_cast<FullscreenTransitionWindow*>(aData);
  // This will be released at the end of the last timeout callback for it.
  auto* transitionData =
      new FullscreenTransitionData(aStage, aDuration, aCallback, data);
  g_timeout_add_full(G_PRIORITY_HIGH, FullscreenTransitionData::sInterval,
                     FullscreenTransitionData::TimeoutCallback, transitionData,
                     nullptr);
}

already_AddRefed<widget::Screen> nsWindow::GetWidgetScreen() {
  // Wayland can read screen directly
  if (GdkIsWaylandDisplay()) {
    if (RefPtr<Screen> screen = ScreenHelperGTK::GetScreenForWindow(this)) {
      return screen.forget();
    }
  }

  // GetScreenBounds() is slow for the GTK port so we override and use
  // mBounds directly.
  ScreenManager& screenManager = ScreenManager::GetSingleton();
  LayoutDeviceIntRect bounds = mBounds;
  DesktopIntRect deskBounds = RoundedToInt(bounds / GetDesktopToDeviceScale());
  return screenManager.ScreenForRect(deskBounds);
}

RefPtr<VsyncDispatcher> nsWindow::GetVsyncDispatcher() {
#ifdef MOZ_WAYLAND
  if (mWaylandVsyncDispatcher) {
    return mWaylandVsyncDispatcher;
  }
#endif
  return nullptr;
}

bool nsWindow::SynchronouslyRepaintOnResize() {
  if (GdkIsWaylandDisplay()) {
    // See Bug 1734368
    // Don't request synchronous repaint on HW accelerated backend - mesa can be
    // deadlocked when it's missing back buffer and main event loop is blocked.
    return false;
  }

  // default is synced repaint.
  return true;
}

void nsWindow::KioskLockOnMonitor() {
  // Available as of GTK 3.18+
  static auto sGdkWindowFullscreenOnMonitor =
      (void (*)(GdkWindow* window, gint monitor))dlsym(
          RTLD_DEFAULT, "gdk_window_fullscreen_on_monitor");

  if (!sGdkWindowFullscreenOnMonitor) {
    return;
  }

  int monitor = mKioskMonitor.value();
  if (monitor < 0 || monitor >= ScreenHelperGTK::GetMonitorCount()) {
    LOG("nsWindow::KioskLockOnMonitor() wrong monitor number! (%d)\n", monitor);
    return;
  }

  LOG("nsWindow::KioskLockOnMonitor() locked on %d\n", monitor);
  sGdkWindowFullscreenOnMonitor(GetToplevelGdkWindow(), monitor);
}

static bool IsFullscreenSupported(GtkWidget* aShell) {
#ifdef MOZ_X11
  GdkScreen* screen = gtk_widget_get_screen(aShell);
  GdkAtom atom = gdk_atom_intern("_NET_WM_STATE_FULLSCREEN", FALSE);
  return gdk_x11_screen_supports_net_wm_hint(screen, atom);
#else
  return true;
#endif
}

nsresult nsWindow::MakeFullScreen(bool aFullScreen) {
  LOG("nsWindow::MakeFullScreen aFullScreen %d\n", aFullScreen);

  if (GdkIsX11Display() && !IsFullscreenSupported(mShell)) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  if (aFullScreen) {
    if (mSizeMode != nsSizeMode_Fullscreen &&
        mSizeMode != nsSizeMode_Minimized) {
      mLastSizeModeBeforeFullscreen = mSizeMode;
    }
    if (mIsPIPWindow) {
      gtk_window_set_type_hint(GTK_WINDOW(mShell), GDK_WINDOW_TYPE_HINT_NORMAL);
      if (gUseAspectRatio) {
        mAspectRatioSaved = mAspectRatio;
        mAspectRatio = 0.0f;
        ApplySizeConstraints();
      }
    }

    if (mKioskMonitor.isSome()) {
      KioskLockOnMonitor();
    } else {
      gtk_window_fullscreen(GTK_WINDOW(mShell));
    }
  } else {
    // Kiosk mode always use fullscreen mode.
    if (gKioskMode) {
      return NS_ERROR_NOT_AVAILABLE;
    }

    gtk_window_unfullscreen(GTK_WINDOW(mShell));

    if (mIsPIPWindow) {
      gtk_window_set_type_hint(GTK_WINDOW(mShell),
                               GDK_WINDOW_TYPE_HINT_UTILITY);
      if (gUseAspectRatio) {
        mAspectRatio = mAspectRatioSaved;
        // ApplySizeConstraints();
      }
    }
  }

  MOZ_ASSERT(mLastSizeModeBeforeFullscreen != nsSizeMode_Fullscreen);
  return NS_OK;
}

void nsWindow::SetWindowDecoration(BorderStyle aStyle) {
  LOG("nsWindow::SetWindowDecoration() Border style %x\n", int(aStyle));

  // We can't use mGdkWindow directly here as it can be
  // derived from mContainer which is not a top-level GdkWindow.
  GdkWindow* window = GetToplevelGdkWindow();

  // Sawfish, metacity, and presumably other window managers get
  // confused if we change the window decorations while the window
  // is visible.
  bool wasVisible = false;
  if (gdk_window_is_visible(window)) {
    gdk_window_hide(window);
    wasVisible = true;
  }

  gint wmd = ConvertBorderStyles(aStyle);
  if (wmd != -1) gdk_window_set_decorations(window, (GdkWMDecoration)wmd);

  if (wasVisible) gdk_window_show(window);

    // For some window managers, adding or removing window decorations
    // requires unmapping and remapping our toplevel window.  Go ahead
    // and flush the queue here so that we don't end up with a BadWindow
    // error later when this happens (when the persistence timer fires
    // and GetWindowPos is called)
#ifdef MOZ_X11
  if (GdkIsX11Display()) {
    XSync(GDK_DISPLAY_XDISPLAY(gdk_display_get_default()), X11False);
  } else
#endif /* MOZ_X11 */
  {
    gdk_flush();
  }
}

void nsWindow::HideWindowChrome(bool aShouldHide) {
  SetWindowDecoration(aShouldHide ? BorderStyle::None : mBorderStyle);
}

bool nsWindow::CheckForRollup(gdouble aMouseX, gdouble aMouseY, bool aIsWheel,
                              bool aAlwaysRollup) {
  LOG("nsWindow::CheckForRollup() aAlwaysRollup %d", aAlwaysRollup);
  nsIRollupListener* rollupListener = GetActiveRollupListener();
  nsCOMPtr<nsIWidget> rollupWidget;
  if (rollupListener) {
    rollupWidget = rollupListener->GetRollupWidget();
  }
  if (!rollupWidget) {
    return false;
  }

  auto* currentPopup =
      (GdkWindow*)rollupWidget->GetNativeData(NS_NATIVE_WINDOW);
  if (!aAlwaysRollup && is_mouse_in_window(currentPopup, aMouseX, aMouseY)) {
    return false;
  }
  bool retVal = false;
  if (aIsWheel) {
    retVal = rollupListener->ShouldConsumeOnMouseWheelEvent();
    if (!rollupListener->ShouldRollupOnMouseWheelEvent()) {
      return retVal;
    }
  }
  LayoutDeviceIntPoint point;
  nsIRollupListener::RollupOptions options{0,
                                           nsIRollupListener::FlushViews::Yes};
  // if we're dealing with menus, we probably have submenus and
  // we don't want to rollup if the click is in a parent menu of
  // the current submenu
  if (!aAlwaysRollup) {
    AutoTArray<nsIWidget*, 5> widgetChain;
    uint32_t sameTypeCount =
        rollupListener->GetSubmenuWidgetChain(&widgetChain);
    for (unsigned long i = 0; i < widgetChain.Length(); ++i) {
      nsIWidget* widget = widgetChain[i];
      auto* currWindow = (GdkWindow*)widget->GetNativeData(NS_NATIVE_WINDOW);
      if (is_mouse_in_window(currWindow, aMouseX, aMouseY)) {
        // Don't roll up if the mouse event occurred within a menu of the same
        // type.
        // If the mouse event occurred in a menu higher than that, roll up, but
        // pass the number of popups to Rollup so that only those of the same
        // type close up.
        if (i < sameTypeCount) {
          return retVal;
        }
        options.mCount = sameTypeCount;
        break;
      }
    }  // foreach parent menu widget
    if (!aIsWheel) {
      point = GdkEventCoordsToDevicePixels(aMouseX, aMouseY);
      options.mPoint = &point;
    }
  }

  if (mSizeMode == nsSizeMode_Minimized) {
    // When we try to rollup in a minimized window, transitionend events for
    // panels might not fire and thus we might not hide the popup after all,
    // see bug 1810797.
    options.mAllowAnimations = nsIRollupListener::AllowAnimations::No;
  }

  if (rollupListener->Rollup(options)) {
    retVal = true;
  }
  return retVal;
}

/* static */
bool nsWindow::DragInProgress() {
  nsCOMPtr<nsIDragService> dragService =
      do_GetService("@mozilla.org/widget/dragservice;1");
  if (!dragService) {
    return false;
  }

  nsCOMPtr<nsIDragSession> currentDragSession;
  dragService->GetCurrentSession(getter_AddRefs(currentDragSession));
  return !!currentDragSession;
}

// This is an ugly workaround for
// https://bugzilla.mozilla.org/show_bug.cgi?id=1622107
// We try to detect when Wayland compositor / gtk fails to deliver
// info about finished D&D operations and cancel it on our own.
MOZ_CAN_RUN_SCRIPT static void WaylandDragWorkaround(GdkEventButton* aEvent) {
  static int buttonPressCountWithDrag = 0;

  // We track only left button state as Firefox performs D&D on left
  // button only.
  if (aEvent->button != 1 || aEvent->type != GDK_BUTTON_PRESS) {
    return;
  }

  nsCOMPtr<nsIDragService> dragService =
      do_GetService("@mozilla.org/widget/dragservice;1");
  if (!dragService) {
    return;
  }
  nsCOMPtr<nsIDragSession> currentDragSession;
  dragService->GetCurrentSession(getter_AddRefs(currentDragSession));

  if (!currentDragSession) {
    buttonPressCountWithDrag = 0;
    return;
  }

  buttonPressCountWithDrag++;
  if (buttonPressCountWithDrag > 1) {
    NS_WARNING(
        "Quit unfinished Wayland Drag and Drop operation. Buggy Wayland "
        "compositor?");
    buttonPressCountWithDrag = 0;
    dragService->EndDragSession(false, 0);
  }
}

static nsWindow* get_window_for_gtk_widget(GtkWidget* widget) {
  gpointer user_data = g_object_get_data(G_OBJECT(widget), "nsWindow");
  return static_cast<nsWindow*>(user_data);
}

static nsWindow* get_window_for_gdk_window(GdkWindow* window) {
  gpointer user_data = g_object_get_data(G_OBJECT(window), "nsWindow");
  return static_cast<nsWindow*>(user_data);
}

static bool is_mouse_in_window(GdkWindow* aWindow, gdouble aMouseX,
                               gdouble aMouseY) {
  GdkWindow* window = aWindow;
  if (!window) {
    return false;
  }

  gint x = 0;
  gint y = 0;

  {
    gint offsetX = 0;
    gint offsetY = 0;

    while (window) {
      gint tmpX = 0;
      gint tmpY = 0;

      gdk_window_get_position(window, &tmpX, &tmpY);
      GtkWidget* widget = get_gtk_widget_for_gdk_window(window);

      // if this is a window, compute x and y given its origin and our
      // offset
      if (GTK_IS_WINDOW(widget)) {
        x = tmpX + offsetX;
        y = tmpY + offsetY;
        break;
      }

      offsetX += tmpX;
      offsetY += tmpY;
      window = gdk_window_get_parent(window);
    }
  }

  gint margin = 0;
  if (nsWindow* w = get_window_for_gdk_window(aWindow)) {
    margin = w->GetInputRegionMarginInGdkCoords();
  }

  x += margin;
  y += margin;

  gint w = gdk_window_get_width(aWindow) - margin;
  gint h = gdk_window_get_height(aWindow) - margin;

  return aMouseX > x && aMouseX < x + w && aMouseY > y && aMouseY < y + h;
}

static GtkWidget* get_gtk_widget_for_gdk_window(GdkWindow* window) {
  gpointer user_data = nullptr;
  gdk_window_get_user_data(window, &user_data);

  return GTK_WIDGET(user_data);
}

static GdkCursor* get_gtk_cursor(nsCursor aCursor) {
  GdkCursor* gdkcursor = nullptr;
  uint8_t newType = 0xff;

  if ((gdkcursor = gCursorCache[aCursor])) {
    return gdkcursor;
  }

  GdkDisplay* defaultDisplay = gdk_display_get_default();

  // The strategy here is to use standard GDK cursors, and, if not available,
  // load by standard name with gdk_cursor_new_from_name.
  // Spec is here: http://www.freedesktop.org/wiki/Specifications/cursor-spec/
  switch (aCursor) {
    case eCursor_standard:
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_LEFT_PTR);
      break;
    case eCursor_wait:
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_WATCH);
      break;
    case eCursor_select:
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_XTERM);
      break;
    case eCursor_hyperlink:
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_HAND2);
      break;
    case eCursor_n_resize:
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_TOP_SIDE);
      break;
    case eCursor_s_resize:
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_BOTTOM_SIDE);
      break;
    case eCursor_w_resize:
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_LEFT_SIDE);
      break;
    case eCursor_e_resize:
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_RIGHT_SIDE);
      break;
    case eCursor_nw_resize:
      gdkcursor =
          gdk_cursor_new_for_display(defaultDisplay, GDK_TOP_LEFT_CORNER);
      break;
    case eCursor_se_resize:
      gdkcursor =
          gdk_cursor_new_for_display(defaultDisplay, GDK_BOTTOM_RIGHT_CORNER);
      break;
    case eCursor_ne_resize:
      gdkcursor =
          gdk_cursor_new_for_display(defaultDisplay, GDK_TOP_RIGHT_CORNER);
      break;
    case eCursor_sw_resize:
      gdkcursor =
          gdk_cursor_new_for_display(defaultDisplay, GDK_BOTTOM_LEFT_CORNER);
      break;
    case eCursor_crosshair:
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_CROSSHAIR);
      break;
    case eCursor_move:
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_FLEUR);
      break;
    case eCursor_help:
      gdkcursor =
          gdk_cursor_new_for_display(defaultDisplay, GDK_QUESTION_ARROW);
      break;
    case eCursor_copy:  // CSS3
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "copy");
      if (!gdkcursor) newType = MOZ_CURSOR_COPY;
      break;
    case eCursor_alias:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "alias");
      if (!gdkcursor) newType = MOZ_CURSOR_ALIAS;
      break;
    case eCursor_context_menu:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "context-menu");
      if (!gdkcursor) newType = MOZ_CURSOR_CONTEXT_MENU;
      break;
    case eCursor_cell:
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_PLUS);
      break;
    // Those two aren’t standardized. Trying both KDE’s and GNOME’s names
    case eCursor_grab:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "openhand");
      if (!gdkcursor) newType = MOZ_CURSOR_HAND_GRAB;
      break;
    case eCursor_grabbing:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "closedhand");
      if (!gdkcursor) {
        gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "grabbing");
      }
      if (!gdkcursor) newType = MOZ_CURSOR_HAND_GRABBING;
      break;
    case eCursor_spinning:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "progress");
      if (!gdkcursor) newType = MOZ_CURSOR_SPINNING;
      break;
    case eCursor_zoom_in:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "zoom-in");
      if (!gdkcursor) newType = MOZ_CURSOR_ZOOM_IN;
      break;
    case eCursor_zoom_out:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "zoom-out");
      if (!gdkcursor) newType = MOZ_CURSOR_ZOOM_OUT;
      break;
    case eCursor_not_allowed:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "not-allowed");
      if (!gdkcursor) {  // nonstandard, yet common
        gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "crossed_circle");
      }
      if (!gdkcursor) newType = MOZ_CURSOR_NOT_ALLOWED;
      break;
    case eCursor_no_drop:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "no-drop");
      if (!gdkcursor) {  // this nonstandard sequence makes it work on KDE and
                         // GNOME
        gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "forbidden");
      }
      if (!gdkcursor) {
        gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "circle");
      }
      if (!gdkcursor) newType = MOZ_CURSOR_NOT_ALLOWED;
      break;
    case eCursor_vertical_text:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "vertical-text");
      if (!gdkcursor) {
        newType = MOZ_CURSOR_VERTICAL_TEXT;
      }
      break;
    case eCursor_all_scroll:
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_FLEUR);
      break;
    case eCursor_nesw_resize:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "size_bdiag");
      if (!gdkcursor) newType = MOZ_CURSOR_NESW_RESIZE;
      break;
    case eCursor_nwse_resize:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "size_fdiag");
      if (!gdkcursor) newType = MOZ_CURSOR_NWSE_RESIZE;
      break;
    case eCursor_ns_resize:
      gdkcursor =
          gdk_cursor_new_for_display(defaultDisplay, GDK_SB_V_DOUBLE_ARROW);
      break;
    case eCursor_ew_resize:
      gdkcursor =
          gdk_cursor_new_for_display(defaultDisplay, GDK_SB_H_DOUBLE_ARROW);
      break;
    // Here, two better fitting cursors exist in some cursor themes. Try those
    // first
    case eCursor_row_resize:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "split_v");
      if (!gdkcursor) {
        gdkcursor =
            gdk_cursor_new_for_display(defaultDisplay, GDK_SB_V_DOUBLE_ARROW);
      }
      break;
    case eCursor_col_resize:
      gdkcursor = gdk_cursor_new_from_name(defaultDisplay, "split_h");
      if (!gdkcursor) {
        gdkcursor =
            gdk_cursor_new_for_display(defaultDisplay, GDK_SB_H_DOUBLE_ARROW);
      }
      break;
    case eCursor_none:
      newType = MOZ_CURSOR_NONE;
      break;
    default:
      NS_ASSERTION(aCursor, "Invalid cursor type");
      gdkcursor = gdk_cursor_new_for_display(defaultDisplay, GDK_LEFT_PTR);
      break;
  }

  // If by now we don't have a xcursor, this means we have to make a custom
  // one. First, we try creating a named cursor based on the hash of our
  // custom bitmap, as libXcursor has some magic to convert bitmapped cursors
  // to themed cursors
  if (newType != 0xFF && GtkCursors[newType].hash) {
    gdkcursor =
        gdk_cursor_new_from_name(defaultDisplay, GtkCursors[newType].hash);
  }

  // If we still don't have a xcursor, we now really create a bitmap cursor
  if (newType != 0xff && !gdkcursor) {
    GdkPixbuf* cursor_pixbuf =
        gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, 32, 32);
    if (!cursor_pixbuf) return nullptr;

    guchar* data = gdk_pixbuf_get_pixels(cursor_pixbuf);

    // Read data from GtkCursors and compose RGBA surface from 1bit bitmap and
    // mask GtkCursors bits and mask are 32x32 monochrome bitmaps (1 bit for
    // each pixel) so it's 128 byte array (4 bytes for are one bitmap row and
    // there are 32 rows here).
    const unsigned char* bits = GtkCursors[newType].bits;
    const unsigned char* mask_bits = GtkCursors[newType].mask_bits;

    for (int i = 0; i < 128; i++) {
      char bit = (char)*bits++;
      char mask = (char)*mask_bits++;
      for (int j = 0; j < 8; j++) {
        unsigned char pix = ~(((bit >> j) & 0x01) * 0xff);
        *data++ = pix;
        *data++ = pix;
        *data++ = pix;
        *data++ = (((mask >> j) & 0x01) * 0xff);
      }
    }

    gdkcursor = gdk_cursor_new_from_pixbuf(
        gdk_display_get_default(), cursor_pixbuf, GtkCursors[newType].hot_x,
        GtkCursors[newType].hot_y);

    g_object_unref(cursor_pixbuf);
  }

  gCursorCache[aCursor] = gdkcursor;

  return gdkcursor;
}

// gtk callbacks

void draw_window_of_widget(GtkWidget* widget, GdkWindow* aWindow, cairo_t* cr) {
  if (gtk_cairo_should_draw_window(cr, aWindow)) {
    RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
    if (!window) {
      NS_WARNING("Cannot get nsWindow from GtkWidget");
    } else {
      cairo_save(cr);
      gtk_cairo_transform_to_window(cr, widget, aWindow);
      // TODO - window->OnExposeEvent() can destroy this or other windows,
      // do we need to handle it somehow?
      window->OnExposeEvent(cr);
      cairo_restore(cr);
    }
  }
}

/* static */
gboolean expose_event_cb(GtkWidget* widget, cairo_t* cr) {
  draw_window_of_widget(widget, gtk_widget_get_window(widget), cr);

  // A strong reference is already held during "draw" signal emission,
  // but GTK+ 3.4 wants the object to live a little longer than that
  // (bug 1225970).
  g_object_ref(widget);
  g_idle_add(
      [](gpointer data) -> gboolean {
        g_object_unref(data);
        return G_SOURCE_REMOVE;
      },
      widget);

  return FALSE;
}

static gboolean configure_event_cb(GtkWidget* widget,
                                   GdkEventConfigure* event) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
  if (!window) {
    return FALSE;
  }

  return window->OnConfigureEvent(widget, event);
}

// Some Gtk widget code may call gtk_widget_unrealize() which destroys
// mGdkWindow. We need to listen on this signal and re-create
// mGdkWindow when we're already mapped.
static void widget_map_cb(GtkWidget* widget) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
  if (!window) {
    return;
  }
  window->OnMap();
}

static void widget_unmap_cb(GtkWidget* widget) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
  if (!window) {
    return;
  }
  window->OnUnmap();
}

static void widget_unrealize_cb(GtkWidget* widget) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
  if (!window) {
    return;
  }
  window->OnUnrealize();
}

static void size_allocate_cb(GtkWidget* widget, GtkAllocation* allocation) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
  if (!window) {
    return;
  }

  window->OnSizeAllocate(allocation);
}

static void toplevel_window_size_allocate_cb(GtkWidget* widget,
                                             GtkAllocation* allocation) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
  if (!window) {
    return;
  }

  window->UpdateTopLevelOpaqueRegion();
}

static gboolean delete_event_cb(GtkWidget* widget, GdkEventAny* event) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
  if (!window) {
    return FALSE;
  }

  window->OnDeleteEvent();

  return TRUE;
}

static gboolean enter_notify_event_cb(GtkWidget* widget,
                                      GdkEventCrossing* event) {
  RefPtr<nsWindow> window = get_window_for_gdk_window(event->window);
  if (!window) {
    return TRUE;
  }

  // We have stored leave notify - check if it's the correct one and
  // fire it before enter notify in such case.
  if (sStoredLeaveNotifyEvent) {
    auto clearNofityEvent =
        MakeScopeExit([&] { sStoredLeaveNotifyEvent = nullptr; });
    if (event->x_root == sStoredLeaveNotifyEvent->x_root &&
        event->y_root == sStoredLeaveNotifyEvent->y_root &&
        window->ApplyEnterLeaveMutterWorkaround()) {
      // Enter/Leave notify events has the same coordinates
      // and uses know buggy window config.
      // Consider it as a bogus one.
      return TRUE;
    }
    RefPtr<nsWindow> leftWindow =
        get_window_for_gdk_window(sStoredLeaveNotifyEvent->window);
    if (leftWindow) {
      leftWindow->OnLeaveNotifyEvent(sStoredLeaveNotifyEvent.get());
    }
  }

  window->OnEnterNotifyEvent(event);
  return TRUE;
}

static gboolean leave_notify_event_cb(GtkWidget* widget,
                                      GdkEventCrossing* event) {
  RefPtr<nsWindow> window = get_window_for_gdk_window(event->window);
  if (!window) {
    return TRUE;
  }

  if (window->ApplyEnterLeaveMutterWorkaround()) {
    // The leave event is potentially wrong, don't fire it now but store
    // it for further check at enter_notify_event_cb().
    sStoredLeaveNotifyEvent.reset(reinterpret_cast<GdkEventCrossing*>(
        gdk_event_copy(reinterpret_cast<GdkEvent*>(event))));
  } else {
    sStoredLeaveNotifyEvent = nullptr;
    window->OnLeaveNotifyEvent(event);
  }

  return TRUE;
}

static nsWindow* GetFirstNSWindowForGDKWindow(GdkWindow* aGdkWindow) {
  nsWindow* window;
  while (!(window = get_window_for_gdk_window(aGdkWindow))) {
    // The event has bubbled to the moz_container widget as passed into each
    // caller's *widget parameter, but its corresponding nsWindow is an ancestor
    // of the window that we need.  Instead, look at event->window and find the
    // first ancestor nsWindow of it because event->window may be in a plugin.
    aGdkWindow = gdk_window_get_parent(aGdkWindow);
    if (!aGdkWindow) {
      window = nullptr;
      break;
    }
  }
  return window;
}

static gboolean motion_notify_event_cb(GtkWidget* widget,
                                       GdkEventMotion* event) {
  UpdateLastInputEventTime(event);

  RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(event->window);
  if (!window) return FALSE;

  window->OnMotionNotifyEvent(event);

  return TRUE;
}

static gboolean button_press_event_cb(GtkWidget* widget,
                                      GdkEventButton* event) {
  UpdateLastInputEventTime(event);

  RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(event->window);
  if (!window) return FALSE;

  window->OnButtonPressEvent(event);

  if (GdkIsWaylandDisplay()) {
    WaylandDragWorkaround(event);
  }

  return TRUE;
}

static gboolean button_release_event_cb(GtkWidget* widget,
                                        GdkEventButton* event) {
  UpdateLastInputEventTime(event);

  RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(event->window);
  if (!window) return FALSE;

  window->OnButtonReleaseEvent(event);

  return TRUE;
}

static gboolean focus_in_event_cb(GtkWidget* widget, GdkEventFocus* event) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
  if (!window) return FALSE;

  window->OnContainerFocusInEvent(event);

  return FALSE;
}

static gboolean focus_out_event_cb(GtkWidget* widget, GdkEventFocus* event) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
  if (!window) return FALSE;

  window->OnContainerFocusOutEvent(event);

  return FALSE;
}

#ifdef MOZ_X11
// For long-lived popup windows that don't really take focus themselves but
// may have elements that accept keyboard input when the parent window is
// active, focus is handled specially.  These windows include noautohide
// panels.  (This special handling is not necessary for temporary popups where
// the keyboard is grabbed.)
//
// Mousing over or clicking on these windows should not cause them to steal
// focus from their parent windows, so, the input field of WM_HINTS is set to
// False to request that the window manager not set the input focus to this
// window.  http://tronche.com/gui/x/icccm/sec-4.html#s-4.1.7
//
// However, these windows can still receive WM_TAKE_FOCUS messages from the
// window manager, so they can still detect when the user has indicated that
// they wish to direct keyboard input at these windows.  When the window
// manager offers focus to these windows (after a mouse over or click, for
// example), a request to make the parent window active is issued.  When the
// parent window becomes active, keyboard events will be received.

static GdkFilterReturn popup_take_focus_filter(GdkXEvent* gdk_xevent,
                                               GdkEvent* event, gpointer data) {
  auto* xevent = static_cast<XEvent*>(gdk_xevent);
  if (xevent->type != ClientMessage) return GDK_FILTER_CONTINUE;

  XClientMessageEvent& xclient = xevent->xclient;
  if (xclient.message_type != gdk_x11_get_xatom_by_name("WM_PROTOCOLS")) {
    return GDK_FILTER_CONTINUE;
  }

  Atom atom = xclient.data.l[0];
  if (atom != gdk_x11_get_xatom_by_name("WM_TAKE_FOCUS")) {
    return GDK_FILTER_CONTINUE;
  }

  guint32 timestamp = xclient.data.l[1];

  GtkWidget* widget = get_gtk_widget_for_gdk_window(event->any.window);
  if (!widget) return GDK_FILTER_CONTINUE;

  GtkWindow* parent = gtk_window_get_transient_for(GTK_WINDOW(widget));
  if (!parent) return GDK_FILTER_CONTINUE;

  if (gtk_window_is_active(parent)) {
    return GDK_FILTER_REMOVE;  // leave input focus on the parent
  }

  GdkWindow* parent_window = gtk_widget_get_window(GTK_WIDGET(parent));
  if (!parent_window) return GDK_FILTER_CONTINUE;

  // In case the parent has not been deconified.
  gdk_window_show_unraised(parent_window);

  // Request focus on the parent window.
  // Use gdk_window_focus rather than gtk_window_present to avoid
  // raising the parent window.
  gdk_window_focus(parent_window, timestamp);
  return GDK_FILTER_REMOVE;
}
#endif /* MOZ_X11 */

static gboolean key_press_event_cb(GtkWidget* widget, GdkEventKey* event) {
  LOGW("key_press_event_cb\n");

  UpdateLastInputEventTime(event);

  // find the window with focus and dispatch this event to that widget
  nsWindow* window = get_window_for_gtk_widget(widget);
  if (!window) return FALSE;

  RefPtr<nsWindow> focusWindow = gFocusWindow ? gFocusWindow : window;

#ifdef MOZ_X11
  // Keyboard repeat can cause key press events to queue up when there are
  // slow event handlers (bug 301029).  Throttle these events by removing
  // consecutive pending duplicate KeyPress events to the same window.
  // We use the event time of the last one.
  // Note: GDK calls XkbSetDetectableAutorepeat so that KeyRelease events
  // are generated only when the key is physically released.
#  define NS_GDKEVENT_MATCH_MASK 0x1FFF  // GDK_SHIFT_MASK .. GDK_BUTTON5_MASK
  // Our headers undefine X11 KeyPress - let's redefine it here.
#  ifndef KeyPress
#    define KeyPress 2
#  endif
  GdkDisplay* gdkDisplay = gtk_widget_get_display(widget);
  if (GdkIsX11Display(gdkDisplay)) {
    Display* dpy = GDK_DISPLAY_XDISPLAY(gdkDisplay);
    while (XPending(dpy)) {
      XEvent next_event;
      XPeekEvent(dpy, &next_event);
      GdkWindow* nextGdkWindow =
          gdk_x11_window_lookup_for_display(gdkDisplay, next_event.xany.window);
      if (nextGdkWindow != event->window || next_event.type != KeyPress ||
          next_event.xkey.keycode != event->hardware_keycode ||
          next_event.xkey.state != (event->state & NS_GDKEVENT_MATCH_MASK)) {
        break;
      }
      XNextEvent(dpy, &next_event);
      event->time = next_event.xkey.time;
    }
  }
#endif

  return focusWindow->OnKeyPressEvent(event);
}

static gboolean key_release_event_cb(GtkWidget* widget, GdkEventKey* event) {
  LOGW("key_release_event_cb\n");

  UpdateLastInputEventTime(event);

  // find the window with focus and dispatch this event to that widget
  nsWindow* window = get_window_for_gtk_widget(widget);
  if (!window) return FALSE;

  RefPtr<nsWindow> focusWindow = gFocusWindow ? gFocusWindow : window;
  return focusWindow->OnKeyReleaseEvent(event);
}

static gboolean property_notify_event_cb(GtkWidget* aWidget,
                                         GdkEventProperty* aEvent) {
  RefPtr<nsWindow> window = get_window_for_gdk_window(aEvent->window);
  if (!window) return FALSE;

  return window->OnPropertyNotifyEvent(aWidget, aEvent);
}

static gboolean scroll_event_cb(GtkWidget* widget, GdkEventScroll* event) {
  RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(event->window);
  if (!window) return FALSE;

  window->OnScrollEvent(event);

  return TRUE;
}

static void hierarchy_changed_cb(GtkWidget* widget,
                                 GtkWidget* previous_toplevel) {
  GtkWidget* toplevel = gtk_widget_get_toplevel(widget);
  GdkWindowState old_window_state = GDK_WINDOW_STATE_WITHDRAWN;
  GdkEventWindowState event;

  event.new_window_state = GDK_WINDOW_STATE_WITHDRAWN;

  if (GTK_IS_WINDOW(previous_toplevel)) {
    g_signal_handlers_disconnect_by_func(
        previous_toplevel, FuncToGpointer(window_state_event_cb), widget);
    GdkWindow* win = gtk_widget_get_window(previous_toplevel);
    if (win) {
      old_window_state = gdk_window_get_state(win);
    }
  }

  if (GTK_IS_WINDOW(toplevel)) {
    g_signal_connect_swapped(toplevel, "window-state-event",
                             G_CALLBACK(window_state_event_cb), widget);
    GdkWindow* win = gtk_widget_get_window(toplevel);
    if (win) {
      event.new_window_state = gdk_window_get_state(win);
    }
  }

  event.changed_mask =
      static_cast<GdkWindowState>(old_window_state ^ event.new_window_state);

  if (event.changed_mask) {
    event.type = GDK_WINDOW_STATE;
    event.window = nullptr;
    event.send_event = TRUE;
    window_state_event_cb(widget, &event);
  }
}

static gboolean window_state_event_cb(GtkWidget* widget,
                                      GdkEventWindowState* event) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
  if (!window) return FALSE;

  window->OnWindowStateEvent(widget, event);

  return FALSE;
}

static void settings_xft_dpi_changed_cb(GtkSettings* gtk_settings,
                                        GParamSpec* pspec, nsWindow* data) {
  RefPtr<nsWindow> window = data;
  window->OnDPIChanged();
  // Even though the window size in screen pixels has not changed,
  // nsViewManager stores the dimensions in app units.
  // DispatchResized() updates those.
  window->DispatchResized();
}

static void check_resize_cb(GtkContainer* container, gpointer user_data) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(GTK_WIDGET(container));
  if (!window) {
    return;
  }
  window->OnCheckResize();
}

static void screen_composited_changed_cb(GdkScreen* screen,
                                         gpointer user_data) {
  // This callback can run before gfxPlatform::Init() in rare
  // cases involving the profile manager. When this happens,
  // we have no reason to reset any compositors as graphics
  // hasn't been initialized yet.
  if (GPUProcessManager::Get()) {
    GPUProcessManager::Get()->ResetCompositors();
  }
}

static void widget_composited_changed_cb(GtkWidget* widget,
                                         gpointer user_data) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
  if (!window) {
    return;
  }
  window->OnCompositedChanged();
}

static void scale_changed_cb(GtkWidget* widget, GParamSpec* aPSpec,
                             gpointer aPointer) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(widget);
  if (!window) {
    return;
  }

  window->OnScaleChanged(/* aNotify = */ true);
}

static gboolean touch_event_cb(GtkWidget* aWidget, GdkEventTouch* aEvent) {
  UpdateLastInputEventTime(aEvent);

  RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(aEvent->window);
  if (!window) {
    return FALSE;
  }

  return window->OnTouchEvent(aEvent);
}

// This function called generic because there is no signal specific to touchpad
// pinch events.
static gboolean generic_event_cb(GtkWidget* widget, GdkEvent* aEvent) {
  if (aEvent->type != GDK_TOUCHPAD_PINCH) {
    return FALSE;
  }
  // Using reinterpret_cast because the touchpad_pinch field of GdkEvent is not
  // available in GTK+ versions lower than v3.18
  GdkEventTouchpadPinch* event =
      reinterpret_cast<GdkEventTouchpadPinch*>(aEvent);

  RefPtr<nsWindow> window = GetFirstNSWindowForGDKWindow(event->window);

  if (!window) {
    return FALSE;
  }
  return window->OnTouchpadPinchEvent(event);
}

//////////////////////////////////////////////////////////////////////
// These are all of our drag and drop operations

void nsWindow::InitDragEvent(WidgetDragEvent& aEvent) {
  // set the keyboard modifiers
  guint modifierState = KeymapWrapper::GetCurrentModifierState();
  KeymapWrapper::InitInputEvent(aEvent, modifierState);
}

static LayoutDeviceIntPoint GetWindowDropPosition(nsWindow* aWindow, int aX,
                                                  int aY) {
  // Workaround for Bug 1710344
  // Caused by Gtk issue https://gitlab.gnome.org/GNOME/gtk/-/issues/4437
  if (aWindow->IsWaylandPopup()) {
    int tx = 0, ty = 0;
    gdk_window_get_position(aWindow->GetToplevelGdkWindow(), &tx, &ty);
    aX += tx;
    aY += ty;
  }
  LOGDRAG("WindowDropPosition [%d, %d]", aX, aY);
  return aWindow->GdkPointToDevicePixels({aX, aY});
}

gboolean WindowDragMotionHandler(GtkWidget* aWidget,
                                 GdkDragContext* aDragContext, gint aX, gint aY,
                                 guint aTime) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget);
  if (!window) {
    return FALSE;
  }

  // figure out which internal widget this drag motion actually happened on
  nscoord retx = 0;
  nscoord rety = 0;

  GdkWindow* innerWindow = get_inner_gdk_window(gtk_widget_get_window(aWidget),
                                                aX, aY, &retx, &rety);
  RefPtr<nsWindow> innerMostWindow = get_window_for_gdk_window(innerWindow);
  if (!innerMostWindow) {
    innerMostWindow = window;
  }
  LOGDRAG("WindowDragMotionHandler target nsWindow [%p]",
          innerMostWindow.get());

  RefPtr<nsDragService> dragService = nsDragService::GetInstance();
  nsDragService::AutoEventLoop loop(dragService);
  if (!dragService->ScheduleMotionEvent(
          innerMostWindow, aDragContext,
          GetWindowDropPosition(innerMostWindow, retx, rety), aTime)) {
    return FALSE;
  }
  return TRUE;
}

static gboolean drag_motion_event_cb(GtkWidget* aWidget,
                                     GdkDragContext* aDragContext, gint aX,
                                     gint aY, guint aTime, gpointer aData) {
  return WindowDragMotionHandler(aWidget, aDragContext, aX, aY, aTime);
}

void WindowDragLeaveHandler(GtkWidget* aWidget) {
  LOGDRAG("WindowDragLeaveHandler()\n");

  RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget);
  if (!window) {
    LOGDRAG("    Failed - can't find nsWindow!\n");
    return;
  }

  RefPtr<nsDragService> dragService = nsDragService::GetInstance();
  nsDragService::AutoEventLoop loop(dragService);

  nsWindow* mostRecentDragWindow = dragService->GetMostRecentDestWindow();
  if (!mostRecentDragWindow) {
    // This can happen when the target will not accept a drop.  A GTK drag
    // source sends the leave message to the destination before the
    // drag-failed signal on the source widget, but the leave message goes
    // via the X server, and so doesn't get processed at least until the
    // event loop runs again.
    LOGDRAG("    Failed - GetMostRecentDestWindow()!\n");
    return;
  }

  if (aWidget != window->GetGtkWidget()) {
    // When the drag moves between widgets, GTK can send leave signal for
    // the old widget after the motion or drop signal for the new widget.
    // We'll send the leave event when the motion or drop event is run.
    LOGDRAG("    Failed - GtkWidget mismatch!\n");
    return;
  }

  LOGDRAG("WindowDragLeaveHandler nsWindow %p\n", (void*)mostRecentDragWindow);
  dragService->ScheduleLeaveEvent();
}

static void drag_leave_event_cb(GtkWidget* aWidget,
                                GdkDragContext* aDragContext, guint aTime,
                                gpointer aData) {
  WindowDragLeaveHandler(aWidget);
}

gboolean WindowDragDropHandler(GtkWidget* aWidget, GdkDragContext* aDragContext,
                               gint aX, gint aY, guint aTime) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget);
  if (!window) return FALSE;

  // figure out which internal widget this drag motion actually happened on
  nscoord retx = 0;
  nscoord rety = 0;

  GdkWindow* innerWindow = get_inner_gdk_window(gtk_widget_get_window(aWidget),
                                                aX, aY, &retx, &rety);
  RefPtr<nsWindow> innerMostWindow = get_window_for_gdk_window(innerWindow);

  if (!innerMostWindow) {
    innerMostWindow = window;
  }

  LOGDRAG("WindowDragDropHandler nsWindow [%p]", innerMostWindow.get());
  RefPtr<nsDragService> dragService = nsDragService::GetInstance();
  nsDragService::AutoEventLoop loop(dragService);
  return dragService->ScheduleDropEvent(
      innerMostWindow, aDragContext,
      GetWindowDropPosition(innerMostWindow, retx, rety), aTime);
}

static gboolean drag_drop_event_cb(GtkWidget* aWidget,
                                   GdkDragContext* aDragContext, gint aX,
                                   gint aY, guint aTime, gpointer aData) {
  return WindowDragDropHandler(aWidget, aDragContext, aX, aY, aTime);
}

static void drag_data_received_event_cb(GtkWidget* aWidget,
                                        GdkDragContext* aDragContext, gint aX,
                                        gint aY,
                                        GtkSelectionData* aSelectionData,
                                        guint aInfo, guint aTime,
                                        gpointer aData) {
  RefPtr<nsWindow> window = get_window_for_gtk_widget(aWidget);
  if (!window) return;

  window->OnDragDataReceivedEvent(aWidget, aDragContext, aX, aY, aSelectionData,
                                  aInfo, aTime, aData);
}

static nsresult initialize_prefs(void) {
  if (Preferences::HasUserValue("widget.use-aspect-ratio")) {
    gUseAspectRatio = Preferences::GetBool("widget.use-aspect-ratio", true);
  } else {
    gUseAspectRatio = IsGnomeDesktopEnvironment() || IsKdeDesktopEnvironment();
  }
  return NS_OK;
}

// TODO: Can we simplify it for mShell/mContainer only scenario?
static GdkWindow* get_inner_gdk_window(GdkWindow* aWindow, gint x, gint y,
                                       gint* retx, gint* rety) {
  gint cx, cy, cw, ch;
  GList* children = gdk_window_peek_children(aWindow);
  for (GList* child = g_list_last(children); child;
       child = g_list_previous(child)) {
    auto* childWindow = (GdkWindow*)child->data;
    if (get_window_for_gdk_window(childWindow)) {
      gdk_window_get_geometry(childWindow, &cx, &cy, &cw, &ch);
      if ((cx < x) && (x < (cx + cw)) && (cy < y) && (y < (cy + ch)) &&
          gdk_window_is_visible(childWindow)) {
        return get_inner_gdk_window(childWindow, x - cx, y - cy, retx, rety);
      }
    }
  }
  *retx = x;
  *rety = y;
  return aWindow;
}

#ifdef ACCESSIBILITY
void nsWindow::CreateRootAccessible() {
  if (!mRootAccessible) {
    LOG("nsWindow:: Create Toplevel Accessibility\n");
    mRootAccessible = GetRootAccessible();
  }
}

void nsWindow::DispatchEventToRootAccessible(uint32_t aEventType) {
  if (!a11y::ShouldA11yBeEnabled()) {
    return;
  }

  nsAccessibilityService* accService = GetOrCreateAccService();
  if (!accService) {
    return;
  }

  // Get the root document accessible and fire event to it.
  a11y::LocalAccessible* acc = GetRootAccessible();
  if (acc) {
    accService->FireAccessibleEvent(aEventType, acc);
  }
}

void nsWindow::DispatchActivateEventAccessible(void) {
  DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_ACTIVATE);
}

void nsWindow::DispatchDeactivateEventAccessible(void) {
  DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_DEACTIVATE);
}

void nsWindow::DispatchMaximizeEventAccessible(void) {
  DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_MAXIMIZE);
}

void nsWindow::DispatchMinimizeEventAccessible(void) {
  DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_MINIMIZE);
}

void nsWindow::DispatchRestoreEventAccessible(void) {
  DispatchEventToRootAccessible(nsIAccessibleEvent::EVENT_WINDOW_RESTORE);
}

#endif /* #ifdef ACCESSIBILITY */

void nsWindow::SetInputContext(const InputContext& aContext,
                               const InputContextAction& aAction) {
  if (!mIMContext) {
    return;
  }
  mIMContext->SetInputContext(this, &aContext, &aAction);
}

InputContext nsWindow::GetInputContext() {
  InputContext context;
  if (!mIMContext) {
    context.mIMEState.mEnabled = IMEEnabled::Disabled;
    context.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
  } else {
    context = mIMContext->GetInputContext();
  }
  return context;
}

TextEventDispatcherListener* nsWindow::GetNativeTextEventDispatcherListener() {
  if (NS_WARN_IF(!mIMContext)) {
    return nullptr;
  }
  return mIMContext;
}

bool nsWindow::GetEditCommands(NativeKeyBindingsType aType,
                               const WidgetKeyboardEvent& aEvent,
                               nsTArray<CommandInt>& aCommands) {
  // Validate the arguments.
  if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
    return false;
  }

  Maybe<WritingMode> writingMode;
  if (aEvent.NeedsToRemapNavigationKey()) {
    if (RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher()) {
      writingMode = dispatcher->MaybeQueryWritingModeAtSelection();
    }
  }

  NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
  keyBindings->GetEditCommands(aEvent, writingMode, aCommands);
  return true;
}

already_AddRefed<DrawTarget> nsWindow::StartRemoteDrawingInRegion(
    const LayoutDeviceIntRegion& aInvalidRegion, BufferMode* aBufferMode) {
  return mSurfaceProvider.StartRemoteDrawingInRegion(aInvalidRegion,
                                                     aBufferMode);
}

void nsWindow::EndRemoteDrawingInRegion(
    DrawTarget* aDrawTarget, const LayoutDeviceIntRegion& aInvalidRegion) {
  mSurfaceProvider.EndRemoteDrawingInRegion(aDrawTarget, aInvalidRegion);
}

bool nsWindow::GetDragInfo(WidgetMouseEvent* aMouseEvent, GdkWindow** aWindow,
                           gint* aButton, gint* aRootX, gint* aRootY) {
  if (aMouseEvent->mButton != MouseButton::ePrimary) {
    // we can only begin a move drag with the left mouse button
    return false;
  }
  *aButton = 1;

  // get the gdk window for this widget
  GdkWindow* gdk_window = mGdkWindow;
  if (!gdk_window) {
    return false;
  }
#ifdef DEBUG
  // GDK_IS_WINDOW(...) expands to a statement-expression, and
  // statement-expressions are not allowed in template-argument lists. So we
  // have to make the MOZ_ASSERT condition indirect.
  if (!GDK_IS_WINDOW(gdk_window)) {
    MOZ_ASSERT(false, "must really be window");
  }
#endif

  // find the top-level window
  gdk_window = gdk_window_get_toplevel(gdk_window);
  MOZ_ASSERT(gdk_window, "gdk_window_get_toplevel should not return null");
  *aWindow = gdk_window;

  if (!aMouseEvent->mWidget) {
    return false;
  }

#ifdef MOZ_X11
  if (GdkIsX11Display()) {
    // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=789054
    // To avoid crashes disable double-click on WM without _NET_WM_MOVERESIZE.
    // See _should_perform_ewmh_drag() at gdkwindow-x11.c
    // XXXsmaug remove this old hack. gtk should be fixed now.
    GdkScreen* screen = gdk_window_get_screen(gdk_window);
    GdkAtom atom = gdk_atom_intern("_NET_WM_MOVERESIZE", FALSE);
    if (!gdk_x11_screen_supports_net_wm_hint(screen, atom)) {
      static TimeStamp lastTimeStamp;
      if (lastTimeStamp != aMouseEvent->mTimeStamp) {
        lastTimeStamp = aMouseEvent->mTimeStamp;
      } else {
        return false;
      }
    }
  }
#endif

  // FIXME: It would be nice to have the widget position at the time
  // of the event, but it's relatively unlikely that the widget has
  // moved since the mousedown.  (On the other hand, it's quite likely
  // that the mouse has moved, which is why we use the mouse position
  // from the event.)
  LayoutDeviceIntPoint offset = aMouseEvent->mWidget->WidgetToScreenOffset();
  *aRootX = aMouseEvent->mRefPoint.x + offset.x;
  *aRootY = aMouseEvent->mRefPoint.y + offset.y;

  return true;
}

nsIWidget::WindowRenderer* nsWindow::GetWindowRenderer() {
  if (mIsDestroyed) {
    // Prevent external code from triggering the re-creation of the
    // LayerManager/Compositor during shutdown. Just return what we currently
    // have, which is most likely null.
    return mWindowRenderer;
  }

  return nsBaseWidget::GetWindowRenderer();
}

void nsWindow::DidGetNonBlankPaint() {
  if (mGotNonBlankPaint) {
    return;
  }
  mGotNonBlankPaint = true;
  if (!mConfiguredClearColor) {
    // Nothing to do, we hadn't overridden the clear color.
    mConfiguredClearColor = true;
    return;
  }
  // Reset the clear color set in the expose event to transparent.
  GetWindowRenderer()->AsWebRender()->WrBridge()->SendSetDefaultClearColor(
      NS_TRANSPARENT);
}

/* nsWindow::SetCompositorWidgetDelegate() sets remote GtkCompositorWidget
 * to render into with compositor.
 *
 * SetCompositorWidgetDelegate(delegate) is called from
 * nsBaseWidget::CreateCompositor(), i.e. nsWindow::GetWindowRenderer().
 *
 * SetCompositorWidgetDelegate(null) is called from
 * nsBaseWidget::DestroyCompositor().
 */
void nsWindow::SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) {
  LOG("nsWindow::SetCompositorWidgetDelegate %p mIsMapped %d "
      "mCompositorWidgetDelegate %p\n",
      delegate, mIsMapped, mCompositorWidgetDelegate);

  MOZ_RELEASE_ASSERT(NS_IsMainThread());
  if (delegate) {
    mCompositorWidgetDelegate = delegate->AsPlatformSpecificDelegate();
    MOZ_ASSERT(mCompositorWidgetDelegate,
               "nsWindow::SetCompositorWidgetDelegate called with a "
               "non-PlatformCompositorWidgetDelegate");
    if (mIsMapped) {
      ConfigureCompositor();
    }
  } else {
    mCompositorWidgetDelegate = nullptr;
  }
}

nsresult nsWindow::SetNonClientMargins(const LayoutDeviceIntMargin& aMargins) {
  SetDrawsInTitlebar(aMargins.top == 0);
  return NS_OK;
}

bool nsWindow::IsAlwaysUndecoratedWindow() const {
  if (mIsPIPWindow || mIsWaylandPanelWindow || gKioskMode) {
    return true;
  }
  if (mWindowType == WindowType::Dialog &&
      mBorderStyle != BorderStyle::Default &&
      mBorderStyle != BorderStyle::All &&
      !(mBorderStyle & BorderStyle::Title) &&
      !(mBorderStyle & BorderStyle::ResizeH)) {
    return true;
  }
  return false;
}

void nsWindow::SetDrawsInTitlebar(bool aState) {
  LOG("nsWindow::SetDrawsInTitlebar() State %d mGtkWindowDecoration %d\n",
      aState, (int)mGtkWindowDecoration);

  if (mGtkWindowDecoration == GTK_DECORATION_NONE ||
      aState == mDrawInTitlebar) {
    LOG("  already set, quit");
    return;
  }

  if (IsAlwaysUndecoratedWindow()) {
    MOZ_ASSERT(aState, "Unexpected decoration request");
    MOZ_ASSERT(!gtk_window_get_decorated(GTK_WINDOW(mShell)));
    return;
  }

  mDrawInTitlebar = aState;

  if (mGtkWindowDecoration == GTK_DECORATION_SYSTEM) {
    SetWindowDecoration(aState ? BorderStyle::Border : mBorderStyle);
  } else if (mGtkWindowDecoration == GTK_DECORATION_CLIENT) {
    LOG("    Using CSD mode\n");

    /* Window manager does not support GDK_DECOR_BORDER,
     * emulate it by CSD.
     *
     * gtk_window_set_titlebar() works on unrealized widgets only,
     * we need to handle mShell carefully here.
     * When CSD is enabled mGdkWindow is owned by mContainer which is good
     * as we can't delete our mGdkWindow. To make mShell unrealized while
     * mContainer is preserved we temporary reparent mContainer to an
     * invisible GtkWindow.
     */
    bool visible = !mNeedsShow && mIsShown;
    if (visible) {
      NativeShow(false);
    }

    // Using GTK_WINDOW_POPUP rather than
    // GTK_WINDOW_TOPLEVEL in the hope that POPUP results in less
    // initialization and window manager interaction.
    GtkWidget* tmpWindow = gtk_window_new(GTK_WINDOW_POPUP);
    gtk_widget_realize(tmpWindow);

    gtk_widget_reparent(GTK_WIDGET(mContainer), tmpWindow);
    gtk_widget_unrealize(GTK_WIDGET(mShell));

    // Add a hidden titlebar widget to trigger CSD, but disable the default
    // titlebar.  GtkFixed is a somewhat random choice for a simple unused
    // widget. gtk_window_set_titlebar() takes ownership of the titlebar
    // widget.
    gtk_window_set_titlebar(GTK_WINDOW(mShell),
                            aState ? gtk_fixed_new() : nullptr);

    /* A workaround for https://bugzilla.gnome.org/show_bug.cgi?id=791081
     * gtk_widget_realize() throws:
     * "In pixman_region32_init_rect: Invalid rectangle passed"
     * when mShell has default 1x1 size.
     */
    GtkAllocation allocation = {0, 0, 0, 0};
    gtk_widget_get_preferred_width(GTK_WIDGET(mShell), nullptr,
                                   &allocation.width);
    gtk_widget_get_preferred_height(GTK_WIDGET(mShell), nullptr,
                                    &allocation.height);
    gtk_widget_size_allocate(GTK_WIDGET(mShell), &allocation);

    gtk_widget_realize(GTK_WIDGET(mShell));
    gtk_widget_reparent(GTK_WIDGET(mContainer), GTK_WIDGET(mShell));

    // Label mShell toplevel window so property_notify_event_cb callback
    // can find its way home.
    g_object_set_data(G_OBJECT(GetToplevelGdkWindow()), "nsWindow", this);

    if (AreBoundsSane()) {
      GdkRectangle size = DevicePixelsToGdkSizeRoundUp(mBounds.Size());
      LOG("    resize to %d x %d\n", size.width, size.height);
      gtk_window_resize(GTK_WINDOW(mShell), size.width, size.height);
    }

    if (visible) {
      mNeedsShow = true;
      NativeShow(true);
    }

    gtk_widget_destroy(tmpWindow);
  }

  if (mTransparencyBitmapForTitlebar) {
    if (mDrawInTitlebar && mSizeMode == nsSizeMode_Normal && !mIsTiled) {
      UpdateTitlebarTransparencyBitmap();
    } else {
      ClearTransparencyBitmap();
    }
  }
}

GtkWindow* nsWindow::GetCurrentTopmostWindow() const {
  GtkWindow* parentWindow = GTK_WINDOW(GetGtkWidget());
  GtkWindow* topmostParentWindow = nullptr;
  while (parentWindow) {
    topmostParentWindow = parentWindow;
    parentWindow = gtk_window_get_transient_for(parentWindow);
  }
  return topmostParentWindow;
}

gint nsWindow::GdkCeiledScaleFactor() {
  if (IsTopLevelWindowType()) {
    return mCeiledScaleFactor;
  }
  if (nsWindow* topmost = GetTopmostWindow()) {
    return topmost->mCeiledScaleFactor;
  }
  return ScreenHelperGTK::GetGTKMonitorScaleFactor();
}

double nsWindow::FractionalScaleFactor() {
#ifdef MOZ_WAYLAND
  double fractional_scale = [&] {
    if (IsTopLevelWindowType()) {
      return mFractionalScaleFactor;
    }
    if (nsWindow* topmost = GetTopmostWindow()) {
      return topmost->mFractionalScaleFactor;
    }
    return 0.0;
  }();
  if (fractional_scale != 0.0) {
    return fractional_scale;
  }
#endif
  return GdkCeiledScaleFactor();
}

gint nsWindow::DevicePixelsToGdkCoordRoundUp(int aPixels) {
  double scale = FractionalScaleFactor();
  return ceil(aPixels / scale);
}

gint nsWindow::DevicePixelsToGdkCoordRoundDown(int aPixels) {
  double scale = FractionalScaleFactor();
  return floor(aPixels / scale);
}

GdkPoint nsWindow::DevicePixelsToGdkPointRoundDown(
    const LayoutDeviceIntPoint& aPoint) {
  double scale = FractionalScaleFactor();
  return {int(aPoint.x / scale), int(aPoint.y / scale)};
}

GdkRectangle nsWindow::DevicePixelsToGdkRectRoundOut(
    const LayoutDeviceIntRect& aRect) {
  double scale = FractionalScaleFactor();
  int x = floor(aRect.x / scale);
  int y = floor(aRect.y / scale);
  int right = ceil((aRect.x + aRect.width) / scale);
  int bottom = ceil((aRect.y + aRect.height) / scale);
  return {x, y, right - x, bottom - y};
}

GdkRectangle nsWindow::DevicePixelsToGdkSizeRoundUp(
    const LayoutDeviceIntSize& aSize) {
  double scale = FractionalScaleFactor();
  gint width = ceil(aSize.width / scale);
  gint height = ceil(aSize.height / scale);
  return {0, 0, width, height};
}

int nsWindow::GdkCoordToDevicePixels(gint aCoord) {
  return (int)(aCoord * FractionalScaleFactor());
}

LayoutDeviceIntPoint nsWindow::GdkEventCoordsToDevicePixels(gdouble aX,
                                                            gdouble aY) {
  double scale = FractionalScaleFactor();
  return LayoutDeviceIntPoint::Floor((float)(aX * scale), (float)(aY * scale));
}

LayoutDeviceIntPoint nsWindow::GdkPointToDevicePixels(const GdkPoint& aPoint) {
  double scale = FractionalScaleFactor();
  return LayoutDeviceIntPoint::Floor((float)(aPoint.x * scale),
                                     (float)(aPoint.y * scale));
}

LayoutDeviceIntRect nsWindow::GdkRectToDevicePixels(const GdkRectangle& aRect) {
  double scale = FractionalScaleFactor();
  return LayoutDeviceIntRect::RoundIn(
      (float)(aRect.x * scale), (float)(aRect.y * scale),
      (float)(aRect.width * scale), (float)(aRect.height * scale));
}

nsresult nsWindow::SynthesizeNativeMouseEvent(
    LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
    MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
    nsIObserver* aObserver) {
  LOG("SynthesizeNativeMouseEvent(%d, %d, %d, %d, %d)", aPoint.x.value,
      aPoint.y.value, int(aNativeMessage), int(aButton), int(aModifierFlags));

  AutoObserverNotifier notifier(aObserver, "mouseevent");

  if (!mGdkWindow) {
    return NS_OK;
  }

  // When a button-press/release event is requested, create it here and put it
  // in the event queue. This will not emit a motion event - this needs to be
  // done explicitly *before* requesting a button-press/release. You will also
  // need to wait for the motion event to be dispatched before requesting a
  // button-press/release event to maintain the desired event order.
  switch (aNativeMessage) {
    case NativeMouseMessage::ButtonDown:
    case NativeMouseMessage::ButtonUp: {
      GdkEvent event;
      memset(&event, 0, sizeof(GdkEvent));
      event.type = aNativeMessage == NativeMouseMessage::ButtonDown
                       ? GDK_BUTTON_PRESS
                       : GDK_BUTTON_RELEASE;
      switch (aButton) {
        case MouseButton::ePrimary:
        case MouseButton::eMiddle:
        case MouseButton::eSecondary:
        case MouseButton::eX1:
        case MouseButton::eX2:
          event.button.button = aButton + 1;
          break;
        default:
          return NS_ERROR_INVALID_ARG;
      }
      event.button.state =
          KeymapWrapper::ConvertWidgetModifierToGdkState(aModifierFlags);
      event.button.window = mGdkWindow;
      event.button.time = GDK_CURRENT_TIME;

      // Get device for event source
      event.button.device = GdkGetPointer();

      event.button.x_root = DevicePixelsToGdkCoordRoundDown(aPoint.x);
      event.button.y_root = DevicePixelsToGdkCoordRoundDown(aPoint.y);

      LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset();
      event.button.x = DevicePixelsToGdkCoordRoundDown(pointInWindow.x);
      event.button.y = DevicePixelsToGdkCoordRoundDown(pointInWindow.y);

      gdk_event_put(&event);
      return NS_OK;
    }
    case NativeMouseMessage::Move: {
      // We don't support specific events other than button-press/release. In
      // all other cases we'll synthesize a motion event that will be emitted by
      // gdk_display_warp_pointer().
      // XXX How to activate native modifier for the other events?
#ifdef MOZ_WAYLAND
      // Impossible to warp the pointer on Wayland.
      // For pointer lock, pointer-constraints and relative-pointer are used.
      if (GdkIsWaylandDisplay()) {
        return NS_OK;
      }
#endif
      GdkScreen* screen = gdk_window_get_screen(mGdkWindow);
      GdkPoint point = DevicePixelsToGdkPointRoundDown(aPoint);
      gdk_device_warp(GdkGetPointer(), screen, point.x, point.y);
      return NS_OK;
    }
    case NativeMouseMessage::EnterWindow:
    case NativeMouseMessage::LeaveWindow:
      MOZ_ASSERT_UNREACHABLE("Non supported mouse event on Linux");
      return NS_ERROR_INVALID_ARG;
  }
  return NS_ERROR_UNEXPECTED;
}

void nsWindow::CreateAndPutGdkScrollEvent(mozilla::LayoutDeviceIntPoint aPoint,
                                          double aDeltaX, double aDeltaY) {
  GdkEvent event;
  memset(&event, 0, sizeof(GdkEvent));
  event.type = GDK_SCROLL;
  event.scroll.window = mGdkWindow;
  event.scroll.time = GDK_CURRENT_TIME;
  // Get device for event source
  GdkDisplay* display = gdk_window_get_display(mGdkWindow);
  GdkDeviceManager* device_manager = gdk_display_get_device_manager(display);
  // See note in nsWindow::SynthesizeNativeTouchpadPan about the device we use
  // here.
  event.scroll.device = gdk_device_manager_get_client_pointer(device_manager);
  event.scroll.x_root = DevicePixelsToGdkCoordRoundDown(aPoint.x);
  event.scroll.y_root = DevicePixelsToGdkCoordRoundDown(aPoint.y);

  LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset();
  event.scroll.x = DevicePixelsToGdkCoordRoundDown(pointInWindow.x);
  event.scroll.y = DevicePixelsToGdkCoordRoundDown(pointInWindow.y);

  // The delta values are backwards on Linux compared to Windows and Cocoa,
  // hence the negation.
  event.scroll.direction = GDK_SCROLL_SMOOTH;
  event.scroll.delta_x = -aDeltaX;
  event.scroll.delta_y = -aDeltaY;

  gdk_event_put(&event);
}

nsresult nsWindow::SynthesizeNativeMouseScrollEvent(
    mozilla::LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage,
    double aDeltaX, double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
    uint32_t aAdditionalFlags, nsIObserver* aObserver) {
  AutoObserverNotifier notifier(aObserver, "mousescrollevent");

  if (!mGdkWindow) {
    return NS_OK;
  }

  CreateAndPutGdkScrollEvent(aPoint, aDeltaX, aDeltaY);

  return NS_OK;
}

nsresult nsWindow::SynthesizeNativeTouchPoint(uint32_t aPointerId,
                                              TouchPointerState aPointerState,
                                              LayoutDeviceIntPoint aPoint,
                                              double aPointerPressure,
                                              uint32_t aPointerOrientation,
                                              nsIObserver* aObserver) {
  AutoObserverNotifier notifier(aObserver, "touchpoint");

  if (!mGdkWindow) {
    return NS_OK;
  }

  GdkEvent event;
  memset(&event, 0, sizeof(GdkEvent));

  static std::map<uint32_t, GdkEventSequence*> sKnownPointers;

  auto result = sKnownPointers.find(aPointerId);
  switch (aPointerState) {
    case TOUCH_CONTACT:
      if (result == sKnownPointers.end()) {
        // GdkEventSequence isn't a thing we can instantiate, and never gets
        // dereferenced in the gtk code. It's an opaque pointer, the only
        // requirement is that it be distinct from other instances of
        // GdkEventSequence*.
        event.touch.sequence = (GdkEventSequence*)((uintptr_t)aPointerId);
        sKnownPointers[aPointerId] = event.touch.sequence;
        event.type = GDK_TOUCH_BEGIN;
      } else {
        event.touch.sequence = result->second;
        event.type = GDK_TOUCH_UPDATE;
      }
      break;
    case TOUCH_REMOVE:
      event.type = GDK_TOUCH_END;
      if (result == sKnownPointers.end()) {
        NS_WARNING("Tried to synthesize touch-end for unknown pointer!");
        return NS_ERROR_UNEXPECTED;
      }
      event.touch.sequence = result->second;
      sKnownPointers.erase(result);
      break;
    case TOUCH_CANCEL:
      event.type = GDK_TOUCH_CANCEL;
      if (result == sKnownPointers.end()) {
        NS_WARNING("Tried to synthesize touch-cancel for unknown pointer!");
        return NS_ERROR_UNEXPECTED;
      }
      event.touch.sequence = result->second;
      sKnownPointers.erase(result);
      break;
    case TOUCH_HOVER:
    default:
      return NS_ERROR_NOT_IMPLEMENTED;
  }

  event.touch.window = mGdkWindow;
  event.touch.time = GDK_CURRENT_TIME;

  GdkDisplay* display = gdk_window_get_display(mGdkWindow);
  GdkDeviceManager* device_manager = gdk_display_get_device_manager(display);
  event.touch.device = gdk_device_manager_get_client_pointer(device_manager);

  event.touch.x_root = DevicePixelsToGdkCoordRoundDown(aPoint.x);
  event.touch.y_root = DevicePixelsToGdkCoordRoundDown(aPoint.y);

  LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset();
  event.touch.x = DevicePixelsToGdkCoordRoundDown(pointInWindow.x);
  event.touch.y = DevicePixelsToGdkCoordRoundDown(pointInWindow.y);

  gdk_event_put(&event);

  return NS_OK;
}

nsresult nsWindow::SynthesizeNativeTouchPadPinch(
    TouchpadGesturePhase aEventPhase, float aScale, LayoutDeviceIntPoint aPoint,
    int32_t aModifierFlags) {
  if (!mGdkWindow) {
    return NS_OK;
  }
  GdkEvent event;
  memset(&event, 0, sizeof(GdkEvent));

  GdkEventTouchpadPinch* touchpad_event =
      reinterpret_cast<GdkEventTouchpadPinch*>(&event);
  touchpad_event->type = GDK_TOUCHPAD_PINCH;

  const ScreenIntPoint widgetToScreenOffset = ViewAs<ScreenPixel>(
      WidgetToScreenOffset(),
      PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);

  ScreenPoint pointInWindow =
      ViewAs<ScreenPixel>(
          aPoint,
          PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent) -
      widgetToScreenOffset;

  gdouble dx = 0, dy = 0;

  switch (aEventPhase) {
    case PHASE_BEGIN:
      touchpad_event->phase = GDK_TOUCHPAD_GESTURE_PHASE_BEGIN;
      mCurrentSynthesizedTouchpadPinch = {pointInWindow, pointInWindow};
      break;
    case PHASE_UPDATE:
      dx = pointInWindow.x - mCurrentSynthesizedTouchpadPinch.mCurrentFocus.x;
      dy = pointInWindow.y - mCurrentSynthesizedTouchpadPinch.mCurrentFocus.y;
      mCurrentSynthesizedTouchpadPinch.mCurrentFocus = pointInWindow;
      touchpad_event->phase = GDK_TOUCHPAD_GESTURE_PHASE_UPDATE;
      break;
    case PHASE_END:
      touchpad_event->phase = GDK_TOUCHPAD_GESTURE_PHASE_END;
      break;

    default:
      return NS_ERROR_NOT_IMPLEMENTED;
  }

  touchpad_event->window = mGdkWindow;
  // We only set the fields of GdkEventTouchpadPinch which are
  // actually used in OnTouchpadPinchEvent().
  // GdkEventTouchpadPinch has additional fields.
  // If OnTouchpadPinchEvent() is changed to use other fields, this function
  // will need to change to set them as well.
  touchpad_event->time = GDK_CURRENT_TIME;
  touchpad_event->scale = aScale;
  touchpad_event->x_root = DevicePixelsToGdkCoordRoundDown(
      mCurrentSynthesizedTouchpadPinch.mBeginFocus.x +
      ScreenCoord(widgetToScreenOffset.x));
  touchpad_event->y_root = DevicePixelsToGdkCoordRoundDown(
      mCurrentSynthesizedTouchpadPinch.mBeginFocus.y +
      ScreenCoord(widgetToScreenOffset.y));

  touchpad_event->x = DevicePixelsToGdkCoordRoundDown(
      mCurrentSynthesizedTouchpadPinch.mBeginFocus.x);
  touchpad_event->y = DevicePixelsToGdkCoordRoundDown(
      mCurrentSynthesizedTouchpadPinch.mBeginFocus.y);

  touchpad_event->dx = dx;
  touchpad_event->dy = dy;

  touchpad_event->state = aModifierFlags;

  gdk_event_put(&event);

  return NS_OK;
}

nsresult nsWindow::SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase,
                                               LayoutDeviceIntPoint aPoint,
                                               double aDeltaX, double aDeltaY,
                                               int32_t aModifierFlags,
                                               nsIObserver* aObserver) {
  AutoObserverNotifier notifier(aObserver, "touchpadpanevent");

  if (!mGdkWindow) {
    return NS_OK;
  }

  // This should/could maybe send GdkEventTouchpadSwipe events, however we don't
  // currently consume those (either real user input or testing events). So we
  // send gdk scroll events to be more like what we do for real user input. If
  // we start consuming GdkEventTouchpadSwipe and get those hooked up to swipe
  // to nav, then maybe we should test those too.

  mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase = Some(aEventPhase);
  MOZ_ASSERT(mCurrentSynthesizedTouchpadPan.mSavedObserver == 0);
  mCurrentSynthesizedTouchpadPan.mSavedObserver = notifier.SaveObserver();

  // Note that CreateAndPutGdkScrollEvent sets the device source for the created
  // event as the "client pointer" (a kind of default device) which will
  // probably be of type mouse. We would ideally want to set the device of the
  // created event to be a touchpad, but the system might not have a touchpad.
  // To get around this we use
  // mCurrentSynthesizedTouchpadPan.mTouchpadGesturePhase being something to
  // indicate that we should treat the source of the event as touchpad in
  // OnScrollEvent.
  CreateAndPutGdkScrollEvent(aPoint, aDeltaX, aDeltaY);

  return NS_OK;
}

nsWindow::GtkWindowDecoration nsWindow::GetSystemGtkWindowDecoration() {
  static GtkWindowDecoration sGtkWindowDecoration = [] {
    // Allow MOZ_GTK_TITLEBAR_DECORATION to override our heuristics
    if (const char* decorationOverride =
            getenv("MOZ_GTK_TITLEBAR_DECORATION")) {
      if (strcmp(decorationOverride, "none") == 0) {
        return GTK_DECORATION_NONE;
      }
      if (strcmp(decorationOverride, "client") == 0) {
        return GTK_DECORATION_CLIENT;
      }
      if (strcmp(decorationOverride, "system") == 0) {
        return GTK_DECORATION_SYSTEM;
      }
    }

    // nsWindow::GetSystemGtkWindowDecoration can be called from various
    // threads so we can't use gfxPlatformGtk here.
    if (GdkIsWaylandDisplay()) {
      return GTK_DECORATION_CLIENT;
    }

    // GTK_CSD forces CSD mode - use also CSD because window manager
    // decorations does not work with CSD.
    // We check GTK_CSD as well as gtk_window_should_use_csd() does.
    if (const char* csdOverride = getenv("GTK_CSD")) {
      return *csdOverride == '0' ? GTK_DECORATION_NONE : GTK_DECORATION_CLIENT;
    }

    // TODO: Consider switching this to GetDesktopEnvironmentIdentifier().
    const char* currentDesktop = getenv("XDG_CURRENT_DESKTOP");
    if (!currentDesktop) {
      return GTK_DECORATION_NONE;
    }
    if (strstr(currentDesktop, "i3")) {
      return GTK_DECORATION_NONE;
    }

    // Tested desktops: pop:GNOME, KDE, Enlightenment, LXDE, openbox, MATE,
    // X-Cinnamon, Pantheon, Deepin, GNOME, LXQt, Unity.
    return GTK_DECORATION_CLIENT;
  }();
  return sGtkWindowDecoration;
}

bool nsWindow::TitlebarUseShapeMask() {
  static int useShapeMask = []() {
    // Don't use titlebar shape mask on Wayland
    if (!GdkIsX11Display()) {
      return false;
    }

    // We can't use shape masks on Mutter/X.org as we can't resize Firefox
    // window there (Bug 1530252).
    if (IsGnomeDesktopEnvironment()) {
      return false;
    }

    return Preferences::GetBool("widget.titlebar-x11-use-shape-mask", false);
  }();
  return useShapeMask;
}

int32_t nsWindow::RoundsWidgetCoordinatesTo() { return GdkCeiledScaleFactor(); }

void nsWindow::GetCompositorWidgetInitData(
    mozilla::widget::CompositorWidgetInitData* aInitData) {
  nsCString displayName;

  LOG("nsWindow::GetCompositorWidgetInitData");
  EnsureGdkWindow();

  *aInitData = mozilla::widget::GtkCompositorWidgetInitData(
      GetX11Window(), displayName, GetShapedState(), GdkIsX11Display(),
      GetClientSize());

#ifdef MOZ_X11
  if (GdkIsX11Display()) {
    // Make sure the window XID is propagated to X server, we can fail otherwise
    // in GPU process (Bug 1401634).
    Display* display = DefaultXDisplay();
    XFlush(display);
    displayName = nsCString(XDisplayString(display));
  }
#endif
}

#ifdef MOZ_X11
/* XApp progress support currently works by setting a property
 * on a window with this Atom name.  A supporting window manager
 * will notice this and pass it along to whatever handling has
 * been implemented on that end (e.g. passing it on to a taskbar
 * widget.)  There is no issue if WM support is lacking, this is
 * simply ignored in that case.
 *
 * See https://github.com/linuxmint/xapps/blob/master/libxapp/xapp-gtk-window.c
 * for further details.
 */

#  define PROGRESS_HINT "_NET_WM_XAPP_PROGRESS"

static void set_window_hint_cardinal(Window xid, const gchar* atom_name,
                                     gulong cardinal) {
  GdkDisplay* display;

  display = gdk_display_get_default();

  if (cardinal > 0) {
    XChangeProperty(GDK_DISPLAY_XDISPLAY(display), xid,
                    gdk_x11_get_xatom_by_name_for_display(display, atom_name),
                    XA_CARDINAL, 32, PropModeReplace, (guchar*)&cardinal, 1);
  } else {
    XDeleteProperty(GDK_DISPLAY_XDISPLAY(display), xid,
                    gdk_x11_get_xatom_by_name_for_display(display, atom_name));
  }
}
#endif  // MOZ_X11

void nsWindow::SetProgress(unsigned long progressPercent) {
#ifdef MOZ_X11

  if (!GdkIsX11Display()) {
    return;
  }

  if (!mShell) {
    return;
  }

  progressPercent = MIN(progressPercent, 100);

  set_window_hint_cardinal(GDK_WINDOW_XID(GetToplevelGdkWindow()),
                           PROGRESS_HINT, progressPercent);
#endif  // MOZ_X11
}

#ifdef MOZ_X11
void nsWindow::SetCompositorHint(WindowComposeRequest aState) {
  if (!GdkIsX11Display()) {
    return;
  }

  gulong value = aState;
  GdkAtom cardinal_atom = gdk_x11_xatom_to_atom(XA_CARDINAL);
  gdk_property_change(GetToplevelGdkWindow(),
                      gdk_atom_intern("_NET_WM_BYPASS_COMPOSITOR", FALSE),
                      cardinal_atom,
                      32,  // format
                      GDK_PROP_MODE_REPLACE, (guchar*)&value, 1);
}
#endif

nsresult nsWindow::SetSystemFont(const nsCString& aFontName) {
  GtkSettings* settings = gtk_settings_get_default();
  g_object_set(settings, "gtk-font-name", aFontName.get(), nullptr);
  return NS_OK;
}

nsresult nsWindow::GetSystemFont(nsCString& aFontName) {
  GtkSettings* settings = gtk_settings_get_default();
  gchar* fontName = nullptr;
  g_object_get(settings, "gtk-font-name", &fontName, nullptr);
  if (fontName) {
    aFontName.Assign(fontName);
    g_free(fontName);
  }
  return NS_OK;
}

already_AddRefed<nsIWidget> nsIWidget::CreateTopLevelWindow() {
  nsCOMPtr<nsIWidget> window = new nsWindow();
  return window.forget();
}

already_AddRefed<nsIWidget> nsIWidget::CreateChildWindow() {
  nsCOMPtr<nsIWidget> window = new nsWindow();
  return window.forget();
}

#ifdef MOZ_WAYLAND
static void relative_pointer_handle_relative_motion(
    void* data, struct zwp_relative_pointer_v1* pointer, uint32_t time_hi,
    uint32_t time_lo, wl_fixed_t dx_w, wl_fixed_t dy_w, wl_fixed_t dx_unaccel_w,
    wl_fixed_t dy_unaccel_w) {
  RefPtr<nsWindow> window(reinterpret_cast<nsWindow*>(data));

  WidgetMouseEvent event(true, eMouseMove, window, WidgetMouseEvent::eReal);

  double scale = window->FractionalScaleFactor();
  event.mRefPoint = window->GetNativePointerLockCenter();
  event.mRefPoint.x += int(wl_fixed_to_double(dx_w) * scale);
  event.mRefPoint.y += int(wl_fixed_to_double(dy_w) * scale);

  event.AssignEventTime(window->GetWidgetEventTime(time_lo));
  window->DispatchInputEvent(&event);
}

static const struct zwp_relative_pointer_v1_listener relative_pointer_listener =
    {
        relative_pointer_handle_relative_motion,
};

void nsWindow::SetNativePointerLockCenter(
    const LayoutDeviceIntPoint& aLockCenter) {
  mNativePointerLockCenter = aLockCenter;
}

void nsWindow::LockNativePointer() {
  if (!GdkIsWaylandDisplay()) {
    return;
  }

  auto* waylandDisplay = WaylandDisplayGet();

  auto* pointerConstraints = waylandDisplay->GetPointerConstraints();
  if (!pointerConstraints) {
    return;
  }

  auto* relativePointerMgr = waylandDisplay->GetRelativePointerManager();
  if (!relativePointerMgr) {
    return;
  }

  GdkDisplay* display = gdk_display_get_default();

  GdkDeviceManager* manager = gdk_display_get_device_manager(display);
  MOZ_ASSERT(manager);

  GdkDevice* device = gdk_device_manager_get_client_pointer(manager);
  if (!device) {
    NS_WARNING("Could not find Wayland pointer to lock");
    return;
  }
  wl_pointer* pointer = gdk_wayland_device_get_wl_pointer(device);
  MOZ_ASSERT(pointer);

  wl_surface* surface =
      gdk_wayland_window_get_wl_surface(gtk_widget_get_window(GetGtkWidget()));
  if (!surface) {
    /* Can be null when the window is hidden.
     * Though it's unlikely that a lock request comes in that case, be
     * defensive. */
    return;
  }

  if (mLockedPointer || mRelativePointer) {
    UnlockNativePointer();
  }

  mLockedPointer = zwp_pointer_constraints_v1_lock_pointer(
      pointerConstraints, surface, pointer, nullptr,
      ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
  if (!mLockedPointer) {
    NS_WARNING("Could not lock Wayland pointer");
    return;
  }

  mRelativePointer = zwp_relative_pointer_manager_v1_get_relative_pointer(
      relativePointerMgr, pointer);
  if (!mRelativePointer) {
    NS_WARNING("Could not create relative Wayland pointer");
    zwp_locked_pointer_v1_destroy(mLockedPointer);
    mLockedPointer = nullptr;
    return;
  }

  zwp_relative_pointer_v1_add_listener(mRelativePointer,
                                       &relative_pointer_listener, this);
}

void nsWindow::UnlockNativePointer() {
  if (!GdkIsWaylandDisplay()) {
    return;
  }
  if (mRelativePointer) {
    zwp_relative_pointer_v1_destroy(mRelativePointer);
    mRelativePointer = nullptr;
  }
  if (mLockedPointer) {
    zwp_locked_pointer_v1_destroy(mLockedPointer);
    mLockedPointer = nullptr;
  }
}
#endif

bool nsWindow::GetTopLevelWindowActiveState(nsIFrame* aFrame) {
  // Used by window frame and button box rendering. We can end up in here in
  // the content process when rendering one of these moz styles freely in a
  // page. Fail in this case, there is no applicable window focus state.
  if (!XRE_IsParentProcess()) {
    return false;
  }
  // All headless windows are considered active so they are painted.
  if (gfxPlatform::IsHeadless()) {
    return true;
  }
  // Get the widget. nsIFrame's GetNearestWidget walks up the view chain
  // until it finds a real window.
  nsWindow* window = static_cast<nsWindow*>(aFrame->GetNearestWidget());
  if (!window) {
    return false;
  }
  return !window->mTitlebarBackdropState;
}

static nsIFrame* FindTitlebarFrame(nsIFrame* aFrame) {
  for (nsIFrame* childFrame : aFrame->PrincipalChildList()) {
    StyleAppearance appearance =
        childFrame->StyleDisplay()->EffectiveAppearance();
    if (appearance == StyleAppearance::MozWindowTitlebar ||
        appearance == StyleAppearance::MozWindowTitlebarMaximized) {
      return childFrame;
    }

    if (nsIFrame* foundFrame = FindTitlebarFrame(childFrame)) {
      return foundFrame;
    }
  }
  return nullptr;
}

nsIFrame* nsWindow::GetFrame() const {
  nsView* view = nsView::GetViewFor(this);
  if (!view) {
    return nullptr;
  }
  return view->GetFrame();
}

void nsWindow::UpdateMozWindowActive() {
  // Update activation state for the :-moz-window-inactive pseudoclass.
  // Normally, this follows focus; we override it here to follow
  // GDK_WINDOW_STATE_FOCUSED.
  if (mozilla::dom::Document* document = GetDocument()) {
    if (nsPIDOMWindowOuter* window = document->GetWindow()) {
      if (RefPtr<mozilla::dom::BrowsingContext> bc =
              window->GetBrowsingContext()) {
        bc->SetIsActiveBrowserWindow(!mTitlebarBackdropState);
      }
    }
  }
}

void nsWindow::ForceTitlebarRedraw() {
  MOZ_ASSERT(mDrawInTitlebar, "We should not redraw invisible titlebar.");

  if (!mWidgetListener || !mWidgetListener->GetPresShell()) {
    return;
  }

  nsIFrame* frame = GetFrame();
  if (!frame) {
    return;
  }

  frame = FindTitlebarFrame(frame);
  if (frame) {
    nsIContent* content = frame->GetContent();
    if (content) {
      nsLayoutUtils::PostRestyleEvent(content->AsElement(), RestyleHint{0},
                                      nsChangeHint_RepaintFrame);
    }
  }
}

void nsWindow::LockAspectRatio(bool aShouldLock) {
  if (!gUseAspectRatio) {
    return;
  }

  if (aShouldLock) {
    int decWidth = 0, decHeight = 0;
    AddCSDDecorationSize(&decWidth, &decHeight);

    float width =
        DevicePixelsToGdkCoordRoundDown(mLastSizeRequest.width) + decWidth;
    float height =
        DevicePixelsToGdkCoordRoundDown(mLastSizeRequest.height) + decHeight;

    mAspectRatio = width / height;
    LOG("nsWindow::LockAspectRatio() width %f height %f aspect %f", width,
        height, mAspectRatio);
  } else {
    mAspectRatio = 0.0;
    LOG("nsWindow::LockAspectRatio() removed aspect ratio");
  }

  ApplySizeConstraints();
}

nsWindow* nsWindow::GetFocusedWindow() { return gFocusWindow; }

#ifdef MOZ_WAYLAND
bool nsWindow::SetEGLNativeWindowSize(
    const LayoutDeviceIntSize& aEGLWindowSize) {
  if (!GdkIsWaylandDisplay()) {
    return true;
  }

  // SetEGLNativeWindowSize() is called from renderer/compositor thread,
  // make sure nsWindow is not destroyed.
  bool paint = false;

  // See NS_NATIVE_EGL_WINDOW why we can't block here.
  if (mDestroyMutex.TryLock()) {
    if (!mIsDestroyed && mContainer) {
      gint scale = GdkCeiledScaleFactor();
      LOG("nsWindow::SetEGLNativeWindowSize() %d x %d scale %d (unscaled %d x "
          "%d)",
          aEGLWindowSize.width, aEGLWindowSize.height, scale,
          aEGLWindowSize.width / scale, aEGLWindowSize.height / scale);
      paint = moz_container_wayland_egl_window_set_size(
          mContainer, aEGLWindowSize.ToUnknownSize(), scale);
    }
    mDestroyMutex.Unlock();
  }
  return paint;
}
#endif

nsWindow* nsWindow::GetWindow(GdkWindow* window) {
  return get_window_for_gdk_window(window);
}

void nsWindow::ClearRenderingQueue() {
  LOG("nsWindow::ClearRenderingQueue()");

  if (mWidgetListener) {
    mWidgetListener->RequestWindowClose(this);
  }
  DestroyLayerManager();
}

// Apply workaround for Mutter compositor bug (mzbz#1777269).
//
// When we open a popup window (tooltip for instance) attached to
// GDK_WINDOW_TYPE_HINT_UTILITY parent popup, Mutter compositor sends bogus
// leave/enter events to the GDK_WINDOW_TYPE_HINT_UTILITY popup.
// That leads to immediate tooltip close. As a workaround ignore these
// bogus events.
//
// We need to check two affected window types:
//
// - toplevel window with at least two child popups where the first one is
//   GDK_WINDOW_TYPE_HINT_UTILITY.
// - GDK_WINDOW_TYPE_HINT_UTILITY popup with a child popup
//
// We need to mask two bogus leave/enter sequences:
//  1) Leave (popup) -> Enter (toplevel)
//  2) Leave (toplevel) -> Enter (popup)
//
// TODO: persistent (non-tracked) popups with tooltip/child popups?
//
bool nsWindow::ApplyEnterLeaveMutterWorkaround() {
  // Leave (toplevel) case
  if (mWindowType == WindowType::TopLevel && mWaylandPopupNext &&
      mWaylandPopupNext->mWaylandPopupNext &&
      gtk_window_get_type_hint(GTK_WINDOW(mWaylandPopupNext->GetGtkWidget())) ==
          GDK_WINDOW_TYPE_HINT_UTILITY) {
    LOG("nsWindow::ApplyEnterLeaveMutterWorkaround(): leave toplevel");
    return true;
  }
  // Leave (popup) case
  if (IsWaylandPopup() && mWaylandPopupNext &&
      gtk_window_get_type_hint(GTK_WINDOW(mShell)) ==
          GDK_WINDOW_TYPE_HINT_UTILITY) {
    LOG("nsWindow::ApplyEnterLeaveMutterWorkaround(): leave popup");
    return true;
  }
  return false;
}

void nsWindow::NotifyOcclusionState(mozilla::widget::OcclusionState aState) {
  if (!IsTopLevelWindowType()) {
    return;
  }

  bool isFullyOccluded = aState == mozilla::widget::OcclusionState::OCCLUDED;
  if (mIsFullyOccluded == isFullyOccluded) {
    return;
  }
  mIsFullyOccluded = isFullyOccluded;

  LOG("nsWindow::NotifyOcclusionState() mIsFullyOccluded %d", mIsFullyOccluded);
  if (mWidgetListener) {
    mWidgetListener->OcclusionStateChanged(mIsFullyOccluded);
  }
}

void nsWindow::SetDragSource(GdkDragContext* aSourceDragContext) {
  mSourceDragContext = aSourceDragContext;
  if (IsPopup() &&
      (widget::GdkIsWaylandDisplay() || widget::IsXWaylandProtocol())) {
    if (auto* menuPopupFrame = GetMenuPopupFrame(GetFrame())) {
      menuPopupFrame->SetIsDragSource(!!aSourceDragContext);
    }
  }
}
