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 "nsRangeFrame.h"
9 #include "ListMutationObserver.h"
10 #include "mozilla/Assertions.h"
11 #include "mozilla/PresShell.h"
12 #include "mozilla/TouchEvents.h"
14 #include "gfxContext.h"
15 #include "nsContentCreatorFunctions.h"
16 #include "nsCSSRendering.h"
17 #include "nsDisplayList.h"
18 #include "nsIContent.h"
19 #include "nsLayoutUtils.h"
20 #include "mozilla/dom/Document.h"
21 #include "nsGkAtoms.h"
22 #include "mozilla/dom/HTMLDataListElement.h"
23 #include "mozilla/dom/HTMLInputElement.h"
24 #include "mozilla/dom/HTMLOptionElement.h"
25 #include "mozilla/dom/MutationEventBinding.h"
26 #include "nsPresContext.h"
27 #include "nsNodeInfoManager.h"
28 #include "mozilla/dom/Element.h"
29 #include "mozilla/ServoStyleSet.h"
33 # include "nsAccessibilityService.h"
36 // Our intrinsic size is 12em in the main-axis and 1.3em in the cross-axis.
37 #define MAIN_AXIS_EM_SIZE 12
38 #define CROSS_AXIS_EM_SIZE 1.3f
40 using namespace mozilla
;
41 using namespace mozilla::dom
;
42 using namespace mozilla::image
;
44 nsIFrame
* NS_NewRangeFrame(PresShell
* aPresShell
, ComputedStyle
* aStyle
) {
45 return new (aPresShell
) nsRangeFrame(aStyle
, aPresShell
->GetPresContext());
48 nsRangeFrame::nsRangeFrame(ComputedStyle
* aStyle
, nsPresContext
* aPresContext
)
49 : nsContainerFrame(aStyle
, aPresContext
, kClassID
) {}
51 void nsRangeFrame::Init(nsIContent
* aContent
, nsContainerFrame
* aParent
,
52 nsIFrame
* aPrevInFlow
) {
53 nsContainerFrame::Init(aContent
, aParent
, aPrevInFlow
);
54 if (InputElement().HasAttr(nsGkAtoms::list_
)) {
55 mListMutationObserver
= new ListMutationObserver(*this);
59 nsRangeFrame::~nsRangeFrame() = default;
61 NS_IMPL_FRAMEARENA_HELPERS(nsRangeFrame
)
63 NS_QUERYFRAME_HEAD(nsRangeFrame
)
64 NS_QUERYFRAME_ENTRY(nsRangeFrame
)
65 NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator
)
66 NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame
)
68 void nsRangeFrame::Destroy(DestroyContext
& aContext
) {
69 NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(),
70 "nsRangeFrame should not have continuations; if it does we "
71 "need to call RegUnregAccessKey only for the first.");
73 if (mListMutationObserver
) {
74 mListMutationObserver
->Detach();
76 aContext
.AddAnonymousContent(mTrackDiv
.forget());
77 aContext
.AddAnonymousContent(mProgressDiv
.forget());
78 aContext
.AddAnonymousContent(mThumbDiv
.forget());
79 nsContainerFrame::Destroy(aContext
);
82 static already_AddRefed
<Element
> MakeAnonymousDiv(
83 Document
& aDoc
, PseudoStyleType aOldPseudoType
,
84 PseudoStyleType aModernPseudoType
,
85 nsTArray
<nsIAnonymousContentCreator::ContentInfo
>& aElements
) {
86 RefPtr
<Element
> result
= aDoc
.CreateHTMLElement(nsGkAtoms::div
);
88 // Associate the pseudo-element with the anonymous child.
89 if (StaticPrefs::layout_css_modern_range_pseudos_enabled()) {
90 result
->SetPseudoElementType(aModernPseudoType
);
92 result
->SetPseudoElementType(aOldPseudoType
);
95 // XXX(Bug 1631371) Check if this should use a fallible operation as it
96 // pretended earlier, or change the return type to void.
97 aElements
.AppendElement(result
);
99 return result
.forget();
102 nsresult
nsRangeFrame::CreateAnonymousContent(
103 nsTArray
<ContentInfo
>& aElements
) {
104 Document
* doc
= mContent
->OwnerDoc();
105 // Create the ::-moz-range-track pseudo-element (a div):
106 mTrackDiv
= MakeAnonymousDiv(*doc
, PseudoStyleType::mozRangeTrack
,
107 PseudoStyleType::sliderTrack
, aElements
);
108 // Create the ::-moz-range-progress pseudo-element (a div):
109 mProgressDiv
= MakeAnonymousDiv(*doc
, PseudoStyleType::mozRangeProgress
,
110 PseudoStyleType::sliderFill
, aElements
);
111 // Create the ::-moz-range-thumb pseudo-element (a div):
112 mThumbDiv
= MakeAnonymousDiv(*doc
, PseudoStyleType::mozRangeThumb
,
113 PseudoStyleType::sliderThumb
, aElements
);
117 void nsRangeFrame::AppendAnonymousContentTo(nsTArray
<nsIContent
*>& aElements
,
120 aElements
.AppendElement(mTrackDiv
);
124 aElements
.AppendElement(mProgressDiv
);
128 aElements
.AppendElement(mThumbDiv
);
132 void nsRangeFrame::BuildDisplayList(nsDisplayListBuilder
* aBuilder
,
133 const nsDisplayListSet
& aLists
) {
134 const nsStyleDisplay
* disp
= StyleDisplay();
135 if (IsThemed(disp
)) {
136 DisplayBorderBackgroundOutline(aBuilder
, aLists
);
137 // Only create items for the thumb. Specifically, we do not want the track
138 // to paint, since *our* background is used to paint the track, and we don't
139 // want the unthemed track painting over the top of the themed track.
140 // This logic is copied from
141 // nsContainerFrame::BuildDisplayListForNonBlockChildren as
142 // called by BuildDisplayListForInline.
143 if (nsIFrame
* thumb
= mThumbDiv
->GetPrimaryFrame()) {
144 nsDisplayListSet
set(aLists
, aLists
.Content());
145 BuildDisplayListForChild(aBuilder
, thumb
, set
, DisplayChildFlag::Inline
);
148 BuildDisplayListForInline(aBuilder
, aLists
);
152 void nsRangeFrame::Reflow(nsPresContext
* aPresContext
,
153 ReflowOutput
& aDesiredSize
,
154 const ReflowInput
& aReflowInput
,
155 nsReflowStatus
& aStatus
) {
157 DO_GLOBAL_REFLOW_COUNT("nsRangeFrame");
158 DISPLAY_REFLOW(aPresContext
, this, aReflowInput
, aDesiredSize
, aStatus
);
159 MOZ_ASSERT(aStatus
.IsEmpty(), "Caller should pass a fresh reflow status!");
161 NS_ASSERTION(mTrackDiv
, "::-moz-range-track div must exist!");
162 NS_ASSERTION(mProgressDiv
, "::-moz-range-progress div must exist!");
163 NS_ASSERTION(mThumbDiv
, "::-moz-range-thumb div must exist!");
164 NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(),
165 "nsRangeFrame should not have continuations; if it does we "
166 "need to call RegUnregAccessKey only for the first.");
168 WritingMode wm
= aReflowInput
.GetWritingMode();
169 nscoord computedBSize
= aReflowInput
.ComputedBSize();
170 if (computedBSize
== NS_UNCONSTRAINEDSIZE
) {
173 const auto borderPadding
= aReflowInput
.ComputedLogicalBorderPadding(wm
);
174 LogicalSize
finalSize(
175 wm
, aReflowInput
.ComputedISize() + borderPadding
.IStartEnd(wm
),
176 computedBSize
+ borderPadding
.BStartEnd(wm
));
177 aDesiredSize
.SetSize(wm
, finalSize
);
179 ReflowAnonymousContent(aPresContext
, aDesiredSize
, aReflowInput
);
181 aDesiredSize
.SetOverflowAreasToDesiredBounds();
183 nsIFrame
* trackFrame
= mTrackDiv
->GetPrimaryFrame();
185 ConsiderChildOverflow(aDesiredSize
.mOverflowAreas
, trackFrame
);
188 nsIFrame
* rangeProgressFrame
= mProgressDiv
->GetPrimaryFrame();
189 if (rangeProgressFrame
) {
190 ConsiderChildOverflow(aDesiredSize
.mOverflowAreas
, rangeProgressFrame
);
193 nsIFrame
* thumbFrame
= mThumbDiv
->GetPrimaryFrame();
195 ConsiderChildOverflow(aDesiredSize
.mOverflowAreas
, thumbFrame
);
198 FinishAndStoreOverflow(&aDesiredSize
);
200 MOZ_ASSERT(aStatus
.IsEmpty(), "This type of frame can't be split.");
203 void nsRangeFrame::ReflowAnonymousContent(nsPresContext
* aPresContext
,
204 ReflowOutput
& aDesiredSize
,
205 const ReflowInput
& aReflowInput
) {
206 // The width/height of our content box, which is the available width/height
207 // for our anonymous content:
208 nscoord rangeFrameContentBoxWidth
= aReflowInput
.ComputedWidth();
209 nscoord rangeFrameContentBoxHeight
= aReflowInput
.ComputedHeight();
210 if (rangeFrameContentBoxHeight
== NS_UNCONSTRAINEDSIZE
) {
211 rangeFrameContentBoxHeight
= 0;
214 nsIFrame
* trackFrame
= mTrackDiv
->GetPrimaryFrame();
216 if (trackFrame
) { // display:none?
218 // Position the track:
219 // The idea here is that we allow content authors to style the width,
220 // height, border and padding of the track, but we ignore margin and
221 // positioning properties and do the positioning ourself to keep the center
222 // of the track's border box on the center of the nsRangeFrame's content
225 WritingMode wm
= trackFrame
->GetWritingMode();
226 LogicalSize availSize
= aReflowInput
.ComputedSize(wm
);
227 availSize
.BSize(wm
) = NS_UNCONSTRAINEDSIZE
;
228 ReflowInput
trackReflowInput(aPresContext
, aReflowInput
, trackFrame
,
231 // Find the x/y position of the track frame such that it will be positioned
232 // as described above. These coordinates are with respect to the
233 // nsRangeFrame's border-box.
234 nscoord trackX
= rangeFrameContentBoxWidth
/ 2;
235 nscoord trackY
= rangeFrameContentBoxHeight
/ 2;
237 // Account for the track's border and padding (we ignore its margin):
238 trackX
-= trackReflowInput
.ComputedPhysicalBorderPadding().left
+
239 trackReflowInput
.ComputedWidth() / 2;
240 trackY
-= trackReflowInput
.ComputedPhysicalBorderPadding().top
+
241 trackReflowInput
.ComputedHeight() / 2;
243 // Make relative to our border box instead of our content box:
244 trackX
+= aReflowInput
.ComputedPhysicalBorderPadding().left
;
245 trackY
+= aReflowInput
.ComputedPhysicalBorderPadding().top
;
247 nsReflowStatus frameStatus
;
248 ReflowOutput
trackDesiredSize(aReflowInput
);
249 ReflowChild(trackFrame
, aPresContext
, trackDesiredSize
, trackReflowInput
,
250 trackX
, trackY
, ReflowChildFlags::Default
, frameStatus
);
252 frameStatus
.IsFullyComplete(),
253 "We gave our child unconstrained height, so it should be complete");
254 FinishReflowChild(trackFrame
, aPresContext
, trackDesiredSize
,
255 &trackReflowInput
, trackX
, trackY
,
256 ReflowChildFlags::Default
);
259 nsIFrame
* thumbFrame
= mThumbDiv
->GetPrimaryFrame();
261 if (thumbFrame
) { // display:none?
262 WritingMode wm
= thumbFrame
->GetWritingMode();
263 LogicalSize availSize
= aReflowInput
.ComputedSize(wm
);
264 availSize
.BSize(wm
) = NS_UNCONSTRAINEDSIZE
;
265 ReflowInput
thumbReflowInput(aPresContext
, aReflowInput
, thumbFrame
,
268 // Where we position the thumb depends on its size, so we first reflow
269 // the thumb at {0,0} to obtain its size, then position it afterwards.
271 nsReflowStatus frameStatus
;
272 ReflowOutput
thumbDesiredSize(aReflowInput
);
273 ReflowChild(thumbFrame
, aPresContext
, thumbDesiredSize
, thumbReflowInput
, 0,
274 0, ReflowChildFlags::Default
, frameStatus
);
276 frameStatus
.IsFullyComplete(),
277 "We gave our child unconstrained height, so it should be complete");
278 FinishReflowChild(thumbFrame
, aPresContext
, thumbDesiredSize
,
279 &thumbReflowInput
, 0, 0, ReflowChildFlags::Default
);
280 DoUpdateThumbPosition(thumbFrame
,
281 nsSize(aDesiredSize
.Width(), aDesiredSize
.Height()));
284 nsIFrame
* rangeProgressFrame
= mProgressDiv
->GetPrimaryFrame();
286 if (rangeProgressFrame
) { // display:none?
287 WritingMode wm
= rangeProgressFrame
->GetWritingMode();
288 LogicalSize availSize
= aReflowInput
.ComputedSize(wm
);
289 availSize
.BSize(wm
) = NS_UNCONSTRAINEDSIZE
;
290 ReflowInput
progressReflowInput(aPresContext
, aReflowInput
,
291 rangeProgressFrame
, availSize
);
293 // We first reflow the range-progress frame at {0,0} to obtain its
294 // unadjusted dimensions, then we adjust it to so that the appropriate edge
295 // ends at the thumb.
297 nsReflowStatus frameStatus
;
298 ReflowOutput
progressDesiredSize(aReflowInput
);
299 ReflowChild(rangeProgressFrame
, aPresContext
, progressDesiredSize
,
300 progressReflowInput
, 0, 0, ReflowChildFlags::Default
,
303 frameStatus
.IsFullyComplete(),
304 "We gave our child unconstrained height, so it should be complete");
305 FinishReflowChild(rangeProgressFrame
, aPresContext
, progressDesiredSize
,
306 &progressReflowInput
, 0, 0, ReflowChildFlags::Default
);
307 DoUpdateRangeProgressFrame(
309 nsSize(aDesiredSize
.Width(), aDesiredSize
.Height()));
314 a11y::AccType
nsRangeFrame::AccessibleType() { return a11y::eHTMLRangeType
; }
317 double nsRangeFrame::GetValueAsFractionOfRange() {
318 const auto& input
= InputElement();
319 if (MOZ_UNLIKELY(!input
.IsDoneCreating())) {
320 // Our element isn't done being created, so its values haven't yet been
321 // sanitized! (It's rare that we'd be reflowed when our element is in this
322 // state, but it can happen if the parser decides to yield while processing
323 // its tasks to build the element.) We can't trust that any of our numeric
324 // values will make sense until they've been sanitized; so for now, just
325 // use 0.0 as a fallback fraction-of-range value here (i.e. behave as if
326 // we're at our minimum, which is how the spec handles some edge cases).
329 return GetDoubleAsFractionOfRange(input
.GetValueAsDecimal());
332 double nsRangeFrame::GetDoubleAsFractionOfRange(const Decimal
& aValue
) {
333 auto& input
= InputElement();
335 Decimal minimum
= input
.GetMinimum();
336 Decimal maximum
= input
.GetMaximum();
338 MOZ_ASSERT(aValue
.isFinite() && minimum
.isFinite() && maximum
.isFinite(),
339 "type=range should have a default maximum/minimum");
341 if (maximum
<= minimum
) {
342 // Avoid rounding triggering the assert by checking against an epsilon.
343 MOZ_ASSERT((aValue
- minimum
).abs().toDouble() <
344 std::numeric_limits
<float>::epsilon(),
345 "Unsanitized value");
349 MOZ_ASSERT(aValue
>= minimum
&& aValue
<= maximum
, "Unsanitized value");
351 return ((aValue
- minimum
) / (maximum
- minimum
)).toDouble();
354 Decimal
nsRangeFrame::GetValueAtEventPoint(WidgetGUIEvent
* aEvent
) {
356 aEvent
->mClass
== eMouseEventClass
|| aEvent
->mClass
== eTouchEventClass
,
357 "Unexpected event type - aEvent->mRefPoint may be meaningless");
359 MOZ_ASSERT(mContent
->IsHTMLElement(nsGkAtoms::input
), "bad cast");
360 dom::HTMLInputElement
* input
=
361 static_cast<dom::HTMLInputElement
*>(GetContent());
363 MOZ_ASSERT(input
->ControlType() == FormControlType::InputRange
);
365 Decimal minimum
= input
->GetMinimum();
366 Decimal maximum
= input
->GetMaximum();
367 MOZ_ASSERT(minimum
.isFinite() && maximum
.isFinite(),
368 "type=range should have a default maximum/minimum");
369 if (maximum
<= minimum
) {
372 Decimal range
= maximum
- minimum
;
374 LayoutDeviceIntPoint absPoint
;
375 if (aEvent
->mClass
== eTouchEventClass
) {
376 MOZ_ASSERT(aEvent
->AsTouchEvent()->mTouches
.Length() == 1,
377 "Unexpected number of mTouches");
378 absPoint
= aEvent
->AsTouchEvent()->mTouches
[0]->mRefPoint
;
380 absPoint
= aEvent
->mRefPoint
;
382 nsPoint point
= nsLayoutUtils::GetEventCoordinatesRelativeTo(
383 aEvent
, absPoint
, RelativeTo
{this});
385 if (point
== nsPoint(NS_UNCONSTRAINEDSIZE
, NS_UNCONSTRAINEDSIZE
)) {
386 // We don't want to change the current value for this error state.
387 return static_cast<dom::HTMLInputElement
*>(GetContent())
388 ->GetValueAsDecimal();
391 nsRect rangeContentRect
= GetContentRectRelativeToSelf();
395 // We need to get the size of the thumb from the theme.
396 nsPresContext
* pc
= PresContext();
397 LayoutDeviceIntSize size
= pc
->Theme()->GetMinimumWidgetSize(
398 pc
, this, StyleAppearance::RangeThumb
);
400 LayoutDeviceIntSize::ToAppUnits(size
, pc
->AppUnitsPerDevPixel());
401 // For GTK, GetMinimumWidgetSize returns zero for the thumb dimension
402 // perpendicular to the orientation of the slider. That's okay since we
403 // only care about the dimension in the direction of the slider when using
404 // |thumbSize| below, but it means this assertion need to check
406 MOZ_ASSERT((IsHorizontal() && thumbSize
.width
> 0) ||
407 (!IsHorizontal() && thumbSize
.height
> 0),
408 "The thumb is expected to take up some slider space");
410 nsIFrame
* thumbFrame
= mThumbDiv
->GetPrimaryFrame();
411 if (thumbFrame
) { // diplay:none?
412 thumbSize
= thumbFrame
->GetSize();
417 if (IsHorizontal()) {
418 nscoord traversableDistance
= rangeContentRect
.width
- thumbSize
.width
;
419 if (traversableDistance
<= 0) {
422 nscoord posAtStart
= rangeContentRect
.x
+ thumbSize
.width
/ 2;
423 nscoord posAtEnd
= posAtStart
+ traversableDistance
;
424 nscoord posOfPoint
= mozilla::clamped(point
.x
, posAtStart
, posAtEnd
);
425 fraction
= Decimal(posOfPoint
- posAtStart
) / Decimal(traversableDistance
);
426 if (IsRightToLeft()) {
427 fraction
= Decimal(1) - fraction
;
430 nscoord traversableDistance
= rangeContentRect
.height
- thumbSize
.height
;
431 if (traversableDistance
<= 0) {
434 nscoord posAtStart
= rangeContentRect
.y
+ thumbSize
.height
/ 2;
435 nscoord posAtEnd
= posAtStart
+ traversableDistance
;
436 nscoord posOfPoint
= mozilla::clamped(point
.y
, posAtStart
, posAtEnd
);
437 // For a vertical range, the top (posAtStart) is the highest value, so we
438 // subtract the fraction from 1.0 to get that polarity correct.
439 fraction
= Decimal(posOfPoint
- posAtStart
) / Decimal(traversableDistance
);
441 fraction
= Decimal(1) - fraction
;
445 MOZ_ASSERT(fraction
>= Decimal(0) && fraction
<= Decimal(1));
446 return minimum
+ fraction
* range
;
449 void nsRangeFrame::UpdateForValueChange() {
450 if (IsSubtreeDirty()) {
451 return; // we're going to be updated when we reflow
453 nsIFrame
* rangeProgressFrame
= mProgressDiv
->GetPrimaryFrame();
454 nsIFrame
* thumbFrame
= mThumbDiv
->GetPrimaryFrame();
455 if (!rangeProgressFrame
&& !thumbFrame
) {
456 return; // diplay:none?
458 if (rangeProgressFrame
) {
459 DoUpdateRangeProgressFrame(rangeProgressFrame
, GetSize());
462 DoUpdateThumbPosition(thumbFrame
, GetSize());
465 // We don't know the exact dimensions or location of the thumb when native
466 // theming is applied, so we just repaint the entire range.
471 if (nsAccessibilityService
* accService
= GetAccService()) {
472 accService
->RangeValueChanged(PresShell(), mContent
);
479 nsTArray
<Decimal
> nsRangeFrame::TickMarks() {
480 nsTArray
<Decimal
> tickMarks
;
481 auto& input
= InputElement();
482 auto* list
= input
.GetList();
486 auto min
= input
.GetMinimum();
487 auto max
= input
.GetMaximum();
488 auto* options
= list
->Options();
490 for (uint32_t i
= 0; i
< options
->Length(); ++i
) {
491 auto* item
= options
->Item(i
);
492 auto* option
= HTMLOptionElement::FromNode(item
);
494 if (option
->Disabled()) {
498 option
->GetValue(str
);
499 auto tickMark
= HTMLInputElement::StringToDecimal(str
);
500 if (tickMark
.isNaN() || tickMark
< min
|| tickMark
> max
||
501 input
.ValueIsStepMismatch(tickMark
)) {
504 tickMarks
.AppendElement(tickMark
);
510 Decimal
nsRangeFrame::NearestTickMark(const Decimal
& aValue
) {
511 auto tickMarks
= TickMarks();
512 if (tickMarks
.IsEmpty() || aValue
.isNaN()) {
513 return Decimal::nan();
516 if (BinarySearch(tickMarks
, 0, tickMarks
.Length(), aValue
, &index
)) {
517 return tickMarks
[index
];
519 if (index
== tickMarks
.Length()) {
520 return tickMarks
.LastElement();
525 const auto& smallerTickMark
= tickMarks
[index
- 1];
526 const auto& largerTickMark
= tickMarks
[index
];
527 MOZ_ASSERT(smallerTickMark
< aValue
);
528 MOZ_ASSERT(largerTickMark
> aValue
);
529 return (aValue
- smallerTickMark
).abs() < (aValue
- largerTickMark
).abs()
534 mozilla::dom::HTMLInputElement
& nsRangeFrame::InputElement() const {
535 MOZ_ASSERT(mContent
->IsHTMLElement(nsGkAtoms::input
), "bad cast");
536 auto& input
= *static_cast<dom::HTMLInputElement
*>(GetContent());
537 MOZ_ASSERT(input
.ControlType() == FormControlType::InputRange
);
541 void nsRangeFrame::DoUpdateThumbPosition(nsIFrame
* aThumbFrame
,
542 const nsSize
& aRangeSize
) {
543 MOZ_ASSERT(aThumbFrame
);
545 // The idea here is that we want to position the thumb so that the center
546 // of the thumb is on an imaginary line drawn from the middle of one edge
547 // of the range frame's content box to the middle of the opposite edge of
548 // its content box (the opposite edges being the left/right edge if the
549 // range is horizontal, or else the top/bottom edges if the range is
550 // vertical). How far along this line the center of the thumb is placed
551 // depends on the value of the range.
553 nsMargin borderAndPadding
= GetUsedBorderAndPadding();
554 nsPoint
newPosition(borderAndPadding
.left
, borderAndPadding
.top
);
556 nsSize
rangeContentBoxSize(aRangeSize
);
557 rangeContentBoxSize
.width
-= borderAndPadding
.LeftRight();
558 rangeContentBoxSize
.height
-= borderAndPadding
.TopBottom();
560 nsSize thumbSize
= aThumbFrame
->GetSize();
561 double fraction
= GetValueAsFractionOfRange();
562 MOZ_ASSERT(fraction
>= 0.0 && fraction
<= 1.0);
564 if (IsHorizontal()) {
565 if (thumbSize
.width
< rangeContentBoxSize
.width
) {
566 nscoord traversableDistance
= rangeContentBoxSize
.width
- thumbSize
.width
;
567 if (IsRightToLeft()) {
568 newPosition
.x
+= NSToCoordRound((1.0 - fraction
) * traversableDistance
);
570 newPosition
.x
+= NSToCoordRound(fraction
* traversableDistance
);
572 newPosition
.y
+= (rangeContentBoxSize
.height
- thumbSize
.height
) / 2;
575 if (thumbSize
.height
< rangeContentBoxSize
.height
) {
576 nscoord traversableDistance
=
577 rangeContentBoxSize
.height
- thumbSize
.height
;
578 newPosition
.x
+= (rangeContentBoxSize
.width
- thumbSize
.width
) / 2;
580 newPosition
.y
+= NSToCoordRound((1.0 - fraction
) * traversableDistance
);
582 newPosition
.y
+= NSToCoordRound(fraction
* traversableDistance
);
586 aThumbFrame
->SetPosition(newPosition
);
589 void nsRangeFrame::DoUpdateRangeProgressFrame(nsIFrame
* aRangeProgressFrame
,
590 const nsSize
& aRangeSize
) {
591 MOZ_ASSERT(aRangeProgressFrame
);
593 // The idea here is that we want to position the ::-moz-range-progress
594 // pseudo-element so that the center line running along its length is on the
595 // corresponding center line of the nsRangeFrame's content box. In the other
596 // dimension, we align the "start" edge of the ::-moz-range-progress
597 // pseudo-element's border-box with the corresponding edge of the
598 // nsRangeFrame's content box, and we size the progress element's border-box
599 // to have a length of GetValueAsFractionOfRange() times the nsRangeFrame's
602 nsMargin borderAndPadding
= GetUsedBorderAndPadding();
603 nsSize progSize
= aRangeProgressFrame
->GetSize();
604 nsRect
progRect(borderAndPadding
.left
, borderAndPadding
.top
, progSize
.width
,
607 nsSize
rangeContentBoxSize(aRangeSize
);
608 rangeContentBoxSize
.width
-= borderAndPadding
.LeftRight();
609 rangeContentBoxSize
.height
-= borderAndPadding
.TopBottom();
611 double fraction
= GetValueAsFractionOfRange();
612 MOZ_ASSERT(fraction
>= 0.0 && fraction
<= 1.0);
614 if (IsHorizontal()) {
615 nscoord progLength
= NSToCoordRound(fraction
* rangeContentBoxSize
.width
);
616 if (IsRightToLeft()) {
617 progRect
.x
+= rangeContentBoxSize
.width
- progLength
;
619 progRect
.y
+= (rangeContentBoxSize
.height
- progSize
.height
) / 2;
620 progRect
.width
= progLength
;
622 nscoord progLength
= NSToCoordRound(fraction
* rangeContentBoxSize
.height
);
623 progRect
.x
+= (rangeContentBoxSize
.width
- progSize
.width
) / 2;
625 progRect
.y
+= rangeContentBoxSize
.height
- progLength
;
627 progRect
.height
= progLength
;
629 aRangeProgressFrame
->SetRect(progRect
);
632 nsresult
nsRangeFrame::AttributeChanged(int32_t aNameSpaceID
,
633 nsAtom
* aAttribute
, int32_t aModType
) {
634 NS_ASSERTION(mTrackDiv
, "The track div must exist!");
635 NS_ASSERTION(mThumbDiv
, "The thumb div must exist!");
637 if (aNameSpaceID
== kNameSpaceID_None
) {
638 if (aAttribute
== nsGkAtoms::value
|| aAttribute
== nsGkAtoms::min
||
639 aAttribute
== nsGkAtoms::max
|| aAttribute
== nsGkAtoms::step
) {
640 // We want to update the position of the thumb, except in one special
641 // case: If the value attribute is being set, it is possible that we are
642 // in the middle of a type change away from type=range, under the
643 // SetAttr(..., nsGkAtoms::value, ...) call in HTMLInputElement::
644 // HandleTypeChange. In that case the HTMLInputElement's type will
645 // already have changed, and if we call UpdateForValueChange()
646 // we'll fail the asserts under that call that check the type of our
647 // HTMLInputElement. Given that we're changing away from being a range
648 // and this frame will shortly be destroyed, there's no point in calling
649 // UpdateForValueChange() anyway.
650 MOZ_ASSERT(mContent
->IsHTMLElement(nsGkAtoms::input
), "bad cast");
652 static_cast<dom::HTMLInputElement
*>(GetContent())->ControlType() ==
653 FormControlType::InputRange
;
654 // If script changed the <input>'s type before setting these attributes
655 // then we don't need to do anything since we are going to be reframed.
657 UpdateForValueChange();
659 } else if (aAttribute
== nsGkAtoms::orient
) {
660 PresShell()->FrameNeedsReflow(this, IntrinsicDirty::None
,
662 } else if (aAttribute
== nsGkAtoms::list_
) {
663 const bool isRemoval
= aModType
== MutationEvent_Binding::REMOVAL
;
664 if (mListMutationObserver
) {
665 mListMutationObserver
->Detach();
667 mListMutationObserver
= nullptr;
669 mListMutationObserver
->Attach();
671 } else if (!isRemoval
) {
672 mListMutationObserver
= new ListMutationObserver(*this, true);
677 return nsContainerFrame::AttributeChanged(aNameSpaceID
, aAttribute
, aModType
);
680 nscoord
nsRangeFrame::AutoCrossSize(Length aEm
) {
681 nscoord
minCrossSize(0);
683 nsPresContext
* pc
= PresContext();
684 LayoutDeviceIntSize size
= pc
->Theme()->GetMinimumWidgetSize(
685 pc
, this, StyleAppearance::RangeThumb
);
687 pc
->DevPixelsToAppUnits(IsHorizontal() ? size
.height
: size
.width
);
689 return std::max(minCrossSize
, aEm
.ScaledBy(CROSS_AXIS_EM_SIZE
).ToAppUnits());
692 static mozilla::Length
OneEm(nsRangeFrame
* aFrame
) {
693 return aFrame
->StyleFont()->mFont
.size
.ScaledBy(
694 nsLayoutUtils::FontSizeInflationFor(aFrame
));
697 LogicalSize
nsRangeFrame::ComputeAutoSize(
698 gfxContext
* aRenderingContext
, WritingMode aWM
, const LogicalSize
& aCBSize
,
699 nscoord aAvailableISize
, const LogicalSize
& aMargin
,
700 const LogicalSize
& aBorderPadding
, const StyleSizeOverrides
& aSizeOverrides
,
701 ComputeSizeFlags aFlags
) {
702 bool isInlineOriented
= IsInlineOriented();
703 auto em
= OneEm(this);
705 const WritingMode wm
= GetWritingMode();
706 LogicalSize
autoSize(wm
);
707 if (isInlineOriented
) {
708 autoSize
.ISize(wm
) = em
.ScaledBy(MAIN_AXIS_EM_SIZE
).ToAppUnits();
709 autoSize
.BSize(wm
) = AutoCrossSize(em
);
711 autoSize
.ISize(wm
) = AutoCrossSize(em
);
712 autoSize
.BSize(wm
) = em
.ScaledBy(MAIN_AXIS_EM_SIZE
).ToAppUnits();
715 return autoSize
.ConvertTo(aWM
, wm
);
718 nscoord
nsRangeFrame::GetMinISize(gfxContext
* aRenderingContext
) {
719 const auto* pos
= StylePosition();
720 auto wm
= GetWritingMode();
721 if (pos
->ISize(wm
).HasPercent()) {
722 // https://drafts.csswg.org/css-sizing-3/#percentage-sizing
723 // https://drafts.csswg.org/css-sizing-3/#min-content-zero
724 return nsLayoutUtils::ResolveToLength
<true>(
725 pos
->ISize(wm
).AsLengthPercentage(), nscoord(0));
727 return GetPrefISize(aRenderingContext
);
730 nscoord
nsRangeFrame::GetPrefISize(gfxContext
* aRenderingContext
) {
731 auto em
= OneEm(this);
732 if (IsInlineOriented()) {
733 return em
.ScaledBy(MAIN_AXIS_EM_SIZE
).ToAppUnits();
735 return AutoCrossSize(em
);
738 bool nsRangeFrame::IsHorizontal() const {
739 dom::HTMLInputElement
* element
=
740 static_cast<dom::HTMLInputElement
*>(GetContent());
741 return element
->AttrValueIs(kNameSpaceID_None
, nsGkAtoms::orient
,
742 nsGkAtoms::horizontal
, eCaseMatters
) ||
743 (!element
->AttrValueIs(kNameSpaceID_None
, nsGkAtoms::orient
,
744 nsGkAtoms::vertical
, eCaseMatters
) &&
745 GetWritingMode().IsVertical() ==
746 element
->AttrValueIs(kNameSpaceID_None
, nsGkAtoms::orient
,
747 nsGkAtoms::block
, eCaseMatters
));
750 double nsRangeFrame::GetMin() const {
751 return static_cast<dom::HTMLInputElement
*>(GetContent())
756 double nsRangeFrame::GetMax() const {
757 return static_cast<dom::HTMLInputElement
*>(GetContent())
762 double nsRangeFrame::GetValue() const {
763 return static_cast<dom::HTMLInputElement
*>(GetContent())
764 ->GetValueAsDecimal()
768 bool nsRangeFrame::ShouldUseNativeStyle() const {
769 nsIFrame
* trackFrame
= mTrackDiv
->GetPrimaryFrame();
770 nsIFrame
* progressFrame
= mProgressDiv
->GetPrimaryFrame();
771 nsIFrame
* thumbFrame
= mThumbDiv
->GetPrimaryFrame();
773 return StyleDisplay()->EffectiveAppearance() == StyleAppearance::Range
&&
775 !trackFrame
->Style()->HasAuthorSpecifiedBorderOrBackground() &&
777 !progressFrame
->Style()->HasAuthorSpecifiedBorderOrBackground() &&
779 !thumbFrame
->Style()->HasAuthorSpecifiedBorderOrBackground();