nsScrollbarFrame.cpp (15958B)
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/. */ 6 7 // 8 // Eric Vaughan 9 // Netscape Communications 10 // 11 // See documentation in associated header file 12 // 13 14 #include "nsScrollbarFrame.h" 15 16 #include "mozilla/LookAndFeel.h" 17 #include "mozilla/PresShell.h" 18 #include "mozilla/ScrollContainerFrame.h" 19 #include "mozilla/dom/Element.h" 20 #include "nsContentCreatorFunctions.h" 21 #include "nsGkAtoms.h" 22 #include "nsIContent.h" 23 #include "nsIScrollbarMediator.h" 24 #include "nsLayoutUtils.h" 25 #include "nsScrollbarButtonFrame.h" 26 #include "nsSliderFrame.h" 27 #include "nsStyleConsts.h" 28 29 using namespace mozilla; 30 using mozilla::dom::Element; 31 32 // 33 // NS_NewScrollbarFrame 34 // 35 // Creates a new scrollbar frame and returns it 36 // 37 nsIFrame* NS_NewScrollbarFrame(PresShell* aPresShell, ComputedStyle* aStyle) { 38 return new (aPresShell) 39 nsScrollbarFrame(aStyle, aPresShell->GetPresContext()); 40 } 41 42 NS_IMPL_FRAMEARENA_HELPERS(nsScrollbarFrame) 43 44 NS_QUERYFRAME_HEAD(nsScrollbarFrame) 45 NS_QUERYFRAME_ENTRY(nsScrollbarFrame) 46 NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) 47 NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) 48 49 void nsScrollbarFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, 50 nsIFrame* aPrevInFlow) { 51 nsContainerFrame::Init(aContent, aParent, aPrevInFlow); 52 53 // We want to be a reflow root since we use reflows to move the 54 // slider. Any reflow inside the scrollbar frame will be a reflow to 55 // move the slider and will thus not change anything outside of the 56 // scrollbar or change the size of the scrollbar frame. 57 AddStateBits(NS_FRAME_REFLOW_ROOT); 58 } 59 60 nsScrollbarFrame* nsScrollbarFrame::GetOppositeScrollbar() const { 61 ScrollContainerFrame* sc = do_QueryFrame(GetParent()); 62 if (!sc) { 63 return nullptr; 64 } 65 auto* vScrollbar = sc->GetScrollbarBox(/* aVertical= */ true); 66 if (vScrollbar == this) { 67 return sc->GetScrollbarBox(/* aVertical= */ false); 68 } 69 MOZ_ASSERT(sc->GetScrollbarBox(/* aVertical= */ false) == this, 70 "Which scrollbar are we?"); 71 return vScrollbar; 72 } 73 74 void nsScrollbarFrame::InvalidateForHoverChange(bool aIsNowHovered) { 75 // Hover state on the scrollbar changes both the scrollbar and potentially 76 // descendants too, so invalidate when it changes. 77 InvalidateFrameSubtree(); 78 if (!aIsNowHovered) { 79 return; 80 } 81 mHasBeenHovered = true; 82 // When hovering over one scrollbar, remove the sticky hover effect from the 83 // opposite scrollbar, if needed. 84 if (auto* opposite = GetOppositeScrollbar(); 85 opposite && opposite->mHasBeenHovered) { 86 opposite->mHasBeenHovered = false; 87 opposite->InvalidateFrameSubtree(); 88 } 89 } 90 91 void nsScrollbarFrame::ActivityChanged(bool aIsNowActive) { 92 if (ScrollContainerFrame* sc = do_QueryFrame(GetParent())) { 93 if (aIsNowActive) { 94 sc->ScrollbarActivityStarted(); 95 } else { 96 sc->ScrollbarActivityStopped(); 97 } 98 } 99 } 100 101 void nsScrollbarFrame::ElementStateChanged(dom::ElementState aStates) { 102 if (aStates.HasState(dom::ElementState::HOVER)) { 103 const bool hovered = 104 mContent->AsElement()->State().HasState(dom::ElementState::HOVER); 105 InvalidateForHoverChange(hovered); 106 ActivityChanged(hovered); 107 } 108 } 109 110 void nsScrollbarFrame::WillBecomeActive() { 111 // Reset our sticky hover state before becoming active. 112 mHasBeenHovered = false; 113 } 114 115 void nsScrollbarFrame::Destroy(DestroyContext& aContext) { 116 aContext.AddAnonymousContent(mUpTopButton.forget()); 117 aContext.AddAnonymousContent(mDownTopButton.forget()); 118 aContext.AddAnonymousContent(mSlider.forget()); 119 aContext.AddAnonymousContent(mUpBottomButton.forget()); 120 aContext.AddAnonymousContent(mDownBottomButton.forget()); 121 nsContainerFrame::Destroy(aContext); 122 } 123 124 void nsScrollbarFrame::Reflow(nsPresContext* aPresContext, 125 ReflowOutput& aDesiredSize, 126 const ReflowInput& aReflowInput, 127 nsReflowStatus& aStatus) { 128 MarkInReflow(); 129 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); 130 131 // We always take all the space we're given, and our track size in the other 132 // axis. 133 const bool horizontal = IsHorizontal(); 134 const auto wm = GetWritingMode(); 135 const auto minSize = aReflowInput.ComputedMinSize(); 136 137 aDesiredSize.ISize(wm) = aReflowInput.ComputedISize(); 138 aDesiredSize.BSize(wm) = [&] { 139 if (aReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE) { 140 return aReflowInput.ComputedBSize(); 141 } 142 // We don't want to change our size during incremental reflow, see the 143 // reflow root comment in init. 144 if (!aReflowInput.mParentReflowInput) { 145 return GetLogicalSize(wm).BSize(wm); 146 } 147 return minSize.BSize(wm); 148 }(); 149 150 const nsSize containerSize = aDesiredSize.PhysicalSize(); 151 const LogicalSize totalAvailSize = aDesiredSize.Size(wm); 152 LogicalPoint nextKidPos(wm); 153 154 MOZ_ASSERT(!wm.IsVertical()); 155 const bool movesInInlineDirection = horizontal; 156 157 // Layout our kids left to right / top to bottom. 158 for (nsIFrame* kid : mFrames) { 159 MOZ_ASSERT(!kid->GetWritingMode().IsOrthogonalTo(wm), 160 "We don't expect orthogonal scrollbar parts"); 161 const bool isSlider = kid->GetContent() == mSlider; 162 LogicalSize availSize = totalAvailSize; 163 { 164 // Assume we'll consume the same size before and after the slider. This is 165 // not a technically correct assumption if we have weird scrollbar button 166 // setups, but those will be going away, see bug 1824254. 167 const int32_t factor = isSlider ? 2 : 1; 168 if (movesInInlineDirection) { 169 availSize.ISize(wm) = 170 std::max(0, totalAvailSize.ISize(wm) - nextKidPos.I(wm) * factor); 171 } else { 172 availSize.BSize(wm) = 173 std::max(0, totalAvailSize.BSize(wm) - nextKidPos.B(wm) * factor); 174 } 175 } 176 177 ReflowInput kidRI(aPresContext, aReflowInput, kid, availSize); 178 if (isSlider) { 179 // We want for the slider to take all the remaining available space. 180 kidRI.SetComputedISize(availSize.ISize(wm)); 181 kidRI.SetComputedBSize(availSize.BSize(wm)); 182 } else if (movesInInlineDirection) { 183 // Otherwise we want all the space in the axis we're not advancing in, and 184 // the default / minimum size on the other axis. 185 kidRI.SetComputedBSize(availSize.BSize(wm)); 186 } else { 187 kidRI.SetComputedISize(availSize.ISize(wm)); 188 } 189 190 ReflowOutput kidDesiredSize(wm); 191 nsReflowStatus status; 192 const auto flags = ReflowChildFlags::Default; 193 ReflowChild(kid, aPresContext, kidDesiredSize, kidRI, wm, nextKidPos, 194 containerSize, flags, status); 195 // We haven't seen the slider yet, we can advance 196 FinishReflowChild(kid, aPresContext, kidDesiredSize, &kidRI, wm, nextKidPos, 197 containerSize, flags); 198 if (movesInInlineDirection) { 199 nextKidPos.I(wm) += kidDesiredSize.ISize(wm); 200 } else { 201 nextKidPos.B(wm) += kidDesiredSize.BSize(wm); 202 } 203 } 204 205 aDesiredSize.SetOverflowAreasToDesiredBounds(); 206 } 207 208 bool nsScrollbarFrame::SetCurPos(CSSIntCoord aCurPos) { 209 if (mCurPos == aCurPos) { 210 return false; 211 } 212 mCurPos = aCurPos; 213 if (ScrollContainerFrame* scrollContainerFrame = do_QueryFrame(GetParent())) { 214 scrollContainerFrame->ScrollbarCurPosChanged(); 215 } 216 if (nsSliderFrame* slider = do_QueryFrame(mSlider->GetPrimaryFrame())) { 217 slider->CurrentPositionChanged(); 218 } 219 return true; 220 } 221 222 void nsScrollbarFrame::RequestSliderReflow() { 223 // These affect the slider. 224 if (nsSliderFrame* slider = do_QueryFrame(mSlider->GetPrimaryFrame())) { 225 PresShell()->FrameNeedsReflow(slider, IntrinsicDirty::None, 226 NS_FRAME_IS_DIRTY); 227 } 228 } 229 230 bool nsScrollbarFrame::SetMaxPos(CSSIntCoord aMaxPos) { 231 if (mMaxPos == aMaxPos) { 232 return false; 233 } 234 RequestSliderReflow(); 235 mMaxPos = aMaxPos; 236 return true; 237 } 238 239 bool nsScrollbarFrame::SetPageIncrement(CSSIntCoord aPageIncrement) { 240 if (mPageIncrement == aPageIncrement) { 241 return false; 242 } 243 RequestSliderReflow(); 244 mPageIncrement = aPageIncrement; 245 return true; 246 } 247 248 bool nsScrollbarFrame::IsEnabled() const { 249 return !mContent->AsElement()->State().HasState(dom::ElementState::DISABLED); 250 } 251 252 bool nsScrollbarFrame::SetEnabled(bool aEnabled) { 253 if (IsEnabled() == aEnabled) { 254 return false; 255 } 256 mContent->AsElement()->SetStates(dom::ElementState::DISABLED, !aEnabled); 257 return true; 258 } 259 260 NS_IMETHODIMP 261 nsScrollbarFrame::HandlePress(nsPresContext* aPresContext, 262 WidgetGUIEvent* aEvent, 263 nsEventStatus* aEventStatus) { 264 return NS_OK; 265 } 266 267 NS_IMETHODIMP 268 nsScrollbarFrame::HandleMultiplePress(nsPresContext* aPresContext, 269 WidgetGUIEvent* aEvent, 270 nsEventStatus* aEventStatus, 271 bool aControlHeld) { 272 return NS_OK; 273 } 274 275 NS_IMETHODIMP 276 nsScrollbarFrame::HandleDrag(nsPresContext* aPresContext, 277 WidgetGUIEvent* aEvent, 278 nsEventStatus* aEventStatus) { 279 return NS_OK; 280 } 281 282 NS_IMETHODIMP 283 nsScrollbarFrame::HandleRelease(nsPresContext* aPresContext, 284 WidgetGUIEvent* aEvent, 285 nsEventStatus* aEventStatus) { 286 return NS_OK; 287 } 288 289 void nsScrollbarFrame::SetOverrideScrollbarMediator( 290 nsIScrollbarMediator* aMediator) { 291 mOverriddenScrollbarMediator = do_QueryFrame(aMediator); 292 } 293 294 nsIScrollbarMediator* nsScrollbarFrame::GetScrollbarMediator() { 295 if (auto* override = mOverriddenScrollbarMediator.GetFrame()) { 296 return do_QueryFrame(override); 297 } 298 return do_QueryFrame(GetParent()); 299 } 300 301 bool nsScrollbarFrame::IsHorizontal() const { 302 auto appearance = StyleDisplay()->EffectiveAppearance(); 303 MOZ_ASSERT(appearance == StyleAppearance::ScrollbarHorizontal || 304 appearance == StyleAppearance::ScrollbarVertical); 305 return appearance == StyleAppearance::ScrollbarHorizontal; 306 } 307 308 nsSize nsScrollbarFrame::ScrollbarMinSize() const { 309 nsPresContext* pc = PresContext(); 310 const LayoutDeviceIntSize widget = 311 pc->Theme()->GetMinimumWidgetSize(pc, const_cast<nsScrollbarFrame*>(this), 312 StyleDisplay()->EffectiveAppearance()); 313 return LayoutDeviceIntSize::ToAppUnits(widget, pc->AppUnitsPerDevPixel()); 314 } 315 316 StyleScrollbarWidth nsScrollbarFrame::ScrollbarWidth() const { 317 return nsLayoutUtils::StyleForScrollbar(this) 318 ->StyleUIReset() 319 ->ScrollbarWidth(); 320 } 321 322 nscoord nsScrollbarFrame::ScrollbarTrackSize() const { 323 nsPresContext* pc = PresContext(); 324 auto overlay = pc->UseOverlayScrollbars() ? nsITheme::Overlay::Yes 325 : nsITheme::Overlay::No; 326 return LayoutDevicePixel::ToAppUnits( 327 pc->Theme()->GetScrollbarSize(pc, ScrollbarWidth(), overlay), 328 pc->AppUnitsPerDevPixel()); 329 } 330 331 void nsScrollbarFrame::MoveToNewPosition() { 332 nsIScrollbarMediator* m = GetScrollbarMediator(); 333 if (!m) { 334 return; 335 } 336 // Note that this `MoveToNewPosition` is used for scrolling triggered by 337 // repeating scrollbar button press, so we'd use an intended-direction 338 // scroll snap flag. 339 m->ScrollByUnit(this, ScrollMode::Smooth, mButtonScrollDirection, 340 mButtonScrollUnit, ScrollSnapFlags::IntendedDirection); 341 } 342 343 static already_AddRefed<Element> MakeScrollbarButton( 344 dom::NodeInfo* aNodeInfo, bool aVertical, bool aBottom, bool aDown, 345 AnonymousContentKey& aKey) { 346 MOZ_ASSERT(aNodeInfo); 347 MOZ_ASSERT( 348 aNodeInfo->Equals(nsGkAtoms::scrollbarbutton, nullptr, kNameSpaceID_XUL)); 349 350 static constexpr nsLiteralString kSbattrValues[2][2] = { 351 { 352 u"scrollbar-up-top"_ns, 353 u"scrollbar-up-bottom"_ns, 354 }, 355 { 356 u"scrollbar-down-top"_ns, 357 u"scrollbar-down-bottom"_ns, 358 }, 359 }; 360 361 static constexpr nsLiteralString kTypeValues[2] = { 362 u"decrement"_ns, 363 u"increment"_ns, 364 }; 365 366 aKey = AnonymousContentKey::Type_ScrollbarButton; 367 if (aVertical) { 368 aKey |= AnonymousContentKey::Flag_Vertical; 369 } 370 if (aBottom) { 371 aKey |= AnonymousContentKey::Flag_ScrollbarButton_Bottom; 372 } 373 if (aDown) { 374 aKey |= AnonymousContentKey::Flag_ScrollbarButton_Down; 375 } 376 377 RefPtr<Element> e; 378 NS_TrustedNewXULElement(getter_AddRefs(e), do_AddRef(aNodeInfo)); 379 e->SetAttr(kNameSpaceID_None, nsGkAtoms::sbattr, 380 kSbattrValues[aDown][aBottom], false); 381 e->SetAttr(kNameSpaceID_None, nsGkAtoms::type, kTypeValues[aDown], false); 382 return e.forget(); 383 } 384 385 nsresult nsScrollbarFrame::CreateAnonymousContent( 386 nsTArray<ContentInfo>& aElements) { 387 nsNodeInfoManager* nodeInfoManager = mContent->NodeInfo()->NodeInfoManager(); 388 Element* el = GetContent()->AsElement(); 389 390 // If there are children already in the node, don't create any anonymous 391 // content (this only apply to crashtests/369038-1.xhtml) 392 if (el->HasChildren()) { 393 return NS_OK; 394 } 395 396 const bool vertical = el->HasAttr(nsGkAtoms::vertical); 397 RefPtr<dom::NodeInfo> sbbNodeInfo = 398 nodeInfoManager->GetNodeInfo(nsGkAtoms::scrollbarbutton, nullptr, 399 kNameSpaceID_XUL, nsINode::ELEMENT_NODE); 400 401 const int32_t buttons = 402 PresContext()->Theme()->ThemeSupportsScrollbarButtons() 403 ? LookAndFeel::GetInt(LookAndFeel::IntID::ScrollArrowStyle) 404 : 0; 405 406 if (buttons & LookAndFeel::eScrollArrow_StartBackward) { 407 AnonymousContentKey key; 408 mUpTopButton = 409 MakeScrollbarButton(sbbNodeInfo, vertical, /* aBottom */ false, 410 /* aDown */ false, key); 411 aElements.AppendElement(ContentInfo(mUpTopButton, key)); 412 } 413 414 if (buttons & LookAndFeel::eScrollArrow_StartForward) { 415 AnonymousContentKey key; 416 mDownTopButton = 417 MakeScrollbarButton(sbbNodeInfo, vertical, /* aBottom */ false, 418 /* aDown */ true, key); 419 aElements.AppendElement(ContentInfo(mDownTopButton, key)); 420 } 421 422 { 423 AnonymousContentKey key = AnonymousContentKey::Type_Slider; 424 if (vertical) { 425 key |= AnonymousContentKey::Flag_Vertical; 426 } 427 428 NS_TrustedNewXULElement( 429 getter_AddRefs(mSlider), 430 nodeInfoManager->GetNodeInfo(nsGkAtoms::slider, nullptr, 431 kNameSpaceID_XUL, nsINode::ELEMENT_NODE)); 432 433 aElements.AppendElement(ContentInfo(mSlider, key)); 434 435 NS_TrustedNewXULElement( 436 getter_AddRefs(mThumb), 437 nodeInfoManager->GetNodeInfo(nsGkAtoms::thumb, nullptr, 438 kNameSpaceID_XUL, nsINode::ELEMENT_NODE)); 439 mSlider->AppendChildTo(mThumb, false, IgnoreErrors()); 440 } 441 442 if (buttons & LookAndFeel::eScrollArrow_EndBackward) { 443 AnonymousContentKey key; 444 mUpBottomButton = 445 MakeScrollbarButton(sbbNodeInfo, vertical, /* aBottom */ true, 446 /* aDown */ false, key); 447 aElements.AppendElement(ContentInfo(mUpBottomButton, key)); 448 } 449 450 if (buttons & LookAndFeel::eScrollArrow_EndForward) { 451 AnonymousContentKey key; 452 mDownBottomButton = 453 MakeScrollbarButton(sbbNodeInfo, vertical, /* aBottom */ true, 454 /* aDown */ true, key); 455 aElements.AppendElement(ContentInfo(mDownBottomButton, key)); 456 } 457 458 return NS_OK; 459 } 460 461 void nsScrollbarFrame::AppendAnonymousContentTo( 462 nsTArray<nsIContent*>& aElements, uint32_t aFilter) { 463 if (mUpTopButton) { 464 aElements.AppendElement(mUpTopButton); 465 } 466 467 if (mDownTopButton) { 468 aElements.AppendElement(mDownTopButton); 469 } 470 471 if (mSlider) { 472 aElements.AppendElement(mSlider); 473 } 474 475 if (mUpBottomButton) { 476 aElements.AppendElement(mUpBottomButton); 477 } 478 479 if (mDownBottomButton) { 480 aElements.AppendElement(mDownBottomButton); 481 } 482 }