diff options
Diffstat (limited to 'gfx/layers/apz/util/DoubleTapToZoom.cpp')
-rw-r--r-- | gfx/layers/apz/util/DoubleTapToZoom.cpp | 178 |
1 files changed, 178 insertions, 0 deletions
diff --git a/gfx/layers/apz/util/DoubleTapToZoom.cpp b/gfx/layers/apz/util/DoubleTapToZoom.cpp new file mode 100644 index 000000000..62dd5feaa --- /dev/null +++ b/gfx/layers/apz/util/DoubleTapToZoom.cpp @@ -0,0 +1,178 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "DoubleTapToZoom.h" + +#include <algorithm> // for std::min, std::max + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/dom/Element.h" +#include "nsCOMPtr.h" +#include "nsIContent.h" +#include "nsIDocument.h" +#include "nsIDOMHTMLLIElement.h" +#include "nsIDOMHTMLQuoteElement.h" +#include "nsIDOMWindow.h" +#include "nsIFrame.h" +#include "nsIFrameInlines.h" +#include "nsIPresShell.h" +#include "nsLayoutUtils.h" +#include "nsStyleConsts.h" + +namespace mozilla { +namespace layers { + +// Returns the DOM element found at |aPoint|, interpreted as being relative to +// the root frame of |aShell|. If the point is inside a subdocument, returns +// an element inside the subdocument, rather than the subdocument element +// (and does so recursively). +// The implementation was adapted from nsDocument::ElementFromPoint(), with +// the notable exception that we don't pass nsLayoutUtils::IGNORE_CROSS_DOC +// to GetFrameForPoint(), so as to get the behaviour described above in the +// presence of subdocuments. +static already_AddRefed<dom::Element> +ElementFromPoint(const nsCOMPtr<nsIPresShell>& aShell, + const CSSPoint& aPoint) +{ + if (nsIFrame* rootFrame = aShell->GetRootFrame()) { + if (nsIFrame* frame = nsLayoutUtils::GetFrameForPoint(rootFrame, + CSSPoint::ToAppUnits(aPoint), + nsLayoutUtils::IGNORE_PAINT_SUPPRESSION | + nsLayoutUtils::IGNORE_ROOT_SCROLL_FRAME)) { + while (frame && (!frame->GetContent() || frame->GetContent()->IsInAnonymousSubtree())) { + frame = nsLayoutUtils::GetParentOrPlaceholderFor(frame); + } + nsIContent* content = frame->GetContent(); + if (content && !content->IsElement()) { + content = content->GetParent(); + } + if (content) { + nsCOMPtr<dom::Element> result = content->AsElement(); + return result.forget(); + } + } + } + return nullptr; +} + +static bool +ShouldZoomToElement(const nsCOMPtr<dom::Element>& aElement) { + if (nsIFrame* frame = aElement->GetPrimaryFrame()) { + if (frame->GetDisplay() == StyleDisplay::Inline) { + return false; + } + } + if (aElement->IsAnyOfHTMLElements(nsGkAtoms::li, nsGkAtoms::q)) { + return false; + } + return true; +} + +static bool +IsRectZoomedIn(const CSSRect& aRect, const CSSRect& aCompositedArea) +{ + // This functions checks to see if the area of the rect visible in the + // composition bounds (i.e. the overlapArea variable below) is approximately + // the max area of the rect we can show. + CSSRect overlap = aCompositedArea.Intersect(aRect); + float overlapArea = overlap.width * overlap.height; + float availHeight = std::min(aRect.width * aCompositedArea.height / aCompositedArea.width, + aRect.height); + float showing = overlapArea / (aRect.width * availHeight); + float ratioW = aRect.width / aCompositedArea.width; + float ratioH = aRect.height / aCompositedArea.height; + + return showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9); +} + +CSSRect +CalculateRectToZoomTo(const nsCOMPtr<nsIDocument>& aRootContentDocument, + const CSSPoint& aPoint) +{ + // Ensure the layout information we get is up-to-date. + aRootContentDocument->FlushPendingNotifications(Flush_Layout); + + // An empty rect as return value is interpreted as "zoom out". + const CSSRect zoomOut; + + nsCOMPtr<nsIPresShell> shell = aRootContentDocument->GetShell(); + if (!shell) { + return zoomOut; + } + + nsIScrollableFrame* rootScrollFrame = shell->GetRootScrollFrameAsScrollable(); + if (!rootScrollFrame) { + return zoomOut; + } + + nsCOMPtr<dom::Element> element = ElementFromPoint(shell, aPoint); + if (!element) { + return zoomOut; + } + + while (element && !ShouldZoomToElement(element)) { + element = element->GetParentElement(); + } + + if (!element) { + return zoomOut; + } + + FrameMetrics metrics = nsLayoutUtils::CalculateBasicFrameMetrics(rootScrollFrame); + CSSRect compositedArea(metrics.GetScrollOffset(), metrics.CalculateCompositedSizeInCssPixels()); + const CSSCoord margin = 15; + CSSRect rect = nsLayoutUtils::GetBoundingContentRect(element, rootScrollFrame); + + // If the element is taller than the visible area of the page scale + // the height of the |rect| so that it has the same aspect ratio as + // the root frame. The clipped |rect| is centered on the y value of + // the touch point. This allows tall narrow elements to be zoomed. + if (!rect.IsEmpty() && compositedArea.width > 0.0f) { + const float widthRatio = rect.width / compositedArea.width; + float targetHeight = compositedArea.height * widthRatio; + if (widthRatio < 0.9 && targetHeight < rect.height) { + const CSSPoint scrollPoint = CSSPoint::FromAppUnits(rootScrollFrame->GetScrollPosition()); + float newY = aPoint.y + scrollPoint.y - (targetHeight * 0.5f); + if ((newY + targetHeight) > (rect.y + rect.height)) { + rect.y += rect.height - targetHeight; + } else if (newY > rect.y) { + rect.y = newY; + } + rect.height = targetHeight; + } + } + + rect = CSSRect(std::max(metrics.GetScrollableRect().x, rect.x - margin), + rect.y, + rect.width + 2 * margin, + rect.height); + // Constrict the rect to the screen's right edge + rect.width = std::min(rect.width, metrics.GetScrollableRect().XMost() - rect.x); + + // If the rect is already taking up most of the visible area and is + // stretching the width of the page, then we want to zoom out instead. + if (IsRectZoomedIn(rect, compositedArea)) { + return zoomOut; + } + + CSSRect rounded(rect); + rounded.Round(); + + // If the block we're zooming to is really tall, and the user double-tapped + // more than a screenful of height from the top of it, then adjust the + // y-coordinate so that we center the actual point the user double-tapped + // upon. This prevents flying to the top of the page when double-tapping + // to zoom in (bug 761721). The 1.2 multiplier is just a little fuzz to + // compensate for 'rect' including horizontal margins but not vertical ones. + CSSCoord cssTapY = metrics.GetScrollOffset().y + aPoint.y; + if ((rect.height > rounded.height) && (cssTapY > rounded.y + (rounded.height * 1.2))) { + rounded.y = cssTapY - (rounded.height / 2); + } + + return rounded; +} + +} +} |