Bumping gaia.json for 8 gaia revision(s) a=gaia-bump
[gecko.git] / dom / base / ResponsiveImageSelector.cpp
blobde6dcd1acfcb30376085dae2586166622d8333c5
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "mozilla/dom/ResponsiveImageSelector.h"
7 #include "nsIURI.h"
8 #include "nsIDocument.h"
9 #include "nsContentUtils.h"
10 #include "nsPresContext.h"
11 #include "nsNetUtil.h"
13 #include "nsCSSParser.h"
14 #include "nsCSSProps.h"
15 #include "nsIMediaList.h"
16 #include "nsRuleNode.h"
17 #include "nsRuleData.h"
19 using namespace mozilla;
20 using namespace mozilla::dom;
22 namespace mozilla {
23 namespace dom {
25 NS_IMPL_CYCLE_COLLECTION(ResponsiveImageSelector, mContent)
27 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(ResponsiveImageSelector, AddRef)
28 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(ResponsiveImageSelector, Release)
30 static bool
31 ParseInteger(const nsAString& aString, int32_t& aInt)
33 nsContentUtils::ParseHTMLIntegerResultFlags parseResult;
34 aInt = nsContentUtils::ParseHTMLInteger(aString, &parseResult);
35 return !(parseResult &
36 ( nsContentUtils::eParseHTMLInteger_Error |
37 nsContentUtils::eParseHTMLInteger_DidNotConsumeAllInput |
38 nsContentUtils::eParseHTMLInteger_IsPercent ));
41 ResponsiveImageSelector::ResponsiveImageSelector(nsIContent *aContent)
42 : mContent(aContent),
43 mBestCandidateIndex(-1)
47 ResponsiveImageSelector::~ResponsiveImageSelector()
50 // http://www.whatwg.org/specs/web-apps/current-work/#processing-the-image-candidates
51 bool
52 ResponsiveImageSelector::SetCandidatesFromSourceSet(const nsAString & aSrcSet)
54 nsIDocument* doc = mContent ? mContent->OwnerDoc() : nullptr;
55 nsCOMPtr<nsIURI> docBaseURI = mContent ? mContent->GetBaseURI() : nullptr;
57 if (!mContent || !doc || !docBaseURI) {
58 MOZ_ASSERT(false,
59 "Should not be parsing SourceSet without a content and document");
60 return false;
63 // Preserve the default source if we have one, it has a separate setter.
64 uint32_t prevNumCandidates = mCandidates.Length();
65 nsCOMPtr<nsIURI> defaultURL;
66 if (prevNumCandidates && (mCandidates[prevNumCandidates - 1].Type() ==
67 ResponsiveImageCandidate::eCandidateType_Default)) {
68 defaultURL = mCandidates[prevNumCandidates - 1].URL();
71 mCandidates.Clear();
73 nsAString::const_iterator iter, end;
74 aSrcSet.BeginReading(iter);
75 aSrcSet.EndReading(end);
77 // Read URL / descriptor pairs
78 while (iter != end) {
79 nsAString::const_iterator url, desc;
81 // Skip whitespace
82 for (; iter != end && nsContentUtils::IsHTMLWhitespace(*iter); ++iter);
84 if (iter == end) {
85 break;
88 url = iter;
90 // Find end of url
91 for (;iter != end && !nsContentUtils::IsHTMLWhitespace(*iter); ++iter);
93 desc = iter;
95 // Find end of descriptor
96 for (; iter != end && *iter != char16_t(','); ++iter);
97 const nsDependentSubstring &descriptor = Substring(desc, iter);
99 nsresult rv;
100 nsCOMPtr<nsIURI> candidateURL;
101 rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(candidateURL),
102 Substring(url, desc),
103 doc,
104 docBaseURI);
105 if (NS_SUCCEEDED(rv) && candidateURL) {
106 NS_TryToSetImmutable(candidateURL);
107 ResponsiveImageCandidate candidate;
108 if (candidate.SetParamaterFromDescriptor(descriptor)) {
109 candidate.SetURL(candidateURL);
110 AppendCandidateIfUnique(candidate);
114 // Advance past comma
115 if (iter != end) {
116 ++iter;
120 bool parsedCandidates = mCandidates.Length() > 0;
122 // Re-add default to end of list
123 if (defaultURL) {
124 AppendDefaultCandidate(defaultURL);
127 return parsedCandidates;
130 nsresult
131 ResponsiveImageSelector::SetDefaultSource(const nsAString & aSpec)
133 nsIDocument* doc = mContent ? mContent->OwnerDoc() : nullptr;
134 nsCOMPtr<nsIURI> docBaseURI = mContent ? mContent->GetBaseURI() : nullptr;
136 if (!mContent || !doc || !docBaseURI) {
137 MOZ_ASSERT(false,
138 "Should not be calling this without a content and document");
139 return NS_ERROR_UNEXPECTED;
142 if (aSpec.IsEmpty()) {
143 SetDefaultSource(nullptr);
144 return NS_OK;
147 nsresult rv;
148 nsCOMPtr<nsIURI> candidateURL;
149 rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(candidateURL),
150 aSpec, doc, docBaseURI);
151 NS_ENSURE_SUCCESS(rv, rv);
153 SetDefaultSource(candidateURL);
154 return NS_OK;
157 uint32_t
158 ResponsiveImageSelector::NumCandidates(bool aIncludeDefault)
160 uint32_t candidates = mCandidates.Length();
162 // If present, the default candidate is the last item
163 if (!aIncludeDefault && candidates &&
164 (mCandidates[candidates - 1].Type() ==
165 ResponsiveImageCandidate::eCandidateType_Default)) {
166 candidates--;
169 return candidates;
172 void
173 ResponsiveImageSelector::SetDefaultSource(nsIURI *aURL)
175 // Check if the last element of our candidates is a default
176 int32_t candidates = mCandidates.Length();
177 if (candidates && (mCandidates[candidates - 1].Type() ==
178 ResponsiveImageCandidate::eCandidateType_Default)) {
179 mCandidates.RemoveElementAt(candidates - 1);
180 if (mBestCandidateIndex == candidates - 1) {
181 mBestCandidateIndex = -1;
185 // Add new default if set
186 if (aURL) {
187 AppendDefaultCandidate(aURL);
191 bool
192 ResponsiveImageSelector::SetSizesFromDescriptor(const nsAString & aSizes)
194 mSizeQueries.Clear();
195 mSizeValues.Clear();
196 mBestCandidateIndex = -1;
198 nsCSSParser cssParser;
200 if (!cssParser.ParseSourceSizeList(aSizes, nullptr, 0,
201 mSizeQueries, mSizeValues, true)) {
202 return false;
205 return mSizeQueries.Length() > 0;
208 void
209 ResponsiveImageSelector::AppendCandidateIfUnique(const ResponsiveImageCandidate & aCandidate)
211 int numCandidates = mCandidates.Length();
213 // With the exception of Default, which should not be added until we are done
214 // building the list, the spec forbids mixing width and explicit density
215 // selectors in the same set.
216 if (numCandidates && mCandidates[0].Type() != aCandidate.Type()) {
217 return;
220 // Discard candidates with identical parameters, they will never match
221 for (int i = 0; i < numCandidates; i++) {
222 if (mCandidates[i].HasSameParameter(aCandidate)) {
223 return;
227 mBestCandidateIndex = -1;
228 mCandidates.AppendElement(aCandidate);
231 void
232 ResponsiveImageSelector::AppendDefaultCandidate(nsIURI *aURL)
234 NS_ENSURE_TRUE(aURL, /* void */);
236 ResponsiveImageCandidate defaultCandidate;
237 defaultCandidate.SetParameterDefault();
238 defaultCandidate.SetURL(aURL);
239 // We don't use MaybeAppend since we want to keep this even if it can never
240 // match, as it may if the source set changes.
241 mBestCandidateIndex = -1;
242 mCandidates.AppendElement(defaultCandidate);
245 already_AddRefed<nsIURI>
246 ResponsiveImageSelector::GetSelectedImageURL()
248 int bestIndex = GetBestCandidateIndex();
249 if (bestIndex < 0) {
250 return nullptr;
253 nsCOMPtr<nsIURI> bestURL = mCandidates[bestIndex].URL();
254 MOZ_ASSERT(bestURL, "Shouldn't have candidates with no URL in the array");
255 return bestURL.forget();
258 double
259 ResponsiveImageSelector::GetSelectedImageDensity()
261 int bestIndex = GetBestCandidateIndex();
262 if (bestIndex < 0) {
263 return 1.0;
266 return mCandidates[bestIndex].Density(this);
269 bool
270 ResponsiveImageSelector::SelectImage(bool aReselect)
272 if (!aReselect && mBestCandidateIndex != -1) {
273 // Already have selection
274 return false;
277 int oldBest = mBestCandidateIndex;
278 mBestCandidateIndex = -1;
279 return GetBestCandidateIndex() != oldBest;
283 ResponsiveImageSelector::GetBestCandidateIndex()
285 if (mBestCandidateIndex != -1) {
286 return mBestCandidateIndex;
289 int numCandidates = mCandidates.Length();
290 if (!numCandidates) {
291 return -1;
294 nsIDocument* doc = mContent ? mContent->OwnerDoc() : nullptr;
295 nsIPresShell *shell = doc ? doc->GetShell() : nullptr;
296 nsPresContext *pctx = shell ? shell->GetPresContext() : nullptr;
298 if (!pctx) {
299 MOZ_ASSERT(false, "Unable to find document prescontext");
300 return -1;
303 double displayDensity = pctx->CSSPixelsToDevPixels(1.0f);
305 // Per spec, "In a UA-specific manner, choose one image source"
306 // - For now, select the lowest density greater than displayDensity, otherwise
307 // the greatest density available
309 // If the list contains computed width candidates, compute the current
310 // effective image width. Note that we currently disallow both computed and
311 // static density candidates in the same selector, so checking the first
312 // candidate is sufficient.
313 int32_t computedWidth = -1;
314 if (numCandidates && mCandidates[0].IsComputedFromWidth()) {
315 DebugOnly<bool> computeResult = \
316 ComputeFinalWidthForCurrentViewport(&computedWidth);
317 MOZ_ASSERT(computeResult,
318 "Computed candidates not allowed without sizes data");
320 // If we have a default candidate in the list, don't consider it when using
321 // computed widths. (It has a static 1.0 density that is inapplicable to a
322 // sized-image)
323 if (numCandidates > 1 && mCandidates[numCandidates - 1].Type() ==
324 ResponsiveImageCandidate::eCandidateType_Default) {
325 numCandidates--;
329 int bestIndex = -1;
330 double bestDensity = -1.0;
331 for (int i = 0; i < numCandidates; i++) {
332 double candidateDensity = \
333 (computedWidth == -1) ? mCandidates[i].Density(this)
334 : mCandidates[i].Density(computedWidth);
335 // - If bestIndex is below display density, pick anything larger.
336 // - Otherwise, prefer if less dense than bestDensity but still above
337 // displayDensity.
338 if (bestIndex == -1 ||
339 (bestDensity < displayDensity && candidateDensity > bestDensity) ||
340 (candidateDensity >= displayDensity && candidateDensity < bestDensity)) {
341 bestIndex = i;
342 bestDensity = candidateDensity;
346 MOZ_ASSERT(bestIndex >= 0 && bestIndex < numCandidates);
347 mBestCandidateIndex = bestIndex;
348 return bestIndex;
351 bool
352 ResponsiveImageSelector::ComputeFinalWidthForCurrentViewport(int32_t *aWidth)
354 unsigned int numSizes = mSizeQueries.Length();
355 nsIDocument* doc = mContent ? mContent->OwnerDoc() : nullptr;
356 nsIPresShell *presShell = doc ? doc->GetShell() : nullptr;
357 nsPresContext *pctx = presShell ? presShell->GetPresContext() : nullptr;
359 if (!pctx) {
360 MOZ_ASSERT(false, "Unable to find presContext for this content");
361 return false;
364 MOZ_ASSERT(numSizes == mSizeValues.Length(),
365 "mSizeValues length differs from mSizeQueries");
367 unsigned int i;
368 for (i = 0; i < numSizes; i++) {
369 if (mSizeQueries[i]->Matches(pctx, nullptr)) {
370 break;
374 nscoord effectiveWidth;
375 if (i == numSizes) {
376 // No match defaults to 100% viewport
377 nsCSSValue defaultWidth(100.0f, eCSSUnit_ViewportWidth);
378 effectiveWidth = nsRuleNode::CalcLengthWithInitialFont(pctx,
379 defaultWidth);
380 } else {
381 effectiveWidth = nsRuleNode::CalcLengthWithInitialFont(pctx,
382 mSizeValues[i]);
385 MOZ_ASSERT(effectiveWidth >= 0);
386 *aWidth = nsPresContext::AppUnitsToIntCSSPixels(std::max(effectiveWidth, 0));
387 return true;
390 ResponsiveImageCandidate::ResponsiveImageCandidate()
392 mType = eCandidateType_Invalid;
393 mValue.mDensity = 1.0;
396 ResponsiveImageCandidate::ResponsiveImageCandidate(nsIURI *aURL,
397 double aDensity)
398 : mURL(aURL)
400 mType = eCandidateType_Density;
401 mValue.mDensity = aDensity;
405 void
406 ResponsiveImageCandidate::SetURL(nsIURI *aURL)
408 mURL = aURL;
411 void
412 ResponsiveImageCandidate::SetParameterAsComputedWidth(int32_t aWidth)
414 mType = eCandidateType_ComputedFromWidth;
415 mValue.mWidth = aWidth;
418 void
419 ResponsiveImageCandidate::SetParameterDefault()
421 MOZ_ASSERT(mType == eCandidateType_Invalid, "double setting candidate type");
423 mType = eCandidateType_Default;
424 // mValue shouldn't actually be used for this type, but set it to default
425 // anyway
426 mValue.mDensity = 1.0;
429 void
430 ResponsiveImageCandidate::SetParameterAsDensity(double aDensity)
432 MOZ_ASSERT(mType == eCandidateType_Invalid, "double setting candidate type");
434 mType = eCandidateType_Density;
435 mValue.mDensity = aDensity;
438 bool
439 ResponsiveImageCandidate::SetParamaterFromDescriptor(const nsAString & aDescriptor)
441 // Valid input values must be positive, using -1 for not-set
442 double density = -1.0;
443 int32_t width = -1;
445 nsAString::const_iterator iter, end;
446 aDescriptor.BeginReading(iter);
447 aDescriptor.EndReading(end);
449 // Parse descriptor list
450 // We currently only support a single density descriptor of the form:
451 // <floating-point number>x
452 // Silently ignore other descriptors in the list for forward-compat
453 while (iter != end) {
454 // Skip initial whitespace
455 for (; iter != end && nsContentUtils::IsHTMLWhitespace(*iter); ++iter);
456 if (iter == end) {
457 break;
460 // Find end of type
461 nsAString::const_iterator start = iter;
462 for (; iter != end && !nsContentUtils::IsHTMLWhitespace(*iter); ++iter);
464 if (start == iter) {
465 // Empty descriptor
466 break;
469 // Iter is at end of descriptor, type is single character previous to that.
470 // Safe because we just verified that iter > start
471 --iter;
472 nsAString::const_iterator type(iter);
473 ++iter;
475 const nsDependentSubstring& valStr = Substring(start, type);
476 if (*type == char16_t('w')) {
477 int32_t possibleWidth;
478 if (width == -1 && density == -1.0) {
479 if (ParseInteger(valStr, possibleWidth) && possibleWidth > 0) {
480 width = possibleWidth;
482 } else {
483 return false;
485 } else if (*type == char16_t('x')) {
486 if (width == -1 && density == -1.0) {
487 nsresult rv;
488 double possibleDensity = PromiseFlatString(valStr).ToDouble(&rv);
489 if (NS_SUCCEEDED(rv) && possibleDensity > 0.0) {
490 density = possibleDensity;
492 } else {
493 return false;
498 if (width != -1) {
499 SetParameterAsComputedWidth(width);
500 } else if (density != -1.0) {
501 SetParameterAsDensity(density);
502 } else {
503 // No valid descriptors -> 1.0 density
504 SetParameterAsDensity(1.0);
507 return true;
510 bool
511 ResponsiveImageCandidate::HasSameParameter(const ResponsiveImageCandidate & aOther) const
513 if (aOther.mType != mType) {
514 return false;
517 if (mType == eCandidateType_Default) {
518 return true;
521 if (mType == eCandidateType_Density) {
522 return aOther.mValue.mDensity == mValue.mDensity;
525 if (mType == eCandidateType_Invalid) {
526 MOZ_ASSERT(false, "Comparing invalid candidates?");
527 return true;
528 } else if (mType == eCandidateType_ComputedFromWidth) {
529 return aOther.mValue.mWidth == mValue.mWidth;
532 MOZ_ASSERT(false, "Somebody forgot to check for all uses of this enum");
533 return false;
536 already_AddRefed<nsIURI>
537 ResponsiveImageCandidate::URL() const
539 nsCOMPtr<nsIURI> url = mURL;
540 return url.forget();
543 double
544 ResponsiveImageCandidate::Density(ResponsiveImageSelector *aSelector) const
546 if (mType == eCandidateType_ComputedFromWidth) {
547 int32_t width;
548 if (!aSelector->ComputeFinalWidthForCurrentViewport(&width)) {
549 return 1.0;
551 return Density(width);
554 // Other types don't need matching width
555 MOZ_ASSERT(mType == eCandidateType_Default || mType == eCandidateType_Density,
556 "unhandled candidate type");
557 return Density(-1);
560 double
561 ResponsiveImageCandidate::Density(int32_t aMatchingWidth) const
563 if (mType == eCandidateType_Invalid) {
564 MOZ_ASSERT(false, "Getting density for uninitialized candidate");
565 return 1.0;
568 if (mType == eCandidateType_Default) {
569 return 1.0;
572 if (mType == eCandidateType_Density) {
573 return mValue.mDensity;
574 } else if (mType == eCandidateType_ComputedFromWidth) {
575 if (aMatchingWidth <= 0) {
576 MOZ_ASSERT(false, "0 or negative matching width is invalid per spec");
577 return 1.0;
579 double density = double(mValue.mWidth) / double(aMatchingWidth);
580 MOZ_ASSERT(density > 0.0);
581 return density;
584 MOZ_ASSERT(false, "Unknown candidate type");
585 return 1.0;
588 bool
589 ResponsiveImageCandidate::IsComputedFromWidth() const
591 if (mType == eCandidateType_ComputedFromWidth) {
592 return true;
595 MOZ_ASSERT(mType == eCandidateType_Default || mType == eCandidateType_Density,
596 "Unknown candidate type");
597 return false;
600 } // namespace dom
601 } // namespace mozilla