/* -*- 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 #include #include #include #include #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 # include # include # include # 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 # 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(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& TimeConverter() { static SystemTimeConverter 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 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 idleService = do_GetService("@mozilla.org/widget/useridleservice;1"); if (idleService) { idleService->ResetIdleTimeOut(0); } guint timestamp = gdk_event_get_time(static_cast(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() : mIsDestroyed(false), mIsShown(false), mNeedsShow(false), mIsMapped(false), mEnabled(true), mCreated(false), mHandleTouchEvent(false), mIsDragPopup(false), mWindowScaleFactorChanged(true), mCompositedScreen(gdk_screen_is_composited(gdk_screen_get_default())), mIsAccelerated(false), mWindowShouldStartDragging(false), mHasMappedToplevel(false), mRetryPointerGrab(false), mPanInProgress(false), mDrawToContainer(false), mTitlebarBackdropState(false), mIsPIPWindow(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 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 (mWindowType == WindowType::TopLevel) { 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 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"); 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 rollupWidget = rollupListener->GetRollupWidget(); if (static_cast(this) == rollupWidget) { rollupListener->Rollup({}); } } // dragService will be null after shutdown of the service manager. RefPtr 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 screen = GetWidgetScreen(); if (screen) { screen->GetDpi(&dpi); } return dpi; } double nsWindow::GetDefaultScaleInternal() { return FractionalScaleFactor(); } DesktopToLayoutDeviceScale nsWindow::GetDesktopToDeviceScale() { #ifdef MOZ_WAYLAND if (GdkIsWaylandDisplay()) { return DesktopToLayoutDeviceScale(GdkCeiledScaleFactor()); } #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 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(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(aNewParent)->mIsDestroyed, ""); MOZ_ASSERT( !mParent, "nsWindow::ReparentNativeWidget() works on toplevel windows only."); auto* newParent = static_cast(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 screenmgr = do_GetService("@mozilla.org/gfx/screenmanager;1"); if (!screenmgr) { return; } nsCOMPtr 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& 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 && (mWindowType == WindowType::TopLevel || mWindowType == WindowType::Dialog)) { 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 = gtk_widget_get_window(mShell); 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* 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* 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* 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* 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 widgetChain; GetLayoutPopupWidgetChain(&widgetChain); if (widgetChain.Length() == 0) { LOG(" Empty\n"); } else { for (unsigned long i = 0; i < widgetChain.Length(); i++) { nsWindow* window = static_cast(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::WaylandPopupGetTopmostWindow() { nsView* view = nsView::GetViewFor(this); if (view) { nsView* parentView = view->GetParent(); if (parentView) { nsIWidget* parentWidget = parentView->GetNearestWidget(nullptr); if (parentWidget) { nsWindow* parentnsWindow = static_cast(parentWidget); LOG(" Topmost window: %p [nsWindow]\n", parentnsWindow); return parentnsWindow; } } } 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 = WaylandPopupGetTopmostWindow(); 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 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 = 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 mVertical; Maybe 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 = gtk_widget_get_window(GTK_WIDGET(mShell)); nsMenuPopupFrame* popupFrame = GetMenuPopupFrame(GetFrame()); if (!gdkWindow || !popupFrame) { LOG(" can't use move-to-rect due missing gdkWindow or popupFrame"); return false; } if (popupFrame->IsFlippedByLayout()) { 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)) { GdkWindow* window = gtk_widget_get_window(mShell); if (window) { 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(>kAnchorRect, &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 = gtk_widget_get_window(GTK_WIDGET(mShell)); 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, >kAnchorRect, 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(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 gsettings = do_GetService(NS_GSETTINGSSERVICE_CONTRACTID); if (gsettings) { nsCOMPtr 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 = gtk_widget_get_window(mShell); 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 = gtk_widget_get_window(mShell); 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(gtk_widget_get_window(mShell), 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(gtk_widget_get_window(mShell)); // Unset the urgency hint if possible. SetUrgencyHint(mShell, false); } } RefPtr 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 (mWindowType != WindowType::Dialog && mWindowType != WindowType::TopLevel) { 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 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 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; #ifdef MOZ_X11 if (GdkIsX11Display()) { eglWindow = mGdkWindow ? (void*)GDK_WINDOW_XID(mGdkWindow) : nullptr; } #endif #ifdef MOZ_WAYLAND if (GdkIsWaylandDisplay()) { if (mCompositorWidgetDelegate && mCompositorWidgetDelegate->AsGtkCompositorWidget() && mCompositorWidgetDelegate->AsGtkCompositorWidget()->IsHidden()) { NS_WARNING("Getting OpenGL EGL window for hidden Gtk window!"); return nullptr; } eglWindow = moz_container_wayland_get_egl_window( mContainer, FractionalScaleFactor()); } #endif 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 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("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 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(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 dt = StartRemoteDrawingInRegion(region, &layerBuffering); if (!dt || !dt->IsValid()) { return FALSE; } Maybe 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 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 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(bufferSize); { RefPtr 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. LOG("configure event %d,%d -> %d x %d scale %d\n", aEvent->x, aEvent->y, aEvent->width, aEvent->height, mGdkWindow ? gdk_window_get_scale_factor(mGdkWindow) : -1); 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 && (mWindowType == WindowType::TopLevel || mWindowType == WindowType::Dialog)) { if (mWindowScaleFactor != 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 (mWindowType == WindowType::TopLevel || mWindowType == WindowType::Dialog) { // 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 = mWindowType == WindowType::TopLevel || mWindowType == WindowType::Dialog; 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 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 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 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 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(gtk_widget_get_window(mShell), *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 (mWindowType == WindowType::TopLevel || mWindowType == WindowType::Dialog) { // 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 dragService = do_GetService("@mozilla.org/widget/dragservice;1"); nsCOMPtr dragSession; dragService->GetCurrentSession(getter_AddRefs(dragSession)); if (!dragSession) { return true; } // We also roll up when a drag is from a different application nsCOMPtr sourceNode; dragSession->GetSourceNode(getter_AddRefs(sourceNode)); return !sourceNode; }(); if (shouldRollupMenus) { RollupAllMenus(); } if (RefPtr pm = nsXULPopupManager::GetInstance()) { pm->RollupTooltips(); } } if (gFocusWindow) { RefPtr 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(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); PanGestureInput::PanGestureType 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 = StaticPrefs::apz_gtk_pangesture_delta_mode() != 2; 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( 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( 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(); } NotifyThemeChanged(ThemeChangeKind::StyleAndLayout); } 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() { // Force scale factor recalculation if (!mGdkWindow) { mWindowScaleFactorChanged = true; return; } LOG("OnScaleChanged -> %d\n", gdk_window_get_scale_factor(mGdkWindow)); // Gtk supply us sometimes with doubled events so stay calm in such case. if (gdk_window_get_scale_factor(mGdkWindow) == mWindowScaleFactor) { 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(); // Force scale factor recalculation mWindowScaleFactorChanged = true; 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(); } } // This affects style / layout because it affects system font sizes. // Update menu's font size etc. NotifyThemeChanged(ThemeChangeKind::StyleAndLayout); 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 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( 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 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(); #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; mCreated = true; // 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 GdkWindow* parentGdkWindow = nullptr; nsWindow* parentnsWindow = nullptr; if (aParent) { parentnsWindow = static_cast(aParent); parentGdkWindow = parentnsWindow->mGdkWindow; } else if (aNativeParent && GDK_IS_WINDOW(aNativeParent)) { parentGdkWindow = GDK_WINDOW(aNativeParent); parentnsWindow = get_window_for_gdk_window(parentGdkWindow); 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; mIsPIPWindow = aInitData && aInitData->mPIPWindow; // 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 (mWindowType == WindowType::TopLevel || mWindowType == WindowType::Dialog) { mGtkWindowDecoration = GetSystemGtkWindowDecoration(); } // 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(gtk_widget_get_window(mShell), 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::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(gtk_widget_get_window(mShell)), "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() && mWindowType == WindowType::TopLevel) { 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; } return NS_OK; } void nsWindow::RefreshWindowClass(void) { GdkWindow* gdkWindow = gtk_widget_get_window(mShell); 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(res_name); class_hint->res_class = const_cast(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 = (mWindowType == WindowType::TopLevel) && 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(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); // mCompositorWidgetDelegate can be deleted during timeout. // In such case just flip compositor back to enabled and let // SetCompositorWidgetDelegate() or Map event resume it. if (!mCompositorWidgetDelegate) { mCompositorState = COMPOSITOR_ENABLED; return; } ResumeCompositorImpl(); } void nsWindow::ResumeCompositorFromCompositorThread() { nsCOMPtr 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 ? gtk_widget_get_window(mShell) : 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 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); } void nsWindow::UpdateTopLevelOpaqueRegion(void) { if (!mCompositedScreen) { return; } GdkWindow* window = mDrawToContainer ? gtk_widget_get_window(mShell) : 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); int radius = DoDrawTilebarCorners() ? int(GetTitlebarRadius()) : 0; SubtractTitlebarCorners(region, x, y, width, 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() { if (!mGdkWindow || !mDrawInTitlebar) { return LayoutDeviceIntRect(); } int height = 0; if (DoDrawTilebarCorners()) { height = GdkCeiledScaleFactor() * TITLEBAR_HEIGHT; } return LayoutDeviceIntRect(0, 0, mBounds.width, height); } 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 mCallback; RefPtr mWindow; }; /* static */ gboolean FullscreenTransitionData::TimeoutCallback(gpointer aData) { bool finishing = false; auto* data = static_cast(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(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 nsWindow::GetWidgetScreen() { // Wayland can read screen directly if (GdkIsWaylandDisplay()) { if (RefPtr 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 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; } 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(); } } gtk_window_fullscreen(GTK_WINDOW(mShell)); } else { 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 = gtk_widget_get_window(mShell); // 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 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 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 dragService = do_GetService("@mozilla.org/widget/dragservice;1"); if (!dragService) { return false; } nsCOMPtr 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 dragService = do_GetService("@mozilla.org/widget/dragservice;1"); if (!dragService) { return; } nsCOMPtr 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(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(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 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 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 window = get_window_for_gtk_widget(widget); if (!window) { return; } window->OnMap(); } static void widget_unmap_cb(GtkWidget* widget) { RefPtr window = get_window_for_gtk_widget(widget); if (!window) { return; } window->OnUnmap(); } static void widget_unrealize_cb(GtkWidget* widget) { RefPtr window = get_window_for_gtk_widget(widget); if (!window) { return; } window->OnUnrealize(); } static void size_allocate_cb(GtkWidget* widget, GtkAllocation* allocation) { RefPtr 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 window = get_window_for_gtk_widget(widget); if (!window) { return; } window->UpdateTopLevelOpaqueRegion(); } static gboolean delete_event_cb(GtkWidget* widget, GdkEventAny* event) { RefPtr 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 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 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 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( gdk_event_copy(reinterpret_cast(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 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 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 window = GetFirstNSWindowForGDKWindow(event->window); if (!window) return FALSE; window->OnButtonReleaseEvent(event); return TRUE; } static gboolean focus_in_event_cb(GtkWidget* widget, GdkEventFocus* event) { RefPtr 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 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(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 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 focusWindow = gFocusWindow ? gFocusWindow : window; return focusWindow->OnKeyReleaseEvent(event); } static gboolean property_notify_event_cb(GtkWidget* aWidget, GdkEventProperty* aEvent) { RefPtr 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 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(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 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 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 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 window = get_window_for_gtk_widget(widget); if (!window) { return; } window->OnCompositedChanged(); } static void scale_changed_cb(GtkWidget* widget, GParamSpec* aPSpec, gpointer aPointer) { RefPtr window = get_window_for_gtk_widget(widget); if (!window) { return; } window->OnScaleChanged(); } static gboolean touch_event_cb(GtkWidget* aWidget, GdkEventTouch* aEvent) { UpdateLastInputEventTime(aEvent); RefPtr 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(aEvent); RefPtr 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 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 innerMostWindow = get_window_for_gdk_window(innerWindow); if (!innerMostWindow) { innerMostWindow = window; } LOGDRAG("WindowDragMotionHandler target nsWindow [%p]", innerMostWindow.get()); RefPtr 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 window = get_window_for_gtk_widget(aWidget); if (!window) { LOGDRAG(" Failed - can't find nsWindow!\n"); return; } RefPtr 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 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 innerMostWindow = get_window_for_gdk_window(innerWindow); if (!innerMostWindow) { innerMostWindow = window; } LOGDRAG("WindowDragDropHandler nsWindow [%p]", innerMostWindow.get()); RefPtr 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 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& aCommands) { // Validate the arguments. if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) { return false; } Maybe writingMode; if (aEvent.NeedsToRemapNavigationKey()) { if (RefPtr dispatcher = GetTextEventDispatcher()) { writingMode = dispatcher->MaybeQueryWritingModeAtSelection(); } } NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType); keyBindings->GetEditCommands(aEvent, writingMode, aCommands); return true; } already_AddRefed 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); 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) { 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; } 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)); if (aState) { // 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), gtk_fixed_new()); } else { gtk_window_set_titlebar(GTK_WINDOW(mShell), 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(gtk_widget_get_window(mShell)), "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); } mDrawInTitlebar = aState; 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() { // We depend on notify::scale-factor callback which is reliable for toplevel // windows only, so don't use scale cache for popup windows. if (mWindowType == WindowType::TopLevel && !mWindowScaleFactorChanged) { return mWindowScaleFactor; } GdkWindow* scaledGdkWindow = nullptr; if (GdkIsWaylandDisplay()) { // For popup windows/dialogs with parent window we need to get scale factor // of the topmost window. Otherwise the scale factor of the popup is // not updated during it's hidden. if (mWindowType == WindowType::Popup || mWindowType == WindowType::Dialog) { // Get toplevel window for scale factor: GtkWindow* topmostParentWindow = GetCurrentTopmostWindow(); if (topmostParentWindow) { scaledGdkWindow = gtk_widget_get_window(GTK_WIDGET(topmostParentWindow)); } else { NS_WARNING("Popup/Dialog has no parent."); } } } // Fallback for windows which parent has been unrealized. if (!scaledGdkWindow) { scaledGdkWindow = mGdkWindow; } if (scaledGdkWindow) { mWindowScaleFactor = gdk_window_get_scale_factor(scaledGdkWindow); mWindowScaleFactorChanged = false; } else { mWindowScaleFactor = ScreenHelperGTK::GetGTKMonitorScaleFactor(); } return mWindowScaleFactor; } bool nsWindow::UseFractionalScale() const { #ifdef MOZ_WAYLAND return GdkIsWaylandDisplay() && StaticPrefs::widget_wayland_fractional_buffer_scale_AtStartup() > 0 && WaylandDisplayGet()->GetViewporter(); #else return false; #endif } double nsWindow::FractionalScaleFactor() { #ifdef MOZ_WAYLAND if (UseFractionalScale()) { double scale = StaticPrefs::widget_wayland_fractional_buffer_scale_AtStartup(); scale = std::max(scale, 0.5); scale = std::min(scale, 8.0); return 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 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(&event); touchpad_event->type = GDK_TOUCHPAD_PINCH; const ScreenIntPoint widgetToScreenOffset = ViewAs( WidgetToScreenOffset(), PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent); ScreenPoint pointInWindow = ViewAs( 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(gtk_widget_get_window(mShell)), 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(gtk_widget_get_window(mShell), 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::CreateTopLevelWindow() { nsCOMPtr window = new nsWindow(); return window.forget(); } already_AddRefed nsIWidget::CreateChildWindow() { nsCOMPtr 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 window(reinterpret_cast(data)); WidgetMouseEvent event(true, eMouseMove, window, WidgetMouseEvent::eReal); event.mRefPoint = window->GetNativePointerLockCenter(); event.mRefPoint.x += wl_fixed_to_int(dx_w); event.mRefPoint.y += wl_fixed_to_int(dy_w); 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(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 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 void nsWindow::SetEGLNativeWindowSize( const LayoutDeviceIntSize& aEGLWindowSize) { if (!mContainer || !GdkIsWaylandDisplay()) { return; } if (moz_container_wayland_egl_window_needs_size_update( mContainer, aEGLWindowSize.ToUnknownSize(), GdkCeiledScaleFactor())) { LOG("nsWindow::SetEGLNativeWindowSize() %d x %d", aEGLWindowSize.width, aEGLWindowSize.height); moz_container_wayland_egl_window_set_size(mContainer, aEGLWindowSize.ToUnknownSize()); moz_container_wayland_set_scale_factor(mContainer); } } #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 (mWindowType != WindowType::TopLevel) { 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); } } }