Bug 873923, add a method to allow popups to be anchored at a rectangle, r=tn,mark,khuey
authorNeil Deakin <enndeakin@gmail.com>
Fri, 08 May 2015 14:49:54 -0400
changeset 244912 3034d392919dd5d1028bddd60de5cb4968f6d3db
parent 244911 eabe83ede1e3cd132eaf049ea5f53ce927c0b19f
child 244913 810c13b0ac029e2463722eb74834342d8045ce2f
push id28792
push usercbook@mozilla.com
push dateThu, 21 May 2015 12:56:47 +0000
treeherdermozilla-central@e5d4a9bc3e23 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstn, mark, khuey
bugs873923
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 873923, add a method to allow popups to be anchored at a rectangle, r=tn,mark,khuey
dom/webidl/PopupBoxObject.webidl
layout/xul/PopupBoxObject.cpp
layout/xul/PopupBoxObject.h
layout/xul/nsMenuPopupFrame.cpp
layout/xul/nsMenuPopupFrame.h
layout/xul/nsXULPopupManager.cpp
layout/xul/nsXULPopupManager.h
toolkit/content/widgets/popup.xml
--- a/dom/webidl/PopupBoxObject.webidl
+++ b/dom/webidl/PopupBoxObject.webidl
@@ -125,16 +125,31 @@ interface PopupBoxObject : BoxObject
    * @param y vertical screen position
    * @param triggerEvent the event that triggered this popup (mouse click for example)
    */
   void openPopupAtScreen(long x, long y,
                          boolean isContextMenu,
                          Event? triggerEvent);
 
   /**
+   * Open the popup anchored at a specific screen rectangle. This function is
+   * similar to openPopup except that that rectangle of the anchor is supplied
+   * rather than an element. The anchor rectangle arguments are screen
+   * coordinates.
+   */
+  void openPopupAtScreenRect(optional DOMString position = "",
+                             long x,
+                             long y,
+                             long width,
+                             long height,
+                             boolean isContextMenu,
+                             boolean attributesOverride,
+                             Event? triggerEvent);
+
+  /**
    * Returns the state of the popup:
    *   closed - the popup is closed
    *   open - the popup is open
    *   showing - the popup is in the process of being shown
    *   hiding - the popup is in the process of being hidden
    */
   readonly attribute DOMString popupState;
 
--- a/layout/xul/PopupBoxObject.cpp
+++ b/layout/xul/PopupBoxObject.cpp
@@ -105,16 +105,32 @@ PopupBoxObject::OpenPopupAtScreen(int32_
                                   Event* aTriggerEvent)
 {
   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
   if (pm && mContent)
     pm->ShowPopupAtScreen(mContent, aXPos, aYPos, aIsContextMenu, aTriggerEvent);
 }
 
 void
+PopupBoxObject::OpenPopupAtScreenRect(const nsAString& aPosition,
+                                      int32_t aXPos, int32_t aYPos,
+                                      int32_t aWidth, int32_t aHeight,
+                                      bool aIsContextMenu,
+                                      bool aAttributesOverride,
+                                      Event* aTriggerEvent)
+{
+  nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+  if (pm && mContent) {
+    pm->ShowPopupAtScreenRect(mContent, aPosition,
+                              nsIntRect(aXPos, aYPos, aWidth, aHeight),
+                              aIsContextMenu, aAttributesOverride, aTriggerEvent);
+  }
+}
+
+void
 PopupBoxObject::MoveTo(int32_t aLeft, int32_t aTop)
 {
   nsMenuPopupFrame *menuPopupFrame = mContent ? do_QueryFrame(mContent->GetPrimaryFrame()) : nullptr;
   if (menuPopupFrame) {
     menuPopupFrame->MoveTo(aLeft, aTop, true);
   }
 }
 
--- a/layout/xul/PopupBoxObject.h
+++ b/layout/xul/PopupBoxObject.h
@@ -70,16 +70,23 @@ public:
                  bool aIsContextMenu, bool aAttributesOverride,
                  Event* aTriggerEvent);
 
   void OpenPopupAtScreen(int32_t aXPos,
                          int32_t aYPos,
                          bool aIsContextMenu,
                          Event* aTriggerEvent);
 
+  void OpenPopupAtScreenRect(const nsAString& aPosition,
+                             int32_t aXPos, int32_t aYPos,
+                             int32_t aWidth, int32_t aHeight,
+                             bool aIsContextMenu,
+                             bool aAttributesOverride,
+                             Event* aTriggerEvent);
+
   void GetPopupState(nsString& aState);
 
   nsINode* GetTriggerNode() const;
 
   Element* GetAnchorNode() const;
 
   already_AddRefed<DOMRect> GetOuterScreenRect();
 
--- a/layout/xul/nsMenuPopupFrame.cpp
+++ b/layout/xul/nsMenuPopupFrame.cpp
@@ -100,17 +100,18 @@ nsMenuPopupFrame::nsMenuPopupFrame(nsSty
   mAdjustOffsetForContextMenu(false),
   mGeneratedChildren(false),
   mMenuCanOverlapOSBar(false),
   mShouldAutoPosition(true),
   mInContentShell(true),
   mIsMenuLocked(false),
   mMouseTransparent(false),
   mHFlip(false),
-  mVFlip(false)
+  mVFlip(false),
+  mAnchorType(MenuPopupAnchorType_Node)
 {
   // the preference name is backwards here. True means that the 'top' level is
   // the default, and false means that the 'parent' level is the default.
   if (sDefaultLevelIsTop >= 0)
     return;
   sDefaultLevelIsTop =
     Preferences::GetBool("ui.panel.default_level_parent", false);
   Preferences::AddUintVarCache(&sTimeoutOfIncrementalSearch,
@@ -616,34 +617,37 @@ nsMenuPopupFrame::InitPositionFromAnchor
   mPosition = POPUPPOSITION_UNKNOWN;
 }
 
 void
 nsMenuPopupFrame::InitializePopup(nsIContent* aAnchorContent,
                                   nsIContent* aTriggerContent,
                                   const nsAString& aPosition,
                                   int32_t aXPos, int32_t aYPos,
+                                  MenuPopupAnchorType aAnchorType,
                                   bool aAttributesOverride)
 {
   EnsureWidget();
 
   mPopupState = ePopupShowing;
   mAnchorContent = aAnchorContent;
   mTriggerContent = aTriggerContent;
   mXPos = aXPos;
   mYPos = aYPos;
   mAdjustOffsetForContextMenu = false;
   mVFlip = false;
   mHFlip = false;
   mAlignmentOffset = 0;
 
+  mAnchorType = aAnchorType;
+
   // if aAttributesOverride is true, then the popupanchor, popupalign and
   // position attributes on the <popup> override those values passed in.
   // If false, those attributes are only used if the values passed in are empty
-  if (aAnchorContent) {
+  if (aAnchorContent || aAnchorType == MenuPopupAnchorType_Rect) {
     nsAutoString anchor, align, position, flip;
     mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::popupanchor, anchor);
     mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::popupalign, align);
     mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::position, position);
     mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::flip, flip);
 
     if (aAttributesOverride) {
       // if the attributes are set, clear the offset position. Otherwise,
@@ -725,57 +729,69 @@ nsMenuPopupFrame::InitializePopup(nsICon
       // horizontal position as the mouse pointer.
       mYPos += 21;
     }
     else {
       InitPositionFromAnchorAlign(anchor, align);
     }
   }
 
-  mScreenXPos = -1;
-  mScreenYPos = -1;
+  mScreenRect = nsIntRect(-1, -1, 0, 0);
 
   if (aAttributesOverride) {
     // Use |left| and |top| dimension attributes to position the popup if
     // present, as they may have been persisted. 
     nsAutoString left, top;
     mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::left, left);
     mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::top, top);
 
     nsresult err;
     if (!left.IsEmpty()) {
       int32_t x = left.ToInteger(&err);
       if (NS_SUCCEEDED(err))
-        mScreenXPos = x;
+        mScreenRect.x = x;
     }
     if (!top.IsEmpty()) {
       int32_t y = top.ToInteger(&err);
       if (NS_SUCCEEDED(err))
-        mScreenYPos = y;
+        mScreenRect.y = y;
     }
   }
 }
 
 void
 nsMenuPopupFrame::InitializePopupAtScreen(nsIContent* aTriggerContent,
                                           int32_t aXPos, int32_t aYPos,
                                           bool aIsContextMenu)
 {
   EnsureWidget();
 
   mPopupState = ePopupShowing;
   mAnchorContent = nullptr;
   mTriggerContent = aTriggerContent;
-  mScreenXPos = aXPos;
-  mScreenYPos = aYPos;
+  mScreenRect = nsIntRect(aXPos, aYPos, 0, 0);
+  mXPos = 0;
+  mYPos = 0;
   mFlip = FlipType_Default;
   mPopupAnchor = POPUPALIGNMENT_NONE;
   mPopupAlignment = POPUPALIGNMENT_NONE;
   mIsContextMenu = aIsContextMenu;
   mAdjustOffsetForContextMenu = aIsContextMenu;
+  mAnchorType = MenuPopupAnchorType_Point;
+}
+
+void
+nsMenuPopupFrame::InitializePopupAtRect(nsIContent* aTriggerContent,
+                                        const nsAString& aPosition,
+                                        const nsIntRect& aRect,
+                                        bool aAttributesOverride)
+{
+  InitializePopup(nullptr, aTriggerContent, aPosition, 0, 0,
+                  MenuPopupAnchorType_Rect, aAttributesOverride);
+  mScreenRect = aRect;
 }
 
 void
 nsMenuPopupFrame::InitializePopupWithAnchorAlign(nsIContent* aAnchorContent,
                                                  nsAString& aAnchor,
                                                  nsAString& aAlign,
                                                  int32_t aXPos, int32_t aYPos)
 {
@@ -785,28 +801,28 @@ nsMenuPopupFrame::InitializePopupWithAnc
   mAdjustOffsetForContextMenu = false;
   mFlip = FlipType_Default;
 
   // this popup opening function is provided for backwards compatibility
   // only. It accepts either coordinates or an anchor and alignment value
   // but doesn't use both together.
   if (aXPos == -1 && aYPos == -1) {
     mAnchorContent = aAnchorContent;
-    mScreenXPos = -1;
-    mScreenYPos = -1;
+    mAnchorType = MenuPopupAnchorType_Node;
+    mScreenRect = nsIntRect(-1, -1, 0, 0);
     mXPos = 0;
     mYPos = 0;
     InitPositionFromAnchorAlign(aAnchor, aAlign);
   }
   else {
     mAnchorContent = nullptr;
+    mAnchorType = MenuPopupAnchorType_Point;
     mPopupAnchor = POPUPALIGNMENT_NONE;
     mPopupAlignment = POPUPALIGNMENT_NONE;
-    mScreenXPos = aXPos;
-    mScreenYPos = aYPos;
+    mScreenRect = nsIntRect(aXPos, aYPos, 0, 0);
     mXPos = aXPos;
     mYPos = aYPos;
   }
 }
 
 void
 nsMenuPopupFrame::ShowPopup(bool aIsContextMenu, bool aSelectFirstItem)
 {
@@ -1206,94 +1222,111 @@ nsMenuPopupFrame::SetPopupPosition(nsIFr
   }
 
   nsPresContext* presContext = PresContext();
   nsIFrame* rootFrame = presContext->PresShell()->FrameManager()->GetRootFrame();
   NS_ASSERTION(rootFrame->GetView() && GetView() &&
                rootFrame->GetView() == GetView()->GetParent(),
                "rootFrame's view is not our view's parent???");
 
-  // if the frame is not specified, use the anchor node passed to OpenPopup. If
-  // that wasn't specified either, use the root frame. Note that mAnchorContent
-  // might be a different document so its presshell must be used.
-  if (!aAnchorFrame) {
-    if (mAnchorContent) {
-      aAnchorFrame = mAnchorContent->GetPrimaryFrame();
+  // For anchored popups, the anchor rectangle. For non-anchored popups, the
+  // size will be 0.
+  nsRect anchorRect;
+
+  // Width of the parent, used when aSizedToPopup is true.
+  int32_t parentWidth;
+
+  bool anchored = IsAnchored();
+  if (anchored || aSizedToPopup) {
+    // In order to deal with transforms, we need the root prescontext:
+    nsPresContext* rootPresContext = presContext->GetRootPresContext();
+
+    // If we can't reach a root pres context, don't bother continuing:
+    if (!rootPresContext) {
+      return NS_OK;
     }
 
-    if (!aAnchorFrame) {
-      aAnchorFrame = rootFrame;
-      if (!aAnchorFrame)
-        return NS_OK;
+    // If anchored to a rectangle, use that rectangle. Otherwise, determine the
+    // rectangle from the anchor.
+    if (mAnchorType == MenuPopupAnchorType_Rect) {
+      anchorRect = ToAppUnits(mScreenRect, presContext->AppUnitsPerCSSPixel());
     }
-  }
+    else {
+      // if the frame is not specified, use the anchor node passed to OpenPopup. If
+      // that wasn't specified either, use the root frame. Note that mAnchorContent
+      // might be a different document so its presshell must be used.
+      if (!aAnchorFrame) {
+        if (mAnchorContent) {
+          aAnchorFrame = mAnchorContent->GetPrimaryFrame();
+        }
 
-  // In order to deal with transforms, we need the root prescontext:
-  nsPresContext* rootPresContext = presContext->GetRootPresContext();
-
-  // If we can't reach a root pres context, don't bother continuing:
-  if (!rootPresContext) {
-    return NS_OK;
-  }
+        if (!aAnchorFrame) {
+          aAnchorFrame = rootFrame;
+          if (!aAnchorFrame)
+            return NS_OK;
+        }
+      }
 
-  // And then we need its root frame for a reference
-  nsIFrame* referenceFrame = rootPresContext->FrameManager()->GetRootFrame();
+      // And then we need its root frame for a reference
+      nsIFrame* referenceFrame = rootPresContext->FrameManager()->GetRootFrame();
 
-  // the dimensions of the anchor
-  nsRect parentRect = aAnchorFrame->GetRectRelativeToSelf();
-  // Relative to the root
-  parentRect = nsLayoutUtils::TransformFrameRectToAncestor(aAnchorFrame,
-                                                           parentRect,
-                                                           referenceFrame);
-  // Relative to the screen
-  parentRect.MoveBy(referenceFrame->GetScreenRectInAppUnits().TopLeft());
-  // In its own app units
-  parentRect =
-    parentRect.ScaleToOtherAppUnitsRoundOut(rootPresContext->AppUnitsPerDevPixel(),
-                                            presContext->AppUnitsPerDevPixel());
+      // the dimensions of the anchor
+      nsRect parentRect = aAnchorFrame->GetRectRelativeToSelf();
+      // Relative to the root
+      anchorRect = nsLayoutUtils::TransformFrameRectToAncestor(aAnchorFrame,
+                                                               parentRect,
+                                                               referenceFrame);
+      // Relative to the screen
+      anchorRect.MoveBy(referenceFrame->GetScreenRectInAppUnits().TopLeft());
+
+      // In its own app units
+      anchorRect =
+        anchorRect.ScaleToOtherAppUnitsRoundOut(rootPresContext->AppUnitsPerDevPixel(),
+                                                presContext->AppUnitsPerDevPixel());
+    }
+
+    // The width is needed when aSizedToPopup is true
+    parentWidth = anchorRect.width;
+  }
 
   // Set the popup's size to the preferred size. Below, this size will be
   // adjusted to fit on the screen or within the content area. If the anchor
   // is sized to the popup, use the anchor's width instead of the preferred
   // width. The preferred size should already be set by the parent frame.
   NS_ASSERTION(mPrefSize.width >= 0 || mPrefSize.height >= 0,
                "preferred size of popup not set");
-  mRect.width = aSizedToPopup ? parentRect.width : mPrefSize.width;
+  mRect.width = aSizedToPopup ? parentWidth : mPrefSize.width;
   mRect.height = mPrefSize.height;
 
   // the screen position in app units where the popup should appear
   nsPoint screenPoint;
 
-  // For anchored popups, the anchor rectangle. For non-anchored popups, the
-  // size will be 0.
-  nsRect anchorRect = parentRect;
-
   // indicators of whether the popup should be flipped or resized.
   FlipStyle hFlip = FlipStyle_None, vFlip = FlipStyle_None;
 
   nsMargin margin(0, 0, 0, 0);
   StyleMargin()->GetMargin(margin);
 
   // the screen rectangle of the root frame, in dev pixels.
   nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits();
 
   nsDeviceContext* devContext = presContext->DeviceContext();
   nscoord offsetForContextMenu = 0;
 
   bool isNoAutoHide = IsNoAutoHide();
   nsPopupLevel popupLevel = PopupLevel(isNoAutoHide);
 
-  if (IsAnchored()) {
+  if (anchored) {
     // if we are anchored, there are certain things we don't want to do when
     // repositioning the popup to fit on the screen, such as end up positioned
     // over the anchor, for instance a popup appearing over the menu label.
     // When doing this reposition, we want to move the popup to the side with
     // the most room. The combination of anchor and alignment dictate if we 
     // readjust above/below or to the left/right.
-    if (mAnchorContent) {
+    if (mAnchorContent || mAnchorType == MenuPopupAnchorType_Rect) {
       // move the popup according to the anchor and alignment. This will also
       // tell us which axis the popup is flush against in case we have to move
       // it around later. The AdjustPositionForAnchorAlign method accounts for
       // the popup's margin.
       screenPoint = AdjustPositionForAnchorAlign(anchorRect, hFlip, vFlip);
     }
     else {
       // with no anchor, the popup is positioned relative to the root frame
@@ -1316,44 +1349,46 @@ nsMenuPopupFrame::SetPopupPosition(nsIFr
     screenPoint.y += anchorYOffset;
     anchorRect.y += anchorYOffset;
 
     // If this is a noautohide popup, set the screen coordinates of the popup.
     // This way, the popup stays at the location where it was opened even when
     // the window is moved. Popups at the parent level follow the parent
     // window as it is moved and remained anchored, so we want to maintain the
     // anchoring instead.
-    if (isNoAutoHide && popupLevel != ePopupLevelParent) {
+    if (isNoAutoHide &&
+        (popupLevel != ePopupLevelParent || mAnchorType == MenuPopupAnchorType_Rect)) {
       // Account for the margin that will end up being added to the screen coordinate
       // the next time SetPopupPosition is called.
-      mScreenXPos = presContext->AppUnitsToIntCSSPixels(screenPoint.x - margin.left);
-      mScreenYPos = presContext->AppUnitsToIntCSSPixels(screenPoint.y - margin.top);
+      mAnchorType = MenuPopupAnchorType_Point;
+      mScreenRect.x = presContext->AppUnitsToIntCSSPixels(screenPoint.x - margin.left);
+      mScreenRect.y = presContext->AppUnitsToIntCSSPixels(screenPoint.y - margin.top);
     }
   }
   else {
-    // the popup is positioned at a screen coordinate.
-    // first convert the screen position in mScreenXPos and mScreenYPos from
-    // CSS pixels into device pixels, ignoring any zoom as mScreenXPos and
-    // mScreenYPos are unzoomed screen coordinates.
+    // The popup is positioned at a screen coordinate.
+    // First convert the screen position in mScreenRect from CSS pixels into
+    // device pixels, ignoring any zoom as mScreenRect holds unzoomed screen
+    // coordinates.
     int32_t factor = devContext->AppUnitsPerDevPixelAtUnitFullZoom();
 
     // context menus should be offset by two pixels so that they don't appear
     // directly where the cursor is. Otherwise, it is too easy to have the
     // context menu close up again.
     if (mAdjustOffsetForContextMenu) {
       int32_t offsetForContextMenuDev =
         nsPresContext::CSSPixelsToAppUnits(CONTEXT_MENU_OFFSET_PIXELS) / factor;
       offsetForContextMenu = presContext->DevPixelsToAppUnits(offsetForContextMenuDev);
     }
 
     // next, convert into app units accounting for the zoom
     screenPoint.x = presContext->DevPixelsToAppUnits(
-                      nsPresContext::CSSPixelsToAppUnits(mScreenXPos) / factor);
+                      nsPresContext::CSSPixelsToAppUnits(mScreenRect.x) / factor);
     screenPoint.y = presContext->DevPixelsToAppUnits(
-                      nsPresContext::CSSPixelsToAppUnits(mScreenYPos) / factor);
+                      nsPresContext::CSSPixelsToAppUnits(mScreenRect.y) / factor);
     anchorRect = nsRect(screenPoint, nsSize(0, 0));
 
     // add the margins on the popup
     screenPoint.MoveBy(margin.left + offsetForContextMenu,
                        margin.top + offsetForContextMenu);
 
     // screen positioned popups can be flipped vertically but never horizontally
     vFlip = FlipStyle_Outside;
@@ -1443,17 +1478,17 @@ nsMenuPopupFrame::SetPopupPosition(nsIFr
     MoveViewTo(view, viewPoint.x, viewPoint.y);
 
   // Now that we've positioned the view, sync up the frame's origin.
   nsBoxFrame::SetPosition(viewPoint - GetParent()->GetOffsetTo(rootFrame));
 
   if (aSizedToPopup) {
     nsBoxLayoutState state(PresContext());
     // XXXndeakin can parentSize.width still extend outside?
-    SetBounds(state, nsRect(mRect.x, mRect.y, parentRect.width, mRect.height));
+    SetBounds(state, nsRect(mRect.x, mRect.y, parentWidth, mRect.height));
   }
 
   return NS_OK;
 }
 
 /* virtual */ nsMenuFrame*
 nsMenuPopupFrame::GetCurrentMenuItem()
 {
@@ -2076,17 +2111,17 @@ nsMenuPopupFrame::DestroyFrom(nsIFrame* 
   nsBoxFrame::DestroyFrom(aDestructRoot);
 }
 
 
 void
 nsMenuPopupFrame::MoveTo(int32_t aLeft, int32_t aTop, bool aUpdateAttrs)
 {
   nsIWidget* widget = GetWidget();
-  if ((mScreenXPos == aLeft && mScreenYPos == aTop) &&
+  if ((mScreenRect.x == aLeft && mScreenRect.y == aTop) &&
       (!widget || widget->GetClientOffset() == mLastClientOffset)) {
     return;
   }
 
   // reposition the popup at the specified coordinates. Don't clear the anchor
   // and position, because the popup can be reset to its anchor position by
   // using (-1, -1) as coordinates. Subtract off the margin as it will be
   // added to the position when SetPopupPosition is called.
@@ -2097,18 +2132,20 @@ nsMenuPopupFrame::MoveTo(int32_t aLeft, 
   if (mAdjustOffsetForContextMenu) {
     nscoord offsetForContextMenu =
       nsPresContext::CSSPixelsToAppUnits(CONTEXT_MENU_OFFSET_PIXELS);
     margin.left += offsetForContextMenu;
     margin.top += offsetForContextMenu;
   }
 
   nsPresContext* presContext = PresContext();
-  mScreenXPos = aLeft - presContext->AppUnitsToIntCSSPixels(margin.left);
-  mScreenYPos = aTop - presContext->AppUnitsToIntCSSPixels(margin.top);
+  mAnchorType = aLeft == -1 || aTop == -1 ?
+                MenuPopupAnchorType_Node : MenuPopupAnchorType_Point;
+  mScreenRect.x = aLeft - presContext->AppUnitsToIntCSSPixels(margin.left);
+  mScreenRect.y = aTop - presContext->AppUnitsToIntCSSPixels(margin.top);
 
   SetPopupPosition(nullptr, true, false);
 
   nsCOMPtr<nsIContent> popup = mContent;
   if (aUpdateAttrs && (popup->HasAttr(kNameSpaceID_None, nsGkAtoms::left) ||
                        popup->HasAttr(kNameSpaceID_None, nsGkAtoms::top)))
   {
     nsAutoString left, top;
@@ -2124,17 +2161,17 @@ nsMenuPopupFrame::MoveToAnchor(nsIConten
                                const nsAString& aPosition,
                                int32_t aXPos, int32_t aYPos,
                                bool aAttributesOverride)
 {
   NS_ASSERTION(IsVisible(), "popup must be visible to move it");
 
   nsPopupState oldstate = mPopupState;
   InitializePopup(aAnchorContent, mTriggerContent, aPosition,
-                  aXPos, aYPos, aAttributesOverride);
+                  aXPos, aYPos, MenuPopupAnchorType_Node, aAttributesOverride);
   // InitializePopup changed the state so reset it.
   mPopupState = oldstate;
 
   // Pass false here so that flipping and adjusting to fit on the screen happen.
   SetPopupPosition(nullptr, false, false);
 }
 
 bool
--- a/layout/xul/nsMenuPopupFrame.h
+++ b/layout/xul/nsMenuPopupFrame.h
@@ -81,16 +81,22 @@ enum FlipStyle {
 // Values for the flip attribute
 enum FlipType {
   FlipType_Default = 0,
   FlipType_None = 1,    // don't try to flip or translate to stay onscreen
   FlipType_Both = 2,    // flip in both directions
   FlipType_Slide = 3    // allow the arrow to "slide" instead of resizing
 };
 
+enum MenuPopupAnchorType {
+  MenuPopupAnchorType_Node = 0, // anchored to a node
+  MenuPopupAnchorType_Point = 1, // unanchored and positioned at a screen point
+  MenuPopupAnchorType_Rect = 2, // anchored at a screen rectangle
+};
+
 // values are selected so that the direction can be flipped just by
 // changing the sign
 #define POPUPALIGNMENT_NONE 0
 #define POPUPALIGNMENT_TOPLEFT 1
 #define POPUPALIGNMENT_TOPRIGHT -1
 #define POPUPALIGNMENT_BOTTOMLEFT 2
 #define POPUPALIGNMENT_BOTTOMRIGHT -2
 
@@ -237,21 +243,21 @@ public:
   virtual bool IsLeaf() const override;
 
   // layout, position and display the popup as needed
   void LayoutPopup(nsBoxLayoutState& aState, nsIFrame* aParentMenu,
                    nsIFrame* aAnchor, bool aSizedToPopup);
 
   nsView* GetRootViewForPopup(nsIFrame* aStartFrame);
 
-  // set the position of the popup either relative to the anchor aAnchorFrame
-  // (or the frame for mAnchorContent if aAnchorFrame is null) or at a specific
-  // point if a screen position (mScreenXPos and mScreenYPos) are set. The popup
-  // will be adjusted so that it is on screen. If aIsMove is true, then the popup
-  // is being moved, and should not be flipped.
+  // Set the position of the popup either relative to the anchor aAnchorFrame
+  // (or the frame for mAnchorContent if aAnchorFrame is null), anchored at a
+  // rectangle, or at a specific point if a screen position is set. The popup
+  // will be adjusted so that it is on screen. If aIsMove is true, then the
+  // popup is being moved, and should not be flipped.
   nsresult SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove, bool aSizedToPopup);
 
   bool HasGeneratedChildren() { return mGeneratedChildren; }
   void SetGeneratedChildren() { mGeneratedChildren = true; }
 
   // called when the Enter key is pressed while the popup is open. This will
   // just pass the call down to the current menu, if any. If a current menu
   // should be opened as a result, this method should return the frame for
@@ -278,18 +284,24 @@ public:
   bool IsInContentShell() { return mInContentShell; }
 
   // the Initialize methods are used to set the anchor position for
   // each way of opening a popup.
   void InitializePopup(nsIContent* aAnchorContent,
                        nsIContent* aTriggerContent,
                        const nsAString& aPosition,
                        int32_t aXPos, int32_t aYPos,
+                       MenuPopupAnchorType aAnchorType,
                        bool aAttributesOverride);
 
+  void InitializePopupAtRect(nsIContent* aTriggerContent,
+                             const nsAString& aPosition,
+                             const nsIntRect& aRect,
+                             bool aAttributesOverride);
+
   /**
    * @param aIsContextMenu if true, then the popup is
    * positioned at a slight offset from aXPos/aYPos to ensure the
    * (presumed) mouse position is not over the menu.
    */
   void InitializePopupAtScreen(nsIContent* aTriggerContent,
                                int32_t aXPos, int32_t aYPos,
                                bool aIsContextMenu);
@@ -366,24 +378,24 @@ public:
   // Later, when bug 357725 is implemented, we can make this adjust aChange by
   // the amount that the side can be resized, so that minimums and maximums
   // can be taken into account.
   void CanAdjustEdges(int8_t aHorizontalSide,
                       int8_t aVerticalSide,
                       mozilla::LayoutDeviceIntPoint& aChange);
 
   // Return true if the popup is positioned relative to an anchor.
-  bool IsAnchored() const { return mScreenXPos == -1 && mScreenYPos == -1; }
+  bool IsAnchored() const { return mAnchorType != MenuPopupAnchorType_Point; }
 
   // Return the anchor if there is one.
   nsIContent* GetAnchor() const { return mAnchorContent; }
 
   // Return the screen coordinates of the popup, or (-1, -1) if anchored.
   // This position is in CSS pixels.
-  nsIntPoint ScreenPosition() const { return nsIntPoint(mScreenXPos, mScreenYPos); }
+  nsIntPoint ScreenPosition() const { return mScreenRect.TopLeft(); }
 
   nsIntPoint GetLastClientOffset() const { return mLastClientOffset; }
 
   // Return the alignment of the popup
   int8_t GetAlignmentPosition() const;
 
   // Return the offset applied to the alignment of the popup
   nscoord GetAlignmentOffset() const { return mAlignmentOffset; }
@@ -502,18 +514,17 @@ protected:
   // would be before resizing. Computations are performed using this size.
   nsSize mPrefSize;
 
   // The position of the popup, in CSS pixels.
   // The screen coordinates, if set to values other than -1,
   // override mXPos and mYPos.
   int32_t mXPos;
   int32_t mYPos;
-  int32_t mScreenXPos;
-  int32_t mScreenYPos;
+  nsIntRect mScreenRect;
 
   // If the panel prefers to "slide" rather than resize, then the arrow gets
   // positioned at this offset (along either the x or y axis, depending on
   // mPosition)
   nscoord mAlignmentOffset;
 
   // The value of the client offset of our widget the last time we positioned
   // ourselves. We store this so that we can detect when it changes but the
@@ -565,15 +576,18 @@ protected:
   bool mInContentShell; // True if the popup is in a content shell
   bool mIsMenuLocked; // Should events inside this menu be ignored?
   bool mMouseTransparent; // True if this is a popup is transparent to mouse events
 
   // the flip modes that were used when the popup was opened
   bool mHFlip;
   bool mVFlip;
 
+  // How the popup is anchored.
+  MenuPopupAnchorType mAnchorType;
+
   static int8_t sDefaultLevelIsTop;
 
   // If 0, never timed out.  Otherwise, the value is in milliseconds.
   static uint32_t sTimeoutOfIncrementalSearch;
 }; // class nsMenuPopupFrame
 
 #endif
--- a/layout/xul/nsXULPopupManager.cpp
+++ b/layout/xul/nsXULPopupManager.cpp
@@ -667,17 +667,18 @@ nsXULPopupManager::ShowMenu(nsIContent *
   nsAutoString position;
   if (onMenuBar || !onmenu)
     position.AssignLiteral("after_start");
   else
     position.AssignLiteral("end_before");
 
   // there is no trigger event for menus
   InitTriggerEvent(nullptr, nullptr, nullptr);
-  popupFrame->InitializePopup(menuFrame->GetAnchor(), nullptr, position, 0, 0, true);
+  popupFrame->InitializePopup(menuFrame->GetAnchor(), nullptr, position, 0, 0,
+                              MenuPopupAnchorType_Node, true);
 
   if (aAsynchronous) {
     nsCOMPtr<nsIRunnable> event =
       new nsXULPopupShowingEvent(popupFrame->GetContent(),
                                  parentIsContextMenu, aSelectFirstItem);
     NS_DispatchToCurrentThread(event);
   }
   else {
@@ -699,17 +700,17 @@ nsXULPopupManager::ShowPopup(nsIContent*
   nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
   if (!popupFrame || !MayShowPopup(popupFrame))
     return;
 
   nsCOMPtr<nsIContent> triggerContent;
   InitTriggerEvent(aTriggerEvent, aPopup, getter_AddRefs(triggerContent));
 
   popupFrame->InitializePopup(aAnchorContent, triggerContent, aPosition,
-                              aXPos, aYPos, aAttributesOverride);
+                              aXPos, aYPos, MenuPopupAnchorType_Node, aAttributesOverride);
 
   FirePopupShowingEvent(aPopup, aIsContextMenu, aSelectFirstItem);
 }
 
 void
 nsXULPopupManager::ShowPopupAtScreen(nsIContent* aPopup,
                                      int32_t aXPos, int32_t aYPos,
                                      bool aIsContextMenu,
@@ -722,16 +723,37 @@ nsXULPopupManager::ShowPopupAtScreen(nsI
   nsCOMPtr<nsIContent> triggerContent;
   InitTriggerEvent(aTriggerEvent, aPopup, getter_AddRefs(triggerContent));
 
   popupFrame->InitializePopupAtScreen(triggerContent, aXPos, aYPos, aIsContextMenu);
   FirePopupShowingEvent(aPopup, aIsContextMenu, false);
 }
 
 void
+nsXULPopupManager::ShowPopupAtScreenRect(nsIContent* aPopup,
+                                         const nsAString& aPosition,
+                                         const nsIntRect& aRect,
+                                         bool aIsContextMenu,
+                                         bool aAttributesOverride,
+                                         nsIDOMEvent* aTriggerEvent)
+{
+  nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
+  if (!popupFrame || !MayShowPopup(popupFrame))
+    return;
+
+  nsCOMPtr<nsIContent> triggerContent;
+  InitTriggerEvent(aTriggerEvent, aPopup, getter_AddRefs(triggerContent));
+
+  popupFrame->InitializePopupAtRect(triggerContent, aPosition,
+                                    aRect, aAttributesOverride);
+
+  FirePopupShowingEvent(aPopup, aIsContextMenu, false);
+}
+
+void
 nsXULPopupManager::ShowTooltipAtScreen(nsIContent* aPopup,
                                        nsIContent* aTriggerContent,
                                        int32_t aXPos, int32_t aYPos)
 {
   nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
   if (!popupFrame || !MayShowPopup(popupFrame))
     return;
 
--- a/layout/xul/nsXULPopupManager.h
+++ b/layout/xul/nsXULPopupManager.h
@@ -400,16 +400,26 @@ public:
    * offset from aXPos/aYPos to ensure that it is not under the mouse
    * cursor.
    */
   void ShowPopupAtScreen(nsIContent* aPopup,
                          int32_t aXPos, int32_t aYPos,
                          bool aIsContextMenu,
                          nsIDOMEvent* aTriggerEvent);
 
+  /* Open a popup anchored at a screen rectangle specified by aRect.
+   * The remaining arguments are similar to ShowPopup.
+   */
+  void ShowPopupAtScreenRect(nsIContent* aPopup,
+                             const nsAString& aPosition,
+                             const nsIntRect& aRect,
+                             bool aIsContextMenu,
+                             bool aAttributesOverride,
+                             nsIDOMEvent* aTriggerEvent);
+
   /**
    * Open a tooltip at a specific screen position specified by aXPos and aYPos,
    * measured in CSS pixels.
    *
    * This fires the popupshowing event synchronously.
    */
   void ShowTooltipAtScreen(nsIContent* aPopup,
                            nsIContent* aTriggerContent,
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -64,17 +64,38 @@
           try {
             var popupBox = this.popupBoxObject;
             if (popupBox)
               popupBox.openPopupAtScreen(aX, aY, aIsContextMenu, aTriggerEvent);
           } catch(e) {}
         ]]>
         </body>
       </method>
-      
+
+      <method name="openPopupAtScreenRect">
+        <parameter name="aPosition"/>
+        <parameter name="aX"/>
+        <parameter name="aY"/>
+        <parameter name="aWidth"/>
+        <parameter name="aHeight"/>
+        <parameter name="aIsContextMenu"/>
+        <parameter name="aAttributesOverride"/>
+        <parameter name="aTriggerEvent"/>
+        <body>
+        <![CDATA[
+          try {
+            var popupBox = this.popupBoxObject;
+            if (popupBox)
+              popupBox.openPopupAtScreenRect(aPosition, aX, aY, aWidth, aHeight,
+                                             aIsContextMenu, aAttributesOverride, aTriggerEvent);
+          } catch(e) {}
+        ]]>
+        </body>
+      </method>
+
       <method name="showPopup">
         <parameter name="element"/>
         <parameter name="xpos"/>
         <parameter name="ypos"/>
         <parameter name="popuptype"/>
         <parameter name="anchoralignment"/>
         <parameter name="popupalignment"/>
         <body>
@@ -89,17 +110,17 @@
           } catch(e) {}
           if (menuBox instanceof MenuBoxObject)
             menuBox.openMenu(true);
           else if (popupBox)
             popupBox.showPopup(element, this, xpos, ypos, popuptype, anchoralignment, popupalignment);
         ]]>
         </body>
       </method>
-      
+
       <method name="hidePopup">
         <parameter name="cancel"/>
         <body>
         <![CDATA[
           var popupBox = null;
           var menuBox = null;
           try {
             popupBox = this.popupBoxObject;