Bug 1856663 - Add more chunks for Android mochitest-plain. r=jmaher,taskgraph-reviewe...
[gecko.git] / layout / xul / nsMenuPopupFrame.cpp
blobca94156172af107beb4ccd76c09e525c634a815e
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "nsMenuPopupFrame.h"
8 #include "LayoutConstants.h"
9 #include "XULButtonElement.h"
10 #include "XULPopupElement.h"
11 #include "mozilla/dom/XULPopupElement.h"
12 #include "nsGkAtoms.h"
13 #include "nsIContent.h"
14 #include "nsIFrameInlines.h"
15 #include "nsAtom.h"
16 #include "nsPresContext.h"
17 #include "mozilla/ComputedStyle.h"
18 #include "nsCSSRendering.h"
19 #include "nsNameSpaceManager.h"
20 #include "nsIFrameInlines.h"
21 #include "nsViewManager.h"
22 #include "nsWidgetsCID.h"
23 #include "nsPIDOMWindow.h"
24 #include "nsFrameManager.h"
25 #include "mozilla/dom/Document.h"
26 #include "nsRect.h"
27 #include "nsIScrollableFrame.h"
28 #include "nsIPopupContainer.h"
29 #include "nsIDocShell.h"
30 #include "nsReadableUtils.h"
31 #include "nsUnicharUtils.h"
32 #include "nsLayoutUtils.h"
33 #include "nsContentUtils.h"
34 #include "nsCSSFrameConstructor.h"
35 #include "nsPIWindowRoot.h"
36 #include "nsIReflowCallback.h"
37 #include "nsIDocShellTreeOwner.h"
38 #include "nsIBaseWindow.h"
39 #include "nsISound.h"
40 #include "nsIScreenManager.h"
41 #include "nsServiceManagerUtils.h"
42 #include "nsStyleConsts.h"
43 #include "nsStyleStructInlines.h"
44 #include "nsTransitionManager.h"
45 #include "nsDisplayList.h"
46 #include "nsIDOMXULSelectCntrlEl.h"
47 #include "mozilla/widget/ScreenManager.h"
48 #include "mozilla/AnimationUtils.h"
49 #include "mozilla/BasePrincipal.h"
50 #include "mozilla/EventDispatcher.h"
51 #include "mozilla/EventStateManager.h"
52 #include "mozilla/Preferences.h"
53 #include "mozilla/LookAndFeel.h"
54 #include "mozilla/MouseEvents.h"
55 #include "mozilla/PresShell.h"
56 #include "mozilla/Services.h"
57 #include "mozilla/dom/BrowserParent.h"
58 #include "mozilla/dom/Element.h"
59 #include "mozilla/dom/Event.h"
60 #include "mozilla/dom/KeyboardEvent.h"
61 #include "mozilla/dom/KeyboardEventBinding.h"
62 #include <algorithm>
64 #include "X11UndefineNone.h"
65 #include "nsXULPopupManager.h"
67 using namespace mozilla;
68 using mozilla::dom::Document;
69 using mozilla::dom::Element;
70 using mozilla::dom::Event;
71 using mozilla::dom::XULButtonElement;
73 int8_t nsMenuPopupFrame::sDefaultLevelIsTop = -1;
75 TimeStamp nsMenuPopupFrame::sLastKeyTime;
77 #ifdef MOZ_WAYLAND
78 # include "mozilla/WidgetUtilsGtk.h"
79 # define IS_WAYLAND_DISPLAY() mozilla::widget::GdkIsWaylandDisplay()
80 extern mozilla::LazyLogModule gWidgetPopupLog;
81 # define LOG_WAYLAND(...) \
82 MOZ_LOG(gWidgetPopupLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
83 #else
84 # define IS_WAYLAND_DISPLAY() false
85 # define LOG_WAYLAND(...)
86 #endif
88 // NS_NewMenuPopupFrame
90 // Wrapper for creating a new menu popup container
92 nsIFrame* NS_NewMenuPopupFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
93 return new (aPresShell)
94 nsMenuPopupFrame(aStyle, aPresShell->GetPresContext());
97 NS_IMPL_FRAMEARENA_HELPERS(nsMenuPopupFrame)
99 NS_QUERYFRAME_HEAD(nsMenuPopupFrame)
100 NS_QUERYFRAME_ENTRY(nsMenuPopupFrame)
101 NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame)
104 // nsMenuPopupFrame ctor
106 nsMenuPopupFrame::nsMenuPopupFrame(ComputedStyle* aStyle,
107 nsPresContext* aPresContext)
108 : nsBlockFrame(aStyle, aPresContext, kClassID) {
109 // the preference name is backwards here. True means that the 'top' level is
110 // the default, and false means that the 'parent' level is the default.
111 if (sDefaultLevelIsTop >= 0) return;
112 sDefaultLevelIsTop =
113 Preferences::GetBool("ui.panel.default_level_parent", false);
114 } // ctor
116 nsMenuPopupFrame::~nsMenuPopupFrame() = default;
118 static bool IsMouseTransparent(const ComputedStyle& aStyle) {
119 // If pointer-events: none; is set on the popup, then the widget should
120 // ignore mouse events, passing them through to the content behind.
121 return aStyle.PointerEvents() == StylePointerEvents::None;
124 static nsIWidget::InputRegion ComputeInputRegion(const ComputedStyle& aStyle,
125 const nsPresContext& aPc) {
126 return {IsMouseTransparent(aStyle),
127 (aStyle.StyleUIReset()->mMozWindowInputRegionMargin.ToCSSPixels() *
128 aPc.CSSToDevPixelScale())
129 .Truncated()};
132 bool nsMenuPopupFrame::ShouldCreateWidgetUpfront() const {
133 if (mPopupType != PopupType::Menu) {
134 // Any panel with a type attribute, such as the autocomplete popup, is
135 // always generated right away.
136 return mContent->AsElement()->HasAttr(nsGkAtoms::type);
139 // Generate the widget up-front if the parent menu is a <menulist> unless its
140 // sizetopopup is set to "none".
141 return ShouldExpandToInflowParentOrAnchor();
144 void nsMenuPopupFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
145 nsIFrame* aPrevInFlow) {
146 nsBlockFrame::Init(aContent, aParent, aPrevInFlow);
148 CreatePopupView();
150 // XXX Hack. The popup's view should float above all other views,
151 // so we use the nsView::SetFloating() to tell the view manager
152 // about that constraint.
153 nsView* ourView = GetView();
154 nsViewManager* viewManager = ourView->GetViewManager();
155 viewManager->SetViewFloating(ourView, true);
157 const auto& el = PopupElement();
158 mPopupType = PopupType::Panel;
159 if (el.IsMenu()) {
160 mPopupType = PopupType::Menu;
161 } else if (el.IsXULElement(nsGkAtoms::tooltip)) {
162 mPopupType = PopupType::Tooltip;
165 if (PresContext()->IsChrome()) {
166 mInContentShell = false;
169 // Support incontentshell=false attribute to allow popups to be displayed
170 // outside of the content shell. Chrome only.
171 if (el.NodePrincipal()->IsSystemPrincipal()) {
172 if (el.GetXULBoolAttr(nsGkAtoms::incontentshell)) {
173 mInContentShell = true;
174 } else if (el.AttrValueIs(kNameSpaceID_None, nsGkAtoms::incontentshell,
175 nsGkAtoms::_false, eCaseMatters)) {
176 mInContentShell = false;
180 // To improve performance, create the widget for the popup if needed. Popups
181 // such as menus will create their widgets later when the popup opens.
183 // FIXME(emilio): Doing this up-front for all menupopups causes a bunch of
184 // assertions, while it's supposed to be just an optimization.
185 if (!ourView->HasWidget() && ShouldCreateWidgetUpfront()) {
186 CreateWidgetForView(ourView);
189 AddStateBits(NS_FRAME_IN_POPUP);
192 bool nsMenuPopupFrame::HasRemoteContent() const {
193 return !mInContentShell && mPopupType == PopupType::Panel &&
194 mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
195 nsGkAtoms::remote, nsGkAtoms::_true,
196 eIgnoreCase);
199 bool nsMenuPopupFrame::IsNoAutoHide() const {
200 // Panels with noautohide="true" don't hide when the mouse is clicked
201 // outside of them, or when another application is made active. Non-autohide
202 // panels cannot be used in content windows.
203 return !mInContentShell && mPopupType == PopupType::Panel &&
204 mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
205 nsGkAtoms::noautohide,
206 nsGkAtoms::_true, eIgnoreCase);
209 widget::PopupLevel nsMenuPopupFrame::GetPopupLevel(bool aIsNoAutoHide) const {
210 // The popup level is determined as follows, in this order:
211 // 1. non-panels (menus and tooltips) are always topmost
212 // 2. any specified level attribute
213 // 3. if a titlebar attribute is set, use the 'floating' level
214 // 4. if this is a noautohide panel, use the 'parent' level
215 // 5. use the platform-specific default level
217 // If this is not a panel, this is always a top-most popup.
218 if (mPopupType != PopupType::Panel) {
219 return PopupLevel::Top;
222 // If the level attribute has been set, use that.
223 static Element::AttrValuesArray strings[] = {
224 nsGkAtoms::top, nsGkAtoms::parent, nsGkAtoms::floating, nullptr};
225 switch (mContent->AsElement()->FindAttrValueIn(
226 kNameSpaceID_None, nsGkAtoms::level, strings, eCaseMatters)) {
227 case 0:
228 return PopupLevel::Top;
229 case 1:
230 return PopupLevel::Parent;
231 case 2:
232 return PopupLevel::Floating;
235 // Panels with titlebars most likely want to be floating popups.
236 if (mContent->AsElement()->HasAttr(nsGkAtoms::titlebar)) {
237 return PopupLevel::Floating;
240 // If this panel is a noautohide panel, the default is the parent level.
241 if (aIsNoAutoHide) {
242 return PopupLevel::Parent;
245 // Otherwise, the result depends on the platform.
246 return sDefaultLevelIsTop ? PopupLevel::Top : PopupLevel::Parent;
249 void nsMenuPopupFrame::PrepareWidget(bool aRecreate) {
250 nsView* ourView = GetView();
251 if (aRecreate) {
252 if (auto* widget = GetWidget()) {
253 // Widget's WebRender resources needs to be cleared before creating new
254 // widget.
255 widget->ClearCachedWebrenderResources();
257 ourView->DestroyWidget();
259 if (!ourView->HasWidget()) {
260 CreateWidgetForView(ourView);
261 } else {
262 PropagateStyleToWidget();
266 nsresult nsMenuPopupFrame::CreateWidgetForView(nsView* aView) {
267 // Create a widget for ourselves.
268 widget::InitData widgetData;
269 widgetData.mWindowType = widget::WindowType::Popup;
270 widgetData.mBorderStyle = widget::BorderStyle::Default;
271 widgetData.mForMenupopupFrame = true;
272 widgetData.mClipSiblings = true;
273 widgetData.mPopupHint = mPopupType;
274 widgetData.mNoAutoHide = IsNoAutoHide();
276 if (!mInContentShell) {
277 // A drag popup may be used for non-static translucent drag feedback
278 if (mPopupType == PopupType::Panel &&
279 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
280 nsGkAtoms::drag, eIgnoreCase)) {
281 widgetData.mIsDragPopup = true;
285 nsAutoString title;
286 if (widgetData.mNoAutoHide &&
287 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::titlebar,
288 nsGkAtoms::normal, eCaseMatters)) {
289 widgetData.mBorderStyle = widget::BorderStyle::Title;
291 mContent->AsElement()->GetAttr(nsGkAtoms::label, title);
292 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::close,
293 nsGkAtoms::_true, eCaseMatters)) {
294 widgetData.mBorderStyle =
295 widgetData.mBorderStyle | widget::BorderStyle::Close;
299 bool remote = HasRemoteContent();
301 const auto mode = nsLayoutUtils::GetFrameTransparency(this, this);
302 widgetData.mHasRemoteContent = remote;
303 widgetData.mTransparencyMode = mode;
304 widgetData.mPopupLevel = GetPopupLevel(widgetData.mNoAutoHide);
306 // Panels which have a parent level need a parent widget. This allows them to
307 // always appear in front of the parent window but behind other windows that
308 // should be in front of it.
309 nsCOMPtr<nsIWidget> parentWidget;
310 if (widgetData.mPopupLevel != PopupLevel::Top) {
311 nsCOMPtr<nsIDocShellTreeItem> dsti = PresContext()->GetDocShell();
312 if (!dsti) return NS_ERROR_FAILURE;
314 nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
315 dsti->GetTreeOwner(getter_AddRefs(treeOwner));
316 if (!treeOwner) return NS_ERROR_FAILURE;
318 nsCOMPtr<nsIBaseWindow> baseWindow(do_QueryInterface(treeOwner));
319 if (baseWindow) baseWindow->GetMainWidget(getter_AddRefs(parentWidget));
322 nsresult rv = aView->CreateWidgetForPopup(&widgetData, parentWidget);
323 if (NS_FAILED(rv)) {
324 return rv;
327 nsIWidget* widget = aView->GetWidget();
328 widget->SetTransparencyMode(mode);
330 PropagateStyleToWidget();
332 // most popups don't have a title so avoid setting the title if there isn't
333 // one
334 if (!title.IsEmpty()) {
335 widget->SetTitle(title);
338 return NS_OK;
341 void nsMenuPopupFrame::PropagateStyleToWidget(WidgetStyleFlags aFlags) const {
342 if (aFlags.isEmpty()) {
343 return;
346 nsIWidget* widget = GetWidget();
347 if (!widget) {
348 return;
351 if (aFlags.contains(WidgetStyle::ColorScheme)) {
352 widget->SetColorScheme(Some(LookAndFeel::ColorSchemeForFrame(this)));
354 if (aFlags.contains(WidgetStyle::InputRegion)) {
355 widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext()));
357 if (aFlags.contains(WidgetStyle::Opacity)) {
358 widget->SetWindowOpacity(StyleUIReset()->mWindowOpacity);
360 if (aFlags.contains(WidgetStyle::Shadow)) {
361 widget->SetWindowShadowStyle(GetShadowStyle());
363 if (aFlags.contains(WidgetStyle::Transform)) {
364 widget->SetWindowTransform(ComputeWidgetTransform());
368 bool nsMenuPopupFrame::IsMouseTransparent() const {
369 return ::IsMouseTransparent(*Style());
372 StyleWindowShadow nsMenuPopupFrame::GetShadowStyle() const {
373 StyleWindowShadow shadow = StyleUIReset()->mWindowShadow;
374 if (shadow != StyleWindowShadow::Default) {
375 return shadow;
378 switch (StyleDisplay()->EffectiveAppearance()) {
379 case StyleAppearance::Tooltip:
380 return StyleWindowShadow::Tooltip;
381 case StyleAppearance::Menupopup:
382 return StyleWindowShadow::Menu;
383 default:
384 return StyleWindowShadow::Default;
388 void nsMenuPopupFrame::SetPopupState(nsPopupState aState) {
389 mPopupState = aState;
391 // Work around https://gitlab.gnome.org/GNOME/gtk/-/issues/4166
392 if (aState == ePopupShown && IS_WAYLAND_DISPLAY()) {
393 if (nsIWidget* widget = GetWidget()) {
394 widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext()));
399 // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
400 MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsXULPopupShownEvent::Run() {
401 nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame());
402 // Set the state to visible if the popup is still open.
403 if (popup && popup->IsOpen()) {
404 popup->SetPopupState(ePopupShown);
407 if (!mPopup->IsXULElement(nsGkAtoms::tooltip)) {
408 nsCOMPtr<nsIObserverService> obsService =
409 mozilla::services::GetObserverService();
410 if (obsService) {
411 obsService->NotifyObservers(mPopup, "popup-shown", nullptr);
414 WidgetMouseEvent event(true, eXULPopupShown, nullptr,
415 WidgetMouseEvent::eReal);
416 return EventDispatcher::Dispatch(mPopup, mPresContext, &event);
419 NS_IMETHODIMP nsXULPopupShownEvent::HandleEvent(Event* aEvent) {
420 nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame());
421 // Ignore events not targeted at the popup itself (ie targeted at
422 // descendants):
423 if (mPopup != aEvent->GetTarget()) {
424 return NS_OK;
426 if (popup) {
427 // ResetPopupShownDispatcher will delete the reference to this, so keep
428 // another one until Run is finished.
429 RefPtr<nsXULPopupShownEvent> event = this;
430 // Only call Run if it the dispatcher was assigned. This avoids calling the
431 // Run method if the transitionend event fires multiple times.
432 if (popup->ClearPopupShownDispatcher()) {
433 return Run();
437 CancelListener();
438 return NS_OK;
441 void nsXULPopupShownEvent::CancelListener() {
442 mPopup->RemoveSystemEventListener(u"transitionend"_ns, this, false);
445 NS_IMPL_ISUPPORTS_INHERITED(nsXULPopupShownEvent, Runnable,
446 nsIDOMEventListener);
448 void nsMenuPopupFrame::DidSetComputedStyle(ComputedStyle* aOldStyle) {
449 nsBlockFrame::DidSetComputedStyle(aOldStyle);
451 if (!aOldStyle) {
452 return;
455 WidgetStyleFlags flags;
457 if (aOldStyle->StyleUI()->mColorScheme != StyleUI()->mColorScheme) {
458 flags += WidgetStyle::ColorScheme;
461 auto& newUI = *StyleUIReset();
462 auto& oldUI = *aOldStyle->StyleUIReset();
463 if (newUI.mWindowOpacity != oldUI.mWindowOpacity) {
464 flags += WidgetStyle::Opacity;
467 if (newUI.mMozWindowTransform != oldUI.mMozWindowTransform) {
468 flags += WidgetStyle::Transform;
471 if (newUI.mWindowShadow != oldUI.mWindowShadow) {
472 flags += WidgetStyle::Shadow;
475 const auto& pc = *PresContext();
476 auto oldRegion = ComputeInputRegion(*aOldStyle, pc);
477 auto newRegion = ComputeInputRegion(*Style(), pc);
478 if (oldRegion.mFullyTransparent != newRegion.mFullyTransparent ||
479 oldRegion.mMargin != newRegion.mMargin) {
480 flags += WidgetStyle::InputRegion;
483 PropagateStyleToWidget(flags);
486 void nsMenuPopupFrame::TweakMinPrefISize(nscoord& aSize) {
487 if (!ShouldExpandToInflowParentOrAnchor()) {
488 return;
490 // Make sure to accommodate for our scrollbar if needed. Do it only for
491 // menulists to match previous behavior.
493 // NOTE(emilio): This is somewhat hacky. The "right" fix (which would be
494 // using scrollbar-gutter: stable on the scroller) isn't great, because even
495 // though we want a stable gutter, we want to draw on top of the gutter when
496 // there's no scrollbar, otherwise it looks rather weird.
498 // Automatically accommodating for the scrollbar otherwise would be bug
499 // 764076, but that has its own set of problems.
500 if (nsIScrollableFrame* sf = GetScrollFrame()) {
501 aSize += sf->GetDesiredScrollbarSizes().LeftRight();
504 nscoord menuListOrAnchorWidth = 0;
505 if (nsIFrame* menuList = GetInFlowParent()) {
506 menuListOrAnchorWidth = menuList->GetRect().width;
508 if (mAnchorType == MenuPopupAnchorType_Rect) {
509 menuListOrAnchorWidth = std::max(menuListOrAnchorWidth, mScreenRect.width);
511 // Input margin doesn't have contents, so account for it for popup sizing
512 // purposes.
513 menuListOrAnchorWidth +=
514 2 * StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits();
515 aSize = std::max(aSize, menuListOrAnchorWidth);
518 nscoord nsMenuPopupFrame::GetMinISize(gfxContext* aRC) {
519 nscoord result;
520 DISPLAY_PREF_INLINE_SIZE(this, result);
522 result = nsBlockFrame::GetMinISize(aRC);
523 TweakMinPrefISize(result);
524 return result;
527 nscoord nsMenuPopupFrame::GetPrefISize(gfxContext* aRC) {
528 nscoord result;
529 DISPLAY_PREF_INLINE_SIZE(this, result);
531 result = nsBlockFrame::GetPrefISize(aRC);
532 TweakMinPrefISize(result);
533 return result;
536 void nsMenuPopupFrame::Reflow(nsPresContext* aPresContext,
537 ReflowOutput& aDesiredSize,
538 const ReflowInput& aReflowInput,
539 nsReflowStatus& aStatus) {
540 MarkInReflow();
541 DO_GLOBAL_REFLOW_COUNT("nsMenuPopupFrame");
542 DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
543 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
545 const auto wm = GetWritingMode();
546 // Default to preserving our bounds.
547 aDesiredSize.SetSize(wm, GetLogicalSize(wm));
549 LayoutPopup(aPresContext, aDesiredSize, aReflowInput, aStatus);
551 aDesiredSize.SetBlockStartAscent(aDesiredSize.BSize(wm));
552 aDesiredSize.SetOverflowAreasToDesiredBounds();
553 FinishAndStoreOverflow(&aDesiredSize, aReflowInput.mStyleDisplay);
556 void nsMenuPopupFrame::EnsureActiveMenuListItemIsVisible() {
557 if (!IsMenuList() || !IsOpen()) {
558 return;
560 nsIFrame* frame = GetCurrentMenuItemFrame();
561 if (!frame) {
562 return;
564 RefPtr<mozilla::PresShell> presShell = PresShell();
565 presShell->ScrollFrameIntoView(
566 frame, Nothing(), ScrollAxis(), ScrollAxis(),
567 ScrollFlags::ScrollOverflowHidden | ScrollFlags::ScrollFirstAncestorOnly);
570 void nsMenuPopupFrame::LayoutPopup(nsPresContext* aPresContext,
571 ReflowOutput& aDesiredSize,
572 const ReflowInput& aReflowInput,
573 nsReflowStatus& aStatus) {
574 if (IsNativeMenu()) {
575 return;
578 SchedulePaint();
580 const bool isOpen = IsOpen();
581 if (!isOpen) {
582 // If the popup is not open, only do layout while showing or if we're a
583 // menulist.
585 // This is needed because the SelectParent code wants to limit the height of
586 // the popup before opening it.
588 // TODO(emilio): We should consider adding a way to do that more reliably
589 // instead, but this preserves existing behavior.
590 const bool needsLayout = mPopupState == ePopupShowing ||
591 mPopupState == ePopupPositioning || IsMenuList();
592 if (!needsLayout) {
593 RemoveStateBits(NS_FRAME_FIRST_REFLOW);
594 return;
598 // Do a first reflow, with all our content, in order to find our preferred
599 // size. Then, we do a second reflow with the updated dimensions.
600 const bool needsPrefSize = mPrefSize == nsSize(-1, -1) || IsSubtreeDirty();
601 if (needsPrefSize) {
602 // Get the preferred, minimum and maximum size. If the menu is sized to the
603 // popup, then the popup's width is the menu's width.
604 ReflowOutput preferredSize(aReflowInput);
605 nsBlockFrame::Reflow(aPresContext, preferredSize, aReflowInput, aStatus);
606 mPrefSize = preferredSize.PhysicalSize();
609 // Get our desired position and final size, now that we have a preferred size.
610 auto constraints = GetRects(mPrefSize);
611 const auto finalSize = constraints.mUsedRect.Size();
613 // We need to do an extra reflow if we haven't reflowed, our size doesn't
614 // match with our final intended size, or our bsize is unconstrained (in which
615 // case we need to specify the final size so that percentages work).
616 const bool needDefiniteReflow =
617 aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE || !needsPrefSize ||
618 finalSize != mPrefSize;
620 if (needDefiniteReflow) {
621 ReflowInput constrainedReflowInput(aReflowInput);
622 const auto& bp = aReflowInput.ComputedPhysicalBorderPadding();
623 // TODO: writing-mode handling not terribly correct, but it doesn't matter.
624 const nsSize finalContentSize(finalSize.width - bp.LeftRight(),
625 finalSize.height - bp.TopBottom());
626 constrainedReflowInput.SetComputedISize(finalContentSize.width);
627 constrainedReflowInput.SetComputedBSize(finalContentSize.height);
628 constrainedReflowInput.SetIResize(finalSize.width != mPrefSize.width);
629 constrainedReflowInput.SetBResize([&] {
630 if (finalSize.height != mPrefSize.height) {
631 return true;
633 if (needsPrefSize &&
634 aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE &&
635 aReflowInput.ComputedMaxBSize() == finalContentSize.height) {
636 // If we have measured, and maybe clamped our children via max-height,
637 // they might need to get percentages in the block axis re-resolved.
638 return true;
640 return false;
641 }());
643 aStatus.Reset();
644 nsBlockFrame::Reflow(aPresContext, aDesiredSize, constrainedReflowInput,
645 aStatus);
648 // Set our size, since nsAbsoluteContainingBlock won't.
649 SetRect(constraints.mUsedRect);
651 nsView* view = GetView();
652 if (isOpen) {
653 nsViewManager* viewManager = view->GetViewManager();
654 viewManager->ResizeView(view,
655 nsRect(nsPoint(), constraints.mUsedRect.Size()));
656 if (mPopupState == ePopupOpening) {
657 mPopupState = ePopupVisible;
660 viewManager->SetViewVisibility(view, ViewVisibility::Show);
661 SyncFrameViewProperties(view);
664 // Perform our move now. That will position the view and so on.
665 PerformMove(constraints);
667 // finally, if the popup just opened, send a popupshown event
668 bool openChanged = mIsOpenChanged;
669 if (openChanged) {
670 mIsOpenChanged = false;
672 // Make sure the current selection in a menulist is visible.
673 EnsureActiveMenuListItemIsVisible();
675 // If the animate attribute is set to open, check for a transition and wait
676 // for it to finish before firing the popupshown event.
677 if (LookAndFeel::GetInt(LookAndFeel::IntID::PanelAnimations) &&
678 mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
679 nsGkAtoms::animate, nsGkAtoms::open,
680 eCaseMatters) &&
681 AnimationUtils::HasCurrentTransitions(mContent->AsElement(),
682 PseudoStyleType::NotPseudo)) {
683 mPopupShownDispatcher = new nsXULPopupShownEvent(mContent, aPresContext);
684 mContent->AddSystemEventListener(u"transitionend"_ns,
685 mPopupShownDispatcher, false, false);
686 return;
689 // If there are no transitions, fire the popupshown event right away.
690 nsCOMPtr<nsIRunnable> event =
691 new nsXULPopupShownEvent(GetContent(), aPresContext);
692 mContent->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
696 bool nsMenuPopupFrame::IsMenuList() const {
697 return PopupElement().IsInMenuList();
700 bool nsMenuPopupFrame::ShouldExpandToInflowParentOrAnchor() const {
701 return IsMenuList() && !mContent->GetParent()->AsElement()->AttrValueIs(
702 kNameSpaceID_None, nsGkAtoms::sizetopopup,
703 nsGkAtoms::none, eCaseMatters);
706 nsIContent* nsMenuPopupFrame::GetTriggerContent(
707 nsMenuPopupFrame* aMenuPopupFrame) {
708 while (aMenuPopupFrame) {
709 if (aMenuPopupFrame->mTriggerContent) {
710 return aMenuPopupFrame->mTriggerContent;
713 auto* button = XULButtonElement::FromNodeOrNull(
714 aMenuPopupFrame->GetContent()->GetParent());
715 if (!button || !button->IsMenu()) {
716 break;
719 auto* popup = button->GetContainingPopupElement();
720 if (!popup) {
721 break;
724 // check up the menu hierarchy until a popup with a trigger node is found
725 aMenuPopupFrame = do_QueryFrame(popup->GetPrimaryFrame());
728 return nullptr;
731 void nsMenuPopupFrame::InitPositionFromAnchorAlign(const nsAString& aAnchor,
732 const nsAString& aAlign) {
733 mTriggerContent = nullptr;
735 if (aAnchor.EqualsLiteral("topleft"))
736 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
737 else if (aAnchor.EqualsLiteral("topright"))
738 mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
739 else if (aAnchor.EqualsLiteral("bottomleft"))
740 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
741 else if (aAnchor.EqualsLiteral("bottomright"))
742 mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
743 else if (aAnchor.EqualsLiteral("leftcenter"))
744 mPopupAnchor = POPUPALIGNMENT_LEFTCENTER;
745 else if (aAnchor.EqualsLiteral("rightcenter"))
746 mPopupAnchor = POPUPALIGNMENT_RIGHTCENTER;
747 else if (aAnchor.EqualsLiteral("topcenter"))
748 mPopupAnchor = POPUPALIGNMENT_TOPCENTER;
749 else if (aAnchor.EqualsLiteral("bottomcenter"))
750 mPopupAnchor = POPUPALIGNMENT_BOTTOMCENTER;
751 else
752 mPopupAnchor = POPUPALIGNMENT_NONE;
754 if (aAlign.EqualsLiteral("topleft"))
755 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
756 else if (aAlign.EqualsLiteral("topright"))
757 mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
758 else if (aAlign.EqualsLiteral("bottomleft"))
759 mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
760 else if (aAlign.EqualsLiteral("bottomright"))
761 mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
762 else if (aAlign.EqualsLiteral("leftcenter"))
763 mPopupAlignment = POPUPALIGNMENT_LEFTCENTER;
764 else if (aAlign.EqualsLiteral("rightcenter"))
765 mPopupAlignment = POPUPALIGNMENT_RIGHTCENTER;
766 else if (aAlign.EqualsLiteral("topcenter"))
767 mPopupAlignment = POPUPALIGNMENT_TOPCENTER;
768 else if (aAlign.EqualsLiteral("bottomcenter"))
769 mPopupAlignment = POPUPALIGNMENT_BOTTOMCENTER;
770 else
771 mPopupAlignment = POPUPALIGNMENT_NONE;
773 mPosition = POPUPPOSITION_UNKNOWN;
776 void nsMenuPopupFrame::InitializePopup(nsIContent* aAnchorContent,
777 nsIContent* aTriggerContent,
778 const nsAString& aPosition,
779 int32_t aXPos, int32_t aYPos,
780 MenuPopupAnchorType aAnchorType,
781 bool aAttributesOverride) {
782 auto* widget = GetWidget();
783 bool recreateWidget = widget && widget->NeedsRecreateToReshow();
784 PrepareWidget(recreateWidget);
786 mPopupState = ePopupShowing;
787 mAnchorContent = aAnchorContent;
788 mTriggerContent = aTriggerContent;
789 mXPos = aXPos;
790 mYPos = aYPos;
791 mIsNativeMenu = false;
792 mIsTopLevelContextMenu = false;
793 mVFlip = false;
794 mHFlip = false;
795 mConstrainedByLayout = false;
796 mAlignmentOffset = 0;
797 mPositionedOffset = 0;
798 mPositionedByMoveToRect = false;
800 mAnchorType = aAnchorType;
802 // if aAttributesOverride is true, then the popupanchor, popupalign and
803 // position attributes on the <menupopup> override those values passed in.
804 // If false, those attributes are only used if the values passed in are empty
805 if (aAnchorContent || aAnchorType == MenuPopupAnchorType_Rect) {
806 nsAutoString anchor, align, position, flip;
807 mContent->AsElement()->GetAttr(nsGkAtoms::popupanchor, anchor);
808 mContent->AsElement()->GetAttr(nsGkAtoms::popupalign, align);
809 mContent->AsElement()->GetAttr(nsGkAtoms::position, position);
810 mContent->AsElement()->GetAttr(nsGkAtoms::flip, flip);
812 if (aAttributesOverride) {
813 // if the attributes are set, clear the offset position. Otherwise,
814 // the offset is used to adjust the position from the anchor point
815 if (anchor.IsEmpty() && align.IsEmpty() && position.IsEmpty())
816 position.Assign(aPosition);
817 else
818 mXPos = mYPos = 0;
819 } else if (!aPosition.IsEmpty()) {
820 position.Assign(aPosition);
823 if (flip.EqualsLiteral("none")) {
824 mFlip = FlipType_None;
825 } else if (flip.EqualsLiteral("both")) {
826 mFlip = FlipType_Both;
827 } else if (flip.EqualsLiteral("slide")) {
828 mFlip = FlipType_Slide;
831 position.CompressWhitespace();
832 int32_t spaceIdx = position.FindChar(' ');
833 // if there is a space in the position, assume it is the anchor and
834 // alignment as two separate tokens.
835 if (spaceIdx >= 0) {
836 InitPositionFromAnchorAlign(Substring(position, 0, spaceIdx),
837 Substring(position, spaceIdx + 1));
838 } else if (position.EqualsLiteral("before_start")) {
839 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
840 mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
841 mPosition = POPUPPOSITION_BEFORESTART;
842 } else if (position.EqualsLiteral("before_end")) {
843 mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
844 mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
845 mPosition = POPUPPOSITION_BEFOREEND;
846 } else if (position.EqualsLiteral("after_start")) {
847 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
848 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
849 mPosition = POPUPPOSITION_AFTERSTART;
850 } else if (position.EqualsLiteral("after_end")) {
851 mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
852 mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
853 mPosition = POPUPPOSITION_AFTEREND;
854 } else if (position.EqualsLiteral("start_before")) {
855 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
856 mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
857 mPosition = POPUPPOSITION_STARTBEFORE;
858 } else if (position.EqualsLiteral("start_after")) {
859 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
860 mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
861 mPosition = POPUPPOSITION_STARTAFTER;
862 } else if (position.EqualsLiteral("end_before")) {
863 mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
864 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
865 mPosition = POPUPPOSITION_ENDBEFORE;
866 } else if (position.EqualsLiteral("end_after")) {
867 mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
868 mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
869 mPosition = POPUPPOSITION_ENDAFTER;
870 } else if (position.EqualsLiteral("overlap")) {
871 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
872 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
873 mPosition = POPUPPOSITION_OVERLAP;
874 } else if (position.EqualsLiteral("after_pointer")) {
875 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
876 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
877 mPosition = POPUPPOSITION_AFTERPOINTER;
878 // XXXndeakin this is supposed to anchor vertically after, but with the
879 // horizontal position as the mouse pointer.
880 mYPos += 21;
881 } else if (position.EqualsLiteral("selection")) {
882 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
883 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
884 mPosition = POPUPPOSITION_SELECTION;
885 } else {
886 InitPositionFromAnchorAlign(anchor, align);
889 // When converted back to CSSIntRect it is (-1, -1, 0, 0) - as expected in
890 // nsXULPopupManager::Rollup
891 mScreenRect = nsRect(-AppUnitsPerCSSPixel(), -AppUnitsPerCSSPixel(), 0, 0);
893 if (aAttributesOverride) {
894 // Use |left| and |top| dimension attributes to position the popup if
895 // present, as they may have been persisted.
896 nsAutoString left, top;
897 mContent->AsElement()->GetAttr(nsGkAtoms::left, left);
898 mContent->AsElement()->GetAttr(nsGkAtoms::top, top);
900 nsresult err;
901 if (!left.IsEmpty()) {
902 int32_t x = left.ToInteger(&err);
903 if (NS_SUCCEEDED(err)) {
904 mScreenRect.x = CSSPixel::ToAppUnits(x);
907 if (!top.IsEmpty()) {
908 int32_t y = top.ToInteger(&err);
909 if (NS_SUCCEEDED(err)) {
910 mScreenRect.y = CSSPixel::ToAppUnits(y);
916 void nsMenuPopupFrame::InitializePopupAtScreen(nsIContent* aTriggerContent,
917 int32_t aXPos, int32_t aYPos,
918 bool aIsContextMenu) {
919 auto* widget = GetWidget();
920 bool recreateWidget = widget && widget->NeedsRecreateToReshow();
921 PrepareWidget(recreateWidget);
923 mPopupState = ePopupShowing;
924 mAnchorContent = nullptr;
925 mTriggerContent = aTriggerContent;
926 mScreenRect =
927 nsRect(CSSPixel::ToAppUnits(aXPos), CSSPixel::ToAppUnits(aYPos), 0, 0);
928 mXPos = 0;
929 mYPos = 0;
930 mFlip = FlipType_Default;
931 mPopupAnchor = POPUPALIGNMENT_NONE;
932 mPopupAlignment = POPUPALIGNMENT_NONE;
933 mPosition = POPUPPOSITION_UNKNOWN;
934 mIsContextMenu = aIsContextMenu;
935 mIsTopLevelContextMenu = aIsContextMenu;
936 mIsNativeMenu = false;
937 mAnchorType = MenuPopupAnchorType_Point;
938 mPositionedOffset = 0;
939 mPositionedByMoveToRect = false;
942 void nsMenuPopupFrame::InitializePopupAsNativeContextMenu(
943 nsIContent* aTriggerContent, int32_t aXPos, int32_t aYPos) {
944 mTriggerContent = aTriggerContent;
945 mPopupState = ePopupShowing;
946 mAnchorContent = nullptr;
947 mScreenRect =
948 nsRect(CSSPixel::ToAppUnits(aXPos), CSSPixel::ToAppUnits(aYPos), 0, 0);
949 mXPos = 0;
950 mYPos = 0;
951 mFlip = FlipType_Default;
952 mPopupAnchor = POPUPALIGNMENT_NONE;
953 mPopupAlignment = POPUPALIGNMENT_NONE;
954 mPosition = POPUPPOSITION_UNKNOWN;
955 mIsContextMenu = true;
956 mIsTopLevelContextMenu = true;
957 mIsNativeMenu = true;
958 mAnchorType = MenuPopupAnchorType_Point;
959 mPositionedOffset = 0;
960 mPositionedByMoveToRect = false;
963 void nsMenuPopupFrame::InitializePopupAtRect(nsIContent* aTriggerContent,
964 const nsAString& aPosition,
965 const nsIntRect& aRect,
966 bool aAttributesOverride) {
967 InitializePopup(nullptr, aTriggerContent, aPosition, 0, 0,
968 MenuPopupAnchorType_Rect, aAttributesOverride);
969 mScreenRect = ToAppUnits(aRect, AppUnitsPerCSSPixel());
972 void nsMenuPopupFrame::ShowPopup(bool aIsContextMenu) {
973 mIsContextMenu = aIsContextMenu;
975 InvalidateFrameSubtree();
977 if (mPopupState == ePopupShowing || mPopupState == ePopupPositioning) {
978 mPopupState = ePopupOpening;
979 mIsOpenChanged = true;
981 // Clear mouse capture when a popup is opened.
982 if (mPopupType == PopupType::Menu) {
983 if (auto* activeESM = EventStateManager::GetActiveEventStateManager()) {
984 EventStateManager::ClearGlobalActiveContent(activeESM);
987 PresShell::ReleaseCapturingContent();
990 if (RefPtr menu = PopupElement().GetContainingMenu()) {
991 menu->PopupOpened();
994 // do we need an actual reflow here?
995 // is SetPopupPosition all that is needed?
996 PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors,
997 NS_FRAME_IS_DIRTY);
999 if (mPopupType == PopupType::Menu) {
1000 nsCOMPtr<nsISound> sound(do_GetService("@mozilla.org/sound;1"));
1001 if (sound) sound->PlayEventSound(nsISound::EVENT_MENU_POPUP);
1006 void nsMenuPopupFrame::ClearTriggerContentIncludingDocument() {
1007 // clear the trigger content if the popup is being closed. But don't clear
1008 // it if the popup is just being made invisible as a popuphiding or command
1009 if (mTriggerContent) {
1010 // if the popup had a trigger node set, clear the global window popup node
1011 // as well
1012 Document* doc = mContent->GetUncomposedDoc();
1013 if (doc) {
1014 if (nsPIDOMWindowOuter* win = doc->GetWindow()) {
1015 nsCOMPtr<nsPIWindowRoot> root = win->GetTopWindowRoot();
1016 if (root) {
1017 root->SetPopupNode(nullptr);
1022 mTriggerContent = nullptr;
1025 void nsMenuPopupFrame::HidePopup(bool aDeselectMenu, nsPopupState aNewState,
1026 bool aFromFrameDestruction) {
1027 NS_ASSERTION(aNewState == ePopupClosed || aNewState == ePopupInvisible,
1028 "popup being set to unexpected state");
1030 ClearPopupShownDispatcher();
1032 // don't hide the popup when it isn't open
1033 if (mPopupState == ePopupClosed || mPopupState == ePopupShowing ||
1034 mPopupState == ePopupPositioning) {
1035 return;
1038 if (aNewState == ePopupClosed) {
1039 // clear the trigger content if the popup is being closed. But don't clear
1040 // it if the popup is just being made invisible as a popuphiding or command
1041 // event may want to retrieve it.
1042 ClearTriggerContentIncludingDocument();
1043 mAnchorContent = nullptr;
1046 // when invisible and about to be closed, HidePopup has already been called,
1047 // so just set the new state to closed and return
1048 if (mPopupState == ePopupInvisible) {
1049 if (aNewState == ePopupClosed) {
1050 mPopupState = ePopupClosed;
1052 return;
1055 mPopupState = aNewState;
1057 mIncrementalString.Truncate();
1059 mIsOpenChanged = false;
1060 mHFlip = mVFlip = false;
1061 mConstrainedByLayout = false;
1063 if (auto* widget = GetWidget()) {
1064 // Ideally we should call ClearCachedWebrenderResources but there are
1065 // intermittent failures (see bug 1748788), so we currently call
1066 // ClearWebrenderAnimationResources instead.
1067 widget->ClearWebrenderAnimationResources();
1070 nsView* view = GetView();
1071 nsViewManager* viewManager = view->GetViewManager();
1072 viewManager->SetViewVisibility(view, ViewVisibility::Hide);
1074 RefPtr popup = &PopupElement();
1075 // XXX, bug 137033, In Windows, if mouse is outside the window when the
1076 // menupopup closes, no mouse_enter/mouse_exit event will be fired to clear
1077 // current hover state, we should clear it manually. This code may not the
1078 // best solution, but we can leave it here until we find the better approach.
1079 if (!aFromFrameDestruction &&
1080 popup->State().HasState(dom::ElementState::HOVER)) {
1081 EventStateManager* esm = PresContext()->EventStateManager();
1082 esm->SetContentState(nullptr, dom::ElementState::HOVER);
1084 popup->PopupClosed(aDeselectMenu);
1087 nsPoint nsMenuPopupFrame::AdjustPositionForAnchorAlign(
1088 nsRect& anchorRect, const nsSize& aPrefSize, FlipStyle& aHFlip,
1089 FlipStyle& aVFlip) const {
1090 // flip the anchor and alignment for right-to-left
1091 int8_t popupAnchor(mPopupAnchor);
1092 int8_t popupAlign(mPopupAlignment);
1093 if (IsDirectionRTL()) {
1094 // no need to flip the centered anchor types vertically
1095 if (popupAnchor <= POPUPALIGNMENT_LEFTCENTER) {
1096 popupAnchor = -popupAnchor;
1098 popupAlign = -popupAlign;
1101 nsRect originalAnchorRect(anchorRect);
1103 // first, determine at which corner of the anchor the popup should appear
1104 nsPoint pnt;
1105 switch (popupAnchor) {
1106 case POPUPALIGNMENT_LEFTCENTER:
1107 pnt = nsPoint(anchorRect.x, anchorRect.y + anchorRect.height / 2);
1108 anchorRect.y = pnt.y;
1109 anchorRect.height = 0;
1110 break;
1111 case POPUPALIGNMENT_RIGHTCENTER:
1112 pnt = nsPoint(anchorRect.XMost(), anchorRect.y + anchorRect.height / 2);
1113 anchorRect.y = pnt.y;
1114 anchorRect.height = 0;
1115 break;
1116 case POPUPALIGNMENT_TOPCENTER:
1117 pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.y);
1118 anchorRect.x = pnt.x;
1119 anchorRect.width = 0;
1120 break;
1121 case POPUPALIGNMENT_BOTTOMCENTER:
1122 pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.YMost());
1123 anchorRect.x = pnt.x;
1124 anchorRect.width = 0;
1125 break;
1126 case POPUPALIGNMENT_TOPRIGHT:
1127 pnt = anchorRect.TopRight();
1128 break;
1129 case POPUPALIGNMENT_BOTTOMLEFT:
1130 pnt = anchorRect.BottomLeft();
1131 break;
1132 case POPUPALIGNMENT_BOTTOMRIGHT:
1133 pnt = anchorRect.BottomRight();
1134 break;
1135 case POPUPALIGNMENT_TOPLEFT:
1136 default:
1137 pnt = anchorRect.TopLeft();
1138 break;
1141 // If the alignment is on the right edge of the popup, move the popup left
1142 // by the width. Similarly, if the alignment is on the bottom edge of the
1143 // popup, move the popup up by the height. In addition, account for the
1144 // margins of the popup on the edge on which it is aligned.
1145 nsMargin margin = GetMargin();
1146 switch (popupAlign) {
1147 case POPUPALIGNMENT_LEFTCENTER:
1148 pnt.MoveBy(margin.left, -aPrefSize.height / 2);
1149 break;
1150 case POPUPALIGNMENT_RIGHTCENTER:
1151 pnt.MoveBy(-aPrefSize.width - margin.right, -aPrefSize.height / 2);
1152 break;
1153 case POPUPALIGNMENT_TOPCENTER:
1154 pnt.MoveBy(-aPrefSize.width / 2, margin.top);
1155 break;
1156 case POPUPALIGNMENT_BOTTOMCENTER:
1157 pnt.MoveBy(-aPrefSize.width / 2, -aPrefSize.height - margin.bottom);
1158 break;
1159 case POPUPALIGNMENT_TOPRIGHT:
1160 pnt.MoveBy(-aPrefSize.width - margin.right, margin.top);
1161 break;
1162 case POPUPALIGNMENT_BOTTOMLEFT:
1163 pnt.MoveBy(margin.left, -aPrefSize.height - margin.bottom);
1164 break;
1165 case POPUPALIGNMENT_BOTTOMRIGHT:
1166 pnt.MoveBy(-aPrefSize.width - margin.right,
1167 -aPrefSize.height - margin.bottom);
1168 break;
1169 case POPUPALIGNMENT_TOPLEFT:
1170 default:
1171 pnt.MoveBy(margin.left, margin.top);
1172 break;
1175 // If we aligning to the selected item in the popup, adjust the vertical
1176 // position by the height of the menulist label and the selected item's
1177 // position.
1178 if (mPosition == POPUPPOSITION_SELECTION) {
1179 MOZ_ASSERT(popupAnchor == POPUPALIGNMENT_BOTTOMLEFT ||
1180 popupAnchor == POPUPALIGNMENT_BOTTOMRIGHT);
1181 MOZ_ASSERT(popupAlign == POPUPALIGNMENT_TOPLEFT ||
1182 popupAlign == POPUPALIGNMENT_TOPRIGHT);
1184 // Only adjust the popup if it just opened, otherwise the popup will move
1185 // around if its gets resized or the selection changed. Cache the value in
1186 // mPositionedOffset and use that instead for any future calculations.
1187 if (mIsOpenChanged) {
1188 if (nsIFrame* selectedItemFrame = GetSelectedItemForAlignment()) {
1189 const nscoord itemHeight = selectedItemFrame->GetRect().height;
1190 const nscoord itemOffset =
1191 selectedItemFrame->GetOffsetToIgnoringScrolling(this).y;
1192 // We want to line-up the anchor rect with the selected item, but if the
1193 // selected item is outside of our bounds, we don't want to shift the
1194 // popup up in a way that our box would no longer intersect with the
1195 // anchor.
1196 nscoord maxOffset = aPrefSize.height - itemHeight;
1197 if (const nsIScrollableFrame* sf = GetScrollFrame()) {
1198 // HACK: We ideally would want to use the offset from the bottom
1199 // bottom of our scroll-frame to the bottom of our frame (so as to
1200 // ensure that the bottom of the scrollport is inside the anchor
1201 // rect).
1203 // But at this point of the code, the scroll frame may not be laid out
1204 // with a definite size (might be overflowing us).
1206 // So, we assume the offset from the bottom is symmetric to the offset
1207 // from the top. This holds for all the popups where this matters
1208 // (menulists on macOS, effectively), and seems better than somehow
1209 // moving the popup after the fact as we used to do.
1210 const nsIFrame* f = do_QueryFrame(sf);
1211 maxOffset -= f->GetOffsetTo(this).y;
1213 mPositionedOffset =
1214 originalAnchorRect.height + std::min(itemOffset, maxOffset);
1218 pnt.y -= mPositionedOffset;
1221 // Flipping horizontally is allowed as long as the popup is above or below
1222 // the anchor. This will happen if both the anchor and alignment are top or
1223 // both are bottom, but different values. Similarly, flipping vertically is
1224 // allowed if the popup is to the left or right of the anchor. In this case,
1225 // the values of the constants are such that both must be positive or both
1226 // must be negative. A special case, used for overlap, allows flipping
1227 // vertically as well.
1228 // If we are flipping in both directions, we want to set a flip style both
1229 // horizontally and vertically. However, we want to flip on the inside edge
1230 // of the anchor. Consider the example of a typical dropdown menu.
1231 // Vertically, we flip the popup on the outside edges of the anchor menu,
1232 // however horizontally, we want to to use the inside edges so the popup
1233 // still appears underneath the anchor menu instead of floating off the
1234 // side of the menu.
1235 switch (popupAnchor) {
1236 case POPUPALIGNMENT_LEFTCENTER:
1237 case POPUPALIGNMENT_RIGHTCENTER:
1238 aHFlip = FlipStyle_Outside;
1239 aVFlip = FlipStyle_Inside;
1240 break;
1241 case POPUPALIGNMENT_TOPCENTER:
1242 case POPUPALIGNMENT_BOTTOMCENTER:
1243 aHFlip = FlipStyle_Inside;
1244 aVFlip = FlipStyle_Outside;
1245 break;
1246 default: {
1247 FlipStyle anchorEdge =
1248 mFlip == FlipType_Both ? FlipStyle_Inside : FlipStyle_None;
1249 aHFlip = (popupAnchor == -popupAlign) ? FlipStyle_Outside : anchorEdge;
1250 if (((popupAnchor > 0) == (popupAlign > 0)) ||
1251 (popupAnchor == POPUPALIGNMENT_TOPLEFT &&
1252 popupAlign == POPUPALIGNMENT_TOPLEFT))
1253 aVFlip = FlipStyle_Outside;
1254 else
1255 aVFlip = anchorEdge;
1256 break;
1260 return pnt;
1263 nsIFrame* nsMenuPopupFrame::GetSelectedItemForAlignment() const {
1264 // This method adjusts a menulist's popup such that the selected item is under
1265 // the cursor, aligned with the menulist label.
1266 nsCOMPtr<nsIDOMXULSelectControlElement> select;
1267 if (mAnchorContent) {
1268 select = mAnchorContent->AsElement()->AsXULSelectControl();
1271 if (!select) {
1272 // If there isn't an anchor, then try just getting the parent of the popup.
1273 select = mContent->GetParent()->AsElement()->AsXULSelectControl();
1274 if (!select) {
1275 return nullptr;
1279 nsCOMPtr<Element> selectedElement;
1280 select->GetSelectedItem(getter_AddRefs(selectedElement));
1281 return selectedElement ? selectedElement->GetPrimaryFrame() : nullptr;
1284 nscoord nsMenuPopupFrame::SlideOrResize(nscoord& aScreenPoint, nscoord aSize,
1285 nscoord aScreenBegin,
1286 nscoord aScreenEnd,
1287 nscoord* aOffset) const {
1288 // The popup may be positioned such that either the left/top or bottom/right
1289 // is outside the screen - but never both.
1290 nscoord newPos =
1291 std::max(aScreenBegin, std::min(aScreenEnd - aSize, aScreenPoint));
1292 *aOffset = newPos - aScreenPoint;
1293 aScreenPoint = newPos;
1294 return std::min(aSize, aScreenEnd - aScreenPoint);
1297 nscoord nsMenuPopupFrame::FlipOrResize(nscoord& aScreenPoint, nscoord aSize,
1298 nscoord aScreenBegin, nscoord aScreenEnd,
1299 nscoord aAnchorBegin, nscoord aAnchorEnd,
1300 nscoord aMarginBegin, nscoord aMarginEnd,
1301 FlipStyle aFlip, bool aEndAligned,
1302 bool* aFlipSide) const {
1303 // The flip side argument will be set to true if there wasn't room and we
1304 // flipped to the opposite side.
1305 *aFlipSide = false;
1307 // all of the coordinates used here are in app units relative to the screen
1308 nscoord popupSize = aSize;
1309 if (aScreenPoint < aScreenBegin) {
1310 // at its current position, the popup would extend past the left or top
1311 // edge of the screen, so it will have to be moved or resized.
1312 if (aFlip) {
1313 // for inside flips, we flip on the opposite side of the anchor
1314 nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd;
1315 nscoord endpos = aFlip == FlipStyle_Outside ? aAnchorEnd : aAnchorBegin;
1317 // check whether there is more room to the left and right (or top and
1318 // bottom) of the anchor and put the popup on the side with more room.
1319 if (startpos - aScreenBegin >= aScreenEnd - endpos) {
1320 aScreenPoint = aScreenBegin;
1321 popupSize = startpos - aScreenPoint - aMarginEnd;
1322 *aFlipSide = !aEndAligned;
1323 } else {
1324 // If the newly calculated position is different than the existing
1325 // position, flip such that the popup is to the right or bottom of the
1326 // anchor point instead . However, when flipping use the same margin
1327 // size.
1328 nscoord newScreenPoint = endpos + aMarginEnd;
1329 if (newScreenPoint != aScreenPoint) {
1330 *aFlipSide = aEndAligned;
1331 aScreenPoint = newScreenPoint;
1332 // check if the new position is still off the right or bottom edge of
1333 // the screen. If so, resize the popup.
1334 if (aScreenPoint + aSize > aScreenEnd) {
1335 popupSize = aScreenEnd - aScreenPoint;
1339 } else {
1340 aScreenPoint = aScreenBegin;
1342 } else if (aScreenPoint + aSize > aScreenEnd) {
1343 // at its current position, the popup would extend past the right or
1344 // bottom edge of the screen, so it will have to be moved or resized.
1345 if (aFlip) {
1346 // for inside flips, we flip on the opposite side of the anchor
1347 nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd;
1348 nscoord endpos = aFlip == FlipStyle_Outside ? aAnchorEnd : aAnchorBegin;
1350 // check whether there is more room to the left and right (or top and
1351 // bottom) of the anchor and put the popup on the side with more room.
1352 if (aScreenEnd - endpos >= startpos - aScreenBegin) {
1353 *aFlipSide = aEndAligned;
1354 if (mIsContextMenu) {
1355 aScreenPoint = aScreenEnd - aSize;
1356 } else {
1357 aScreenPoint = endpos + aMarginBegin;
1358 popupSize = aScreenEnd - aScreenPoint;
1360 } else {
1361 // if the newly calculated position is different than the existing
1362 // position, we flip such that the popup is to the left or top of the
1363 // anchor point instead.
1364 nscoord newScreenPoint = startpos - aSize - aMarginBegin;
1365 if (newScreenPoint != aScreenPoint) {
1366 *aFlipSide = !aEndAligned;
1367 aScreenPoint = newScreenPoint;
1369 // check if the new position is still off the left or top edge of the
1370 // screen. If so, resize the popup.
1371 if (aScreenPoint < aScreenBegin) {
1372 aScreenPoint = aScreenBegin;
1373 if (!mIsContextMenu) {
1374 popupSize = startpos - aScreenPoint - aMarginBegin;
1379 } else {
1380 aScreenPoint = aScreenEnd - aSize;
1384 // Make sure that the point is within the screen boundaries and that the
1385 // size isn't off the edge of the screen. This can happen when a large
1386 // positive or negative margin is used.
1387 if (aScreenPoint < aScreenBegin) {
1388 aScreenPoint = aScreenBegin;
1390 if (aScreenPoint > aScreenEnd) {
1391 aScreenPoint = aScreenEnd - aSize;
1394 // If popupSize ended up being negative, or the original size was actually
1395 // smaller than the calculated popup size, just use the original size instead.
1396 if (popupSize <= 0 || aSize < popupSize) {
1397 popupSize = aSize;
1400 return std::min(popupSize, aScreenEnd - aScreenPoint);
1403 nsRect nsMenuPopupFrame::ComputeAnchorRect(nsPresContext* aRootPresContext,
1404 nsIFrame* aAnchorFrame) const {
1405 // Get the root frame for a reference
1406 nsIFrame* rootFrame = aRootPresContext->PresShell()->GetRootFrame();
1408 // The dimensions of the anchor
1409 nsRect anchorRect = aAnchorFrame->GetRectRelativeToSelf();
1411 // Relative to the root
1412 anchorRect = nsLayoutUtils::TransformFrameRectToAncestor(
1413 aAnchorFrame, anchorRect, rootFrame);
1414 // Relative to the screen
1415 anchorRect.MoveBy(rootFrame->GetScreenRectInAppUnits().TopLeft());
1417 // In its own app units
1418 return anchorRect.ScaleToOtherAppUnitsRoundOut(
1419 aRootPresContext->AppUnitsPerDevPixel(),
1420 PresContext()->AppUnitsPerDevPixel());
1423 static nsIFrame* MaybeDelegatedAnchorFrame(nsIFrame* aFrame) {
1424 if (!aFrame) {
1425 return nullptr;
1427 if (auto* element = Element::FromNodeOrNull(aFrame->GetContent())) {
1428 if (element->HasAttr(nsGkAtoms::delegatesanchor)) {
1429 for (nsIFrame* f : aFrame->PrincipalChildList()) {
1430 if (!f->IsPlaceholderFrame()) {
1431 return f;
1436 return aFrame;
1439 auto nsMenuPopupFrame::GetRects(const nsSize& aPrefSize) const -> Rects {
1440 if (NS_WARN_IF(aPrefSize == nsSize(-1, -1))) {
1441 // Return early if the popup hasn't been laid out yet. On Windows, this can
1442 // happen when using a drag popup before it opens.
1443 return {};
1446 nsPresContext* pc = PresContext();
1447 nsIFrame* rootFrame = pc->PresShell()->GetRootFrame();
1448 NS_ASSERTION(rootFrame->GetView() && GetView() &&
1449 rootFrame->GetView() == GetView()->GetParent(),
1450 "rootFrame's view is not our view's parent???");
1452 // Indicators of whether the popup should be flipped or resized.
1453 FlipStyle hFlip = FlipStyle_None, vFlip = FlipStyle_None;
1455 const nsMargin margin = GetMargin();
1457 // the screen rectangle of the root frame, in dev pixels.
1458 const nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits();
1460 const bool isNoAutoHide = IsNoAutoHide();
1461 const PopupLevel popupLevel = GetPopupLevel(isNoAutoHide);
1463 Rects result;
1465 // Set the popup's size to the preferred size. Below, this size will be
1466 // adjusted to fit on the screen or within the content area. If the anchor is
1467 // sized to the popup, use the anchor's width instead of the preferred width.
1468 result.mUsedRect = nsRect(nsPoint(), aPrefSize);
1470 const bool anchored = IsAnchored();
1471 if (anchored) {
1472 // In order to deal with transforms, we need the root prescontext:
1473 nsPresContext* rootPc = pc->GetRootPresContext();
1474 if (NS_WARN_IF(!rootPc)) {
1475 // If we can't reach a root pres context, don't bother continuing.
1476 return result;
1479 result.mAnchorRect = result.mUntransformedAnchorRect = [&] {
1480 // If anchored to a rectangle, use that rectangle. Otherwise, determine
1481 // the rectangle from the anchor.
1482 if (mAnchorType == MenuPopupAnchorType_Rect) {
1483 return mScreenRect;
1485 // if the frame is not specified, use the anchor node passed to OpenPopup.
1486 // If that wasn't specified either, use the root frame. Note that
1487 // mAnchorContent might be a different document so its presshell must be
1488 // used.
1489 nsIFrame* anchorFrame = GetAnchorFrame();
1490 if (!anchorFrame) {
1491 return rootScreenRect;
1493 return ComputeAnchorRect(rootPc, anchorFrame);
1494 }();
1496 // if we are anchored, there are certain things we don't want to do when
1497 // repositioning the popup to fit on the screen, such as end up positioned
1498 // over the anchor, for instance a popup appearing over the menu label.
1499 // When doing this reposition, we want to move the popup to the side with
1500 // the most room. The combination of anchor and alignment dictate if we
1501 // readjust above/below or to the left/right.
1502 if (mAnchorContent || mAnchorType == MenuPopupAnchorType_Rect) {
1503 // move the popup according to the anchor and alignment. This will also
1504 // tell us which axis the popup is flush against in case we have to move
1505 // it around later. The AdjustPositionForAnchorAlign method accounts for
1506 // the popup's margin.
1507 result.mUsedRect.MoveTo(AdjustPositionForAnchorAlign(
1508 result.mAnchorRect, aPrefSize, hFlip, vFlip));
1509 } else {
1510 // With no anchor, the popup is positioned relative to the root frame.
1511 result.mUsedRect.MoveTo(result.mAnchorRect.TopLeft() +
1512 nsPoint(margin.left, margin.top));
1515 // mXPos and mYPos specify an additional offset passed to OpenPopup that
1516 // should be added to the position. We also add the offset to the anchor
1517 // pos so a later flip/resize takes the offset into account.
1518 // FIXME(emilio): Wayland doesn't seem to be accounting for this offset
1519 // anywhere, and it probably should.
1521 nsPoint offset(CSSPixel::ToAppUnits(mXPos), CSSPixel::ToAppUnits(mYPos));
1522 if (IsDirectionRTL()) {
1523 offset.x = -offset.x;
1525 result.mUsedRect.MoveBy(offset);
1526 result.mAnchorRect.MoveBy(offset);
1528 } else {
1529 // Not anchored, use mScreenRect
1530 result.mUsedRect.MoveTo(mScreenRect.TopLeft());
1531 result.mAnchorRect = result.mUntransformedAnchorRect =
1532 nsRect(mScreenRect.TopLeft(), nsSize());
1534 // Right-align RTL context menus, and apply margin and offsets as per the
1535 // platform conventions.
1536 if (mIsContextMenu && IsDirectionRTL()) {
1537 result.mUsedRect.x -= aPrefSize.Width();
1538 result.mUsedRect.MoveBy(-margin.right, margin.top);
1539 } else {
1540 result.mUsedRect.MoveBy(margin.left, margin.top);
1542 #ifdef XP_MACOSX
1543 // OSX tooltips follow standard flip rule but other popups flip horizontally
1544 // not vertically
1545 if (mPopupType == PopupType::Tooltip) {
1546 vFlip = FlipStyle_Outside;
1547 } else {
1548 hFlip = FlipStyle_Outside;
1550 #else
1551 // Other OS screen positioned popups can be flipped vertically but never
1552 // horizontally
1553 vFlip = FlipStyle_Outside;
1554 #endif // #ifdef XP_MACOSX
1557 const int32_t a2d = pc->AppUnitsPerDevPixel();
1559 nsView* view = GetView();
1560 NS_ASSERTION(view, "popup with no view");
1562 nsIWidget* widget = view->GetWidget();
1564 // If a panel has flip="none", don't constrain or flip it.
1565 // Also, always do this for content shells, so that the popup doesn't extend
1566 // outside the containing frame.
1567 if (mInContentShell || mFlip != FlipType_None) {
1568 const Maybe<nsRect> constraintRect =
1569 GetConstraintRect(result.mAnchorRect, rootScreenRect, popupLevel);
1571 if (constraintRect) {
1572 // Ensure that anchorRect is on the constraint rect.
1573 result.mAnchorRect = result.mAnchorRect.Intersect(*constraintRect);
1574 // Shrink the popup down if it is larger than the constraint size
1575 if (result.mUsedRect.width > constraintRect->width) {
1576 result.mUsedRect.width = constraintRect->width;
1578 if (result.mUsedRect.height > constraintRect->height) {
1579 result.mUsedRect.height = constraintRect->height;
1581 result.mConstrainedByLayout = true;
1584 if (IS_WAYLAND_DISPLAY() && widget) {
1585 // Shrink the popup down if it's larger than popup size received from
1586 // Wayland compositor. We don't know screen size on Wayland so this is the
1587 // only info we have there.
1588 const nsSize waylandSize = LayoutDeviceIntRect::ToAppUnits(
1589 widget->GetMoveToRectPopupSize(), a2d);
1590 if (waylandSize.width > 0 && result.mUsedRect.width > waylandSize.width) {
1591 LOG_WAYLAND("Wayland constraint width [%p]: %d to %d", widget,
1592 result.mUsedRect.width, waylandSize.width);
1593 result.mUsedRect.width = waylandSize.width;
1595 if (waylandSize.height > 0 &&
1596 result.mUsedRect.height > waylandSize.height) {
1597 LOG_WAYLAND("Wayland constraint height [%p]: %d to %d", widget,
1598 result.mUsedRect.height, waylandSize.height);
1599 result.mUsedRect.height = waylandSize.height;
1601 if (RefPtr<widget::Screen> s = widget->GetWidgetScreen()) {
1602 const nsSize screenSize =
1603 LayoutDeviceIntSize::ToAppUnits(s->GetAvailRect().Size(), a2d);
1604 if (result.mUsedRect.height > screenSize.height) {
1605 LOG_WAYLAND("Wayland constraint height to screen [%p]: %d to %d",
1606 widget, result.mUsedRect.height, screenSize.height);
1607 result.mUsedRect.height = screenSize.height;
1609 if (result.mUsedRect.width > screenSize.width) {
1610 LOG_WAYLAND("Wayland constraint widthto screen [%p]: %d to %d",
1611 widget, result.mUsedRect.width, screenSize.width);
1612 result.mUsedRect.width = screenSize.width;
1617 // At this point the anchor (anchorRect) is within the available screen
1618 // area (constraintRect) and the popup is known to be no larger than the
1619 // screen.
1620 if (constraintRect) {
1621 // We might want to "slide" an arrow if the panel is of the correct type -
1622 // but we can only slide on one axis - the other axis must be "flipped or
1623 // resized" as normal.
1624 bool slideHorizontal = false, slideVertical = false;
1625 if (mFlip == FlipType_Slide) {
1626 int8_t position = GetAlignmentPosition();
1627 slideHorizontal = position >= POPUPPOSITION_BEFORESTART &&
1628 position <= POPUPPOSITION_AFTEREND;
1629 slideVertical = position >= POPUPPOSITION_STARTBEFORE &&
1630 position <= POPUPPOSITION_ENDAFTER;
1633 // Next, check if there is enough space to show the popup at full size
1634 // when positioned at screenPoint. If not, flip the popups to the opposite
1635 // side of their anchor point, or resize them as necessary.
1636 if (slideHorizontal) {
1637 result.mUsedRect.width = SlideOrResize(
1638 result.mUsedRect.x, result.mUsedRect.width, constraintRect->x,
1639 constraintRect->XMost(), &result.mAlignmentOffset);
1640 } else {
1641 const bool endAligned =
1642 IsDirectionRTL()
1643 ? mPopupAlignment == POPUPALIGNMENT_TOPLEFT ||
1644 mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT ||
1645 mPopupAlignment == POPUPALIGNMENT_LEFTCENTER
1646 : mPopupAlignment == POPUPALIGNMENT_TOPRIGHT ||
1647 mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT ||
1648 mPopupAlignment == POPUPALIGNMENT_RIGHTCENTER;
1649 result.mUsedRect.width = FlipOrResize(
1650 result.mUsedRect.x, result.mUsedRect.width, constraintRect->x,
1651 constraintRect->XMost(), result.mAnchorRect.x,
1652 result.mAnchorRect.XMost(), margin.left, margin.right, hFlip,
1653 endAligned, &result.mHFlip);
1655 if (slideVertical) {
1656 result.mUsedRect.height = SlideOrResize(
1657 result.mUsedRect.y, result.mUsedRect.height, constraintRect->y,
1658 constraintRect->YMost(), &result.mAlignmentOffset);
1659 } else {
1660 bool endAligned = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT ||
1661 mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT ||
1662 mPopupAlignment == POPUPALIGNMENT_BOTTOMCENTER;
1663 result.mUsedRect.height = FlipOrResize(
1664 result.mUsedRect.y, result.mUsedRect.height, constraintRect->y,
1665 constraintRect->YMost(), result.mAnchorRect.y,
1666 result.mAnchorRect.YMost(), margin.top, margin.bottom, vFlip,
1667 endAligned, &result.mVFlip);
1670 #ifdef DEBUG
1671 NS_ASSERTION(constraintRect->Contains(result.mUsedRect),
1672 "Popup is offscreen");
1673 if (!constraintRect->Contains(result.mUsedRect)) {
1674 NS_WARNING(nsPrintfCString("Popup is offscreen (%s vs. %s)",
1675 ToString(constraintRect).c_str(),
1676 ToString(result.mUsedRect).c_str())
1677 .get());
1679 #endif
1682 // snap the popup's position in screen coordinates to device pixels, see
1683 // bug 622507, bug 961431
1684 result.mUsedRect.x = pc->RoundAppUnitsToNearestDevPixels(result.mUsedRect.x);
1685 result.mUsedRect.y = pc->RoundAppUnitsToNearestDevPixels(result.mUsedRect.y);
1687 // determine the x and y position of the view by subtracting the desired
1688 // screen position from the screen position of the root frame.
1689 result.mViewPoint = result.mUsedRect.TopLeft() - rootScreenRect.TopLeft();
1691 // Offset the position by the width and height of the borders and titlebar.
1692 // Even though GetClientOffset should return (0, 0) when there is no titlebar
1693 // or borders, we skip these calculations anyway for non-panels to save time
1694 // since they will never have a titlebar.
1695 if (mPopupType == PopupType::Panel && widget) {
1696 result.mClientOffset = widget->GetClientOffset();
1697 result.mViewPoint +=
1698 LayoutDeviceIntPoint::ToAppUnits(result.mClientOffset, a2d);
1701 return result;
1704 void nsMenuPopupFrame::SetPopupPosition(bool aIsMove) {
1705 if (aIsMove && (mPrefSize.width == -1 || mPrefSize.height == -1)) {
1706 return;
1709 auto rects = GetRects(mPrefSize);
1710 if (rects.mUsedRect.Size() != mRect.Size()) {
1711 MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IN_REFLOW));
1712 // We need to resize on top of moving, trigger an actual reflow.
1713 PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors,
1714 NS_FRAME_IS_DIRTY);
1715 return;
1717 PerformMove(rects);
1720 void nsMenuPopupFrame::PerformMove(const Rects& aRects) {
1721 auto* ps = PresShell();
1723 // We're just moving, sync frame position and offset as needed.
1724 ps->GetViewManager()->MoveViewTo(GetView(), aRects.mViewPoint.x,
1725 aRects.mViewPoint.y);
1727 // Now that we've positioned the view, sync up the frame's origin.
1728 nsBlockFrame::SetPosition(aRects.mViewPoint -
1729 GetParent()->GetOffsetTo(ps->GetRootFrame()));
1731 // If the popup is in the positioned state or if it is shown and the position
1732 // or size changed, dispatch a popuppositioned event if the popup wants it.
1733 if (mPopupState == ePopupPositioning ||
1734 (mPopupState == ePopupShown &&
1735 !aRects.mUsedRect.IsEqualEdges(mUsedScreenRect)) ||
1736 (mPopupState == ePopupShown &&
1737 aRects.mAlignmentOffset != mAlignmentOffset)) {
1738 mUsedScreenRect = aRects.mUsedRect;
1739 if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && !mPendingPositionedEvent) {
1740 mPendingPositionedEvent =
1741 nsXULPopupPositionedEvent::DispatchIfNeeded(mContent->AsElement());
1745 if (!mPositionedByMoveToRect) {
1746 mUntransformedAnchorRect = aRects.mUntransformedAnchorRect;
1749 mAlignmentOffset = aRects.mAlignmentOffset;
1750 mLastClientOffset = aRects.mClientOffset;
1751 mHFlip = aRects.mHFlip;
1752 mVFlip = aRects.mVFlip;
1753 mConstrainedByLayout = aRects.mConstrainedByLayout;
1755 // If this is a noautohide popup, set the screen coordinates of the popup.
1756 // This way, the popup stays at the location where it was opened even when the
1757 // window is moved. Popups at the parent level follow the parent window as it
1758 // is moved and remained anchored, so we want to maintain the anchoring
1759 // instead.
1761 // FIXME: This suffers from issues like bug 1823552, where constraints imposed
1762 // by the anchor are lost, but this is super-old behavior.
1763 const bool fixPositionToPoint =
1764 IsNoAutoHide() && (GetPopupLevel() != PopupLevel::Parent ||
1765 mAnchorType == MenuPopupAnchorType_Rect);
1766 if (fixPositionToPoint) {
1767 // Account for the margin that will end up being added to the screen
1768 // coordinate the next time SetPopupPosition is called.
1769 const auto& margin = GetMargin();
1770 mAnchorType = MenuPopupAnchorType_Point;
1771 mScreenRect.x = aRects.mUsedRect.x - margin.left;
1772 mScreenRect.y = aRects.mUsedRect.y - margin.top;
1775 // For anchored popups that shouldn't follow the anchor, fix the original
1776 // anchor rect.
1777 if (IsAnchored() && !ShouldFollowAnchor() && !mUsedScreenRect.IsEmpty() &&
1778 mAnchorType != MenuPopupAnchorType_Rect) {
1779 mAnchorType = MenuPopupAnchorType_Rect;
1780 mScreenRect = aRects.mUntransformedAnchorRect;
1783 // NOTE(emilio): This call below is kind of a workaround, but we need to do
1784 // this here because some position changes don't go through the
1785 // view system -> popup manager, like:
1787 // https://searchfox.org/mozilla-central/rev/477950cf9ca9c9bb5ff6f34e0d0f6ca4718ea798/widget/gtk/nsWindow.cpp#3847
1789 // So this might be the last chance we have to set the remote browser's
1790 // position.
1792 // Ultimately this probably wants to get fixed in the widget size of things,
1793 // but given this is worst-case a redundant DOM traversal, and that popups
1794 // usually don't have all that much content, this is probably an ok
1795 // workaround.
1796 WidgetPositionOrSizeDidChange();
1799 void nsMenuPopupFrame::WidgetPositionOrSizeDidChange() {
1800 // In the case this popup has remote contents having OOP iframes, it's
1801 // possible that OOP iframe's nsSubDocumentFrame has been already reflowed
1802 // thus, we will never have a chance to tell this parent browser's position
1803 // update to the OOP documents without notifying it explicitly.
1804 if (!HasRemoteContent()) {
1805 return;
1807 for (nsIContent* content = mContent->GetFirstChild(); content;
1808 content = content->GetNextNode(mContent)) {
1809 if (content->IsXULElement(nsGkAtoms::browser) &&
1810 content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::remote,
1811 nsGkAtoms::_true, eIgnoreCase)) {
1812 if (auto* browserParent = dom::BrowserParent::GetFrom(content)) {
1813 browserParent->NotifyPositionUpdatedForContentsInPopup();
1819 Maybe<nsRect> nsMenuPopupFrame::GetConstraintRect(
1820 const nsRect& aAnchorRect, const nsRect& aRootScreenRect,
1821 PopupLevel aPopupLevel) const {
1822 const nsPresContext* pc = PresContext();
1823 const int32_t a2d = PresContext()->AppUnitsPerDevPixel();
1824 Maybe<nsRect> result;
1826 auto AddConstraint = [&result](const nsRect& aConstraint) {
1827 if (result) {
1828 *result = result->Intersect(aConstraint);
1829 } else {
1830 result.emplace(aConstraint);
1834 // Determine the available screen space. It will be reduced by the OS chrome
1835 // such as menubars. It addition, for content shells, it will be the area of
1836 // the content rather than the screen.
1837 // In Wayland we can't use the screen rect because we can't know absolute
1838 // window position.
1839 if (!IS_WAYLAND_DISPLAY()) {
1840 const DesktopToLayoutDeviceScale scale =
1841 pc->DeviceContext()->GetDesktopToDeviceScale();
1842 // For content shells, get the screen where the root frame is located. This
1843 // is because we need to constrain the content to this content area, so we
1844 // should use the same screen. Otherwise, use the screen where the anchor is
1845 // located.
1846 const nsRect& rect = mInContentShell ? aRootScreenRect : aAnchorRect;
1847 auto desktopRect = DesktopIntRect::RoundOut(
1848 LayoutDeviceRect::FromAppUnits(rect, a2d) / scale);
1849 desktopRect.width = std::max(1, desktopRect.width);
1850 desktopRect.height = std::max(1, desktopRect.height);
1852 RefPtr<nsIScreen> screen =
1853 widget::ScreenManager::GetSingleton().ScreenForRect(desktopRect);
1854 MOZ_ASSERT(screen, "We always fall back to the primary screen");
1855 // Non-top-level popups (which will always be panels) should never overlap
1856 // the OS bar.
1857 const bool canOverlapOSBar =
1858 aPopupLevel == PopupLevel::Top &&
1859 LookAndFeel::GetInt(LookAndFeel::IntID::MenusCanOverlapOSBar) &&
1860 !mInContentShell;
1861 // Get the total screen area if the popup is allowed to overlap it.
1862 const auto screenRect =
1863 canOverlapOSBar ? screen->GetRect() : screen->GetAvailRect();
1864 AddConstraint(LayoutDeviceRect::ToAppUnits(screenRect, a2d));
1867 if (mInContentShell) {
1868 // For content shells, clip to the client area rather than the screen area
1869 AddConstraint(aRootScreenRect);
1870 } else if (!mOverrideConstraintRect.IsEmpty()) {
1871 AddConstraint(mOverrideConstraintRect);
1872 // This is currently only used for <select> elements where we want to
1873 // constrain vertically to the screen but not horizontally, so do the
1874 // intersection and then reset the horizontal values.
1876 // FIXME(emilio): This doesn't make any sense to me...
1877 result->x = mOverrideConstraintRect.x;
1878 result->width = mOverrideConstraintRect.width;
1881 // Expand the allowable screen rect by the input margin (which can't be
1882 // interacted with).
1883 if (result) {
1884 const nscoord inputMargin =
1885 StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits();
1886 result->Inflate(inputMargin);
1888 return result;
1891 ConsumeOutsideClicksResult nsMenuPopupFrame::ConsumeOutsideClicks() {
1892 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
1893 nsGkAtoms::consumeoutsideclicks,
1894 nsGkAtoms::_true, eCaseMatters)) {
1895 return ConsumeOutsideClicks_True;
1897 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
1898 nsGkAtoms::consumeoutsideclicks,
1899 nsGkAtoms::_false, eCaseMatters)) {
1900 return ConsumeOutsideClicks_ParentOnly;
1902 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
1903 nsGkAtoms::consumeoutsideclicks,
1904 nsGkAtoms::never, eCaseMatters)) {
1905 return ConsumeOutsideClicks_Never;
1908 nsCOMPtr<nsIContent> parentContent = mContent->GetParent();
1909 if (parentContent) {
1910 dom::NodeInfo* ni = parentContent->NodeInfo();
1911 if (ni->Equals(nsGkAtoms::menulist, kNameSpaceID_XUL)) {
1912 return ConsumeOutsideClicks_True; // Consume outside clicks for combo
1913 // boxes on all platforms
1915 #if defined(XP_WIN)
1916 // Don't consume outside clicks for menus in Windows
1917 if (ni->Equals(nsGkAtoms::menu, kNameSpaceID_XUL) ||
1918 ni->Equals(nsGkAtoms::popupset, kNameSpaceID_XUL) ||
1919 ((ni->Equals(nsGkAtoms::button, kNameSpaceID_XUL) ||
1920 ni->Equals(nsGkAtoms::toolbarbutton, kNameSpaceID_XUL)) &&
1921 parentContent->AsElement()->AttrValueIs(
1922 kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::menu,
1923 eCaseMatters))) {
1924 return ConsumeOutsideClicks_Never;
1926 #endif
1929 return ConsumeOutsideClicks_True;
1932 static nsIScrollableFrame* DoGetScrollFrame(const nsIFrame* aFrame) {
1933 if (const nsIScrollableFrame* sf = do_QueryFrame(aFrame)) {
1934 return const_cast<nsIScrollableFrame*>(sf);
1936 for (nsIFrame* childFrame : aFrame->PrincipalChildList()) {
1937 if (auto* sf = DoGetScrollFrame(childFrame)) {
1938 return sf;
1941 return nullptr;
1944 // XXXroc this is megalame. Fossicking around for a frame of the right
1945 // type is a recipe for disaster in the long term.
1946 nsIScrollableFrame* nsMenuPopupFrame::GetScrollFrame() const {
1947 return DoGetScrollFrame(this);
1950 void nsMenuPopupFrame::ChangeByPage(bool aIsUp) {
1951 // Only scroll by page within menulists.
1952 if (!IsMenuList()) {
1953 return;
1956 nsIScrollableFrame* scrollframe = GetScrollFrame();
1958 RefPtr popup = &PopupElement();
1959 XULButtonElement* currentMenu = popup->GetActiveMenuChild();
1960 XULButtonElement* newMenu = nullptr;
1961 if (!currentMenu) {
1962 // If there is no current menu item, get the first item. When moving up,
1963 // just use this as the newMenu and leave currentMenu null so that no check
1964 // for a later element is performed. When moving down, set currentMenu so
1965 // that we look for one page down from the first item.
1966 newMenu = popup->GetFirstMenuItem();
1967 if (!aIsUp) {
1968 currentMenu = newMenu;
1972 if (currentMenu && currentMenu->GetPrimaryFrame()) {
1973 const nscoord scrollHeight =
1974 scrollframe ? scrollframe->GetScrollPortRect().height : mRect.height;
1975 const nsRect currentRect = currentMenu->GetPrimaryFrame()->GetRect();
1976 const XULButtonElement* startMenu = currentMenu;
1978 // Get the position of the current item and add or subtract one popup's
1979 // height to or from it.
1980 const nscoord targetPos = aIsUp ? currentRect.YMost() - scrollHeight
1981 : currentRect.y + scrollHeight;
1982 // Look for the next child which is just past the target position. This
1983 // child will need to be selected.
1984 for (; currentMenu;
1985 currentMenu = aIsUp ? popup->GetPrevMenuItemFrom(*currentMenu)
1986 : popup->GetNextMenuItemFrom(*currentMenu)) {
1987 if (!currentMenu->GetPrimaryFrame()) {
1988 continue;
1990 const nsRect curRect = currentMenu->GetPrimaryFrame()->GetRect();
1991 const nscoord curPos = aIsUp ? curRect.y : curRect.YMost();
1992 // If the right position was found, break out. Otherwise, look for another
1993 // item.
1994 if (aIsUp ? (curPos < targetPos) : (curPos > targetPos)) {
1995 if (!newMenu || newMenu == startMenu) {
1996 newMenu = currentMenu;
1998 break;
2001 // Assign this item to newMenu. This item will be selected in case we
2002 // don't find any more.
2003 newMenu = currentMenu;
2007 // Select the new menuitem.
2008 if (RefPtr newMenuRef = newMenu) {
2009 popup->SetActiveMenuChild(newMenuRef);
2013 dom::XULPopupElement& nsMenuPopupFrame::PopupElement() const {
2014 auto* popup = dom::XULPopupElement::FromNode(GetContent());
2015 MOZ_DIAGNOSTIC_ASSERT(popup);
2016 return *popup;
2019 XULButtonElement* nsMenuPopupFrame::GetCurrentMenuItem() const {
2020 return PopupElement().GetActiveMenuChild();
2023 nsIFrame* nsMenuPopupFrame::GetCurrentMenuItemFrame() const {
2024 auto* child = GetCurrentMenuItem();
2025 return child ? child->GetPrimaryFrame() : nullptr;
2028 void nsMenuPopupFrame::HandleEnterKeyPress(WidgetEvent& aEvent) {
2029 mIncrementalString.Truncate();
2030 RefPtr popup = &PopupElement();
2031 popup->HandleEnterKeyPress(aEvent);
2034 XULButtonElement* nsMenuPopupFrame::FindMenuWithShortcut(
2035 mozilla::dom::KeyboardEvent& aKeyEvent, bool& aDoAction) {
2036 uint32_t charCode = aKeyEvent.CharCode();
2037 uint32_t keyCode = aKeyEvent.KeyCode();
2039 aDoAction = false;
2041 // Enumerate over our list of frames.
2042 const bool isMenu = !IsMenuList();
2043 TimeStamp keyTime = aKeyEvent.WidgetEventPtr()->mTimeStamp;
2044 if (charCode == 0) {
2045 if (keyCode == dom::KeyboardEvent_Binding::DOM_VK_BACK_SPACE) {
2046 if (!isMenu && !mIncrementalString.IsEmpty()) {
2047 mIncrementalString.SetLength(mIncrementalString.Length() - 1);
2048 return nullptr;
2050 #ifdef XP_WIN
2051 if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
2052 sound->Beep();
2054 #endif // #ifdef XP_WIN
2056 return nullptr;
2058 char16_t uniChar = ToLowerCase(static_cast<char16_t>(charCode));
2059 if (isMenu) {
2060 // Menu supports only first-letter navigation
2061 mIncrementalString = uniChar;
2062 } else if (IsWithinIncrementalTime(keyTime)) {
2063 mIncrementalString.Append(uniChar);
2064 } else {
2065 // Interval too long, treat as new typing
2066 mIncrementalString = uniChar;
2069 // See bug 188199 & 192346, if all letters in incremental string are same,
2070 // just try to match the first one
2071 nsAutoString incrementalString(mIncrementalString);
2072 uint32_t charIndex = 1, stringLength = incrementalString.Length();
2073 while (charIndex < stringLength &&
2074 incrementalString[charIndex] == incrementalString[charIndex - 1]) {
2075 charIndex++;
2077 if (charIndex == stringLength) {
2078 incrementalString.Truncate(1);
2079 stringLength = 1;
2082 sLastKeyTime = keyTime;
2084 auto* item =
2085 PopupElement().FindMenuWithShortcut(incrementalString, aDoAction);
2086 if (item) {
2087 return item;
2090 // If we don't match anything, rollback the last typing
2091 mIncrementalString.SetLength(mIncrementalString.Length() - 1);
2093 // didn't find a matching menu item
2094 #ifdef XP_WIN
2095 // behavior on Windows - this item is in a menu popup off of the
2096 // menu bar, so beep and do nothing else
2097 if (isMenu) {
2098 if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
2099 sound->Beep();
2102 #endif // #ifdef XP_WIN
2104 return nullptr;
2107 nsIWidget* nsMenuPopupFrame::GetWidget() const {
2108 return mView ? mView->GetWidget() : nullptr;
2111 // helpers /////////////////////////////////////////////////////////////
2113 nsresult nsMenuPopupFrame::AttributeChanged(int32_t aNameSpaceID,
2114 nsAtom* aAttribute,
2115 int32_t aModType)
2118 nsresult rv =
2119 nsBlockFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
2121 if (aAttribute == nsGkAtoms::left || aAttribute == nsGkAtoms::top) {
2122 MoveToAttributePosition();
2125 if (aAttribute == nsGkAtoms::remote) {
2126 // When the remote attribute changes, we need to create a new widget to
2127 // ensure that it has the correct compositor and transparency settings to
2128 // match the new value.
2129 PrepareWidget(true);
2132 if (aAttribute == nsGkAtoms::followanchor) {
2133 if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
2134 pm->UpdateFollowAnchor(this);
2138 if (aAttribute == nsGkAtoms::label) {
2139 // set the label for the titlebar
2140 nsView* view = GetView();
2141 if (view) {
2142 nsIWidget* widget = view->GetWidget();
2143 if (widget) {
2144 nsAutoString title;
2145 mContent->AsElement()->GetAttr(nsGkAtoms::label, title);
2146 if (!title.IsEmpty()) {
2147 widget->SetTitle(title);
2151 } else if (aAttribute == nsGkAtoms::ignorekeys) {
2152 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2153 if (pm) {
2154 nsAutoString ignorekeys;
2155 mContent->AsElement()->GetAttr(nsGkAtoms::ignorekeys, ignorekeys);
2156 pm->UpdateIgnoreKeys(ignorekeys.EqualsLiteral("true"));
2160 return rv;
2163 void nsMenuPopupFrame::MoveToAttributePosition() {
2164 // Move the widget around when the user sets the |left| and |top| attributes.
2165 // Note that this is not the best way to move the widget, as it results in
2166 // lots of FE notifications and is likely to be slow as molasses. Use |moveTo|
2167 // on the element if possible.
2168 nsAutoString left, top;
2169 mContent->AsElement()->GetAttr(nsGkAtoms::left, left);
2170 mContent->AsElement()->GetAttr(nsGkAtoms::top, top);
2171 nsresult err1, err2;
2172 mozilla::CSSIntPoint pos(left.ToInteger(&err1), top.ToInteger(&err2));
2174 if (NS_SUCCEEDED(err1) && NS_SUCCEEDED(err2)) MoveTo(pos, false);
2176 PresShell()->FrameNeedsReflow(
2177 this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
2180 void nsMenuPopupFrame::Destroy(DestroyContext& aContext) {
2181 // XXX: Currently we don't fire popuphidden for these popups, that seems wrong
2182 // but alas, also pre-existing.
2183 HidePopup(/* aDeselectMenu = */ false, ePopupClosed,
2184 /* aFromFrameDestruction = */ true);
2186 if (RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance()) {
2187 pm->PopupDestroyed(this);
2190 nsBlockFrame::Destroy(aContext);
2193 nsMargin nsMenuPopupFrame::GetMargin() const {
2194 nsMargin margin;
2195 StyleMargin()->GetMargin(margin);
2196 if (mIsTopLevelContextMenu) {
2197 const CSSIntPoint offset(
2198 LookAndFeel::GetInt(LookAndFeel::IntID::ContextMenuOffsetHorizontal),
2199 LookAndFeel::GetInt(LookAndFeel::IntID::ContextMenuOffsetVertical));
2200 auto auOffset = CSSIntPoint::ToAppUnits(offset);
2201 margin.top += auOffset.y;
2202 margin.bottom += auOffset.y;
2203 margin.left += auOffset.x;
2204 margin.right += auOffset.x;
2206 return margin;
2209 void nsMenuPopupFrame::MoveTo(const CSSPoint& aPos, bool aUpdateAttrs,
2210 bool aByMoveToRect) {
2211 nsIWidget* widget = GetWidget();
2212 nsPoint appUnitsPos = CSSPixel::ToAppUnits(aPos);
2214 // reposition the popup at the specified coordinates. Don't clear the anchor
2215 // and position, because the popup can be reset to its anchor position by
2216 // using (-1, -1) as coordinates.
2218 // Subtract off the margin as it will be added to the position when
2219 // SetPopupPosition is called.
2221 nsMargin margin = GetMargin();
2222 if (mIsContextMenu && IsDirectionRTL()) {
2223 appUnitsPos.x += margin.right + mRect.Width();
2224 } else {
2225 appUnitsPos.x -= margin.left;
2227 appUnitsPos.y -= margin.top;
2230 if (mScreenRect.TopLeft() == appUnitsPos &&
2231 (!widget || widget->GetClientOffset() == mLastClientOffset)) {
2232 return;
2235 mPositionedByMoveToRect = aByMoveToRect;
2236 mScreenRect.MoveTo(appUnitsPos);
2237 if (mAnchorType == MenuPopupAnchorType_Rect) {
2238 // This ensures that the anchor width is still honored, to prevent it from
2239 // changing spuriously.
2240 mScreenRect.height = 0;
2241 // But we still need to make sure that our top left position ends up in
2242 // appUnitsPos.
2243 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
2244 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
2245 mXPos = mYPos = 0;
2246 } else {
2247 mAnchorType = MenuPopupAnchorType_Point;
2250 SetPopupPosition(true);
2252 RefPtr<Element> popup = mContent->AsElement();
2253 if (aUpdateAttrs &&
2254 (popup->HasAttr(nsGkAtoms::left) || popup->HasAttr(nsGkAtoms::top))) {
2255 nsAutoString left, top;
2256 left.AppendInt(RoundedToInt(aPos).x);
2257 top.AppendInt(RoundedToInt(aPos).y);
2258 popup->SetAttr(kNameSpaceID_None, nsGkAtoms::left, left, false);
2259 popup->SetAttr(kNameSpaceID_None, nsGkAtoms::top, top, false);
2263 void nsMenuPopupFrame::MoveToAnchor(nsIContent* aAnchorContent,
2264 const nsAString& aPosition, int32_t aXPos,
2265 int32_t aYPos, bool aAttributesOverride) {
2266 NS_ASSERTION(IsVisible(), "popup must be visible to move it");
2268 nsPopupState oldstate = mPopupState;
2269 InitializePopup(aAnchorContent, mTriggerContent, aPosition, aXPos, aYPos,
2270 MenuPopupAnchorType_Node, aAttributesOverride);
2271 // InitializePopup changed the state so reset it.
2272 mPopupState = oldstate;
2274 // Pass false here so that flipping and adjusting to fit on the screen happen.
2275 SetPopupPosition(false);
2278 int8_t nsMenuPopupFrame::GetAlignmentPosition() const {
2279 // The code below handles most cases of alignment, anchor and position values.
2280 // Those that are not handled just return POPUPPOSITION_UNKNOWN.
2282 if (mPosition == POPUPPOSITION_OVERLAP ||
2283 mPosition == POPUPPOSITION_AFTERPOINTER ||
2284 mPosition == POPUPPOSITION_SELECTION) {
2285 return mPosition;
2288 int8_t position = mPosition;
2290 if (position == POPUPPOSITION_UNKNOWN) {
2291 switch (mPopupAnchor) {
2292 case POPUPALIGNMENT_BOTTOMRIGHT:
2293 case POPUPALIGNMENT_BOTTOMLEFT:
2294 case POPUPALIGNMENT_BOTTOMCENTER:
2295 position = mPopupAlignment == POPUPALIGNMENT_TOPRIGHT
2296 ? POPUPPOSITION_AFTEREND
2297 : POPUPPOSITION_AFTERSTART;
2298 break;
2299 case POPUPALIGNMENT_TOPRIGHT:
2300 case POPUPALIGNMENT_TOPLEFT:
2301 case POPUPALIGNMENT_TOPCENTER:
2302 position = mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT
2303 ? POPUPPOSITION_BEFOREEND
2304 : POPUPPOSITION_BEFORESTART;
2305 break;
2306 case POPUPALIGNMENT_LEFTCENTER:
2307 position = mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT
2308 ? POPUPPOSITION_STARTAFTER
2309 : POPUPPOSITION_STARTBEFORE;
2310 break;
2311 case POPUPALIGNMENT_RIGHTCENTER:
2312 position = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT
2313 ? POPUPPOSITION_ENDAFTER
2314 : POPUPPOSITION_ENDBEFORE;
2315 break;
2316 default:
2317 break;
2321 if (mHFlip) {
2322 position = POPUPPOSITION_HFLIP(position);
2325 if (mVFlip) {
2326 position = POPUPPOSITION_VFLIP(position);
2329 return position;
2333 * KEEP THIS IN SYNC WITH nsIFrame::CreateView
2334 * as much as possible. Until we get rid of views finally...
2336 void nsMenuPopupFrame::CreatePopupView() {
2337 if (HasView()) {
2338 return;
2341 nsViewManager* viewManager = PresContext()->GetPresShell()->GetViewManager();
2342 NS_ASSERTION(nullptr != viewManager, "null view manager");
2344 // Create a view
2345 nsView* parentView = viewManager->GetRootView();
2346 auto visibility = ViewVisibility::Hide;
2348 NS_ASSERTION(parentView, "no parent view");
2350 // Create a view
2351 nsView* view = viewManager->CreateView(GetRect(), parentView, visibility);
2352 auto zIndex = ZIndex();
2353 viewManager->SetViewZIndex(view, zIndex.isNothing(), zIndex.valueOr(0));
2354 // XXX put view last in document order until we can do better
2355 viewManager->InsertChild(parentView, view, nullptr, true);
2357 // Remember our view
2358 SetView(view);
2360 NS_FRAME_LOG(
2361 NS_FRAME_TRACE_CALLS,
2362 ("nsMenuPopupFrame::CreatePopupView: frame=%p view=%p", this, view));
2365 bool nsMenuPopupFrame::ShouldFollowAnchor() const {
2366 if (mAnchorType != MenuPopupAnchorType_Node || !mAnchorContent) {
2367 return false;
2370 // Follow anchor mode is used when followanchor="true" is set or for arrow
2371 // panels.
2372 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
2373 nsGkAtoms::followanchor,
2374 nsGkAtoms::_true, eCaseMatters)) {
2375 return true;
2378 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
2379 nsGkAtoms::followanchor,
2380 nsGkAtoms::_false, eCaseMatters)) {
2381 return false;
2384 return mPopupType == PopupType::Panel &&
2385 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
2386 nsGkAtoms::arrow, eCaseMatters);
2389 bool nsMenuPopupFrame::ShouldFollowAnchor(nsRect& aRect) {
2390 if (!ShouldFollowAnchor()) {
2391 return false;
2394 if (nsIFrame* anchorFrame = GetAnchorFrame()) {
2395 if (nsPresContext* rootPresContext = PresContext()->GetRootPresContext()) {
2396 aRect = ComputeAnchorRect(rootPresContext, anchorFrame);
2400 return true;
2403 bool nsMenuPopupFrame::IsDirectionRTL() const {
2404 const nsIFrame* anchor = GetAnchorFrame();
2405 const nsIFrame* f = anchor ? anchor : this;
2406 return f->StyleVisibility()->mDirection == StyleDirection::Rtl;
2409 nsIFrame* nsMenuPopupFrame::GetAnchorFrame() const {
2410 nsIContent* anchor = mAnchorContent;
2411 if (!anchor) {
2412 return nullptr;
2414 return MaybeDelegatedAnchorFrame(anchor->GetPrimaryFrame());
2417 void nsMenuPopupFrame::CheckForAnchorChange(nsRect& aRect) {
2418 // Don't update if the popup isn't visible or we shouldn't be following the
2419 // anchor.
2420 if (!IsVisible() || !ShouldFollowAnchor()) {
2421 return;
2424 bool shouldHide = false;
2426 nsPresContext* rootPresContext = PresContext()->GetRootPresContext();
2428 // If the frame for the anchor has gone away, hide the popup.
2429 nsIFrame* anchor = GetAnchorFrame();
2430 if (!anchor || !rootPresContext) {
2431 shouldHide = true;
2432 } else if (!anchor->IsVisibleConsideringAncestors(
2433 VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) {
2434 // If the anchor is now inside something that is invisible, hide the popup.
2435 shouldHide = true;
2436 } else {
2437 // If the anchor is now inside a hidden parent popup, hide the popup.
2438 nsIFrame* frame = anchor;
2439 while (frame) {
2440 nsMenuPopupFrame* popup = do_QueryFrame(frame);
2441 if (popup && popup->PopupState() != ePopupShown) {
2442 shouldHide = true;
2443 break;
2446 frame = frame->GetParent();
2450 if (shouldHide) {
2451 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2452 if (pm) {
2453 // As the caller will be iterating over the open popups, hide
2454 // asyncronously.
2455 pm->HidePopup(mContent->AsElement(),
2456 {HidePopupOption::DeselectMenu, HidePopupOption::Async});
2459 return;
2462 nsRect anchorRect = ComputeAnchorRect(rootPresContext, anchor);
2464 // If the rectangles are different, move the popup.
2465 if (!anchorRect.IsEqualEdges(aRect)) {
2466 aRect = anchorRect;
2467 SetPopupPosition(true);