no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / layout / xul / nsMenuPopupFrame.cpp
blob254078c0185e3a8fee0ca3e7d0e3bd729ecad555
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[] = {nsGkAtoms::top,
224 nsGkAtoms::parent, 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 default:
232 break;
235 // If this panel is a noautohide panel, the default is the parent level.
236 if (aIsNoAutoHide) {
237 return PopupLevel::Parent;
240 // Otherwise, the result depends on the platform.
241 return sDefaultLevelIsTop ? PopupLevel::Top : PopupLevel::Parent;
244 void nsMenuPopupFrame::PrepareWidget(bool aRecreate) {
245 nsView* ourView = GetView();
246 if (aRecreate) {
247 if (auto* widget = GetWidget()) {
248 // Widget's WebRender resources needs to be cleared before creating new
249 // widget.
250 widget->ClearCachedWebrenderResources();
252 ourView->DestroyWidget();
254 if (!ourView->HasWidget()) {
255 CreateWidgetForView(ourView);
256 } else {
257 PropagateStyleToWidget();
261 nsresult nsMenuPopupFrame::CreateWidgetForView(nsView* aView) {
262 // Create a widget for ourselves.
263 widget::InitData widgetData;
264 widgetData.mWindowType = widget::WindowType::Popup;
265 widgetData.mBorderStyle = widget::BorderStyle::Default;
266 widgetData.mClipSiblings = true;
267 widgetData.mPopupHint = mPopupType;
268 widgetData.mNoAutoHide = IsNoAutoHide();
270 if (!mInContentShell) {
271 // A drag popup may be used for non-static translucent drag feedback
272 if (mPopupType == PopupType::Panel &&
273 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
274 nsGkAtoms::drag, eIgnoreCase)) {
275 widgetData.mIsDragPopup = true;
279 bool remote = HasRemoteContent();
281 const auto mode = nsLayoutUtils::GetFrameTransparency(this, this);
282 widgetData.mHasRemoteContent = remote;
283 widgetData.mTransparencyMode = mode;
284 widgetData.mPopupLevel = GetPopupLevel(widgetData.mNoAutoHide);
286 // Panels which have a parent level need a parent widget. This allows them to
287 // always appear in front of the parent window but behind other windows that
288 // should be in front of it.
289 nsCOMPtr<nsIWidget> parentWidget;
290 if (widgetData.mPopupLevel != PopupLevel::Top) {
291 nsCOMPtr<nsIDocShellTreeItem> dsti = PresContext()->GetDocShell();
292 if (!dsti) return NS_ERROR_FAILURE;
294 nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
295 dsti->GetTreeOwner(getter_AddRefs(treeOwner));
296 if (!treeOwner) return NS_ERROR_FAILURE;
298 nsCOMPtr<nsIBaseWindow> baseWindow(do_QueryInterface(treeOwner));
299 if (baseWindow) baseWindow->GetMainWidget(getter_AddRefs(parentWidget));
302 nsresult rv = aView->CreateWidgetForPopup(&widgetData, parentWidget);
303 if (NS_FAILED(rv)) {
304 return rv;
307 nsIWidget* widget = aView->GetWidget();
308 widget->SetTransparencyMode(mode);
310 PropagateStyleToWidget();
312 return NS_OK;
315 void nsMenuPopupFrame::PropagateStyleToWidget(WidgetStyleFlags aFlags) const {
316 if (aFlags.isEmpty()) {
317 return;
320 nsIWidget* widget = GetWidget();
321 if (!widget) {
322 return;
325 if (aFlags.contains(WidgetStyle::ColorScheme)) {
326 widget->SetColorScheme(Some(LookAndFeel::ColorSchemeForFrame(this)));
328 if (aFlags.contains(WidgetStyle::InputRegion)) {
329 widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext()));
331 if (aFlags.contains(WidgetStyle::Opacity)) {
332 widget->SetWindowOpacity(StyleUIReset()->mWindowOpacity);
334 if (aFlags.contains(WidgetStyle::Shadow)) {
335 widget->SetWindowShadowStyle(GetShadowStyle());
337 if (aFlags.contains(WidgetStyle::Transform)) {
338 widget->SetWindowTransform(ComputeWidgetTransform());
342 bool nsMenuPopupFrame::IsMouseTransparent() const {
343 return ::IsMouseTransparent(*Style());
346 WindowShadow nsMenuPopupFrame::GetShadowStyle() const {
347 StyleWindowShadow shadow = StyleUIReset()->mWindowShadow;
348 if (shadow != StyleWindowShadow::Auto) {
349 MOZ_ASSERT(shadow == StyleWindowShadow::None);
350 return WindowShadow::None;
353 switch (StyleDisplay()->EffectiveAppearance()) {
354 case StyleAppearance::Tooltip:
355 return WindowShadow::Tooltip;
356 case StyleAppearance::Menupopup:
357 return WindowShadow::Menu;
358 default:
359 return WindowShadow::Panel;
363 void nsMenuPopupFrame::SetPopupState(nsPopupState aState) {
364 mPopupState = aState;
366 // Work around https://gitlab.gnome.org/GNOME/gtk/-/issues/4166
367 if (aState == ePopupShown && IS_WAYLAND_DISPLAY()) {
368 if (nsIWidget* widget = GetWidget()) {
369 widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext()));
374 // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
375 MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsXULPopupShownEvent::Run() {
376 nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame());
377 // Set the state to visible if the popup is still open.
378 if (popup && popup->IsOpen()) {
379 popup->SetPopupState(ePopupShown);
382 if (!mPopup->IsXULElement(nsGkAtoms::tooltip)) {
383 nsCOMPtr<nsIObserverService> obsService =
384 mozilla::services::GetObserverService();
385 if (obsService) {
386 obsService->NotifyObservers(mPopup, "popup-shown", nullptr);
389 WidgetMouseEvent event(true, eXULPopupShown, nullptr,
390 WidgetMouseEvent::eReal);
391 return EventDispatcher::Dispatch(mPopup, mPresContext, &event);
394 NS_IMETHODIMP nsXULPopupShownEvent::HandleEvent(Event* aEvent) {
395 nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame());
396 // Ignore events not targeted at the popup itself (ie targeted at
397 // descendants):
398 if (mPopup != aEvent->GetTarget()) {
399 return NS_OK;
401 if (popup) {
402 // ResetPopupShownDispatcher will delete the reference to this, so keep
403 // another one until Run is finished.
404 RefPtr<nsXULPopupShownEvent> event = this;
405 // Only call Run if it the dispatcher was assigned. This avoids calling the
406 // Run method if the transitionend event fires multiple times.
407 if (popup->ClearPopupShownDispatcher()) {
408 return Run();
412 CancelListener();
413 return NS_OK;
416 void nsXULPopupShownEvent::CancelListener() {
417 mPopup->RemoveSystemEventListener(u"transitionend"_ns, this, false);
420 NS_IMPL_ISUPPORTS_INHERITED(nsXULPopupShownEvent, Runnable,
421 nsIDOMEventListener);
423 void nsMenuPopupFrame::DidSetComputedStyle(ComputedStyle* aOldStyle) {
424 nsBlockFrame::DidSetComputedStyle(aOldStyle);
426 if (!aOldStyle) {
427 return;
430 WidgetStyleFlags flags;
432 if (aOldStyle->StyleUI()->mColorScheme != StyleUI()->mColorScheme) {
433 flags += WidgetStyle::ColorScheme;
436 auto& newUI = *StyleUIReset();
437 auto& oldUI = *aOldStyle->StyleUIReset();
438 if (newUI.mWindowOpacity != oldUI.mWindowOpacity) {
439 flags += WidgetStyle::Opacity;
442 if (newUI.mMozWindowTransform != oldUI.mMozWindowTransform) {
443 flags += WidgetStyle::Transform;
446 if (newUI.mWindowShadow != oldUI.mWindowShadow) {
447 flags += WidgetStyle::Shadow;
450 const auto& pc = *PresContext();
451 auto oldRegion = ComputeInputRegion(*aOldStyle, pc);
452 auto newRegion = ComputeInputRegion(*Style(), pc);
453 if (oldRegion.mFullyTransparent != newRegion.mFullyTransparent ||
454 oldRegion.mMargin != newRegion.mMargin) {
455 flags += WidgetStyle::InputRegion;
458 PropagateStyleToWidget(flags);
461 void nsMenuPopupFrame::TweakMinPrefISize(nscoord& aSize) {
462 if (!ShouldExpandToInflowParentOrAnchor()) {
463 return;
465 // Make sure to accommodate for our scrollbar if needed. Do it only for
466 // menulists to match previous behavior.
468 // NOTE(emilio): This is somewhat hacky. The "right" fix (which would be
469 // using scrollbar-gutter: stable on the scroller) isn't great, because even
470 // though we want a stable gutter, we want to draw on top of the gutter when
471 // there's no scrollbar, otherwise it looks rather weird.
473 // Automatically accommodating for the scrollbar otherwise would be bug
474 // 764076, but that has its own set of problems.
475 if (nsIScrollableFrame* sf = GetScrollFrame()) {
476 aSize += sf->GetDesiredScrollbarSizes().LeftRight();
479 nscoord menuListOrAnchorWidth = 0;
480 if (nsIFrame* menuList = GetInFlowParent()) {
481 menuListOrAnchorWidth = menuList->GetRect().width;
483 if (mAnchorType == MenuPopupAnchorType_Rect) {
484 menuListOrAnchorWidth = std::max(menuListOrAnchorWidth, mScreenRect.width);
486 // Input margin doesn't have contents, so account for it for popup sizing
487 // purposes.
488 menuListOrAnchorWidth +=
489 2 * StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits();
490 aSize = std::max(aSize, menuListOrAnchorWidth);
493 nscoord nsMenuPopupFrame::GetMinISize(gfxContext* aRC) {
494 nscoord result;
495 DISPLAY_PREF_INLINE_SIZE(this, result);
497 result = nsBlockFrame::GetMinISize(aRC);
498 TweakMinPrefISize(result);
499 return result;
502 nscoord nsMenuPopupFrame::GetPrefISize(gfxContext* aRC) {
503 nscoord result;
504 DISPLAY_PREF_INLINE_SIZE(this, result);
506 result = nsBlockFrame::GetPrefISize(aRC);
507 TweakMinPrefISize(result);
508 return result;
511 void nsMenuPopupFrame::Reflow(nsPresContext* aPresContext,
512 ReflowOutput& aDesiredSize,
513 const ReflowInput& aReflowInput,
514 nsReflowStatus& aStatus) {
515 MarkInReflow();
516 DO_GLOBAL_REFLOW_COUNT("nsMenuPopupFrame");
517 DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
518 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
520 const auto wm = GetWritingMode();
521 // Default to preserving our bounds.
522 aDesiredSize.SetSize(wm, GetLogicalSize(wm));
524 LayoutPopup(aPresContext, aDesiredSize, aReflowInput, aStatus);
526 aDesiredSize.SetBlockStartAscent(aDesiredSize.BSize(wm));
527 aDesiredSize.SetOverflowAreasToDesiredBounds();
528 FinishAndStoreOverflow(&aDesiredSize, aReflowInput.mStyleDisplay);
531 void nsMenuPopupFrame::EnsureActiveMenuListItemIsVisible() {
532 if (!IsMenuList() || !IsOpen()) {
533 return;
535 nsIFrame* frame = GetCurrentMenuItemFrame();
536 if (!frame) {
537 return;
539 RefPtr<mozilla::PresShell> presShell = PresShell();
540 presShell->ScrollFrameIntoView(
541 frame, Nothing(), ScrollAxis(), ScrollAxis(),
542 ScrollFlags::ScrollOverflowHidden | ScrollFlags::ScrollFirstAncestorOnly);
545 void nsMenuPopupFrame::LayoutPopup(nsPresContext* aPresContext,
546 ReflowOutput& aDesiredSize,
547 const ReflowInput& aReflowInput,
548 nsReflowStatus& aStatus) {
549 if (IsNativeMenu()) {
550 return;
553 SchedulePaint();
555 const bool isOpen = IsOpen();
556 if (!isOpen) {
557 // If the popup is not open, only do layout while showing or if we're a
558 // menulist.
560 // This is needed because the SelectParent code wants to limit the height of
561 // the popup before opening it.
563 // TODO(emilio): We should consider adding a way to do that more reliably
564 // instead, but this preserves existing behavior.
565 const bool needsLayout = mPopupState == ePopupShowing ||
566 mPopupState == ePopupPositioning || IsMenuList();
567 if (!needsLayout) {
568 RemoveStateBits(NS_FRAME_FIRST_REFLOW);
569 return;
573 // Do a first reflow, with all our content, in order to find our preferred
574 // size. Then, we do a second reflow with the updated dimensions.
575 const bool needsPrefSize = mPrefSize == nsSize(-1, -1) || IsSubtreeDirty();
576 if (needsPrefSize) {
577 // Get the preferred, minimum and maximum size. If the menu is sized to the
578 // popup, then the popup's width is the menu's width.
579 ReflowOutput preferredSize(aReflowInput);
580 nsBlockFrame::Reflow(aPresContext, preferredSize, aReflowInput, aStatus);
581 mPrefSize = preferredSize.PhysicalSize();
584 // Get our desired position and final size, now that we have a preferred size.
585 auto constraints = GetRects(mPrefSize);
586 const auto finalSize = constraints.mUsedRect.Size();
588 // We need to do an extra reflow if we haven't reflowed, our size doesn't
589 // match with our final intended size, or our bsize is unconstrained (in which
590 // case we need to specify the final size so that percentages work).
591 const bool needDefiniteReflow =
592 aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE || !needsPrefSize ||
593 finalSize != mPrefSize;
595 if (needDefiniteReflow) {
596 ReflowInput constrainedReflowInput(aReflowInput);
597 const auto& bp = aReflowInput.ComputedPhysicalBorderPadding();
598 // TODO: writing-mode handling not terribly correct, but it doesn't matter.
599 const nsSize finalContentSize(finalSize.width - bp.LeftRight(),
600 finalSize.height - bp.TopBottom());
601 constrainedReflowInput.SetComputedISize(finalContentSize.width);
602 constrainedReflowInput.SetComputedBSize(finalContentSize.height);
603 constrainedReflowInput.SetIResize(finalSize.width != mPrefSize.width);
604 constrainedReflowInput.SetBResize([&] {
605 if (finalSize.height != mPrefSize.height) {
606 return true;
608 if (needsPrefSize &&
609 aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE &&
610 aReflowInput.ComputedMaxBSize() == finalContentSize.height) {
611 // If we have measured, and maybe clamped our children via max-height,
612 // they might need to get percentages in the block axis re-resolved.
613 return true;
615 return false;
616 }());
618 aStatus.Reset();
619 nsBlockFrame::Reflow(aPresContext, aDesiredSize, constrainedReflowInput,
620 aStatus);
623 // Set our size, since nsAbsoluteContainingBlock won't.
624 SetRect(constraints.mUsedRect);
626 nsView* view = GetView();
627 if (isOpen) {
628 nsViewManager* viewManager = view->GetViewManager();
629 viewManager->ResizeView(view,
630 nsRect(nsPoint(), constraints.mUsedRect.Size()));
631 if (mPopupState == ePopupOpening) {
632 mPopupState = ePopupVisible;
635 viewManager->SetViewVisibility(view, ViewVisibility::Show);
636 SyncFrameViewProperties(view);
639 // Perform our move now. That will position the view and so on.
640 PerformMove(constraints);
642 // finally, if the popup just opened, send a popupshown event
643 bool openChanged = mIsOpenChanged;
644 if (openChanged) {
645 mIsOpenChanged = false;
647 // Make sure the current selection in a menulist is visible.
648 EnsureActiveMenuListItemIsVisible();
650 // If the animate attribute is set to open, check for a transition and wait
651 // for it to finish before firing the popupshown event.
652 if (LookAndFeel::GetInt(LookAndFeel::IntID::PanelAnimations) &&
653 mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
654 nsGkAtoms::animate, nsGkAtoms::open,
655 eCaseMatters) &&
656 AnimationUtils::HasCurrentTransitions(mContent->AsElement(),
657 PseudoStyleType::NotPseudo)) {
658 mPopupShownDispatcher = new nsXULPopupShownEvent(mContent, aPresContext);
659 mContent->AddSystemEventListener(u"transitionend"_ns,
660 mPopupShownDispatcher, false, false);
661 return;
664 // If there are no transitions, fire the popupshown event right away.
665 nsCOMPtr<nsIRunnable> event =
666 new nsXULPopupShownEvent(GetContent(), aPresContext);
667 mContent->OwnerDoc()->Dispatch(event.forget());
671 bool nsMenuPopupFrame::IsMenuList() const {
672 return PopupElement().IsInMenuList();
675 bool nsMenuPopupFrame::ShouldExpandToInflowParentOrAnchor() const {
676 return IsMenuList() && !mContent->GetParent()->AsElement()->AttrValueIs(
677 kNameSpaceID_None, nsGkAtoms::sizetopopup,
678 nsGkAtoms::none, eCaseMatters);
681 nsIContent* nsMenuPopupFrame::GetTriggerContent(
682 nsMenuPopupFrame* aMenuPopupFrame) {
683 while (aMenuPopupFrame) {
684 if (aMenuPopupFrame->mTriggerContent) {
685 return aMenuPopupFrame->mTriggerContent;
688 auto* button = XULButtonElement::FromNodeOrNull(
689 aMenuPopupFrame->GetContent()->GetParent());
690 if (!button || !button->IsMenu()) {
691 break;
694 auto* popup = button->GetContainingPopupElement();
695 if (!popup) {
696 break;
699 // check up the menu hierarchy until a popup with a trigger node is found
700 aMenuPopupFrame = do_QueryFrame(popup->GetPrimaryFrame());
703 return nullptr;
706 void nsMenuPopupFrame::InitPositionFromAnchorAlign(const nsAString& aAnchor,
707 const nsAString& aAlign) {
708 mTriggerContent = nullptr;
710 if (aAnchor.EqualsLiteral("topleft"))
711 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
712 else if (aAnchor.EqualsLiteral("topright"))
713 mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
714 else if (aAnchor.EqualsLiteral("bottomleft"))
715 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
716 else if (aAnchor.EqualsLiteral("bottomright"))
717 mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
718 else if (aAnchor.EqualsLiteral("leftcenter"))
719 mPopupAnchor = POPUPALIGNMENT_LEFTCENTER;
720 else if (aAnchor.EqualsLiteral("rightcenter"))
721 mPopupAnchor = POPUPALIGNMENT_RIGHTCENTER;
722 else if (aAnchor.EqualsLiteral("topcenter"))
723 mPopupAnchor = POPUPALIGNMENT_TOPCENTER;
724 else if (aAnchor.EqualsLiteral("bottomcenter"))
725 mPopupAnchor = POPUPALIGNMENT_BOTTOMCENTER;
726 else
727 mPopupAnchor = POPUPALIGNMENT_NONE;
729 if (aAlign.EqualsLiteral("topleft"))
730 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
731 else if (aAlign.EqualsLiteral("topright"))
732 mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
733 else if (aAlign.EqualsLiteral("bottomleft"))
734 mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
735 else if (aAlign.EqualsLiteral("bottomright"))
736 mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
737 else if (aAlign.EqualsLiteral("leftcenter"))
738 mPopupAlignment = POPUPALIGNMENT_LEFTCENTER;
739 else if (aAlign.EqualsLiteral("rightcenter"))
740 mPopupAlignment = POPUPALIGNMENT_RIGHTCENTER;
741 else if (aAlign.EqualsLiteral("topcenter"))
742 mPopupAlignment = POPUPALIGNMENT_TOPCENTER;
743 else if (aAlign.EqualsLiteral("bottomcenter"))
744 mPopupAlignment = POPUPALIGNMENT_BOTTOMCENTER;
745 else
746 mPopupAlignment = POPUPALIGNMENT_NONE;
748 mPosition = POPUPPOSITION_UNKNOWN;
751 static FlipType FlipFromAttribute(nsMenuPopupFrame* aFrame) {
752 nsAutoString flip;
753 aFrame->PopupElement().GetAttr(nsGkAtoms::flip, flip);
754 if (flip.EqualsLiteral("none")) {
755 return FlipType_None;
757 if (flip.EqualsLiteral("both")) {
758 return FlipType_Both;
760 if (flip.EqualsLiteral("slide")) {
761 return FlipType_Slide;
763 return FlipType_Default;
766 void nsMenuPopupFrame::InitializePopup(nsIContent* aAnchorContent,
767 nsIContent* aTriggerContent,
768 const nsAString& aPosition,
769 int32_t aXPos, int32_t aYPos,
770 MenuPopupAnchorType aAnchorType,
771 bool aAttributesOverride) {
772 auto* widget = GetWidget();
773 bool recreateWidget = widget && widget->NeedsRecreateToReshow();
774 PrepareWidget(recreateWidget);
776 mPopupState = ePopupShowing;
777 mAnchorContent = aAnchorContent;
778 mTriggerContent = aTriggerContent;
779 mXPos = aXPos;
780 mYPos = aYPos;
781 mIsNativeMenu = false;
782 mIsTopLevelContextMenu = false;
783 mVFlip = false;
784 mHFlip = false;
785 mConstrainedByLayout = false;
786 mAlignmentOffset = 0;
787 mPositionedOffset = 0;
788 mPositionedByMoveToRect = false;
790 mAnchorType = aAnchorType;
792 // if aAttributesOverride is true, then the popupanchor, popupalign and
793 // position attributes on the <menupopup> override those values passed in.
794 // If false, those attributes are only used if the values passed in are empty
795 if (aAnchorContent || aAnchorType == MenuPopupAnchorType_Rect) {
796 nsAutoString anchor, align, position;
797 mContent->AsElement()->GetAttr(nsGkAtoms::popupanchor, anchor);
798 mContent->AsElement()->GetAttr(nsGkAtoms::popupalign, align);
799 mContent->AsElement()->GetAttr(nsGkAtoms::position, position);
801 if (aAttributesOverride) {
802 // if the attributes are set, clear the offset position. Otherwise,
803 // the offset is used to adjust the position from the anchor point
804 if (anchor.IsEmpty() && align.IsEmpty() && position.IsEmpty())
805 position.Assign(aPosition);
806 else
807 mXPos = mYPos = 0;
808 } else if (!aPosition.IsEmpty()) {
809 position.Assign(aPosition);
812 mFlip = FlipFromAttribute(this);
814 position.CompressWhitespace();
815 int32_t spaceIdx = position.FindChar(' ');
816 // if there is a space in the position, assume it is the anchor and
817 // alignment as two separate tokens.
818 if (spaceIdx >= 0) {
819 InitPositionFromAnchorAlign(Substring(position, 0, spaceIdx),
820 Substring(position, spaceIdx + 1));
821 } else if (position.EqualsLiteral("before_start")) {
822 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
823 mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
824 mPosition = POPUPPOSITION_BEFORESTART;
825 } else if (position.EqualsLiteral("before_end")) {
826 mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
827 mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
828 mPosition = POPUPPOSITION_BEFOREEND;
829 } else if (position.EqualsLiteral("after_start")) {
830 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
831 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
832 mPosition = POPUPPOSITION_AFTERSTART;
833 } else if (position.EqualsLiteral("after_end")) {
834 mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
835 mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
836 mPosition = POPUPPOSITION_AFTEREND;
837 } else if (position.EqualsLiteral("start_before")) {
838 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
839 mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
840 mPosition = POPUPPOSITION_STARTBEFORE;
841 } else if (position.EqualsLiteral("start_after")) {
842 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
843 mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
844 mPosition = POPUPPOSITION_STARTAFTER;
845 } else if (position.EqualsLiteral("end_before")) {
846 mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
847 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
848 mPosition = POPUPPOSITION_ENDBEFORE;
849 } else if (position.EqualsLiteral("end_after")) {
850 mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
851 mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
852 mPosition = POPUPPOSITION_ENDAFTER;
853 } else if (position.EqualsLiteral("overlap")) {
854 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
855 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
856 mPosition = POPUPPOSITION_OVERLAP;
857 } else if (position.EqualsLiteral("after_pointer")) {
858 mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
859 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
860 mPosition = POPUPPOSITION_AFTERPOINTER;
861 // XXXndeakin this is supposed to anchor vertically after, but with the
862 // horizontal position as the mouse pointer.
863 mYPos += 21;
864 } else if (position.EqualsLiteral("selection")) {
865 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
866 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
867 mPosition = POPUPPOSITION_SELECTION;
868 } else {
869 InitPositionFromAnchorAlign(anchor, align);
872 // When converted back to CSSIntRect it is (-1, -1, 0, 0) - as expected in
873 // nsXULPopupManager::Rollup
874 mScreenRect = nsRect(-AppUnitsPerCSSPixel(), -AppUnitsPerCSSPixel(), 0, 0);
876 if (aAttributesOverride) {
877 // Use |left| and |top| dimension attributes to position the popup if
878 // present, as they may have been persisted.
879 nsAutoString left, top;
880 mContent->AsElement()->GetAttr(nsGkAtoms::left, left);
881 mContent->AsElement()->GetAttr(nsGkAtoms::top, top);
883 nsresult err;
884 if (!left.IsEmpty()) {
885 int32_t x = left.ToInteger(&err);
886 if (NS_SUCCEEDED(err)) {
887 mScreenRect.x = CSSPixel::ToAppUnits(x);
890 if (!top.IsEmpty()) {
891 int32_t y = top.ToInteger(&err);
892 if (NS_SUCCEEDED(err)) {
893 mScreenRect.y = CSSPixel::ToAppUnits(y);
899 void nsMenuPopupFrame::InitializePopupAtScreen(nsIContent* aTriggerContent,
900 int32_t aXPos, int32_t aYPos,
901 bool aIsContextMenu) {
902 auto* widget = GetWidget();
903 bool recreateWidget = widget && widget->NeedsRecreateToReshow();
904 PrepareWidget(recreateWidget);
906 mPopupState = ePopupShowing;
907 mAnchorContent = nullptr;
908 mTriggerContent = aTriggerContent;
909 mScreenRect =
910 nsRect(CSSPixel::ToAppUnits(aXPos), CSSPixel::ToAppUnits(aYPos), 0, 0);
911 mXPos = 0;
912 mYPos = 0;
913 mFlip = FlipFromAttribute(this);
914 mPopupAnchor = POPUPALIGNMENT_NONE;
915 mPopupAlignment = POPUPALIGNMENT_NONE;
916 mPosition = POPUPPOSITION_UNKNOWN;
917 mIsContextMenu = aIsContextMenu;
918 mIsTopLevelContextMenu = aIsContextMenu;
919 mIsNativeMenu = false;
920 mAnchorType = MenuPopupAnchorType_Point;
921 mPositionedOffset = 0;
922 mPositionedByMoveToRect = false;
925 void nsMenuPopupFrame::InitializePopupAsNativeContextMenu(
926 nsIContent* aTriggerContent, int32_t aXPos, int32_t aYPos) {
927 mTriggerContent = aTriggerContent;
928 mPopupState = ePopupShowing;
929 mAnchorContent = nullptr;
930 mScreenRect =
931 nsRect(CSSPixel::ToAppUnits(aXPos), CSSPixel::ToAppUnits(aYPos), 0, 0);
932 mXPos = 0;
933 mYPos = 0;
934 mFlip = FlipType_Default;
935 mPopupAnchor = POPUPALIGNMENT_NONE;
936 mPopupAlignment = POPUPALIGNMENT_NONE;
937 mPosition = POPUPPOSITION_UNKNOWN;
938 mIsContextMenu = true;
939 mIsTopLevelContextMenu = true;
940 mIsNativeMenu = true;
941 mAnchorType = MenuPopupAnchorType_Point;
942 mPositionedOffset = 0;
943 mPositionedByMoveToRect = false;
946 void nsMenuPopupFrame::InitializePopupAtRect(nsIContent* aTriggerContent,
947 const nsAString& aPosition,
948 const nsIntRect& aRect,
949 bool aAttributesOverride) {
950 InitializePopup(nullptr, aTriggerContent, aPosition, 0, 0,
951 MenuPopupAnchorType_Rect, aAttributesOverride);
952 mScreenRect = ToAppUnits(aRect, AppUnitsPerCSSPixel());
955 void nsMenuPopupFrame::ShowPopup(bool aIsContextMenu) {
956 mIsContextMenu = aIsContextMenu;
958 InvalidateFrameSubtree();
960 if (mPopupState == ePopupShowing || mPopupState == ePopupPositioning) {
961 mPopupState = ePopupOpening;
962 mIsOpenChanged = true;
964 // Clear mouse capture when a popup is opened.
965 if (mPopupType == PopupType::Menu) {
966 if (auto* activeESM = EventStateManager::GetActiveEventStateManager()) {
967 EventStateManager::ClearGlobalActiveContent(activeESM);
970 PresShell::ReleaseCapturingContent();
973 if (RefPtr menu = PopupElement().GetContainingMenu()) {
974 menu->PopupOpened();
977 // We skip laying out children if we're closed, so make sure that we do a
978 // full dirty reflow when opening to pick up any potential change.
979 PresShell()->FrameNeedsReflow(
980 this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
982 if (mPopupType == PopupType::Menu) {
983 nsCOMPtr<nsISound> sound(do_GetService("@mozilla.org/sound;1"));
984 if (sound) sound->PlayEventSound(nsISound::EVENT_MENU_POPUP);
989 void nsMenuPopupFrame::ClearTriggerContentIncludingDocument() {
990 // clear the trigger content if the popup is being closed. But don't clear
991 // it if the popup is just being made invisible as a popuphiding or command
992 if (mTriggerContent) {
993 // if the popup had a trigger node set, clear the global window popup node
994 // as well
995 Document* doc = mContent->GetUncomposedDoc();
996 if (doc) {
997 if (nsPIDOMWindowOuter* win = doc->GetWindow()) {
998 nsCOMPtr<nsPIWindowRoot> root = win->GetTopWindowRoot();
999 if (root) {
1000 root->SetPopupNode(nullptr);
1005 mTriggerContent = nullptr;
1008 void nsMenuPopupFrame::HidePopup(bool aDeselectMenu, nsPopupState aNewState,
1009 bool aFromFrameDestruction) {
1010 NS_ASSERTION(aNewState == ePopupClosed || aNewState == ePopupInvisible,
1011 "popup being set to unexpected state");
1013 ClearPopupShownDispatcher();
1015 // don't hide the popup when it isn't open
1016 if (mPopupState == ePopupClosed || mPopupState == ePopupShowing ||
1017 mPopupState == ePopupPositioning) {
1018 return;
1021 if (aNewState == ePopupClosed) {
1022 // clear the trigger content if the popup is being closed. But don't clear
1023 // it if the popup is just being made invisible as a popuphiding or command
1024 // event may want to retrieve it.
1025 ClearTriggerContentIncludingDocument();
1026 mAnchorContent = nullptr;
1029 // when invisible and about to be closed, HidePopup has already been called,
1030 // so just set the new state to closed and return
1031 if (mPopupState == ePopupInvisible) {
1032 if (aNewState == ePopupClosed) {
1033 mPopupState = ePopupClosed;
1035 return;
1038 mPopupState = aNewState;
1040 mIncrementalString.Truncate();
1042 mIsOpenChanged = false;
1043 mHFlip = mVFlip = false;
1044 mConstrainedByLayout = false;
1046 if (auto* widget = GetWidget()) {
1047 // Ideally we should call ClearCachedWebrenderResources but there are
1048 // intermittent failures (see bug 1748788), so we currently call
1049 // ClearWebrenderAnimationResources instead.
1050 widget->ClearWebrenderAnimationResources();
1053 nsView* view = GetView();
1054 nsViewManager* viewManager = view->GetViewManager();
1055 viewManager->SetViewVisibility(view, ViewVisibility::Hide);
1057 RefPtr popup = &PopupElement();
1058 // XXX, bug 137033, In Windows, if mouse is outside the window when the
1059 // menupopup closes, no mouse_enter/mouse_exit event will be fired to clear
1060 // current hover state, we should clear it manually. This code may not the
1061 // best solution, but we can leave it here until we find the better approach.
1062 if (!aFromFrameDestruction &&
1063 popup->State().HasState(dom::ElementState::HOVER)) {
1064 EventStateManager* esm = PresContext()->EventStateManager();
1065 esm->SetContentState(nullptr, dom::ElementState::HOVER);
1067 popup->PopupClosed(aDeselectMenu);
1070 nsPoint nsMenuPopupFrame::AdjustPositionForAnchorAlign(
1071 nsRect& anchorRect, const nsSize& aPrefSize, FlipStyle& aHFlip,
1072 FlipStyle& aVFlip) const {
1073 // flip the anchor and alignment for right-to-left
1074 int8_t popupAnchor(mPopupAnchor);
1075 int8_t popupAlign(mPopupAlignment);
1076 if (IsDirectionRTL()) {
1077 // no need to flip the centered anchor types vertically
1078 if (popupAnchor <= POPUPALIGNMENT_LEFTCENTER) {
1079 popupAnchor = -popupAnchor;
1081 popupAlign = -popupAlign;
1084 nsRect originalAnchorRect(anchorRect);
1086 // first, determine at which corner of the anchor the popup should appear
1087 nsPoint pnt;
1088 switch (popupAnchor) {
1089 case POPUPALIGNMENT_LEFTCENTER:
1090 pnt = nsPoint(anchorRect.x, anchorRect.y + anchorRect.height / 2);
1091 anchorRect.y = pnt.y;
1092 anchorRect.height = 0;
1093 break;
1094 case POPUPALIGNMENT_RIGHTCENTER:
1095 pnt = nsPoint(anchorRect.XMost(), anchorRect.y + anchorRect.height / 2);
1096 anchorRect.y = pnt.y;
1097 anchorRect.height = 0;
1098 break;
1099 case POPUPALIGNMENT_TOPCENTER:
1100 pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.y);
1101 anchorRect.x = pnt.x;
1102 anchorRect.width = 0;
1103 break;
1104 case POPUPALIGNMENT_BOTTOMCENTER:
1105 pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.YMost());
1106 anchorRect.x = pnt.x;
1107 anchorRect.width = 0;
1108 break;
1109 case POPUPALIGNMENT_TOPRIGHT:
1110 pnt = anchorRect.TopRight();
1111 break;
1112 case POPUPALIGNMENT_BOTTOMLEFT:
1113 pnt = anchorRect.BottomLeft();
1114 break;
1115 case POPUPALIGNMENT_BOTTOMRIGHT:
1116 pnt = anchorRect.BottomRight();
1117 break;
1118 case POPUPALIGNMENT_TOPLEFT:
1119 default:
1120 pnt = anchorRect.TopLeft();
1121 break;
1124 // If the alignment is on the right edge of the popup, move the popup left
1125 // by the width. Similarly, if the alignment is on the bottom edge of the
1126 // popup, move the popup up by the height. In addition, account for the
1127 // margins of the popup on the edge on which it is aligned.
1128 nsMargin margin = GetMargin();
1129 switch (popupAlign) {
1130 case POPUPALIGNMENT_LEFTCENTER:
1131 pnt.MoveBy(margin.left, -aPrefSize.height / 2);
1132 break;
1133 case POPUPALIGNMENT_RIGHTCENTER:
1134 pnt.MoveBy(-aPrefSize.width - margin.right, -aPrefSize.height / 2);
1135 break;
1136 case POPUPALIGNMENT_TOPCENTER:
1137 pnt.MoveBy(-aPrefSize.width / 2, margin.top);
1138 break;
1139 case POPUPALIGNMENT_BOTTOMCENTER:
1140 pnt.MoveBy(-aPrefSize.width / 2, -aPrefSize.height - margin.bottom);
1141 break;
1142 case POPUPALIGNMENT_TOPRIGHT:
1143 pnt.MoveBy(-aPrefSize.width - margin.right, margin.top);
1144 break;
1145 case POPUPALIGNMENT_BOTTOMLEFT:
1146 pnt.MoveBy(margin.left, -aPrefSize.height - margin.bottom);
1147 break;
1148 case POPUPALIGNMENT_BOTTOMRIGHT:
1149 pnt.MoveBy(-aPrefSize.width - margin.right,
1150 -aPrefSize.height - margin.bottom);
1151 break;
1152 case POPUPALIGNMENT_TOPLEFT:
1153 default:
1154 pnt.MoveBy(margin.left, margin.top);
1155 break;
1158 // If we aligning to the selected item in the popup, adjust the vertical
1159 // position by the height of the menulist label and the selected item's
1160 // position.
1161 if (mPosition == POPUPPOSITION_SELECTION) {
1162 MOZ_ASSERT(popupAnchor == POPUPALIGNMENT_BOTTOMLEFT ||
1163 popupAnchor == POPUPALIGNMENT_BOTTOMRIGHT);
1164 MOZ_ASSERT(popupAlign == POPUPALIGNMENT_TOPLEFT ||
1165 popupAlign == POPUPALIGNMENT_TOPRIGHT);
1167 // Only adjust the popup if it just opened, otherwise the popup will move
1168 // around if its gets resized or the selection changed. Cache the value in
1169 // mPositionedOffset and use that instead for any future calculations.
1170 if (mIsOpenChanged) {
1171 if (nsIFrame* selectedItemFrame = GetSelectedItemForAlignment()) {
1172 const nscoord itemHeight = selectedItemFrame->GetRect().height;
1173 const nscoord itemOffset =
1174 selectedItemFrame->GetOffsetToIgnoringScrolling(this).y;
1175 // We want to line-up the anchor rect with the selected item, but if the
1176 // selected item is outside of our bounds, we don't want to shift the
1177 // popup up in a way that our box would no longer intersect with the
1178 // anchor.
1179 nscoord maxOffset = aPrefSize.height - itemHeight;
1180 if (const nsIScrollableFrame* sf = GetScrollFrame()) {
1181 // HACK: We ideally would want to use the offset from the bottom
1182 // bottom of our scroll-frame to the bottom of our frame (so as to
1183 // ensure that the bottom of the scrollport is inside the anchor
1184 // rect).
1186 // But at this point of the code, the scroll frame may not be laid out
1187 // with a definite size (might be overflowing us).
1189 // So, we assume the offset from the bottom is symmetric to the offset
1190 // from the top. This holds for all the popups where this matters
1191 // (menulists on macOS, effectively), and seems better than somehow
1192 // moving the popup after the fact as we used to do.
1193 const nsIFrame* f = do_QueryFrame(sf);
1194 maxOffset -= f->GetOffsetTo(this).y;
1196 mPositionedOffset =
1197 originalAnchorRect.height + std::min(itemOffset, maxOffset);
1201 pnt.y -= mPositionedOffset;
1204 // Flipping horizontally is allowed as long as the popup is above or below
1205 // the anchor. This will happen if both the anchor and alignment are top or
1206 // both are bottom, but different values. Similarly, flipping vertically is
1207 // allowed if the popup is to the left or right of the anchor. In this case,
1208 // the values of the constants are such that both must be positive or both
1209 // must be negative. A special case, used for overlap, allows flipping
1210 // vertically as well.
1211 // If we are flipping in both directions, we want to set a flip style both
1212 // horizontally and vertically. However, we want to flip on the inside edge
1213 // of the anchor. Consider the example of a typical dropdown menu.
1214 // Vertically, we flip the popup on the outside edges of the anchor menu,
1215 // however horizontally, we want to to use the inside edges so the popup
1216 // still appears underneath the anchor menu instead of floating off the
1217 // side of the menu.
1218 switch (popupAnchor) {
1219 case POPUPALIGNMENT_LEFTCENTER:
1220 case POPUPALIGNMENT_RIGHTCENTER:
1221 aHFlip = FlipStyle_Outside;
1222 aVFlip = FlipStyle_Inside;
1223 break;
1224 case POPUPALIGNMENT_TOPCENTER:
1225 case POPUPALIGNMENT_BOTTOMCENTER:
1226 aHFlip = FlipStyle_Inside;
1227 aVFlip = FlipStyle_Outside;
1228 break;
1229 default: {
1230 FlipStyle anchorEdge =
1231 mFlip == FlipType_Both ? FlipStyle_Inside : FlipStyle_None;
1232 aHFlip = (popupAnchor == -popupAlign) ? FlipStyle_Outside : anchorEdge;
1233 if (((popupAnchor > 0) == (popupAlign > 0)) ||
1234 (popupAnchor == POPUPALIGNMENT_TOPLEFT &&
1235 popupAlign == POPUPALIGNMENT_TOPLEFT))
1236 aVFlip = FlipStyle_Outside;
1237 else
1238 aVFlip = anchorEdge;
1239 break;
1243 return pnt;
1246 nsIFrame* nsMenuPopupFrame::GetSelectedItemForAlignment() const {
1247 // This method adjusts a menulist's popup such that the selected item is under
1248 // the cursor, aligned with the menulist label.
1249 nsCOMPtr<nsIDOMXULSelectControlElement> select;
1250 if (mAnchorContent) {
1251 select = mAnchorContent->AsElement()->AsXULSelectControl();
1254 if (!select) {
1255 // If there isn't an anchor, then try just getting the parent of the popup.
1256 select = mContent->GetParent()->AsElement()->AsXULSelectControl();
1257 if (!select) {
1258 return nullptr;
1262 nsCOMPtr<Element> selectedElement;
1263 select->GetSelectedItem(getter_AddRefs(selectedElement));
1264 return selectedElement ? selectedElement->GetPrimaryFrame() : nullptr;
1267 nscoord nsMenuPopupFrame::SlideOrResize(nscoord& aScreenPoint, nscoord aSize,
1268 nscoord aScreenBegin,
1269 nscoord aScreenEnd,
1270 nscoord* aOffset) const {
1271 // The popup may be positioned such that either the left/top or bottom/right
1272 // is outside the screen - but never both.
1273 nscoord newPos =
1274 std::max(aScreenBegin, std::min(aScreenEnd - aSize, aScreenPoint));
1275 *aOffset = newPos - aScreenPoint;
1276 aScreenPoint = newPos;
1277 return std::min(aSize, aScreenEnd - aScreenPoint);
1280 nscoord nsMenuPopupFrame::FlipOrResize(nscoord& aScreenPoint, nscoord aSize,
1281 nscoord aScreenBegin, nscoord aScreenEnd,
1282 nscoord aAnchorBegin, nscoord aAnchorEnd,
1283 nscoord aMarginBegin, nscoord aMarginEnd,
1284 FlipStyle aFlip, bool aEndAligned,
1285 bool* aFlipSide) const {
1286 // The flip side argument will be set to true if there wasn't room and we
1287 // flipped to the opposite side.
1288 *aFlipSide = false;
1290 // all of the coordinates used here are in app units relative to the screen
1291 nscoord popupSize = aSize;
1292 if (aScreenPoint < aScreenBegin) {
1293 // at its current position, the popup would extend past the left or top
1294 // edge of the screen, so it will have to be moved or resized.
1295 if (aFlip) {
1296 // for inside flips, we flip on the opposite side of the anchor
1297 nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd;
1298 nscoord endpos = aFlip == FlipStyle_Outside ? aAnchorEnd : aAnchorBegin;
1300 // check whether there is more room to the left and right (or top and
1301 // bottom) of the anchor and put the popup on the side with more room.
1302 if (startpos - aScreenBegin >= aScreenEnd - endpos) {
1303 aScreenPoint = aScreenBegin;
1304 popupSize = startpos - aScreenPoint - aMarginEnd;
1305 *aFlipSide = !aEndAligned;
1306 } else {
1307 // If the newly calculated position is different than the existing
1308 // position, flip such that the popup is to the right or bottom of the
1309 // anchor point instead . However, when flipping use the same margin
1310 // size.
1311 nscoord newScreenPoint = endpos + aMarginEnd;
1312 if (newScreenPoint != aScreenPoint) {
1313 *aFlipSide = aEndAligned;
1314 aScreenPoint = newScreenPoint;
1315 // check if the new position is still off the right or bottom edge of
1316 // the screen. If so, resize the popup.
1317 if (aScreenPoint + aSize > aScreenEnd) {
1318 popupSize = aScreenEnd - aScreenPoint;
1322 } else {
1323 aScreenPoint = aScreenBegin;
1325 } else if (aScreenPoint + aSize > aScreenEnd) {
1326 // at its current position, the popup would extend past the right or
1327 // bottom edge of the screen, so it will have to be moved or resized.
1328 if (aFlip) {
1329 // for inside flips, we flip on the opposite side of the anchor
1330 nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd;
1331 nscoord endpos = aFlip == FlipStyle_Outside ? aAnchorEnd : aAnchorBegin;
1333 // check whether there is more room to the left and right (or top and
1334 // bottom) of the anchor and put the popup on the side with more room.
1335 if (aScreenEnd - endpos >= startpos - aScreenBegin) {
1336 *aFlipSide = aEndAligned;
1337 if (mIsContextMenu) {
1338 aScreenPoint = aScreenEnd - aSize;
1339 } else {
1340 aScreenPoint = endpos + aMarginBegin;
1341 popupSize = aScreenEnd - aScreenPoint;
1343 } else {
1344 // if the newly calculated position is different than the existing
1345 // position, we flip such that the popup is to the left or top of the
1346 // anchor point instead.
1347 nscoord newScreenPoint = startpos - aSize - aMarginBegin;
1348 if (newScreenPoint != aScreenPoint) {
1349 *aFlipSide = !aEndAligned;
1350 aScreenPoint = newScreenPoint;
1352 // check if the new position is still off the left or top edge of the
1353 // screen. If so, resize the popup.
1354 if (aScreenPoint < aScreenBegin) {
1355 aScreenPoint = aScreenBegin;
1356 if (!mIsContextMenu) {
1357 popupSize = startpos - aScreenPoint - aMarginBegin;
1362 } else {
1363 aScreenPoint = aScreenEnd - aSize;
1367 // Make sure that the point is within the screen boundaries and that the
1368 // size isn't off the edge of the screen. This can happen when a large
1369 // positive or negative margin is used.
1370 if (aScreenPoint < aScreenBegin) {
1371 aScreenPoint = aScreenBegin;
1373 if (aScreenPoint > aScreenEnd) {
1374 aScreenPoint = aScreenEnd - aSize;
1377 // If popupSize ended up being negative, or the original size was actually
1378 // smaller than the calculated popup size, just use the original size instead.
1379 if (popupSize <= 0 || aSize < popupSize) {
1380 popupSize = aSize;
1383 return std::min(popupSize, aScreenEnd - aScreenPoint);
1386 nsRect nsMenuPopupFrame::ComputeAnchorRect(nsPresContext* aRootPresContext,
1387 nsIFrame* aAnchorFrame) const {
1388 // Get the root frame for a reference
1389 nsIFrame* rootFrame = aRootPresContext->PresShell()->GetRootFrame();
1391 // The dimensions of the anchor
1392 nsRect anchorRect = aAnchorFrame->GetRectRelativeToSelf();
1394 // Relative to the root
1395 anchorRect = nsLayoutUtils::TransformFrameRectToAncestor(
1396 aAnchorFrame, anchorRect, rootFrame);
1397 // Relative to the screen
1398 anchorRect.MoveBy(rootFrame->GetScreenRectInAppUnits().TopLeft());
1400 // In its own app units
1401 return anchorRect.ScaleToOtherAppUnitsRoundOut(
1402 aRootPresContext->AppUnitsPerDevPixel(),
1403 PresContext()->AppUnitsPerDevPixel());
1406 static nsIFrame* MaybeDelegatedAnchorFrame(nsIFrame* aFrame) {
1407 if (!aFrame) {
1408 return nullptr;
1410 if (auto* element = Element::FromNodeOrNull(aFrame->GetContent())) {
1411 if (element->HasAttr(nsGkAtoms::delegatesanchor)) {
1412 for (nsIFrame* f : aFrame->PrincipalChildList()) {
1413 if (!f->IsPlaceholderFrame()) {
1414 return f;
1419 return aFrame;
1422 auto nsMenuPopupFrame::GetRects(const nsSize& aPrefSize) const -> Rects {
1423 if (NS_WARN_IF(aPrefSize == nsSize(-1, -1))) {
1424 // Return early if the popup hasn't been laid out yet. On Windows, this can
1425 // happen when using a drag popup before it opens.
1426 return {};
1429 nsPresContext* pc = PresContext();
1430 nsIFrame* rootFrame = pc->PresShell()->GetRootFrame();
1431 NS_ASSERTION(rootFrame->GetView() && GetView() &&
1432 rootFrame->GetView() == GetView()->GetParent(),
1433 "rootFrame's view is not our view's parent???");
1435 // Indicators of whether the popup should be flipped or resized.
1436 FlipStyle hFlip = FlipStyle_None, vFlip = FlipStyle_None;
1438 const nsMargin margin = GetMargin();
1440 // the screen rectangle of the root frame, in dev pixels.
1441 const nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits();
1443 const bool isNoAutoHide = IsNoAutoHide();
1444 const PopupLevel popupLevel = GetPopupLevel(isNoAutoHide);
1446 Rects result;
1448 // Set the popup's size to the preferred size. Below, this size will be
1449 // adjusted to fit on the screen or within the content area. If the anchor is
1450 // sized to the popup, use the anchor's width instead of the preferred width.
1451 result.mUsedRect = nsRect(nsPoint(), aPrefSize);
1453 const bool anchored = IsAnchored();
1454 if (anchored) {
1455 // In order to deal with transforms, we need the root prescontext:
1456 nsPresContext* rootPc = pc->GetRootPresContext();
1457 if (NS_WARN_IF(!rootPc)) {
1458 // If we can't reach a root pres context, don't bother continuing.
1459 return result;
1462 result.mAnchorRect = result.mUntransformedAnchorRect = [&] {
1463 // If anchored to a rectangle, use that rectangle. Otherwise, determine
1464 // the rectangle from the anchor.
1465 if (mAnchorType == MenuPopupAnchorType_Rect) {
1466 return mScreenRect;
1468 // if the frame is not specified, use the anchor node passed to OpenPopup.
1469 // If that wasn't specified either, use the root frame. Note that
1470 // mAnchorContent might be a different document so its presshell must be
1471 // used.
1472 nsIFrame* anchorFrame = GetAnchorFrame();
1473 if (!anchorFrame) {
1474 return rootScreenRect;
1476 return ComputeAnchorRect(rootPc, anchorFrame);
1477 }();
1479 // if we are anchored, there are certain things we don't want to do when
1480 // repositioning the popup to fit on the screen, such as end up positioned
1481 // over the anchor, for instance a popup appearing over the menu label.
1482 // When doing this reposition, we want to move the popup to the side with
1483 // the most room. The combination of anchor and alignment dictate if we
1484 // readjust above/below or to the left/right.
1485 if (mAnchorContent || mAnchorType == MenuPopupAnchorType_Rect) {
1486 // move the popup according to the anchor and alignment. This will also
1487 // tell us which axis the popup is flush against in case we have to move
1488 // it around later. The AdjustPositionForAnchorAlign method accounts for
1489 // the popup's margin.
1490 result.mUsedRect.MoveTo(AdjustPositionForAnchorAlign(
1491 result.mAnchorRect, aPrefSize, hFlip, vFlip));
1492 } else {
1493 // With no anchor, the popup is positioned relative to the root frame.
1494 result.mUsedRect.MoveTo(result.mAnchorRect.TopLeft() +
1495 nsPoint(margin.left, margin.top));
1498 // mXPos and mYPos specify an additional offset passed to OpenPopup that
1499 // should be added to the position. We also add the offset to the anchor
1500 // pos so a later flip/resize takes the offset into account.
1501 // FIXME(emilio): Wayland doesn't seem to be accounting for this offset
1502 // anywhere, and it probably should.
1504 nsPoint offset(CSSPixel::ToAppUnits(mXPos), CSSPixel::ToAppUnits(mYPos));
1505 if (IsDirectionRTL()) {
1506 offset.x = -offset.x;
1508 result.mUsedRect.MoveBy(offset);
1509 result.mAnchorRect.MoveBy(offset);
1511 } else {
1512 // Not anchored, use mScreenRect
1513 result.mUsedRect.MoveTo(mScreenRect.TopLeft());
1514 result.mAnchorRect = result.mUntransformedAnchorRect =
1515 nsRect(mScreenRect.TopLeft(), nsSize());
1517 // Right-align RTL context menus, and apply margin and offsets as per the
1518 // platform conventions.
1519 if (mIsContextMenu && IsDirectionRTL()) {
1520 result.mUsedRect.x -= aPrefSize.Width();
1521 result.mUsedRect.MoveBy(-margin.right, margin.top);
1522 } else {
1523 result.mUsedRect.MoveBy(margin.left, margin.top);
1525 #ifdef XP_MACOSX
1526 // OSX tooltips follow standard flip rule but other popups flip horizontally
1527 // not vertically
1528 if (mPopupType == PopupType::Tooltip) {
1529 vFlip = FlipStyle_Outside;
1530 } else {
1531 hFlip = FlipStyle_Outside;
1533 #else
1534 // Other OS screen positioned popups can be flipped vertically but never
1535 // horizontally
1536 vFlip = FlipStyle_Outside;
1537 #endif // #ifdef XP_MACOSX
1540 const int32_t a2d = pc->AppUnitsPerDevPixel();
1542 nsView* view = GetView();
1543 NS_ASSERTION(view, "popup with no view");
1545 nsIWidget* widget = view->GetWidget();
1547 // If a panel has flip="none", don't constrain or flip it.
1548 // Also, always do this for content shells, so that the popup doesn't extend
1549 // outside the containing frame.
1550 if (mInContentShell || mFlip != FlipType_None) {
1551 const Maybe<nsRect> constraintRect =
1552 GetConstraintRect(result.mAnchorRect, rootScreenRect, popupLevel);
1554 if (constraintRect) {
1555 // Ensure that anchorRect is on the constraint rect.
1556 result.mAnchorRect = result.mAnchorRect.Intersect(*constraintRect);
1557 // Shrink the popup down if it is larger than the constraint size
1558 if (result.mUsedRect.width > constraintRect->width) {
1559 result.mUsedRect.width = constraintRect->width;
1561 if (result.mUsedRect.height > constraintRect->height) {
1562 result.mUsedRect.height = constraintRect->height;
1564 result.mConstrainedByLayout = true;
1567 if (IS_WAYLAND_DISPLAY() && widget) {
1568 // Shrink the popup down if it's larger than popup size received from
1569 // Wayland compositor. We don't know screen size on Wayland so this is the
1570 // only info we have there.
1571 const nsSize waylandSize = LayoutDeviceIntRect::ToAppUnits(
1572 widget->GetMoveToRectPopupSize(), a2d);
1573 if (waylandSize.width > 0 && result.mUsedRect.width > waylandSize.width) {
1574 LOG_WAYLAND("Wayland constraint width [%p]: %d to %d", widget,
1575 result.mUsedRect.width, waylandSize.width);
1576 result.mUsedRect.width = waylandSize.width;
1578 if (waylandSize.height > 0 &&
1579 result.mUsedRect.height > waylandSize.height) {
1580 LOG_WAYLAND("Wayland constraint height [%p]: %d to %d", widget,
1581 result.mUsedRect.height, waylandSize.height);
1582 result.mUsedRect.height = waylandSize.height;
1584 if (RefPtr<widget::Screen> s = widget->GetWidgetScreen()) {
1585 const nsSize screenSize =
1586 LayoutDeviceIntSize::ToAppUnits(s->GetAvailRect().Size(), a2d);
1587 if (result.mUsedRect.height > screenSize.height) {
1588 LOG_WAYLAND("Wayland constraint height to screen [%p]: %d to %d",
1589 widget, result.mUsedRect.height, screenSize.height);
1590 result.mUsedRect.height = screenSize.height;
1592 if (result.mUsedRect.width > screenSize.width) {
1593 LOG_WAYLAND("Wayland constraint widthto screen [%p]: %d to %d",
1594 widget, result.mUsedRect.width, screenSize.width);
1595 result.mUsedRect.width = screenSize.width;
1600 // At this point the anchor (anchorRect) is within the available screen
1601 // area (constraintRect) and the popup is known to be no larger than the
1602 // screen.
1603 if (constraintRect) {
1604 // We might want to "slide" an arrow if the panel is of the correct type -
1605 // but we can only slide on one axis - the other axis must be "flipped or
1606 // resized" as normal.
1607 bool slideHorizontal = false, slideVertical = false;
1608 if (mFlip == FlipType_Slide) {
1609 int8_t position = GetAlignmentPosition();
1610 slideHorizontal = position >= POPUPPOSITION_BEFORESTART &&
1611 position <= POPUPPOSITION_AFTEREND;
1612 slideVertical = position >= POPUPPOSITION_STARTBEFORE &&
1613 position <= POPUPPOSITION_ENDAFTER;
1616 // Next, check if there is enough space to show the popup at full size
1617 // when positioned at screenPoint. If not, flip the popups to the opposite
1618 // side of their anchor point, or resize them as necessary.
1619 if (slideHorizontal) {
1620 result.mUsedRect.width = SlideOrResize(
1621 result.mUsedRect.x, result.mUsedRect.width, constraintRect->x,
1622 constraintRect->XMost(), &result.mAlignmentOffset);
1623 } else {
1624 const bool endAligned =
1625 IsDirectionRTL()
1626 ? mPopupAlignment == POPUPALIGNMENT_TOPLEFT ||
1627 mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT ||
1628 mPopupAlignment == POPUPALIGNMENT_LEFTCENTER
1629 : mPopupAlignment == POPUPALIGNMENT_TOPRIGHT ||
1630 mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT ||
1631 mPopupAlignment == POPUPALIGNMENT_RIGHTCENTER;
1632 result.mUsedRect.width = FlipOrResize(
1633 result.mUsedRect.x, result.mUsedRect.width, constraintRect->x,
1634 constraintRect->XMost(), result.mAnchorRect.x,
1635 result.mAnchorRect.XMost(), margin.left, margin.right, hFlip,
1636 endAligned, &result.mHFlip);
1638 if (slideVertical) {
1639 result.mUsedRect.height = SlideOrResize(
1640 result.mUsedRect.y, result.mUsedRect.height, constraintRect->y,
1641 constraintRect->YMost(), &result.mAlignmentOffset);
1642 } else {
1643 bool endAligned = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT ||
1644 mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT ||
1645 mPopupAlignment == POPUPALIGNMENT_BOTTOMCENTER;
1646 result.mUsedRect.height = FlipOrResize(
1647 result.mUsedRect.y, result.mUsedRect.height, constraintRect->y,
1648 constraintRect->YMost(), result.mAnchorRect.y,
1649 result.mAnchorRect.YMost(), margin.top, margin.bottom, vFlip,
1650 endAligned, &result.mVFlip);
1653 #ifdef DEBUG
1654 NS_ASSERTION(constraintRect->Contains(result.mUsedRect),
1655 "Popup is offscreen");
1656 if (!constraintRect->Contains(result.mUsedRect)) {
1657 NS_WARNING(nsPrintfCString("Popup is offscreen (%s vs. %s)",
1658 ToString(constraintRect).c_str(),
1659 ToString(result.mUsedRect).c_str())
1660 .get());
1662 #endif
1665 // snap the popup's position in screen coordinates to device pixels, see
1666 // bug 622507, bug 961431
1667 result.mUsedRect.x = pc->RoundAppUnitsToNearestDevPixels(result.mUsedRect.x);
1668 result.mUsedRect.y = pc->RoundAppUnitsToNearestDevPixels(result.mUsedRect.y);
1670 // determine the x and y position of the view by subtracting the desired
1671 // screen position from the screen position of the root frame.
1672 result.mViewPoint = result.mUsedRect.TopLeft() - rootScreenRect.TopLeft();
1674 // Offset the position by the width and height of the borders and titlebar.
1675 // Even though GetClientOffset should return (0, 0) when there is no titlebar
1676 // or borders, we skip these calculations anyway for non-panels to save time
1677 // since they will never have a titlebar.
1678 if (mPopupType == PopupType::Panel && widget) {
1679 result.mClientOffset = widget->GetClientOffset();
1680 result.mViewPoint +=
1681 LayoutDeviceIntPoint::ToAppUnits(result.mClientOffset, a2d);
1684 return result;
1687 void nsMenuPopupFrame::SetPopupPosition(bool aIsMove) {
1688 if (aIsMove && (mPrefSize.width == -1 || mPrefSize.height == -1)) {
1689 return;
1692 auto rects = GetRects(mPrefSize);
1693 if (rects.mUsedRect.Size() != mRect.Size()) {
1694 MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IN_REFLOW));
1695 // We need to resize on top of moving, trigger an actual reflow.
1696 PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors,
1697 NS_FRAME_IS_DIRTY);
1698 return;
1700 PerformMove(rects);
1703 void nsMenuPopupFrame::PerformMove(const Rects& aRects) {
1704 auto* ps = PresShell();
1706 // We're just moving, sync frame position and offset as needed.
1707 ps->GetViewManager()->MoveViewTo(GetView(), aRects.mViewPoint.x,
1708 aRects.mViewPoint.y);
1710 // Now that we've positioned the view, sync up the frame's origin.
1711 nsBlockFrame::SetPosition(aRects.mViewPoint -
1712 GetParent()->GetOffsetTo(ps->GetRootFrame()));
1714 // If the popup is in the positioned state or if it is shown and the position
1715 // or size changed, dispatch a popuppositioned event if the popup wants it.
1716 if (mPopupState == ePopupPositioning ||
1717 (mPopupState == ePopupShown &&
1718 !aRects.mUsedRect.IsEqualEdges(mUsedScreenRect)) ||
1719 (mPopupState == ePopupShown &&
1720 aRects.mAlignmentOffset != mAlignmentOffset)) {
1721 mUsedScreenRect = aRects.mUsedRect;
1722 if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && !mPendingPositionedEvent) {
1723 mPendingPositionedEvent =
1724 nsXULPopupPositionedEvent::DispatchIfNeeded(mContent->AsElement());
1728 if (!mPositionedByMoveToRect) {
1729 mUntransformedAnchorRect = aRects.mUntransformedAnchorRect;
1732 mAlignmentOffset = aRects.mAlignmentOffset;
1733 mLastClientOffset = aRects.mClientOffset;
1734 mHFlip = aRects.mHFlip;
1735 mVFlip = aRects.mVFlip;
1736 mConstrainedByLayout = aRects.mConstrainedByLayout;
1738 // If this is a noautohide popup, set the screen coordinates of the popup.
1739 // This way, the popup stays at the location where it was opened even when the
1740 // window is moved. Popups at the parent level follow the parent window as it
1741 // is moved and remained anchored, so we want to maintain the anchoring
1742 // instead.
1744 // FIXME: This suffers from issues like bug 1823552, where constraints imposed
1745 // by the anchor are lost, but this is super-old behavior.
1746 const bool fixPositionToPoint =
1747 IsNoAutoHide() && (GetPopupLevel() != PopupLevel::Parent ||
1748 mAnchorType == MenuPopupAnchorType_Rect);
1749 if (fixPositionToPoint) {
1750 // Account for the margin that will end up being added to the screen
1751 // coordinate the next time SetPopupPosition is called.
1752 const auto& margin = GetMargin();
1753 mAnchorType = MenuPopupAnchorType_Point;
1754 mScreenRect.x = aRects.mUsedRect.x - margin.left;
1755 mScreenRect.y = aRects.mUsedRect.y - margin.top;
1758 // For anchored popups that shouldn't follow the anchor, fix the original
1759 // anchor rect.
1760 if (IsAnchored() && !ShouldFollowAnchor() && !mUsedScreenRect.IsEmpty() &&
1761 mAnchorType != MenuPopupAnchorType_Rect) {
1762 mAnchorType = MenuPopupAnchorType_Rect;
1763 mScreenRect = aRects.mUntransformedAnchorRect;
1766 // NOTE(emilio): This call below is kind of a workaround, but we need to do
1767 // this here because some position changes don't go through the
1768 // view system -> popup manager, like:
1770 // https://searchfox.org/mozilla-central/rev/477950cf9ca9c9bb5ff6f34e0d0f6ca4718ea798/widget/gtk/nsWindow.cpp#3847
1772 // So this might be the last chance we have to set the remote browser's
1773 // position.
1775 // Ultimately this probably wants to get fixed in the widget size of things,
1776 // but given this is worst-case a redundant DOM traversal, and that popups
1777 // usually don't have all that much content, this is probably an ok
1778 // workaround.
1779 WidgetPositionOrSizeDidChange();
1782 void nsMenuPopupFrame::WidgetPositionOrSizeDidChange() {
1783 // In the case this popup has remote contents having OOP iframes, it's
1784 // possible that OOP iframe's nsSubDocumentFrame has been already reflowed
1785 // thus, we will never have a chance to tell this parent browser's position
1786 // update to the OOP documents without notifying it explicitly.
1787 if (!HasRemoteContent()) {
1788 return;
1790 for (nsIContent* content = mContent->GetFirstChild(); content;
1791 content = content->GetNextNode(mContent)) {
1792 if (content->IsXULElement(nsGkAtoms::browser) &&
1793 content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::remote,
1794 nsGkAtoms::_true, eIgnoreCase)) {
1795 if (auto* browserParent = dom::BrowserParent::GetFrom(content)) {
1796 browserParent->NotifyPositionUpdatedForContentsInPopup();
1802 Maybe<nsRect> nsMenuPopupFrame::GetConstraintRect(
1803 const nsRect& aAnchorRect, const nsRect& aRootScreenRect,
1804 PopupLevel aPopupLevel) const {
1805 const nsPresContext* pc = PresContext();
1806 const int32_t a2d = PresContext()->AppUnitsPerDevPixel();
1807 Maybe<nsRect> result;
1809 auto AddConstraint = [&result](const nsRect& aConstraint) {
1810 if (result) {
1811 *result = result->Intersect(aConstraint);
1812 } else {
1813 result.emplace(aConstraint);
1817 // Determine the available screen space. It will be reduced by the OS chrome
1818 // such as menubars. It addition, for content shells, it will be the area of
1819 // the content rather than the screen.
1820 // In Wayland we can't use the screen rect because we can't know absolute
1821 // window position.
1822 if (!IS_WAYLAND_DISPLAY()) {
1823 const DesktopToLayoutDeviceScale scale =
1824 pc->DeviceContext()->GetDesktopToDeviceScale();
1825 // For content shells, get the screen where the root frame is located. This
1826 // is because we need to constrain the content to this content area, so we
1827 // should use the same screen. Otherwise, use the screen where the anchor is
1828 // located.
1829 const nsRect& rect = mInContentShell ? aRootScreenRect : aAnchorRect;
1830 auto desktopRect = DesktopIntRect::RoundOut(
1831 LayoutDeviceRect::FromAppUnits(rect, a2d) / scale);
1832 desktopRect.width = std::max(1, desktopRect.width);
1833 desktopRect.height = std::max(1, desktopRect.height);
1835 RefPtr<nsIScreen> screen =
1836 widget::ScreenManager::GetSingleton().ScreenForRect(desktopRect);
1837 MOZ_ASSERT(screen, "We always fall back to the primary screen");
1838 // Non-top-level popups (which will always be panels) should never overlap
1839 // the OS bar.
1840 const bool canOverlapOSBar =
1841 aPopupLevel == PopupLevel::Top &&
1842 LookAndFeel::GetInt(LookAndFeel::IntID::MenusCanOverlapOSBar) &&
1843 !mInContentShell;
1844 // Get the total screen area if the popup is allowed to overlap it.
1845 const auto screenRect =
1846 canOverlapOSBar ? screen->GetRect() : screen->GetAvailRect();
1847 AddConstraint(LayoutDeviceRect::ToAppUnits(screenRect, a2d));
1850 if (mInContentShell) {
1851 // For content shells, clip to the client area rather than the screen area
1852 AddConstraint(aRootScreenRect);
1853 } else if (!mOverrideConstraintRect.IsEmpty()) {
1854 AddConstraint(mOverrideConstraintRect);
1855 // This is currently only used for <select> elements where we want to
1856 // constrain vertically to the screen but not horizontally, so do the
1857 // intersection and then reset the horizontal values.
1859 // FIXME(emilio): This doesn't make any sense to me...
1860 result->x = mOverrideConstraintRect.x;
1861 result->width = mOverrideConstraintRect.width;
1864 // Expand the allowable screen rect by the input margin (which can't be
1865 // interacted with).
1866 if (result) {
1867 const nscoord inputMargin =
1868 StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits();
1869 result->Inflate(inputMargin);
1871 return result;
1874 ConsumeOutsideClicksResult nsMenuPopupFrame::ConsumeOutsideClicks() {
1875 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
1876 nsGkAtoms::consumeoutsideclicks,
1877 nsGkAtoms::_true, eCaseMatters)) {
1878 return ConsumeOutsideClicks_True;
1880 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
1881 nsGkAtoms::consumeoutsideclicks,
1882 nsGkAtoms::_false, eCaseMatters)) {
1883 return ConsumeOutsideClicks_ParentOnly;
1885 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
1886 nsGkAtoms::consumeoutsideclicks,
1887 nsGkAtoms::never, eCaseMatters)) {
1888 return ConsumeOutsideClicks_Never;
1891 nsCOMPtr<nsIContent> parentContent = mContent->GetParent();
1892 if (parentContent) {
1893 dom::NodeInfo* ni = parentContent->NodeInfo();
1894 if (ni->Equals(nsGkAtoms::menulist, kNameSpaceID_XUL)) {
1895 return ConsumeOutsideClicks_True; // Consume outside clicks for combo
1896 // boxes on all platforms
1898 #if defined(XP_WIN)
1899 // Don't consume outside clicks for menus in Windows
1900 if (ni->Equals(nsGkAtoms::menu, kNameSpaceID_XUL) ||
1901 ni->Equals(nsGkAtoms::popupset, kNameSpaceID_XUL) ||
1902 ((ni->Equals(nsGkAtoms::button, kNameSpaceID_XUL) ||
1903 ni->Equals(nsGkAtoms::toolbarbutton, kNameSpaceID_XUL)) &&
1904 parentContent->AsElement()->AttrValueIs(
1905 kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::menu,
1906 eCaseMatters))) {
1907 return ConsumeOutsideClicks_Never;
1909 #endif
1912 return ConsumeOutsideClicks_True;
1915 static nsIScrollableFrame* DoGetScrollFrame(const nsIFrame* aFrame) {
1916 if (const nsIScrollableFrame* sf = do_QueryFrame(aFrame)) {
1917 return const_cast<nsIScrollableFrame*>(sf);
1919 for (nsIFrame* childFrame : aFrame->PrincipalChildList()) {
1920 if (auto* sf = DoGetScrollFrame(childFrame)) {
1921 return sf;
1924 return nullptr;
1927 // XXXroc this is megalame. Fossicking around for a frame of the right
1928 // type is a recipe for disaster in the long term.
1929 nsIScrollableFrame* nsMenuPopupFrame::GetScrollFrame() const {
1930 return DoGetScrollFrame(this);
1933 void nsMenuPopupFrame::ChangeByPage(bool aIsUp) {
1934 // Only scroll by page within menulists.
1935 if (!IsMenuList()) {
1936 return;
1939 nsIScrollableFrame* scrollframe = GetScrollFrame();
1941 RefPtr popup = &PopupElement();
1942 XULButtonElement* currentMenu = popup->GetActiveMenuChild();
1943 XULButtonElement* newMenu = nullptr;
1944 if (!currentMenu) {
1945 // If there is no current menu item, get the first item. When moving up,
1946 // just use this as the newMenu and leave currentMenu null so that no check
1947 // for a later element is performed. When moving down, set currentMenu so
1948 // that we look for one page down from the first item.
1949 newMenu = popup->GetFirstMenuItem();
1950 if (!aIsUp) {
1951 currentMenu = newMenu;
1955 if (currentMenu && currentMenu->GetPrimaryFrame()) {
1956 const nscoord scrollHeight =
1957 scrollframe ? scrollframe->GetScrollPortRect().height : mRect.height;
1958 const nsRect currentRect = currentMenu->GetPrimaryFrame()->GetRect();
1959 const XULButtonElement* startMenu = currentMenu;
1961 // Get the position of the current item and add or subtract one popup's
1962 // height to or from it.
1963 const nscoord targetPos = aIsUp ? currentRect.YMost() - scrollHeight
1964 : currentRect.y + scrollHeight;
1965 // Look for the next child which is just past the target position. This
1966 // child will need to be selected.
1967 for (; currentMenu;
1968 currentMenu = aIsUp ? popup->GetPrevMenuItemFrom(*currentMenu)
1969 : popup->GetNextMenuItemFrom(*currentMenu)) {
1970 if (!currentMenu->GetPrimaryFrame()) {
1971 continue;
1973 const nsRect curRect = currentMenu->GetPrimaryFrame()->GetRect();
1974 const nscoord curPos = aIsUp ? curRect.y : curRect.YMost();
1975 // If the right position was found, break out. Otherwise, look for another
1976 // item.
1977 if (aIsUp ? (curPos < targetPos) : (curPos > targetPos)) {
1978 if (!newMenu || newMenu == startMenu) {
1979 newMenu = currentMenu;
1981 break;
1984 // Assign this item to newMenu. This item will be selected in case we
1985 // don't find any more.
1986 newMenu = currentMenu;
1990 // Select the new menuitem.
1991 if (RefPtr newMenuRef = newMenu) {
1992 popup->SetActiveMenuChild(newMenuRef);
1996 dom::XULPopupElement& nsMenuPopupFrame::PopupElement() const {
1997 auto* popup = dom::XULPopupElement::FromNode(GetContent());
1998 MOZ_DIAGNOSTIC_ASSERT(popup);
1999 return *popup;
2002 XULButtonElement* nsMenuPopupFrame::GetCurrentMenuItem() const {
2003 return PopupElement().GetActiveMenuChild();
2006 nsIFrame* nsMenuPopupFrame::GetCurrentMenuItemFrame() const {
2007 auto* child = GetCurrentMenuItem();
2008 return child ? child->GetPrimaryFrame() : nullptr;
2011 void nsMenuPopupFrame::HandleEnterKeyPress(WidgetEvent& aEvent) {
2012 mIncrementalString.Truncate();
2013 RefPtr popup = &PopupElement();
2014 popup->HandleEnterKeyPress(aEvent);
2017 XULButtonElement* nsMenuPopupFrame::FindMenuWithShortcut(
2018 mozilla::dom::KeyboardEvent& aKeyEvent, bool& aDoAction) {
2019 uint32_t charCode = aKeyEvent.CharCode();
2020 uint32_t keyCode = aKeyEvent.KeyCode();
2022 aDoAction = false;
2024 // Enumerate over our list of frames.
2025 const bool isMenu = !IsMenuList();
2026 TimeStamp keyTime = aKeyEvent.WidgetEventPtr()->mTimeStamp;
2027 if (charCode == 0) {
2028 if (keyCode == dom::KeyboardEvent_Binding::DOM_VK_BACK_SPACE) {
2029 if (!isMenu && !mIncrementalString.IsEmpty()) {
2030 mIncrementalString.SetLength(mIncrementalString.Length() - 1);
2031 return nullptr;
2033 #ifdef XP_WIN
2034 if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
2035 sound->Beep();
2037 #endif // #ifdef XP_WIN
2039 return nullptr;
2041 char16_t uniChar = ToLowerCase(static_cast<char16_t>(charCode));
2042 if (isMenu) {
2043 // Menu supports only first-letter navigation
2044 mIncrementalString = uniChar;
2045 } else if (IsWithinIncrementalTime(keyTime)) {
2046 mIncrementalString.Append(uniChar);
2047 } else {
2048 // Interval too long, treat as new typing
2049 mIncrementalString = uniChar;
2052 // See bug 188199 & 192346, if all letters in incremental string are same,
2053 // just try to match the first one
2054 nsAutoString incrementalString(mIncrementalString);
2055 uint32_t charIndex = 1, stringLength = incrementalString.Length();
2056 while (charIndex < stringLength &&
2057 incrementalString[charIndex] == incrementalString[charIndex - 1]) {
2058 charIndex++;
2060 if (charIndex == stringLength) {
2061 incrementalString.Truncate(1);
2062 stringLength = 1;
2065 sLastKeyTime = keyTime;
2067 auto* item =
2068 PopupElement().FindMenuWithShortcut(incrementalString, aDoAction);
2069 if (item) {
2070 return item;
2073 // If we don't match anything, rollback the last typing
2074 mIncrementalString.SetLength(mIncrementalString.Length() - 1);
2076 // didn't find a matching menu item
2077 #ifdef XP_WIN
2078 // behavior on Windows - this item is in a menu popup off of the
2079 // menu bar, so beep and do nothing else
2080 if (isMenu) {
2081 if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
2082 sound->Beep();
2085 #endif // #ifdef XP_WIN
2087 return nullptr;
2090 nsIWidget* nsMenuPopupFrame::GetWidget() const {
2091 return mView ? mView->GetWidget() : nullptr;
2094 // helpers /////////////////////////////////////////////////////////////
2096 nsresult nsMenuPopupFrame::AttributeChanged(int32_t aNameSpaceID,
2097 nsAtom* aAttribute,
2098 int32_t aModType)
2101 nsresult rv =
2102 nsBlockFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
2104 if (aAttribute == nsGkAtoms::left || aAttribute == nsGkAtoms::top) {
2105 MoveToAttributePosition();
2108 if (aAttribute == nsGkAtoms::remote) {
2109 // When the remote attribute changes, we need to create a new widget to
2110 // ensure that it has the correct compositor and transparency settings to
2111 // match the new value.
2112 PrepareWidget(true);
2115 if (aAttribute == nsGkAtoms::followanchor) {
2116 if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
2117 pm->UpdateFollowAnchor(this);
2121 if (aAttribute == nsGkAtoms::label) {
2122 // set the label for the titlebar
2123 nsView* view = GetView();
2124 if (view) {
2125 nsIWidget* widget = view->GetWidget();
2126 if (widget) {
2127 nsAutoString title;
2128 mContent->AsElement()->GetAttr(nsGkAtoms::label, title);
2129 if (!title.IsEmpty()) {
2130 widget->SetTitle(title);
2134 } else if (aAttribute == nsGkAtoms::ignorekeys) {
2135 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2136 if (pm) {
2137 nsAutoString ignorekeys;
2138 mContent->AsElement()->GetAttr(nsGkAtoms::ignorekeys, ignorekeys);
2139 pm->UpdateIgnoreKeys(ignorekeys.EqualsLiteral("true"));
2143 return rv;
2146 void nsMenuPopupFrame::MoveToAttributePosition() {
2147 // Move the widget around when the user sets the |left| and |top| attributes.
2148 // Note that this is not the best way to move the widget, as it results in
2149 // lots of FE notifications and is likely to be slow as molasses. Use |moveTo|
2150 // on the element if possible.
2151 nsAutoString left, top;
2152 mContent->AsElement()->GetAttr(nsGkAtoms::left, left);
2153 mContent->AsElement()->GetAttr(nsGkAtoms::top, top);
2154 nsresult err1, err2;
2155 mozilla::CSSIntPoint pos(left.ToInteger(&err1), top.ToInteger(&err2));
2157 if (NS_SUCCEEDED(err1) && NS_SUCCEEDED(err2)) MoveTo(pos, false);
2159 PresShell()->FrameNeedsReflow(
2160 this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
2163 void nsMenuPopupFrame::Destroy(DestroyContext& aContext) {
2164 // XXX: Currently we don't fire popuphidden for these popups, that seems wrong
2165 // but alas, also pre-existing.
2166 HidePopup(/* aDeselectMenu = */ false, ePopupClosed,
2167 /* aFromFrameDestruction = */ true);
2169 if (RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance()) {
2170 pm->PopupDestroyed(this);
2173 nsBlockFrame::Destroy(aContext);
2176 nsMargin nsMenuPopupFrame::GetMargin() const {
2177 nsMargin margin;
2178 StyleMargin()->GetMargin(margin);
2179 if (mIsTopLevelContextMenu) {
2180 const CSSIntPoint offset(
2181 LookAndFeel::GetInt(LookAndFeel::IntID::ContextMenuOffsetHorizontal),
2182 LookAndFeel::GetInt(LookAndFeel::IntID::ContextMenuOffsetVertical));
2183 auto auOffset = CSSIntPoint::ToAppUnits(offset);
2184 margin.top += auOffset.y;
2185 margin.bottom += auOffset.y;
2186 margin.left += auOffset.x;
2187 margin.right += auOffset.x;
2189 if (mPopupType == PopupType::Tooltip) {
2190 const auto auOffset =
2191 CSSPixel::ToAppUnits(LookAndFeel::TooltipOffsetVertical());
2192 margin.top += auOffset;
2193 margin.bottom += auOffset;
2195 return margin;
2198 void nsMenuPopupFrame::MoveTo(const CSSPoint& aPos, bool aUpdateAttrs,
2199 bool aByMoveToRect) {
2200 nsIWidget* widget = GetWidget();
2201 nsPoint appUnitsPos = CSSPixel::ToAppUnits(aPos);
2203 // reposition the popup at the specified coordinates. Don't clear the anchor
2204 // and position, because the popup can be reset to its anchor position by
2205 // using (-1, -1) as coordinates.
2207 // Subtract off the margin as it will be added to the position when
2208 // SetPopupPosition is called.
2210 nsMargin margin = GetMargin();
2211 if (mIsContextMenu && IsDirectionRTL()) {
2212 appUnitsPos.x += margin.right + mRect.Width();
2213 } else {
2214 appUnitsPos.x -= margin.left;
2216 appUnitsPos.y -= margin.top;
2219 if (mScreenRect.TopLeft() == appUnitsPos &&
2220 (!widget || widget->GetClientOffset() == mLastClientOffset)) {
2221 return;
2224 mPositionedByMoveToRect = aByMoveToRect;
2225 mScreenRect.MoveTo(appUnitsPos);
2226 if (mAnchorType == MenuPopupAnchorType_Rect) {
2227 // This ensures that the anchor width is still honored, to prevent it from
2228 // changing spuriously.
2229 mScreenRect.height = 0;
2230 // But we still need to make sure that our top left position ends up in
2231 // appUnitsPos.
2232 mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
2233 mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
2234 mXPos = mYPos = 0;
2235 } else {
2236 mAnchorType = MenuPopupAnchorType_Point;
2239 SetPopupPosition(true);
2241 RefPtr<Element> popup = mContent->AsElement();
2242 if (aUpdateAttrs &&
2243 (popup->HasAttr(nsGkAtoms::left) || popup->HasAttr(nsGkAtoms::top))) {
2244 nsAutoString left, top;
2245 left.AppendInt(RoundedToInt(aPos).x);
2246 top.AppendInt(RoundedToInt(aPos).y);
2247 popup->SetAttr(kNameSpaceID_None, nsGkAtoms::left, left, false);
2248 popup->SetAttr(kNameSpaceID_None, nsGkAtoms::top, top, false);
2252 void nsMenuPopupFrame::MoveToAnchor(nsIContent* aAnchorContent,
2253 const nsAString& aPosition, int32_t aXPos,
2254 int32_t aYPos, bool aAttributesOverride) {
2255 NS_ASSERTION(IsVisible(), "popup must be visible to move it");
2257 nsPopupState oldstate = mPopupState;
2258 InitializePopup(aAnchorContent, mTriggerContent, aPosition, aXPos, aYPos,
2259 MenuPopupAnchorType_Node, aAttributesOverride);
2260 // InitializePopup changed the state so reset it.
2261 mPopupState = oldstate;
2263 // Pass false here so that flipping and adjusting to fit on the screen happen.
2264 SetPopupPosition(false);
2267 int8_t nsMenuPopupFrame::GetAlignmentPosition() const {
2268 // The code below handles most cases of alignment, anchor and position values.
2269 // Those that are not handled just return POPUPPOSITION_UNKNOWN.
2271 if (mPosition == POPUPPOSITION_OVERLAP ||
2272 mPosition == POPUPPOSITION_AFTERPOINTER ||
2273 mPosition == POPUPPOSITION_SELECTION) {
2274 return mPosition;
2277 int8_t position = mPosition;
2279 if (position == POPUPPOSITION_UNKNOWN) {
2280 switch (mPopupAnchor) {
2281 case POPUPALIGNMENT_BOTTOMRIGHT:
2282 case POPUPALIGNMENT_BOTTOMLEFT:
2283 case POPUPALIGNMENT_BOTTOMCENTER:
2284 position = mPopupAlignment == POPUPALIGNMENT_TOPRIGHT
2285 ? POPUPPOSITION_AFTEREND
2286 : POPUPPOSITION_AFTERSTART;
2287 break;
2288 case POPUPALIGNMENT_TOPRIGHT:
2289 case POPUPALIGNMENT_TOPLEFT:
2290 case POPUPALIGNMENT_TOPCENTER:
2291 position = mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT
2292 ? POPUPPOSITION_BEFOREEND
2293 : POPUPPOSITION_BEFORESTART;
2294 break;
2295 case POPUPALIGNMENT_LEFTCENTER:
2296 position = mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT
2297 ? POPUPPOSITION_STARTAFTER
2298 : POPUPPOSITION_STARTBEFORE;
2299 break;
2300 case POPUPALIGNMENT_RIGHTCENTER:
2301 position = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT
2302 ? POPUPPOSITION_ENDAFTER
2303 : POPUPPOSITION_ENDBEFORE;
2304 break;
2305 default:
2306 break;
2310 if (mHFlip) {
2311 position = POPUPPOSITION_HFLIP(position);
2314 if (mVFlip) {
2315 position = POPUPPOSITION_VFLIP(position);
2318 return position;
2322 * KEEP THIS IN SYNC WITH nsIFrame::CreateView
2323 * as much as possible. Until we get rid of views finally...
2325 void nsMenuPopupFrame::CreatePopupView() {
2326 if (HasView()) {
2327 return;
2330 nsViewManager* viewManager = PresContext()->GetPresShell()->GetViewManager();
2331 NS_ASSERTION(nullptr != viewManager, "null view manager");
2333 // Create a view
2334 nsView* parentView = viewManager->GetRootView();
2335 auto visibility = ViewVisibility::Hide;
2337 NS_ASSERTION(parentView, "no parent view");
2339 // Create a view
2340 nsView* view = viewManager->CreateView(GetRect(), parentView, visibility);
2341 auto zIndex = ZIndex();
2342 viewManager->SetViewZIndex(view, zIndex.isNothing(), zIndex.valueOr(0));
2343 // XXX put view last in document order until we can do better
2344 viewManager->InsertChild(parentView, view, nullptr, true);
2346 // Remember our view
2347 SetView(view);
2349 NS_FRAME_LOG(
2350 NS_FRAME_TRACE_CALLS,
2351 ("nsMenuPopupFrame::CreatePopupView: frame=%p view=%p", this, view));
2354 bool nsMenuPopupFrame::ShouldFollowAnchor() const {
2355 if (mAnchorType != MenuPopupAnchorType_Node || !mAnchorContent) {
2356 return false;
2359 // Follow anchor mode is used when followanchor="true" is set or for arrow
2360 // panels.
2361 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
2362 nsGkAtoms::followanchor,
2363 nsGkAtoms::_true, eCaseMatters)) {
2364 return true;
2367 if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
2368 nsGkAtoms::followanchor,
2369 nsGkAtoms::_false, eCaseMatters)) {
2370 return false;
2373 return mPopupType == PopupType::Panel &&
2374 mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
2375 nsGkAtoms::arrow, eCaseMatters);
2378 bool nsMenuPopupFrame::ShouldFollowAnchor(nsRect& aRect) {
2379 if (!ShouldFollowAnchor()) {
2380 return false;
2383 if (nsIFrame* anchorFrame = GetAnchorFrame()) {
2384 if (nsPresContext* rootPresContext = PresContext()->GetRootPresContext()) {
2385 aRect = ComputeAnchorRect(rootPresContext, anchorFrame);
2389 return true;
2392 bool nsMenuPopupFrame::IsDirectionRTL() const {
2393 const nsIFrame* anchor = GetAnchorFrame();
2394 const nsIFrame* f = anchor ? anchor : this;
2395 return f->StyleVisibility()->mDirection == StyleDirection::Rtl;
2398 nsIFrame* nsMenuPopupFrame::GetAnchorFrame() const {
2399 nsIContent* anchor = mAnchorContent;
2400 if (!anchor) {
2401 return nullptr;
2403 return MaybeDelegatedAnchorFrame(anchor->GetPrimaryFrame());
2406 void nsMenuPopupFrame::CheckForAnchorChange(nsRect& aRect) {
2407 // Don't update if the popup isn't visible or we shouldn't be following the
2408 // anchor.
2409 if (!IsVisible() || !ShouldFollowAnchor()) {
2410 return;
2413 bool shouldHide = false;
2415 nsPresContext* rootPresContext = PresContext()->GetRootPresContext();
2417 // If the frame for the anchor has gone away, hide the popup.
2418 nsIFrame* anchor = GetAnchorFrame();
2419 if (!anchor || !rootPresContext) {
2420 shouldHide = true;
2421 } else if (!anchor->IsVisibleConsideringAncestors(
2422 VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) {
2423 // If the anchor is now inside something that is invisible, hide the popup.
2424 shouldHide = true;
2425 } else {
2426 // If the anchor is now inside a hidden parent popup, hide the popup.
2427 nsIFrame* frame = anchor;
2428 while (frame) {
2429 nsMenuPopupFrame* popup = do_QueryFrame(frame);
2430 if (popup && popup->PopupState() != ePopupShown) {
2431 shouldHide = true;
2432 break;
2435 frame = frame->GetParent();
2439 if (shouldHide) {
2440 nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
2441 if (pm) {
2442 // As the caller will be iterating over the open popups, hide
2443 // asyncronously.
2444 pm->HidePopup(mContent->AsElement(),
2445 {HidePopupOption::DeselectMenu, HidePopupOption::Async});
2448 return;
2451 nsRect anchorRect = ComputeAnchorRect(rootPresContext, anchor);
2453 // If the rectangles are different, move the popup.
2454 if (!anchorRect.IsEqualEdges(aRect)) {
2455 aRect = anchorRect;
2456 SetPopupPosition(true);