nsRangeFrame.cpp (26294B)
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 #include "nsRangeFrame.h" 8 9 #include "ListMutationObserver.h" 10 #include "gfxContext.h" 11 #include "mozilla/Assertions.h" 12 #include "mozilla/PresShell.h" 13 #include "mozilla/ServoStyleSet.h" 14 #include "mozilla/TouchEvents.h" 15 #include "mozilla/dom/Document.h" 16 #include "mozilla/dom/Element.h" 17 #include "mozilla/dom/HTMLDataListElement.h" 18 #include "mozilla/dom/HTMLInputElement.h" 19 #include "mozilla/dom/HTMLOptionElement.h" 20 #include "nsCSSRendering.h" 21 #include "nsContentCreatorFunctions.h" 22 #include "nsDisplayList.h" 23 #include "nsGkAtoms.h" 24 #include "nsIContent.h" 25 #include "nsIMutationObserver.h" 26 #include "nsLayoutUtils.h" 27 #include "nsNodeInfoManager.h" 28 #include "nsPresContext.h" 29 #include "nsTArray.h" 30 31 #ifdef ACCESSIBILITY 32 # include "nsAccessibilityService.h" 33 #endif 34 35 // Our intrinsic size is 12em in the main-axis and 1.3em in the cross-axis. 36 #define MAIN_AXIS_EM_SIZE 12 37 #define CROSS_AXIS_EM_SIZE 1.3f 38 39 using namespace mozilla; 40 using namespace mozilla::dom; 41 using namespace mozilla::image; 42 43 nsIFrame* NS_NewRangeFrame(PresShell* aPresShell, ComputedStyle* aStyle) { 44 return new (aPresShell) nsRangeFrame(aStyle, aPresShell->GetPresContext()); 45 } 46 47 nsRangeFrame::nsRangeFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) 48 : nsContainerFrame(aStyle, aPresContext, kClassID) {} 49 50 void nsRangeFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, 51 nsIFrame* aPrevInFlow) { 52 nsContainerFrame::Init(aContent, aParent, aPrevInFlow); 53 if (InputElement().HasAttr(nsGkAtoms::list)) { 54 mListMutationObserver = new ListMutationObserver(*this); 55 } 56 } 57 58 nsRangeFrame::~nsRangeFrame() = default; 59 60 NS_IMPL_FRAMEARENA_HELPERS(nsRangeFrame) 61 62 NS_QUERYFRAME_HEAD(nsRangeFrame) 63 NS_QUERYFRAME_ENTRY(nsRangeFrame) 64 NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) 65 NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) 66 67 void nsRangeFrame::Destroy(DestroyContext& aContext) { 68 NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(), 69 "nsRangeFrame should not have continuations; if it does we " 70 "need to call RegUnregAccessKey only for the first."); 71 72 if (mListMutationObserver) { 73 mListMutationObserver->Detach(); 74 } 75 aContext.AddAnonymousContent(mTrackDiv.forget()); 76 aContext.AddAnonymousContent(mProgressDiv.forget()); 77 aContext.AddAnonymousContent(mThumbDiv.forget()); 78 nsContainerFrame::Destroy(aContext); 79 } 80 81 static already_AddRefed<Element> MakeAnonymousDiv( 82 Document& aDoc, PseudoStyleType aOldPseudoType, 83 PseudoStyleType aModernPseudoType, 84 nsTArray<nsIAnonymousContentCreator::ContentInfo>& aElements) { 85 RefPtr<Element> result = aDoc.CreateHTMLElement(nsGkAtoms::div); 86 87 // Associate the pseudo-element with the anonymous child. 88 if (StaticPrefs::layout_css_modern_range_pseudos_enabled()) { 89 result->SetPseudoElementType(aModernPseudoType); 90 } else { 91 result->SetPseudoElementType(aOldPseudoType); 92 } 93 94 // XXX(Bug 1631371) Check if this should use a fallible operation as it 95 // pretended earlier, or change the return type to void. 96 aElements.AppendElement(result); 97 98 return result.forget(); 99 } 100 101 nsresult nsRangeFrame::CreateAnonymousContent( 102 nsTArray<ContentInfo>& aElements) { 103 Document* doc = mContent->OwnerDoc(); 104 // Create the ::-moz-range-track pseudo-element (a div): 105 mTrackDiv = MakeAnonymousDiv(*doc, PseudoStyleType::mozRangeTrack, 106 PseudoStyleType::sliderTrack, aElements); 107 // Create the ::-moz-range-progress pseudo-element (a div): 108 mProgressDiv = MakeAnonymousDiv(*doc, PseudoStyleType::mozRangeProgress, 109 PseudoStyleType::sliderFill, aElements); 110 // Create the ::-moz-range-thumb pseudo-element (a div): 111 mThumbDiv = MakeAnonymousDiv(*doc, PseudoStyleType::mozRangeThumb, 112 PseudoStyleType::sliderThumb, aElements); 113 return NS_OK; 114 } 115 116 void nsRangeFrame::AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements, 117 uint32_t aFilter) { 118 if (mTrackDiv) { 119 aElements.AppendElement(mTrackDiv); 120 } 121 122 if (mProgressDiv) { 123 aElements.AppendElement(mProgressDiv); 124 } 125 126 if (mThumbDiv) { 127 aElements.AppendElement(mThumbDiv); 128 } 129 } 130 131 void nsRangeFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, 132 const nsDisplayListSet& aLists) { 133 const nsStyleDisplay* disp = StyleDisplay(); 134 if (IsThemed(disp)) { 135 DisplayBorderBackgroundOutline(aBuilder, aLists); 136 // Don't paint our children, but let the thumb be hittable for events. 137 if (auto* thumb = mThumbDiv->GetPrimaryFrame(); 138 thumb && aBuilder->IsForEventDelivery() && !HidesContent()) { 139 nsDisplayListSet set(aLists, aLists.Content()); 140 BuildDisplayListForChild(aBuilder, thumb, set, DisplayChildFlag::Inline); 141 } 142 } else { 143 BuildDisplayListForInline(aBuilder, aLists); 144 } 145 } 146 147 void nsRangeFrame::Reflow(nsPresContext* aPresContext, 148 ReflowOutput& aDesiredSize, 149 const ReflowInput& aReflowInput, 150 nsReflowStatus& aStatus) { 151 MarkInReflow(); 152 DO_GLOBAL_REFLOW_COUNT("nsRangeFrame"); 153 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); 154 155 NS_ASSERTION(mTrackDiv, "::-moz-range-track div must exist!"); 156 NS_ASSERTION(mProgressDiv, "::-moz-range-progress div must exist!"); 157 NS_ASSERTION(mThumbDiv, "::-moz-range-thumb div must exist!"); 158 NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(), 159 "nsRangeFrame should not have continuations; if it does we " 160 "need to call RegUnregAccessKey only for the first."); 161 162 WritingMode wm = aReflowInput.GetWritingMode(); 163 const auto contentBoxSize = aReflowInput.ComputedSizeWithBSizeFallback([&] { 164 return IsInlineOriented() ? AutoCrossSize() 165 : OneEmInAppUnits() * MAIN_AXIS_EM_SIZE; 166 }); 167 aDesiredSize.SetSize( 168 wm, 169 contentBoxSize + aReflowInput.ComputedLogicalBorderPadding(wm).Size(wm)); 170 aDesiredSize.SetOverflowAreasToDesiredBounds(); 171 172 ReflowAnonymousContent(aPresContext, aDesiredSize, contentBoxSize, 173 aReflowInput); 174 FinishAndStoreOverflow(&aDesiredSize); 175 176 MOZ_ASSERT(aStatus.IsEmpty(), "This type of frame can't be split."); 177 } 178 179 void nsRangeFrame::ReflowAnonymousContent(nsPresContext* aPresContext, 180 ReflowOutput& aDesiredSize, 181 const LogicalSize& aContentBoxSize, 182 const ReflowInput& aReflowInput) { 183 const auto parentWM = aReflowInput.GetWritingMode(); 184 // The width/height of our content box, which is the available width/height 185 // for our anonymous content. 186 const nsSize rangeFrameContentBoxSize = 187 aContentBoxSize.GetPhysicalSize(parentWM); 188 for (auto* div : {mTrackDiv.get(), mThumbDiv.get(), mProgressDiv.get()}) { 189 nsIFrame* child = div->GetPrimaryFrame(); 190 if (!child) { 191 continue; 192 } 193 const WritingMode wm = child->GetWritingMode(); 194 const LogicalSize parentSizeInChildWM = 195 aContentBoxSize.ConvertTo(wm, parentWM); 196 LogicalSize availSize = parentSizeInChildWM; 197 availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; 198 ReflowInput childReflowInput(aPresContext, aReflowInput, child, availSize, 199 Some(parentSizeInChildWM)); 200 201 const nsPoint pos = [&] { 202 if (div != mTrackDiv) { 203 // Where we position the thumb and range-progress depends on its size, 204 // so we first reflow them at {0,0} to obtain the size, then position 205 // them afterwards. 206 return nsPoint(); 207 } 208 // Find the x/y position of the track. The idea here is that we allow 209 // content authors to style the width, height, border and padding of the 210 // track, but we ignore margin and positioning properties and do the 211 // positioning ourself to keep the center of the track's border box on the 212 // center of the nsRangeFrame's content. These coordinates are with 213 // respect to the nsRangeFrame's border-box. 214 nscoord trackX = rangeFrameContentBoxSize.Width() / 2; 215 nscoord trackY = rangeFrameContentBoxSize.Height() / 2; 216 217 // Account for the track's border and padding (we ignore its margin): 218 // FIXME(emilio): Assumes the track height is constrained, which might not 219 // be true if authors override it. 220 trackX -= childReflowInput.ComputedPhysicalBorderPadding().left + 221 childReflowInput.ComputedWidth() / 2; 222 trackY -= childReflowInput.ComputedPhysicalBorderPadding().top + 223 childReflowInput.ComputedHeight() / 2; 224 225 // Make relative to our border box instead of our content box: 226 trackX += aReflowInput.ComputedPhysicalBorderPadding().left; 227 trackY += aReflowInput.ComputedPhysicalBorderPadding().top; 228 return nsPoint(trackX, trackY); 229 }(); 230 231 nsReflowStatus frameStatus; 232 ReflowOutput childDesiredSize(aReflowInput); 233 ReflowChild(child, aPresContext, childDesiredSize, childReflowInput, pos.x, 234 pos.y, ReflowChildFlags::Default, frameStatus); 235 MOZ_ASSERT( 236 frameStatus.IsFullyComplete(), 237 "We gave our child unconstrained height, so it should be complete"); 238 FinishReflowChild(child, aPresContext, childDesiredSize, &childReflowInput, 239 pos.x, pos.y, ReflowChildFlags::Default); 240 if (div == mThumbDiv) { 241 DoUpdateThumbPosition(child, rangeFrameContentBoxSize); 242 } else if (div == mProgressDiv) { 243 DoUpdateRangeProgressFrame(child, rangeFrameContentBoxSize); 244 } 245 ConsiderChildOverflow(aDesiredSize.mOverflowAreas, child); 246 } 247 } 248 249 #ifdef ACCESSIBILITY 250 a11y::AccType nsRangeFrame::AccessibleType() { return a11y::eHTMLRangeType; } 251 #endif 252 253 double nsRangeFrame::GetValueAsFractionOfRange() { 254 const auto& input = InputElement(); 255 if (MOZ_UNLIKELY(!input.IsDoneCreating())) { 256 // Our element isn't done being created, so its values haven't yet been 257 // sanitized! (It's rare that we'd be reflowed when our element is in this 258 // state, but it can happen if the parser decides to yield while processing 259 // its tasks to build the element.) We can't trust that any of our numeric 260 // values will make sense until they've been sanitized; so for now, just 261 // use 0.0 as a fallback fraction-of-range value here (i.e. behave as if 262 // we're at our minimum, which is how the spec handles some edge cases). 263 return 0.0; 264 } 265 return GetDoubleAsFractionOfRange(input.GetValueAsDecimal()); 266 } 267 268 double nsRangeFrame::GetDoubleAsFractionOfRange(const Decimal& aValue) { 269 auto& input = InputElement(); 270 271 Decimal minimum = input.GetMinimum(); 272 Decimal maximum = input.GetMaximum(); 273 274 MOZ_ASSERT(aValue.isFinite() && minimum.isFinite() && maximum.isFinite(), 275 "type=range should have a default maximum/minimum"); 276 277 if (maximum <= minimum) { 278 // Avoid rounding triggering the assert by checking against an epsilon. 279 MOZ_ASSERT((aValue - minimum).abs().toDouble() < 280 std::numeric_limits<float>::epsilon(), 281 "Unsanitized value"); 282 return 0.0; 283 } 284 285 MOZ_ASSERT(aValue >= minimum && aValue <= maximum, "Unsanitized value"); 286 287 return ((aValue - minimum) / (maximum - minimum)).toDouble(); 288 } 289 290 Decimal nsRangeFrame::GetValueAtEventPoint(WidgetGUIEvent* aEvent) { 291 MOZ_ASSERT( 292 aEvent->mClass == eMouseEventClass || aEvent->mClass == eTouchEventClass, 293 "Unexpected event type - aEvent->mRefPoint may be meaningless"); 294 295 MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); 296 dom::HTMLInputElement* input = 297 static_cast<dom::HTMLInputElement*>(GetContent()); 298 299 MOZ_ASSERT(input->ControlType() == FormControlType::InputRange); 300 301 Decimal minimum = input->GetMinimum(); 302 Decimal maximum = input->GetMaximum(); 303 MOZ_ASSERT(minimum.isFinite() && maximum.isFinite(), 304 "type=range should have a default maximum/minimum"); 305 if (maximum <= minimum) { 306 return minimum; 307 } 308 Decimal range = maximum - minimum; 309 310 LayoutDeviceIntPoint absPoint; 311 if (aEvent->mClass == eTouchEventClass) { 312 MOZ_ASSERT(aEvent->AsTouchEvent()->mTouches.Length() == 1, 313 "Unexpected number of mTouches"); 314 absPoint = aEvent->AsTouchEvent()->mTouches[0]->mRefPoint; 315 } else { 316 absPoint = aEvent->mRefPoint; 317 } 318 nsPoint point = nsLayoutUtils::GetEventCoordinatesRelativeTo( 319 aEvent, absPoint, RelativeTo{this}); 320 321 if (point == nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)) { 322 // We don't want to change the current value for this error state. 323 return static_cast<dom::HTMLInputElement*>(GetContent()) 324 ->GetValueAsDecimal(); 325 } 326 327 nsRect rangeRect; 328 nsSize thumbSize; 329 if (IsThemed()) { 330 // Themed ranges draw on the border-box rect. 331 rangeRect = GetRectRelativeToSelf(); 332 // We need to get the size of the thumb from the theme. 333 nscoord min = CSSPixel::ToAppUnits( 334 PresContext()->Theme()->GetMinimumRangeThumbSize()); 335 MOZ_ASSERT(min, "The thumb is expected to take up some slider space"); 336 thumbSize = nsSize(min, min); 337 } else { 338 rangeRect = GetContentRectRelativeToSelf(); 339 nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); 340 if (thumbFrame) { // diplay:none? 341 thumbSize = thumbFrame->GetSize(); 342 } 343 } 344 345 Decimal fraction; 346 if (IsHorizontal()) { 347 nscoord traversableDistance = rangeRect.width - thumbSize.width; 348 if (traversableDistance <= 0) { 349 return minimum; 350 } 351 nscoord posAtStart = rangeRect.x + thumbSize.width / 2; 352 nscoord posAtEnd = posAtStart + traversableDistance; 353 nscoord posOfPoint = std::clamp(point.x, posAtStart, posAtEnd); 354 fraction = Decimal(posOfPoint - posAtStart) / Decimal(traversableDistance); 355 if (IsRightToLeft()) { 356 fraction = Decimal(1) - fraction; 357 } 358 } else { 359 nscoord traversableDistance = rangeRect.height - thumbSize.height; 360 if (traversableDistance <= 0) { 361 return minimum; 362 } 363 nscoord posAtStart = rangeRect.y + thumbSize.height / 2; 364 nscoord posAtEnd = posAtStart + traversableDistance; 365 nscoord posOfPoint = std::clamp(point.y, posAtStart, posAtEnd); 366 // For a vertical range, the top (posAtStart) is the highest value, so we 367 // subtract the fraction from 1.0 to get that polarity correct. 368 fraction = Decimal(posOfPoint - posAtStart) / Decimal(traversableDistance); 369 if (IsUpwards()) { 370 fraction = Decimal(1) - fraction; 371 } 372 } 373 374 MOZ_ASSERT(fraction >= Decimal(0) && fraction <= Decimal(1)); 375 return minimum + fraction * range; 376 } 377 378 void nsRangeFrame::UpdateForValueChange() { 379 if (IsSubtreeDirty()) { 380 return; // we're going to be updated when we reflow 381 } 382 nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame(); 383 nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); 384 if (!rangeProgressFrame && !thumbFrame) { 385 return; // diplay:none? 386 } 387 const nsSize contentBoxSize = GetContentRect().Size(); 388 if (rangeProgressFrame) { 389 DoUpdateRangeProgressFrame(rangeProgressFrame, contentBoxSize); 390 } 391 if (thumbFrame) { 392 DoUpdateThumbPosition(thumbFrame, contentBoxSize); 393 } 394 if (IsThemed()) { 395 // We don't know the exact dimensions or location of the thumb when native 396 // theming is applied, so we just repaint the entire range. 397 InvalidateFrame(); 398 } 399 400 #ifdef ACCESSIBILITY 401 if (nsAccessibilityService* accService = GetAccService()) { 402 accService->RangeValueChanged(PresShell(), mContent); 403 } 404 #endif 405 406 SchedulePaint(); 407 } 408 409 nsTArray<Decimal> nsRangeFrame::TickMarks() { 410 nsTArray<Decimal> tickMarks; 411 auto& input = InputElement(); 412 auto* list = input.GetList(); 413 if (!list) { 414 return tickMarks; 415 } 416 auto min = input.GetMinimum(); 417 auto max = input.GetMaximum(); 418 auto* options = list->Options(); 419 nsAutoString label; 420 for (uint32_t i = 0; i < options->Length(); ++i) { 421 auto* item = options->Item(i); 422 auto* option = HTMLOptionElement::FromNode(item); 423 MOZ_ASSERT(option); 424 if (option->Disabled()) { 425 continue; 426 } 427 nsAutoString str; 428 option->GetValue(str); 429 auto tickMark = HTMLInputElement::StringToDecimal(str); 430 if (tickMark.isNaN() || tickMark < min || tickMark > max || 431 input.ValueIsStepMismatch(tickMark)) { 432 continue; 433 } 434 tickMarks.AppendElement(tickMark); 435 } 436 tickMarks.Sort(); 437 return tickMarks; 438 } 439 440 Decimal nsRangeFrame::NearestTickMark(const Decimal& aValue) { 441 auto tickMarks = TickMarks(); 442 if (tickMarks.IsEmpty() || aValue.isNaN()) { 443 return Decimal::nan(); 444 } 445 size_t index; 446 if (BinarySearch(tickMarks, 0, tickMarks.Length(), aValue, &index)) { 447 return tickMarks[index]; 448 } 449 if (index == tickMarks.Length()) { 450 return tickMarks.LastElement(); 451 } 452 if (index == 0) { 453 return tickMarks[0]; 454 } 455 const auto& smallerTickMark = tickMarks[index - 1]; 456 const auto& largerTickMark = tickMarks[index]; 457 MOZ_ASSERT(smallerTickMark < aValue); 458 MOZ_ASSERT(largerTickMark > aValue); 459 return (aValue - smallerTickMark).abs() < (aValue - largerTickMark).abs() 460 ? smallerTickMark 461 : largerTickMark; 462 } 463 464 mozilla::dom::HTMLInputElement& nsRangeFrame::InputElement() const { 465 MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); 466 auto& input = *static_cast<dom::HTMLInputElement*>(GetContent()); 467 MOZ_ASSERT(input.ControlType() == FormControlType::InputRange); 468 return input; 469 } 470 471 void nsRangeFrame::DoUpdateThumbPosition(nsIFrame* aThumbFrame, 472 const nsSize& aRangeContentBoxSize) { 473 MOZ_ASSERT(aThumbFrame); 474 475 // The idea here is that we want to position the thumb so that the center 476 // of the thumb is on an imaginary line drawn from the middle of one edge 477 // of the range frame's content box to the middle of the opposite edge of 478 // its content box (the opposite edges being the left/right edge if the 479 // range is horizontal, or else the top/bottom edges if the range is 480 // vertical). How far along this line the center of the thumb is placed 481 // depends on the value of the range. 482 483 nsMargin borderAndPadding = GetUsedBorderAndPadding(); 484 nsPoint newPosition(borderAndPadding.left, borderAndPadding.top); 485 486 nsSize thumbSize = aThumbFrame->GetSize(); 487 double fraction = GetValueAsFractionOfRange(); 488 MOZ_ASSERT(fraction >= 0.0 && fraction <= 1.0); 489 490 if (IsHorizontal()) { 491 if (thumbSize.width < aRangeContentBoxSize.width) { 492 nscoord traversableDistance = 493 aRangeContentBoxSize.width - thumbSize.width; 494 if (IsRightToLeft()) { 495 newPosition.x += NSToCoordRound((1.0 - fraction) * traversableDistance); 496 } else { 497 newPosition.x += NSToCoordRound(fraction * traversableDistance); 498 } 499 newPosition.y += (aRangeContentBoxSize.height - thumbSize.height) / 2; 500 } 501 } else { 502 if (thumbSize.height < aRangeContentBoxSize.height) { 503 nscoord traversableDistance = 504 aRangeContentBoxSize.height - thumbSize.height; 505 newPosition.x += (aRangeContentBoxSize.width - thumbSize.width) / 2; 506 if (IsUpwards()) { 507 newPosition.y += NSToCoordRound((1.0 - fraction) * traversableDistance); 508 } else { 509 newPosition.y += NSToCoordRound(fraction * traversableDistance); 510 } 511 } 512 } 513 aThumbFrame->SetPosition(newPosition); 514 } 515 516 void nsRangeFrame::DoUpdateRangeProgressFrame( 517 nsIFrame* aProgressFrame, const nsSize& aRangeContentBoxSize) { 518 MOZ_ASSERT(aProgressFrame); 519 520 // The idea here is that we want to position the ::-moz-range-progress 521 // pseudo-element so that the center line running along its length is on the 522 // corresponding center line of the nsRangeFrame's content box. In the other 523 // dimension, we align the "start" edge of the ::-moz-range-progress 524 // pseudo-element's border-box with the corresponding edge of the 525 // nsRangeFrame's content box, and we size the progress element's border-box 526 // to have a length of GetValueAsFractionOfRange() times the nsRangeFrame's 527 // content-box size. 528 nsMargin borderAndPadding = GetUsedBorderAndPadding(); 529 nsSize progSize = aProgressFrame->GetSize(); 530 nsRect progRect(borderAndPadding.left, borderAndPadding.top, progSize.width, 531 progSize.height); 532 533 double fraction = GetValueAsFractionOfRange(); 534 MOZ_ASSERT(fraction >= 0.0 && fraction <= 1.0); 535 536 if (IsHorizontal()) { 537 nscoord progLength = NSToCoordRound(fraction * aRangeContentBoxSize.width); 538 if (IsRightToLeft()) { 539 progRect.x += aRangeContentBoxSize.width - progLength; 540 } 541 progRect.y += (aRangeContentBoxSize.height - progSize.height) / 2; 542 progRect.width = progLength; 543 } else { 544 nscoord progLength = NSToCoordRound(fraction * aRangeContentBoxSize.height); 545 progRect.x += (aRangeContentBoxSize.width - progSize.width) / 2; 546 if (IsUpwards()) { 547 progRect.y += aRangeContentBoxSize.height - progLength; 548 } 549 progRect.height = progLength; 550 } 551 aProgressFrame->SetRect(progRect); 552 } 553 554 nsresult nsRangeFrame::AttributeChanged(int32_t aNameSpaceID, 555 nsAtom* aAttribute, 556 AttrModType aModType) { 557 NS_ASSERTION(mTrackDiv, "The track div must exist!"); 558 NS_ASSERTION(mThumbDiv, "The thumb div must exist!"); 559 560 if (aNameSpaceID == kNameSpaceID_None) { 561 if (aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::min || 562 aAttribute == nsGkAtoms::max || aAttribute == nsGkAtoms::step) { 563 // We want to update the position of the thumb, except in one special 564 // case: If the value attribute is being set, it is possible that we are 565 // in the middle of a type change away from type=range, under the 566 // SetAttr(..., nsGkAtoms::value, ...) call in HTMLInputElement:: 567 // HandleTypeChange. In that case the HTMLInputElement's type will 568 // already have changed, and if we call UpdateForValueChange() 569 // we'll fail the asserts under that call that check the type of our 570 // HTMLInputElement. Given that we're changing away from being a range 571 // and this frame will shortly be destroyed, there's no point in calling 572 // UpdateForValueChange() anyway. 573 MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); 574 bool typeIsRange = 575 static_cast<dom::HTMLInputElement*>(GetContent())->ControlType() == 576 FormControlType::InputRange; 577 // If script changed the <input>'s type before setting these attributes 578 // then we don't need to do anything since we are going to be reframed. 579 if (typeIsRange) { 580 UpdateForValueChange(); 581 } 582 } else if (aAttribute == nsGkAtoms::orient) { 583 PresShell()->FrameNeedsReflow(this, IntrinsicDirty::None, 584 NS_FRAME_IS_DIRTY); 585 } else if (aAttribute == nsGkAtoms::list) { 586 const bool isRemoval = aModType == AttrModType::Removal; 587 if (mListMutationObserver) { 588 mListMutationObserver->Detach(); 589 if (isRemoval) { 590 mListMutationObserver = nullptr; 591 } else { 592 mListMutationObserver->Attach(); 593 } 594 } else if (!isRemoval) { 595 mListMutationObserver = new ListMutationObserver(*this, true); 596 } 597 } 598 } 599 600 return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); 601 } 602 603 nscoord nsRangeFrame::AutoCrossSize() { 604 nscoord minCrossSize = 605 IsThemed() ? CSSPixel::ToAppUnits( 606 PresContext()->Theme()->GetMinimumRangeThumbSize()) 607 : 0; 608 return std::max(minCrossSize, 609 NSToCoordRound(OneEmInAppUnits() * CROSS_AXIS_EM_SIZE)); 610 } 611 612 nscoord nsRangeFrame::IntrinsicISize(const IntrinsicSizeInput& aInput, 613 IntrinsicISizeType aType) { 614 if (aType == IntrinsicISizeType::MinISize) { 615 const auto* pos = StylePosition(); 616 auto wm = GetWritingMode(); 617 const auto iSize = pos->ISize(wm, AnchorPosResolutionParams::From(this)); 618 if (iSize->HasPercent()) { 619 // https://drafts.csswg.org/css-sizing-3/#percentage-sizing 620 // https://drafts.csswg.org/css-sizing-3/#min-content-zero 621 return nsLayoutUtils::ResolveToLength<true>(iSize->AsLengthPercentage(), 622 nscoord(0)); 623 } 624 } 625 if (IsInlineOriented()) { 626 return OneEmInAppUnits() * MAIN_AXIS_EM_SIZE; 627 } 628 return AutoCrossSize(); 629 } 630 631 bool nsRangeFrame::IsHorizontal() const { 632 dom::HTMLInputElement* element = 633 static_cast<dom::HTMLInputElement*>(GetContent()); 634 return element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient, 635 nsGkAtoms::horizontal, eCaseMatters) || 636 (!element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient, 637 nsGkAtoms::vertical, eCaseMatters) && 638 GetWritingMode().IsVertical() == 639 element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient, 640 nsGkAtoms::block, eCaseMatters)); 641 } 642 643 double nsRangeFrame::GetMin() const { 644 return static_cast<dom::HTMLInputElement*>(GetContent()) 645 ->GetMinimum() 646 .toDouble(); 647 } 648 649 double nsRangeFrame::GetMax() const { 650 return static_cast<dom::HTMLInputElement*>(GetContent()) 651 ->GetMaximum() 652 .toDouble(); 653 } 654 655 double nsRangeFrame::GetValue() const { 656 return static_cast<dom::HTMLInputElement*>(GetContent()) 657 ->GetValueAsDecimal() 658 .toDouble(); 659 } 660 661 bool nsRangeFrame::ShouldUseNativeStyle() const { 662 nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame(); 663 nsIFrame* progressFrame = mProgressDiv->GetPrimaryFrame(); 664 nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); 665 666 return StyleDisplay()->EffectiveAppearance() == StyleAppearance::Range && 667 trackFrame && 668 !trackFrame->Style()->HasAuthorSpecifiedBorderOrBackground() && 669 progressFrame && 670 !progressFrame->Style()->HasAuthorSpecifiedBorderOrBackground() && 671 thumbFrame && 672 !thumbFrame->Style()->HasAuthorSpecifiedBorderOrBackground(); 673 }