michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.gfx; michael@0: michael@0: import org.mozilla.gecko.GeckoAppShell; michael@0: import org.mozilla.gecko.GeckoEvent; michael@0: import org.mozilla.gecko.gfx.LayerView.DrawListener; michael@0: import org.mozilla.gecko.Tab; michael@0: import org.mozilla.gecko.Tabs; michael@0: import org.mozilla.gecko.ZoomConstraints; michael@0: import org.mozilla.gecko.mozglue.RobocopTarget; michael@0: import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI; michael@0: import org.mozilla.gecko.EventDispatcher; michael@0: import org.mozilla.gecko.util.FloatUtils; michael@0: michael@0: import android.content.Context; michael@0: import android.graphics.PointF; michael@0: import android.graphics.RectF; michael@0: import android.os.SystemClock; michael@0: import android.util.DisplayMetrics; michael@0: import android.util.Log; michael@0: michael@0: import java.util.ArrayList; michael@0: import java.util.List; michael@0: michael@0: class GeckoLayerClient implements LayerView.Listener, PanZoomTarget michael@0: { michael@0: private static final String LOGTAG = "GeckoLayerClient"; michael@0: michael@0: private LayerRenderer mLayerRenderer; michael@0: private boolean mLayerRendererInitialized; michael@0: michael@0: private Context mContext; michael@0: private IntSize mScreenSize; michael@0: private IntSize mWindowSize; michael@0: private DisplayPortMetrics mDisplayPort; michael@0: michael@0: private boolean mRecordDrawTimes; michael@0: private final DrawTimingQueue mDrawTimingQueue; michael@0: michael@0: private VirtualLayer mRootLayer; michael@0: michael@0: /* The Gecko viewport as per the UI thread. Must be touched only on the UI thread. michael@0: * If any events being sent to Gecko that are relative to the Gecko viewport position, michael@0: * they must (a) be relative to this viewport, and (b) be sent on the UI thread to michael@0: * avoid races. As long as these two conditions are satisfied, and the events being michael@0: * sent to Gecko are processed in FIFO order, the events will properly be relative michael@0: * to the Gecko viewport position. Note that if Gecko updates its viewport independently, michael@0: * we get notified synchronously and also update this on the UI thread. michael@0: */ michael@0: private ImmutableViewportMetrics mGeckoViewport; michael@0: michael@0: /* michael@0: * The viewport metrics being used to draw the current frame. This is only michael@0: * accessed by the compositor thread, and so needs no synchronisation. michael@0: */ michael@0: private ImmutableViewportMetrics mFrameMetrics; michael@0: michael@0: private List mDrawListeners; michael@0: michael@0: /* Used as temporaries by syncViewportInfo */ michael@0: private final ViewTransform mCurrentViewTransform; michael@0: private final RectF mCurrentViewTransformMargins; michael@0: michael@0: /* Used as the return value of progressiveUpdateCallback */ michael@0: private final ProgressiveUpdateData mProgressiveUpdateData; michael@0: private DisplayPortMetrics mProgressiveUpdateDisplayPort; michael@0: private boolean mLastProgressiveUpdateWasLowPrecision; michael@0: private boolean mProgressiveUpdateWasInDanger; michael@0: michael@0: private boolean mForceRedraw; michael@0: michael@0: /* The current viewport metrics. michael@0: * This is volatile so that we can read and write to it from different threads. michael@0: * We avoid synchronization to make getting the viewport metrics from michael@0: * the compositor as cheap as possible. The viewport is immutable so michael@0: * we don't need to worry about anyone mutating it while we're reading from it. michael@0: * Specifically: michael@0: * 1) reading mViewportMetrics from any thread is fine without synchronization michael@0: * 2) writing to mViewportMetrics requires synchronizing on the layer controller object michael@0: * 3) whenver reading multiple fields from mViewportMetrics without synchronization (i.e. in michael@0: * case 1 above) you should always frist grab a local copy of the reference, and then use michael@0: * that because mViewportMetrics might get reassigned in between reading the different michael@0: * fields. */ michael@0: private volatile ImmutableViewportMetrics mViewportMetrics; michael@0: private LayerView.OnMetricsChangedListener mViewportChangeListener; michael@0: michael@0: private ZoomConstraints mZoomConstraints; michael@0: michael@0: private boolean mGeckoIsReady; michael@0: michael@0: private final PanZoomController mPanZoomController; michael@0: private final LayerMarginsAnimator mMarginsAnimator; michael@0: private LayerView mView; michael@0: michael@0: /* This flag is true from the time that browser.js detects a first-paint is about to start, michael@0: * to the time that we receive the first-paint composite notification from the compositor. michael@0: * Note that there is a small race condition with this; if there are two paints that both michael@0: * have the first-paint flag set, and the second paint happens concurrently with the michael@0: * composite for the first paint, then this flag may be set to true prematurely. Fixing this michael@0: * is possible but risky; see https://bugzilla.mozilla.org/show_bug.cgi?id=797615#c751 michael@0: */ michael@0: private volatile boolean mContentDocumentIsDisplayed; michael@0: michael@0: public GeckoLayerClient(Context context, LayerView view, EventDispatcher eventDispatcher) { michael@0: // we can fill these in with dummy values because they are always written michael@0: // to before being read michael@0: mContext = context; michael@0: mScreenSize = new IntSize(0, 0); michael@0: mWindowSize = new IntSize(0, 0); michael@0: mDisplayPort = new DisplayPortMetrics(); michael@0: mRecordDrawTimes = true; michael@0: mDrawTimingQueue = new DrawTimingQueue(); michael@0: mCurrentViewTransform = new ViewTransform(0, 0, 1); michael@0: mCurrentViewTransformMargins = new RectF(); michael@0: mProgressiveUpdateData = new ProgressiveUpdateData(); michael@0: mProgressiveUpdateDisplayPort = new DisplayPortMetrics(); michael@0: mLastProgressiveUpdateWasLowPrecision = false; michael@0: mProgressiveUpdateWasInDanger = false; michael@0: michael@0: mForceRedraw = true; michael@0: DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); michael@0: mViewportMetrics = new ImmutableViewportMetrics(displayMetrics) michael@0: .setViewportSize(view.getWidth(), view.getHeight()); michael@0: mZoomConstraints = new ZoomConstraints(false); michael@0: michael@0: Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: if (tab != null) { michael@0: mZoomConstraints = tab.getZoomConstraints(); michael@0: mViewportMetrics = mViewportMetrics.setIsRTL(tab.getIsRTL()); michael@0: } michael@0: michael@0: mFrameMetrics = mViewportMetrics; michael@0: michael@0: mDrawListeners = new ArrayList(); michael@0: mPanZoomController = PanZoomController.Factory.create(this, view, eventDispatcher); michael@0: mMarginsAnimator = new LayerMarginsAnimator(this, view); michael@0: mView = view; michael@0: mView.setListener(this); michael@0: mContentDocumentIsDisplayed = true; michael@0: } michael@0: michael@0: public void setOverscrollHandler(final Overscroll listener) { michael@0: mPanZoomController.setOverscrollHandler(listener); michael@0: } michael@0: michael@0: /** Attaches to root layer so that Gecko appears. */ michael@0: public void notifyGeckoReady() { michael@0: mGeckoIsReady = true; michael@0: michael@0: mRootLayer = new VirtualLayer(new IntSize(mView.getWidth(), mView.getHeight())); michael@0: mLayerRenderer = mView.getRenderer(); michael@0: michael@0: sendResizeEventIfNecessary(true); michael@0: michael@0: DisplayPortCalculator.initPrefs(); michael@0: michael@0: // Gecko being ready is one of the two conditions (along with having an available michael@0: // surface) that cause us to create the compositor. So here, now that we know gecko michael@0: // is ready, call updateCompositor() to see if we can actually do the creation. michael@0: // This needs to run on the UI thread so that the surface validity can't change on michael@0: // us while we're in the middle of creating the compositor. michael@0: mView.post(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mView.getGLController().updateCompositor(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: public void destroy() { michael@0: mPanZoomController.destroy(); michael@0: mMarginsAnimator.destroy(); michael@0: mDrawListeners.clear(); michael@0: } michael@0: michael@0: /** michael@0: * Returns true if this client is fine with performing a redraw operation or false if it michael@0: * would prefer that the action didn't take place. michael@0: */ michael@0: private boolean getRedrawHint() { michael@0: if (mForceRedraw) { michael@0: mForceRedraw = false; michael@0: return true; michael@0: } michael@0: michael@0: if (!mPanZoomController.getRedrawHint()) { michael@0: return false; michael@0: } michael@0: michael@0: return DisplayPortCalculator.aboutToCheckerboard(mViewportMetrics, michael@0: mPanZoomController.getVelocityVector(), mDisplayPort); michael@0: } michael@0: michael@0: Layer getRoot() { michael@0: return mGeckoIsReady ? mRootLayer : null; michael@0: } michael@0: michael@0: public LayerView getView() { michael@0: return mView; michael@0: } michael@0: michael@0: public FloatSize getViewportSize() { michael@0: return mViewportMetrics.getSize(); michael@0: } michael@0: michael@0: /** michael@0: * The view calls this function to indicate that the viewport changed size. It must hold the michael@0: * monitor while calling it. michael@0: * michael@0: * TODO: Refactor this to use an interface. Expose that interface only to the view and not michael@0: * to the layer client. That way, the layer client won't be tempted to call this, which might michael@0: * result in an infinite loop. michael@0: */ michael@0: void setViewportSize(int width, int height) { michael@0: mViewportMetrics = mViewportMetrics.setViewportSize(width, height); michael@0: michael@0: if (mGeckoIsReady) { michael@0: // here we send gecko a resize message. The code in browser.js is responsible for michael@0: // picking up on that resize event, modifying the viewport as necessary, and informing michael@0: // us of the new viewport. michael@0: sendResizeEventIfNecessary(true); michael@0: // the following call also sends gecko a message, which will be processed after the resize michael@0: // message above has updated the viewport. this message ensures that if we have just put michael@0: // focus in a text field, we scroll the content so that the text field is in view. michael@0: GeckoAppShell.viewSizeChanged(); michael@0: } michael@0: } michael@0: michael@0: PanZoomController getPanZoomController() { michael@0: return mPanZoomController; michael@0: } michael@0: michael@0: LayerMarginsAnimator getLayerMarginsAnimator() { michael@0: return mMarginsAnimator; michael@0: } michael@0: michael@0: /* Informs Gecko that the screen size has changed. */ michael@0: private void sendResizeEventIfNecessary(boolean force) { michael@0: DisplayMetrics metrics = mContext.getResources().getDisplayMetrics(); michael@0: michael@0: IntSize newScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels); michael@0: IntSize newWindowSize = new IntSize(mView.getWidth(), mView.getHeight()); michael@0: michael@0: boolean screenSizeChanged = !mScreenSize.equals(newScreenSize); michael@0: boolean windowSizeChanged = !mWindowSize.equals(newWindowSize); michael@0: michael@0: if (!force && !screenSizeChanged && !windowSizeChanged) { michael@0: return; michael@0: } michael@0: michael@0: mScreenSize = newScreenSize; michael@0: mWindowSize = newWindowSize; michael@0: michael@0: if (screenSizeChanged) { michael@0: Log.d(LOGTAG, "Screen-size changed to " + mScreenSize); michael@0: } michael@0: michael@0: if (windowSizeChanged) { michael@0: Log.d(LOGTAG, "Window-size changed to " + mWindowSize); michael@0: } michael@0: michael@0: GeckoEvent event = GeckoEvent.createSizeChangedEvent(mWindowSize.width, mWindowSize.height, michael@0: mScreenSize.width, mScreenSize.height); michael@0: GeckoAppShell.sendEventToGecko(event); michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Window:Resize", "")); michael@0: } michael@0: michael@0: /** Sets the current page rect. You must hold the monitor while calling this. */ michael@0: private void setPageRect(RectF rect, RectF cssRect) { michael@0: // Since the "rect" is always just a multiple of "cssRect" we don't need to michael@0: // check both; this function assumes that both "rect" and "cssRect" are relative michael@0: // the zoom factor in mViewportMetrics. michael@0: if (mViewportMetrics.getCssPageRect().equals(cssRect)) michael@0: return; michael@0: michael@0: mViewportMetrics = mViewportMetrics.setPageRect(rect, cssRect); michael@0: michael@0: // Page size is owned by the layer client, so no need to notify it of michael@0: // this change. michael@0: michael@0: post(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mPanZoomController.pageRectUpdated(); michael@0: mView.requestRender(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Derives content document fixed position margins/fixed layer margins from michael@0: * the view margins in the given metrics object. michael@0: */ michael@0: private void getFixedMargins(ImmutableViewportMetrics metrics, RectF fixedMargins) { michael@0: fixedMargins.left = 0; michael@0: fixedMargins.top = 0; michael@0: fixedMargins.right = 0; michael@0: fixedMargins.bottom = 0; michael@0: michael@0: // The maximum margins are determined by the scrollable area of the page. michael@0: float maxMarginWidth = Math.max(0, metrics.getPageWidth() - metrics.getWidthWithoutMargins()); michael@0: float maxMarginHeight = Math.max(0, metrics.getPageHeight() - metrics.getHeightWithoutMargins()); michael@0: michael@0: // If the margins can't fully hide, they're pinned on - in which case, michael@0: // fixed margins should always be zero. michael@0: if (maxMarginWidth < metrics.marginLeft + metrics.marginRight) { michael@0: maxMarginWidth = 0; michael@0: } michael@0: if (maxMarginHeight < metrics.marginTop + metrics.marginBottom) { michael@0: maxMarginHeight = 0; michael@0: } michael@0: michael@0: PointF offset = metrics.getMarginOffset(); michael@0: RectF overscroll = metrics.getOverscroll(); michael@0: if (offset.x >= 0) { michael@0: fixedMargins.right = Math.max(0, Math.min(offset.x - overscroll.right, maxMarginWidth)); michael@0: } else { michael@0: fixedMargins.left = Math.max(0, Math.min(-offset.x - overscroll.left, maxMarginWidth)); michael@0: } michael@0: if (offset.y >= 0) { michael@0: fixedMargins.bottom = Math.max(0, Math.min(offset.y - overscroll.bottom, maxMarginHeight)); michael@0: } else { michael@0: fixedMargins.top = Math.max(0, Math.min(-offset.y - overscroll.top, maxMarginHeight)); michael@0: } michael@0: michael@0: // Adjust for overscroll. If we're overscrolled on one side, add that michael@0: // distance to the margins of the other side (limiting to the maximum michael@0: // margin size calculated above). michael@0: if (overscroll.left > 0) { michael@0: fixedMargins.right = Math.min(maxMarginWidth - fixedMargins.left, michael@0: fixedMargins.right + overscroll.left); michael@0: } else if (overscroll.right > 0) { michael@0: fixedMargins.left = Math.min(maxMarginWidth - fixedMargins.right, michael@0: fixedMargins.left + overscroll.right); michael@0: } michael@0: if (overscroll.top > 0) { michael@0: fixedMargins.bottom = Math.min(maxMarginHeight - fixedMargins.top, michael@0: fixedMargins.bottom + overscroll.top); michael@0: } else if (overscroll.bottom > 0) { michael@0: fixedMargins.top = Math.min(maxMarginHeight - fixedMargins.bottom, michael@0: fixedMargins.top + overscroll.bottom); michael@0: } michael@0: } michael@0: michael@0: private void adjustViewport(DisplayPortMetrics displayPort) { michael@0: ImmutableViewportMetrics metrics = getViewportMetrics(); michael@0: ImmutableViewportMetrics clampedMetrics = metrics.clamp(); michael@0: michael@0: RectF margins = new RectF(); michael@0: getFixedMargins(metrics, margins); michael@0: clampedMetrics = clampedMetrics.setMargins( michael@0: margins.left, margins.top, margins.right, margins.bottom); michael@0: michael@0: if (displayPort == null) { michael@0: displayPort = DisplayPortCalculator.calculate(metrics, mPanZoomController.getVelocityVector()); michael@0: } michael@0: michael@0: mDisplayPort = displayPort; michael@0: mGeckoViewport = clampedMetrics; michael@0: michael@0: if (mRecordDrawTimes) { michael@0: mDrawTimingQueue.add(displayPort); michael@0: } michael@0: michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createViewportEvent(clampedMetrics, displayPort)); michael@0: } michael@0: michael@0: /** Aborts any pan/zoom animation that is currently in progress. */ michael@0: private void abortPanZoomAnimation() { michael@0: if (mPanZoomController != null) { michael@0: post(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mPanZoomController.abortAnimation(); michael@0: } michael@0: }); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * The different types of Viewport messages handled. All viewport events michael@0: * expect a display-port to be returned, but can handle one not being michael@0: * returned. michael@0: */ michael@0: private enum ViewportMessageType { michael@0: UPDATE, // The viewport has changed and should be entirely updated michael@0: PAGE_SIZE // The viewport's page-size has changed michael@0: } michael@0: michael@0: /** Viewport message handler. */ michael@0: private DisplayPortMetrics handleViewportMessage(ImmutableViewportMetrics messageMetrics, ViewportMessageType type) { michael@0: synchronized (getLock()) { michael@0: ImmutableViewportMetrics newMetrics; michael@0: ImmutableViewportMetrics oldMetrics = getViewportMetrics(); michael@0: michael@0: switch (type) { michael@0: default: michael@0: case UPDATE: michael@0: // Keep the old viewport size michael@0: newMetrics = messageMetrics.setViewportSize(oldMetrics.getWidth(), oldMetrics.getHeight()); michael@0: if (!oldMetrics.fuzzyEquals(newMetrics)) { michael@0: abortPanZoomAnimation(); michael@0: } michael@0: break; michael@0: case PAGE_SIZE: michael@0: // adjust the page dimensions to account for differences in zoom michael@0: // between the rendered content (which is what Gecko tells us) michael@0: // and our zoom level (which may have diverged). michael@0: float scaleFactor = oldMetrics.zoomFactor / messageMetrics.zoomFactor; michael@0: newMetrics = oldMetrics.setPageRect(RectUtils.scale(messageMetrics.getPageRect(), scaleFactor), messageMetrics.getCssPageRect()); michael@0: break; michael@0: } michael@0: michael@0: // Update the Gecko-side viewport metrics. Make sure to do this michael@0: // before modifying the metrics below. michael@0: final ImmutableViewportMetrics geckoMetrics = newMetrics.clamp(); michael@0: post(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mGeckoViewport = geckoMetrics; michael@0: } michael@0: }); michael@0: michael@0: setViewportMetrics(newMetrics, type == ViewportMessageType.UPDATE); michael@0: mDisplayPort = DisplayPortCalculator.calculate(getViewportMetrics(), null); michael@0: } michael@0: return mDisplayPort; michael@0: } michael@0: michael@0: @WrapElementForJNI michael@0: DisplayPortMetrics getDisplayPort(boolean pageSizeUpdate, boolean isBrowserContentDisplayed, int tabId, ImmutableViewportMetrics metrics) { michael@0: Tabs tabs = Tabs.getInstance(); michael@0: if (isBrowserContentDisplayed && tabs.isSelectedTabId(tabId)) { michael@0: // for foreground tabs, send the viewport update unless the document michael@0: // displayed is different from the content document. In that case, just michael@0: // calculate the display port. michael@0: return handleViewportMessage(metrics, pageSizeUpdate ? ViewportMessageType.PAGE_SIZE : ViewportMessageType.UPDATE); michael@0: } else { michael@0: // for background tabs, request a new display port calculation, so that michael@0: // when we do switch to that tab, we have the correct display port and michael@0: // don't need to draw twice (once to allow the first-paint viewport to michael@0: // get to java, and again once java figures out the display port). michael@0: return DisplayPortCalculator.calculate(metrics, null); michael@0: } michael@0: } michael@0: michael@0: @WrapElementForJNI michael@0: void contentDocumentChanged() { michael@0: mContentDocumentIsDisplayed = false; michael@0: } michael@0: michael@0: @WrapElementForJNI michael@0: boolean isContentDocumentDisplayed() { michael@0: return mContentDocumentIsDisplayed; michael@0: } michael@0: michael@0: // This is called on the Gecko thread to determine if we're still interested michael@0: // in the update of this display-port to continue. We can return true here michael@0: // to abort the current update and continue with any subsequent ones. This michael@0: // is useful for slow-to-render pages when the display-port starts lagging michael@0: // behind enough that continuing to draw it is wasted effort. michael@0: @WrapElementForJNI(allowMultithread = true) michael@0: public ProgressiveUpdateData progressiveUpdateCallback(boolean aHasPendingNewThebesContent, michael@0: float x, float y, float width, float height, michael@0: float resolution, boolean lowPrecision) { michael@0: // Reset the checkerboard risk flag when switching to low precision michael@0: // rendering. michael@0: if (lowPrecision && !mLastProgressiveUpdateWasLowPrecision) { michael@0: // Skip low precision rendering until we're at risk of checkerboarding. michael@0: if (!mProgressiveUpdateWasInDanger) { michael@0: mProgressiveUpdateData.abort = true; michael@0: return mProgressiveUpdateData; michael@0: } michael@0: mProgressiveUpdateWasInDanger = false; michael@0: } michael@0: mLastProgressiveUpdateWasLowPrecision = lowPrecision; michael@0: michael@0: // Grab a local copy of the last display-port sent to Gecko and the michael@0: // current viewport metrics to avoid races when accessing them. michael@0: DisplayPortMetrics displayPort = mDisplayPort; michael@0: ImmutableViewportMetrics viewportMetrics = mViewportMetrics; michael@0: mProgressiveUpdateData.setViewport(viewportMetrics); michael@0: mProgressiveUpdateData.abort = false; michael@0: michael@0: // Always abort updates if the resolution has changed. There's no use michael@0: // in drawing at the incorrect resolution. michael@0: if (!FloatUtils.fuzzyEquals(resolution, viewportMetrics.zoomFactor)) { michael@0: Log.d(LOGTAG, "Aborting draw due to resolution change: " + resolution + " != " + viewportMetrics.zoomFactor); michael@0: mProgressiveUpdateData.abort = true; michael@0: return mProgressiveUpdateData; michael@0: } michael@0: michael@0: // Store the high precision displayport for comparison when doing low michael@0: // precision updates. michael@0: if (!lowPrecision) { michael@0: if (!FloatUtils.fuzzyEquals(resolution, mProgressiveUpdateDisplayPort.resolution) || michael@0: !FloatUtils.fuzzyEquals(x, mProgressiveUpdateDisplayPort.getLeft()) || michael@0: !FloatUtils.fuzzyEquals(y, mProgressiveUpdateDisplayPort.getTop()) || michael@0: !FloatUtils.fuzzyEquals(x + width, mProgressiveUpdateDisplayPort.getRight()) || michael@0: !FloatUtils.fuzzyEquals(y + height, mProgressiveUpdateDisplayPort.getBottom())) { michael@0: mProgressiveUpdateDisplayPort = michael@0: new DisplayPortMetrics(x, y, x+width, y+height, resolution); michael@0: } michael@0: } michael@0: michael@0: // If we're not doing low precision draws and we're about to michael@0: // checkerboard, enable low precision drawing. michael@0: if (!lowPrecision && !mProgressiveUpdateWasInDanger) { michael@0: if (DisplayPortCalculator.aboutToCheckerboard(viewportMetrics, michael@0: mPanZoomController.getVelocityVector(), mProgressiveUpdateDisplayPort)) { michael@0: mProgressiveUpdateWasInDanger = true; michael@0: } michael@0: } michael@0: michael@0: // XXX All sorts of rounding happens inside Gecko that becomes hard to michael@0: // account exactly for. Given we align the display-port to tile michael@0: // boundaries (and so they rarely vary by sub-pixel amounts), just michael@0: // check that values are within a couple of pixels of the michael@0: // display-port bounds. michael@0: michael@0: // Never abort drawing if we can't be sure we've sent a more recent michael@0: // display-port. If we abort updating when we shouldn't, we can end up michael@0: // with blank regions on the screen and we open up the risk of entering michael@0: // an endless updating cycle. michael@0: if (Math.abs(displayPort.getLeft() - mProgressiveUpdateDisplayPort.getLeft()) <= 2 && michael@0: Math.abs(displayPort.getTop() - mProgressiveUpdateDisplayPort.getTop()) <= 2 && michael@0: Math.abs(displayPort.getBottom() - mProgressiveUpdateDisplayPort.getBottom()) <= 2 && michael@0: Math.abs(displayPort.getRight() - mProgressiveUpdateDisplayPort.getRight()) <= 2) { michael@0: return mProgressiveUpdateData; michael@0: } michael@0: michael@0: // Abort updates when the display-port no longer contains the visible michael@0: // area of the page (that is, the viewport cropped by the page michael@0: // boundaries). michael@0: // XXX This makes the assumption that we never let the visible area of michael@0: // the page fall outside of the display-port. michael@0: if (Math.max(viewportMetrics.viewportRectLeft, viewportMetrics.pageRectLeft) + 1 < x || michael@0: Math.max(viewportMetrics.viewportRectTop, viewportMetrics.pageRectTop) + 1 < y || michael@0: Math.min(viewportMetrics.viewportRectRight, viewportMetrics.pageRectRight) - 1 > x + width || michael@0: Math.min(viewportMetrics.viewportRectBottom, viewportMetrics.pageRectBottom) - 1 > y + height) { michael@0: Log.d(LOGTAG, "Aborting update due to viewport not in display-port"); michael@0: mProgressiveUpdateData.abort = true; michael@0: michael@0: // Enable low-precision drawing, as we're likely to be in danger if michael@0: // this situation has been encountered. michael@0: mProgressiveUpdateWasInDanger = true; michael@0: michael@0: return mProgressiveUpdateData; michael@0: } michael@0: michael@0: // Abort drawing stale low-precision content if there's a more recent michael@0: // display-port in the pipeline. michael@0: if (lowPrecision && !aHasPendingNewThebesContent) { michael@0: mProgressiveUpdateData.abort = true; michael@0: } michael@0: return mProgressiveUpdateData; michael@0: } michael@0: michael@0: void setZoomConstraints(ZoomConstraints constraints) { michael@0: mZoomConstraints = constraints; michael@0: } michael@0: michael@0: void setIsRTL(boolean aIsRTL) { michael@0: synchronized (getLock()) { michael@0: ImmutableViewportMetrics newMetrics = getViewportMetrics().setIsRTL(aIsRTL); michael@0: setViewportMetrics(newMetrics, false); michael@0: } michael@0: } michael@0: michael@0: /** The compositor invokes this function just before compositing a frame where the document michael@0: * is different from the document composited on the last frame. In these cases, the viewport michael@0: * information we have in Java is no longer valid and needs to be replaced with the new michael@0: * viewport information provided. setPageRect will never be invoked on the same frame that michael@0: * this function is invoked on; and this function will always be called prior to syncViewportInfo. michael@0: */ michael@0: @WrapElementForJNI(allowMultithread = true) michael@0: public void setFirstPaintViewport(float offsetX, float offsetY, float zoom, michael@0: float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) { michael@0: synchronized (getLock()) { michael@0: ImmutableViewportMetrics currentMetrics = getViewportMetrics(); michael@0: michael@0: Tab tab = Tabs.getInstance().getSelectedTab(); michael@0: michael@0: RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom); michael@0: RectF pageRect = RectUtils.scaleAndRound(cssPageRect, zoom); michael@0: michael@0: final ImmutableViewportMetrics newMetrics = currentMetrics michael@0: .setViewportOrigin(offsetX, offsetY) michael@0: .setZoomFactor(zoom) michael@0: .setPageRect(pageRect, cssPageRect) michael@0: .setIsRTL(tab.getIsRTL()); michael@0: // Since we have switched to displaying a different document, we need to update any michael@0: // viewport-related state we have lying around. This includes mGeckoViewport and michael@0: // mViewportMetrics. Usually this information is updated via handleViewportMessage michael@0: // while we remain on the same document. michael@0: post(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: mGeckoViewport = newMetrics; michael@0: } michael@0: }); michael@0: michael@0: setViewportMetrics(newMetrics); michael@0: michael@0: mView.setBackgroundColor(tab.getBackgroundColor()); michael@0: setZoomConstraints(tab.getZoomConstraints()); michael@0: michael@0: // At this point, we have just switched to displaying a different document than we michael@0: // we previously displaying. This means we need to abort any panning/zooming animations michael@0: // that are in progress and send an updated display port request to browser.js as soon michael@0: // as possible. The call to PanZoomController.abortAnimation accomplishes this by calling the michael@0: // forceRedraw function, which sends the viewport to gecko. The display port request is michael@0: // actually a full viewport update, which is fine because if browser.js has somehow moved to michael@0: // be out of sync with this first-paint viewport, then we force them back in sync. michael@0: abortPanZoomAnimation(); michael@0: michael@0: // Indicate that the document is about to be composited so the michael@0: // LayerView background can be removed. michael@0: if (mView.getPaintState() == LayerView.PAINT_START) { michael@0: mView.setPaintState(LayerView.PAINT_BEFORE_FIRST); michael@0: } michael@0: } michael@0: DisplayPortCalculator.resetPageState(); michael@0: mDrawTimingQueue.reset(); michael@0: michael@0: mContentDocumentIsDisplayed = true; michael@0: } michael@0: michael@0: /** The compositor invokes this function whenever it determines that the page rect michael@0: * has changed (based on the information it gets from layout). If setFirstPaintViewport michael@0: * is invoked on a frame, then this function will not be. For any given frame, this michael@0: * function will be invoked before syncViewportInfo. michael@0: */ michael@0: @WrapElementForJNI(allowMultithread = true) michael@0: public void setPageRect(float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) { michael@0: synchronized (getLock()) { michael@0: RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom); michael@0: float ourZoom = getViewportMetrics().zoomFactor; michael@0: setPageRect(RectUtils.scale(cssPageRect, ourZoom), cssPageRect); michael@0: // Here the page size of the document has changed, but the document being displayed michael@0: // is still the same. Therefore, we don't need to send anything to browser.js; any michael@0: // changes we need to make to the display port will get sent the next time we call michael@0: // adjustViewport(). michael@0: } michael@0: } michael@0: michael@0: /** The compositor invokes this function on every frame to figure out what part of the michael@0: * page to display, and to inform Java of the current display port. Since it is called michael@0: * on every frame, it needs to be ultra-fast. michael@0: * It avoids taking any locks or allocating any objects. We keep around a michael@0: * mCurrentViewTransform so we don't need to allocate a new ViewTransform michael@0: * everytime we're called. NOTE: we might be able to return a ImmutableViewportMetrics michael@0: * which would avoid the copy into mCurrentViewTransform. michael@0: */ michael@0: @WrapElementForJNI(allowMultithread = true) michael@0: public ViewTransform syncViewportInfo(int x, int y, int width, int height, float resolution, boolean layersUpdated) { michael@0: // getViewportMetrics is thread safe so we don't need to synchronize. michael@0: // We save the viewport metrics here, so we later use it later in michael@0: // createFrame (which will be called by nsWindow::DrawWindowUnderlay on michael@0: // the native side, by the compositor). The viewport michael@0: // metrics can change between here and there, as it's accessed outside michael@0: // of the compositor thread. michael@0: mFrameMetrics = getViewportMetrics(); michael@0: michael@0: mCurrentViewTransform.x = mFrameMetrics.viewportRectLeft; michael@0: mCurrentViewTransform.y = mFrameMetrics.viewportRectTop; michael@0: mCurrentViewTransform.scale = mFrameMetrics.zoomFactor; michael@0: michael@0: // Adjust the fixed layer margins so that overscroll subtracts from them. michael@0: getFixedMargins(mFrameMetrics, mCurrentViewTransformMargins); michael@0: mCurrentViewTransform.fixedLayerMarginLeft = mCurrentViewTransformMargins.left; michael@0: mCurrentViewTransform.fixedLayerMarginTop = mCurrentViewTransformMargins.top; michael@0: mCurrentViewTransform.fixedLayerMarginRight = mCurrentViewTransformMargins.right; michael@0: mCurrentViewTransform.fixedLayerMarginBottom = mCurrentViewTransformMargins.bottom; michael@0: michael@0: // Offset the view transform so that it renders in the correct place. michael@0: PointF offset = mFrameMetrics.getMarginOffset(); michael@0: mCurrentViewTransform.offsetX = offset.x; michael@0: mCurrentViewTransform.offsetY = offset.y; michael@0: michael@0: mRootLayer.setPositionAndResolution( michael@0: Math.round(x + mCurrentViewTransform.offsetX), michael@0: Math.round(y + mCurrentViewTransform.offsetY), michael@0: Math.round(x + width + mCurrentViewTransform.offsetX), michael@0: Math.round(y + height + mCurrentViewTransform.offsetY), michael@0: resolution); michael@0: michael@0: if (layersUpdated && mRecordDrawTimes) { michael@0: // If we got a layers update, that means a draw finished. Check to see if the area drawn matches michael@0: // one of our requested displayports; if it does calculate the draw time and notify the michael@0: // DisplayPortCalculator michael@0: DisplayPortMetrics drawn = new DisplayPortMetrics(x, y, x + width, y + height, resolution); michael@0: long time = mDrawTimingQueue.findTimeFor(drawn); michael@0: if (time >= 0) { michael@0: long now = SystemClock.uptimeMillis(); michael@0: time = now - time; michael@0: mRecordDrawTimes = DisplayPortCalculator.drawTimeUpdate(time, width * height); michael@0: } michael@0: } michael@0: michael@0: if (layersUpdated) { michael@0: for (DrawListener listener : mDrawListeners) { michael@0: listener.drawFinished(); michael@0: } michael@0: } michael@0: michael@0: return mCurrentViewTransform; michael@0: } michael@0: michael@0: @WrapElementForJNI(allowMultithread = true) michael@0: public ViewTransform syncFrameMetrics(float offsetX, float offsetY, float zoom, michael@0: float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom, michael@0: boolean layersUpdated, int x, int y, int width, int height, float resolution, michael@0: boolean isFirstPaint) michael@0: { michael@0: if (isFirstPaint) { michael@0: setFirstPaintViewport(offsetX, offsetY, zoom, michael@0: cssPageLeft, cssPageTop, cssPageRight, cssPageBottom); michael@0: } michael@0: michael@0: return syncViewportInfo(x, y, width, height, resolution, layersUpdated); michael@0: } michael@0: michael@0: @WrapElementForJNI(allowMultithread = true) michael@0: public LayerRenderer.Frame createFrame() { michael@0: // Create the shaders and textures if necessary. michael@0: if (!mLayerRendererInitialized) { michael@0: mLayerRenderer.checkMonitoringEnabled(); michael@0: mLayerRenderer.createDefaultProgram(); michael@0: mLayerRendererInitialized = true; michael@0: } michael@0: michael@0: try { michael@0: return mLayerRenderer.createFrame(mFrameMetrics); michael@0: } catch (Exception e) { michael@0: Log.w(LOGTAG, e); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: @WrapElementForJNI(allowMultithread = true) michael@0: public void activateProgram() { michael@0: mLayerRenderer.activateDefaultProgram(); michael@0: } michael@0: michael@0: @WrapElementForJNI(allowMultithread = true) michael@0: public void deactivateProgram() { michael@0: mLayerRenderer.deactivateDefaultProgram(); michael@0: } michael@0: michael@0: private void geometryChanged(DisplayPortMetrics displayPort) { michael@0: /* Let Gecko know if the screensize has changed */ michael@0: sendResizeEventIfNecessary(false); michael@0: if (getRedrawHint()) { michael@0: adjustViewport(displayPort); michael@0: } michael@0: } michael@0: michael@0: /** Implementation of LayerView.Listener */ michael@0: @Override michael@0: public void renderRequested() { michael@0: try { michael@0: GeckoAppShell.scheduleComposite(); michael@0: } catch (UnsupportedOperationException uoe) { michael@0: // In some very rare cases this gets called before libxul is loaded, michael@0: // so catch and ignore the exception that will throw. See bug 837821 michael@0: Log.d(LOGTAG, "Dropping renderRequested call before libxul load."); michael@0: } michael@0: } michael@0: michael@0: /** Implementation of LayerView.Listener */ michael@0: @Override michael@0: public void sizeChanged(int width, int height) { michael@0: // We need to make sure a draw happens synchronously at this point, michael@0: // but resizing the surface before the SurfaceView has resized will michael@0: // cause a visible jump. michael@0: mView.getGLController().resumeCompositor(mWindowSize.width, mWindowSize.height); michael@0: } michael@0: michael@0: /** Implementation of LayerView.Listener */ michael@0: @Override michael@0: public void surfaceChanged(int width, int height) { michael@0: setViewportSize(width, height); michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget */ michael@0: @Override michael@0: public ImmutableViewportMetrics getViewportMetrics() { michael@0: return mViewportMetrics; michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget */ michael@0: @Override michael@0: public ZoomConstraints getZoomConstraints() { michael@0: return mZoomConstraints; michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget */ michael@0: @Override michael@0: public boolean isFullScreen() { michael@0: return mView.isFullScreen(); michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget */ michael@0: @Override michael@0: public RectF getMaxMargins() { michael@0: return mMarginsAnimator.getMaxMargins(); michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget */ michael@0: @Override michael@0: public void setAnimationTarget(ImmutableViewportMetrics metrics) { michael@0: if (mGeckoIsReady) { michael@0: // We know what the final viewport of the animation is going to be, so michael@0: // immediately request a draw of that area by setting the display port michael@0: // accordingly. This way we should have the content pre-rendered by the michael@0: // time the animation is done. michael@0: DisplayPortMetrics displayPort = DisplayPortCalculator.calculate(metrics, null); michael@0: adjustViewport(displayPort); michael@0: } michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget michael@0: * You must hold the monitor while calling this. michael@0: */ michael@0: @Override michael@0: public void setViewportMetrics(ImmutableViewportMetrics metrics) { michael@0: setViewportMetrics(metrics, true); michael@0: } michael@0: michael@0: /* michael@0: * You must hold the monitor while calling this. michael@0: */ michael@0: private void setViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko) { michael@0: // This class owns the viewport size and the fixed layer margins; don't let other pieces michael@0: // of code clobber either of them. The only place the viewport size should ever be michael@0: // updated is in GeckoLayerClient.setViewportSize, and the only place the margins should michael@0: // ever be updated is in GeckoLayerClient.setFixedLayerMargins; both of these assign to michael@0: // mViewportMetrics directly. michael@0: metrics = metrics.setViewportSize(mViewportMetrics.getWidth(), mViewportMetrics.getHeight()); michael@0: metrics = metrics.setMarginsFrom(mViewportMetrics); michael@0: mViewportMetrics = metrics; michael@0: michael@0: viewportMetricsChanged(notifyGecko); michael@0: } michael@0: michael@0: /* michael@0: * You must hold the monitor while calling this. michael@0: */ michael@0: private void viewportMetricsChanged(boolean notifyGecko) { michael@0: if (mViewportChangeListener != null) { michael@0: mViewportChangeListener.onMetricsChanged(mViewportMetrics); michael@0: } michael@0: michael@0: mView.requestRender(); michael@0: if (notifyGecko && mGeckoIsReady) { michael@0: geometryChanged(null); michael@0: } michael@0: } michael@0: michael@0: /* michael@0: * Updates the viewport metrics, overriding the viewport size and margins michael@0: * which are normally retained when calling setViewportMetrics. michael@0: * You must hold the monitor while calling this. michael@0: */ michael@0: void forceViewportMetrics(ImmutableViewportMetrics metrics, boolean notifyGecko, boolean forceRedraw) { michael@0: if (forceRedraw) { michael@0: mForceRedraw = true; michael@0: } michael@0: mViewportMetrics = metrics; michael@0: viewportMetricsChanged(notifyGecko); michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget michael@0: * Scroll the viewport by a certain amount. This will take viewport margins michael@0: * and margin animation into account. If margins are currently animating, michael@0: * this will just go ahead and modify the viewport origin, otherwise the michael@0: * delta will be applied to the margins and the remainder will be applied to michael@0: * the viewport origin. michael@0: * michael@0: * You must hold the monitor while calling this. michael@0: */ michael@0: @Override michael@0: public void scrollBy(float dx, float dy) { michael@0: // Set mViewportMetrics manually so the margin changes take. michael@0: mViewportMetrics = mMarginsAnimator.scrollBy(mViewportMetrics, dx, dy); michael@0: viewportMetricsChanged(true); michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget michael@0: * Notification that a subdocument has been scrolled by a certain amount. michael@0: * This is used here to make sure that the margins are still accessible michael@0: * during subdocument scrolling. michael@0: * michael@0: * You must hold the monitor while calling this. michael@0: */ michael@0: @Override michael@0: public void scrollMarginsBy(float dx, float dy) { michael@0: ImmutableViewportMetrics newMarginsMetrics = michael@0: mMarginsAnimator.scrollBy(mViewportMetrics, dx, dy); michael@0: mViewportMetrics = mViewportMetrics.setMarginsFrom(newMarginsMetrics); michael@0: viewportMetricsChanged(true); michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget */ michael@0: @Override michael@0: public void panZoomStopped() { michael@0: if (mViewportChangeListener != null) { michael@0: mViewportChangeListener.onPanZoomStopped(); michael@0: } michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget */ michael@0: @Override michael@0: public void forceRedraw(DisplayPortMetrics displayPort) { michael@0: mForceRedraw = true; michael@0: if (mGeckoIsReady) { michael@0: geometryChanged(displayPort); michael@0: } michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget */ michael@0: @Override michael@0: public boolean post(Runnable action) { michael@0: return mView.post(action); michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget */ michael@0: @Override michael@0: public void postRenderTask(RenderTask task) { michael@0: mView.postRenderTask(task); michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget */ michael@0: @Override michael@0: public void removeRenderTask(RenderTask task) { michael@0: mView.removeRenderTask(task); michael@0: } michael@0: michael@0: michael@0: /** Implementation of PanZoomTarget */ michael@0: @Override michael@0: public boolean postDelayed(Runnable action, long delayMillis) { michael@0: return mView.postDelayed(action, delayMillis); michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget */ michael@0: @Override michael@0: public Object getLock() { michael@0: return this; michael@0: } michael@0: michael@0: /** Implementation of PanZoomTarget michael@0: * Converts a point from layer view coordinates to layer coordinates. In other words, given a michael@0: * point measured in pixels from the top left corner of the layer view, returns the point in michael@0: * pixels measured from the last scroll position we sent to Gecko, in CSS pixels. Assuming the michael@0: * events being sent to Gecko are processed in FIFO order, this calculation should always be michael@0: * correct. michael@0: */ michael@0: @Override michael@0: public PointF convertViewPointToLayerPoint(PointF viewPoint) { michael@0: if (!mGeckoIsReady) { michael@0: return null; michael@0: } michael@0: michael@0: ImmutableViewportMetrics viewportMetrics = mViewportMetrics; michael@0: PointF origin = viewportMetrics.getOrigin(); michael@0: PointF offset = viewportMetrics.getMarginOffset(); michael@0: origin.offset(-offset.x, -offset.y); michael@0: float zoom = viewportMetrics.zoomFactor; michael@0: ImmutableViewportMetrics geckoViewport = mGeckoViewport; michael@0: PointF geckoOrigin = geckoViewport.getOrigin(); michael@0: float geckoZoom = geckoViewport.zoomFactor; michael@0: michael@0: // viewPoint + origin - offset gives the coordinate in device pixels from the top-left corner of the page. michael@0: // Divided by zoom, this gives us the coordinate in CSS pixels from the top-left corner of the page. michael@0: // geckoOrigin / geckoZoom is where Gecko thinks it is (scrollTo position) in CSS pixels from michael@0: // the top-left corner of the page. Subtracting the two gives us the offset of the viewPoint from michael@0: // the current Gecko coordinate in CSS pixels. michael@0: PointF layerPoint = new PointF( michael@0: ((viewPoint.x + origin.x) / zoom) - (geckoOrigin.x / geckoZoom), michael@0: ((viewPoint.y + origin.y) / zoom) - (geckoOrigin.y / geckoZoom)); michael@0: michael@0: return layerPoint; michael@0: } michael@0: michael@0: void setOnMetricsChangedListener(LayerView.OnMetricsChangedListener listener) { michael@0: mViewportChangeListener = listener; michael@0: } michael@0: michael@0: public void addDrawListener(DrawListener listener) { michael@0: mDrawListeners.add(listener); michael@0: } michael@0: michael@0: public void removeDrawListener(DrawListener listener) { michael@0: mDrawListeners.remove(listener); michael@0: } michael@0: }