diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 05:54:39 +0000 |
commit | 267c6f2ac71f92999e969232431ba04678e7437e (patch) | |
tree | 358c9467650e1d0a1d7227a21dac2e3d08b622b2 /android/source/src/java/org/mozilla | |
parent | Initial commit. (diff) | |
download | libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.tar.xz libreoffice-267c6f2ac71f92999e969232431ba04678e7437e.zip |
Adding upstream version 4:24.2.0.upstream/4%24.2.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'android/source/src/java/org/mozilla')
41 files changed, 7560 insertions, 0 deletions
diff --git a/android/source/src/java/org/mozilla/gecko/OnInterceptTouchListener.java b/android/source/src/java/org/mozilla/gecko/OnInterceptTouchListener.java new file mode 100644 index 0000000000..d0cd3d48a9 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/OnInterceptTouchListener.java @@ -0,0 +1,14 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko; + +import android.view.MotionEvent; +import android.view.View; + +public interface OnInterceptTouchListener extends View.OnTouchListener { + /** Override this method for a chance to consume events before the view or its children */ + public boolean onInterceptTouchEvent(View view, MotionEvent event); +} diff --git a/android/source/src/java/org/mozilla/gecko/OnSlideSwipeListener.java b/android/source/src/java/org/mozilla/gecko/OnSlideSwipeListener.java new file mode 100644 index 0000000000..29f50ebf49 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/OnSlideSwipeListener.java @@ -0,0 +1,94 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * 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/. + */ + +package org.mozilla.gecko; + +import android.content.Context; +import android.view.GestureDetector; +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.util.Log; + +import org.libreoffice.LOKitShell; +import org.mozilla.gecko.gfx.GeckoLayerClient; +import org.mozilla.gecko.gfx.ImmutableViewportMetrics; + + +public class OnSlideSwipeListener implements OnTouchListener { + private static String LOGTAG = OnSlideSwipeListener.class.getName(); + + private final GestureDetector mGestureDetector; + private GeckoLayerClient mLayerClient; + + public OnSlideSwipeListener(Context ctx, GeckoLayerClient client){ + mGestureDetector = new GestureDetector(ctx, new GestureListener()); + mLayerClient = client; + } + + private final class GestureListener extends SimpleOnGestureListener { + + private static final int SWIPE_THRESHOLD = 100; + private static final int SWIPE_VELOCITY_THRESHOLD = 100; + + @Override + public boolean onDown(MotionEvent e) { + return false; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velX, float velY) { + // Check if the page is already zoomed-in. + // Disable swiping gesture if that's the case. + ImmutableViewportMetrics viewportMetrics = mLayerClient.getViewportMetrics(); + if (viewportMetrics.viewportRectLeft > viewportMetrics.pageRectLeft || + viewportMetrics.viewportRectRight < viewportMetrics.pageRectRight) { + return false; + } + + // Otherwise, the page is smaller than viewport, perform swipe + // gesture. + try { + float diffY = e2.getY() - e1.getY(); + float diffX = e2.getX() - e1.getX(); + if (Math.abs(diffX) > Math.abs(diffY)) { + if (Math.abs(diffX) > SWIPE_THRESHOLD + && Math.abs(velX) > SWIPE_VELOCITY_THRESHOLD) { + if (diffX > 0) { + onSwipeRight(); + } else { + onSwipeLeft(); + } + } + } + } catch (Exception exception) { + exception.printStackTrace(); + } + return false; + } + } + + public void onSwipeRight() { + Log.d(LOGTAG, "onSwipeRight"); + LOKitShell.sendSwipeRightEvent(); + } + + public void onSwipeLeft() { + Log.d(LOGTAG, "onSwipeLeft"); + LOKitShell.sendSwipeLeftEvent(); + } + + @Override + public boolean onTouch(View v, MotionEvent me) { + return mGestureDetector.onTouchEvent(me); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/android/source/src/java/org/mozilla/gecko/ZoomConstraints.java b/android/source/src/java/org/mozilla/gecko/ZoomConstraints.java new file mode 100644 index 0000000000..dbe2788272 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/ZoomConstraints.java @@ -0,0 +1,30 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko; + +public final class ZoomConstraints { + private final float mDefaultZoom; + private final float mMinZoom; + private final float mMaxZoom; + + public ZoomConstraints(float defaultZoom, float minZoom, float maxZoom) { + mDefaultZoom = defaultZoom; + mMinZoom = minZoom; + mMaxZoom = maxZoom; + } + + public final float getDefaultZoom() { + return mDefaultZoom; + } + + public final float getMinZoom() { + return mMinZoom; + } + + public final float getMaxZoom() { + return mMaxZoom; + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/Axis.java b/android/source/src/java/org/mozilla/gecko/gfx/Axis.java new file mode 100644 index 0000000000..d4a7ac2ce5 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/Axis.java @@ -0,0 +1,337 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.util.Log; +import android.view.View; + +import org.mozilla.gecko.util.FloatUtils; + +import java.util.Map; + +/** + * This class represents the physics for one axis of movement (i.e. either + * horizontal or vertical). It tracks the different properties of movement + * like displacement, velocity, viewport dimensions, etc. pertaining to + * a particular axis. + */ +abstract class Axis { + private static final String LOGTAG = "GeckoAxis"; + + private static final String PREF_SCROLLING_FRICTION_SLOW = "ui.scrolling.friction_slow"; + private static final String PREF_SCROLLING_FRICTION_FAST = "ui.scrolling.friction_fast"; + private static final String PREF_SCROLLING_MAX_EVENT_ACCELERATION = "ui.scrolling.max_event_acceleration"; + private static final String PREF_SCROLLING_OVERSCROLL_DECEL_RATE = "ui.scrolling.overscroll_decel_rate"; + private static final String PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT = "ui.scrolling.overscroll_snap_limit"; + private static final String PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE = "ui.scrolling.min_scrollable_distance"; + + // This fraction of velocity remains after every animation frame when the velocity is low. + private static float FRICTION_SLOW; + // This fraction of velocity remains after every animation frame when the velocity is high. + private static float FRICTION_FAST; + // Below this velocity (in pixels per frame), the friction starts increasing from FRICTION_FAST + // to FRICTION_SLOW. + private static float VELOCITY_THRESHOLD; + // The maximum velocity change factor between events, per ms, in %. + // Direction changes are excluded. + private static float MAX_EVENT_ACCELERATION; + + // The rate of deceleration when the surface has overscrolled. + private static float OVERSCROLL_DECEL_RATE; + // The percentage of the surface which can be overscrolled before it must snap back. + private static float SNAP_LIMIT; + + // The minimum amount of space that must be present for an axis to be considered scrollable, + // in pixels. + private static float MIN_SCROLLABLE_DISTANCE; + + private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) { + Integer value = (prefs == null ? null : prefs.get(prefName)); + return (float)(value == null || value < 0 ? defaultValue : value) / 1000f; + } + + private static int getIntPref(Map<String, Integer> prefs, String prefName, int defaultValue) { + Integer value = (prefs == null ? null : prefs.get(prefName)); + return (value == null || value < 0 ? defaultValue : value); + } + + static final float MS_PER_FRAME = 4.0f; + private static final float FRAMERATE_MULTIPLIER = (1000f/60f) / MS_PER_FRAME; + + // The values we use for friction are based on a 16.6ms frame, adjust them to MS_PER_FRAME: + // FRICTION^1 = FRICTION_ADJUSTED^(16/MS_PER_FRAME) + // FRICTION_ADJUSTED = e ^ ((ln(FRICTION))/FRAMERATE_MULTIPLIER) + static float getFrameAdjustedFriction(float baseFriction) { + return (float)Math.pow(Math.E, (Math.log(baseFriction) / FRAMERATE_MULTIPLIER)); + } + + static void setPrefs(Map<String, Integer> prefs) { + FRICTION_SLOW = getFrameAdjustedFriction(getFloatPref(prefs, PREF_SCROLLING_FRICTION_SLOW, 850)); + FRICTION_FAST = getFrameAdjustedFriction(getFloatPref(prefs, PREF_SCROLLING_FRICTION_FAST, 970)); + VELOCITY_THRESHOLD = 10 / FRAMERATE_MULTIPLIER; + MAX_EVENT_ACCELERATION = getFloatPref(prefs, PREF_SCROLLING_MAX_EVENT_ACCELERATION, 12); + OVERSCROLL_DECEL_RATE = getFrameAdjustedFriction(getFloatPref(prefs, PREF_SCROLLING_OVERSCROLL_DECEL_RATE, 40)); + SNAP_LIMIT = getFloatPref(prefs, PREF_SCROLLING_OVERSCROLL_SNAP_LIMIT, 300); + MIN_SCROLLABLE_DISTANCE = getFloatPref(prefs, PREF_SCROLLING_MIN_SCROLLABLE_DISTANCE, 500); + Log.i(LOGTAG, "Prefs: " + FRICTION_SLOW + "," + FRICTION_FAST + "," + VELOCITY_THRESHOLD + "," + + MAX_EVENT_ACCELERATION + "," + OVERSCROLL_DECEL_RATE + "," + SNAP_LIMIT + "," + MIN_SCROLLABLE_DISTANCE); + } + + static { + // set the scrolling parameters to default values on startup + setPrefs(null); + } + + private enum FlingStates { + STOPPED, + PANNING, + FLINGING, + } + + private enum Overscroll { + NONE, + MINUS, // Overscrolled in the negative direction + PLUS, // Overscrolled in the positive direction + BOTH, // Overscrolled in both directions (page is zoomed to smaller than screen) + } + + private final SubdocumentScrollHelper mSubscroller; + + private int mOverscrollMode; /* Default to only overscrolling if we're allowed to scroll in a direction */ + private float mFirstTouchPos; /* Position of the first touch event on the current drag. */ + private float mTouchPos; /* Position of the most recent touch event on the current drag. */ + private float mLastTouchPos; /* Position of the touch event before touchPos. */ + private float mVelocity; /* Velocity in this direction; pixels per animation frame. */ + private boolean mScrollingDisabled; /* Whether movement on this axis is locked. */ + private boolean mDisableSnap; /* Whether overscroll snapping is disabled. */ + private float mDisplacement; + + private FlingStates mFlingState; /* The fling state we're in on this axis. */ + + protected abstract float getOrigin(); + protected abstract float getViewportLength(); + protected abstract float getPageStart(); + protected abstract float getPageLength(); + + Axis(SubdocumentScrollHelper subscroller) { + mSubscroller = subscroller; + mOverscrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS; + } + + public void setOverScrollMode(int overscrollMode) { + mOverscrollMode = overscrollMode; + } + + public int getOverScrollMode() { + return mOverscrollMode; + } + + private float getViewportEnd() { + return getOrigin() + getViewportLength(); + } + + private float getPageEnd() { + return getPageStart() + getPageLength(); + } + + void startTouch(float pos) { + mVelocity = 0.0f; + mScrollingDisabled = false; + mFirstTouchPos = mTouchPos = mLastTouchPos = pos; + } + + float panDistance(float currentPos) { + return currentPos - mFirstTouchPos; + } + + void setScrollingDisabled(boolean disabled) { + mScrollingDisabled = disabled; + } + + void saveTouchPos() { + mLastTouchPos = mTouchPos; + } + + void updateWithTouchAt(float pos, float timeDelta) { + float newVelocity = (mTouchPos - pos) / timeDelta * MS_PER_FRAME; + + // If there's a direction change, or current velocity is very low, + // allow setting of the velocity outright. Otherwise, use the current + // velocity and a maximum change factor to set the new velocity. + boolean curVelocityIsLow = Math.abs(mVelocity) < 1.0f / FRAMERATE_MULTIPLIER; + boolean directionChange = (mVelocity > 0) != (newVelocity > 0); + if (curVelocityIsLow || (directionChange && !FloatUtils.fuzzyEquals(newVelocity, 0.0f))) { + mVelocity = newVelocity; + } else { + float maxChange = Math.abs(mVelocity * timeDelta * MAX_EVENT_ACCELERATION); + mVelocity = Math.min(mVelocity + maxChange, Math.max(mVelocity - maxChange, newVelocity)); + } + + mTouchPos = pos; + } + + boolean overscrolled() { + return getOverscroll() != Overscroll.NONE; + } + + private Overscroll getOverscroll() { + boolean minus = (getOrigin() < getPageStart()); + boolean plus = (getViewportEnd() > getPageEnd()); + if (minus && plus) { + return Overscroll.BOTH; + } else if (minus) { + return Overscroll.MINUS; + } else if (plus) { + return Overscroll.PLUS; + } else { + return Overscroll.NONE; + } + } + + // Returns the amount that the page has been overscrolled. If the page hasn't been + // overscrolled on this axis, returns 0. + private float getExcess() { + switch (getOverscroll()) { + case MINUS: return getPageStart() - getOrigin(); + case PLUS: return getViewportEnd() - getPageEnd(); + case BOTH: return (getViewportEnd() - getPageEnd()) + (getPageStart() - getOrigin()); + default: return 0.0f; + } + } + + /* + * Returns true if the page is zoomed in to some degree along this axis such that scrolling is + * possible and this axis has not been scroll locked while panning. Otherwise, returns false. + */ + boolean scrollable() { + // If we're scrolling a subdocument, ignore the viewport length restrictions (since those + // apply to the top-level document) and only take into account axis locking. + if (mSubscroller.scrolling()) { + return !mScrollingDisabled; + } + + // if we are axis locked, return false + if (mScrollingDisabled) { + return false; + } + + // there is scrollable space, and we're not disabled, or the document fits the viewport + // but we always allow overscroll anyway + return getViewportLength() <= getPageLength() - MIN_SCROLLABLE_DISTANCE || + getOverScrollMode() == View.OVER_SCROLL_ALWAYS; + } + + /* + * Returns the resistance, as a multiplier, that should be taken into account when + * tracking or pinching. + */ + float getEdgeResistance(boolean forPinching) { + float excess = getExcess(); + if (excess > 0.0f && (getOverscroll() == Overscroll.BOTH || !forPinching)) { + // excess can be greater than viewport length, but the resistance + // must never drop below 0.0 + return Math.max(0.0f, SNAP_LIMIT - excess / getViewportLength()); + } + return 1.0f; + } + + /* Returns the velocity. If the axis is locked, returns 0. */ + float getRealVelocity() { + return scrollable() ? mVelocity : 0f; + } + + void startPan() { + mFlingState = FlingStates.PANNING; + } + + void startFling(boolean stopped) { + mDisableSnap = mSubscroller.scrolling(); + + if (stopped) { + mFlingState = FlingStates.STOPPED; + } else { + mFlingState = FlingStates.FLINGING; + } + } + + /* Advances a fling animation by one step. */ + boolean advanceFling() { + if (mFlingState != FlingStates.FLINGING) { + return false; + } + if (mSubscroller.scrolling() && !mSubscroller.lastScrollSucceeded()) { + // if the subdocument stopped scrolling, it's because it reached the end + // of the subdocument. we don't do overscroll on subdocuments, so there's + // no point in continuing this fling. + return false; + } + + float excess = getExcess(); + Overscroll overscroll = getOverscroll(); + boolean decreasingOverscroll = false; + if ((overscroll == Overscroll.MINUS && mVelocity > 0) || + (overscroll == Overscroll.PLUS && mVelocity < 0)) + { + decreasingOverscroll = true; + } + + if (mDisableSnap || FloatUtils.fuzzyEquals(excess, 0.0f) || decreasingOverscroll) { + // If we aren't overscrolled, just apply friction. + if (Math.abs(mVelocity) >= VELOCITY_THRESHOLD) { + mVelocity *= FRICTION_FAST; + } else { + float t = mVelocity / VELOCITY_THRESHOLD; + mVelocity *= FloatUtils.interpolate(FRICTION_SLOW, FRICTION_FAST, t); + } + } else { + // Otherwise, decrease the velocity linearly. + float elasticity = 1.0f - excess / (getViewportLength() * SNAP_LIMIT); + if (overscroll == Overscroll.MINUS) { + mVelocity = Math.min((mVelocity + OVERSCROLL_DECEL_RATE) * elasticity, 0.0f); + } else { // must be Overscroll.PLUS + mVelocity = Math.max((mVelocity - OVERSCROLL_DECEL_RATE) * elasticity, 0.0f); + } + } + + return true; + } + + void stopFling() { + mVelocity = 0.0f; + mFlingState = FlingStates.STOPPED; + } + + // Performs displacement of the viewport position according to the current velocity. + void displace() { + // if this isn't scrollable just return + if (!scrollable()) + return; + + if (mFlingState == FlingStates.PANNING) + mDisplacement += (mLastTouchPos - mTouchPos) * getEdgeResistance(false); + else + mDisplacement += mVelocity; + + // if overscroll is disabled and we're trying to overscroll, reset the displacement + // to remove any excess. Using getExcess alone isn't enough here since it relies on + // getOverscroll which doesn't take into account any new displacement being applied + if (getOverScrollMode() == View.OVER_SCROLL_NEVER) { + if (mDisplacement + getOrigin() < getPageStart()) { + mDisplacement = getPageStart() - getOrigin(); + stopFling(); + } else if (mDisplacement + getViewportEnd() > getPageEnd()) { + mDisplacement = getPageEnd() - getViewportEnd(); + stopFling(); + } + } + } + + float resetDisplacement() { + float d = mDisplacement; + mDisplacement = 0.0f; + return d; + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/BufferedCairoImage.java b/android/source/src/java/org/mozilla/gecko/gfx/BufferedCairoImage.java new file mode 100644 index 0000000000..a616fcc4da --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/BufferedCairoImage.java @@ -0,0 +1,83 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + + +import android.graphics.Bitmap; +import android.util.Log; + +import org.libreoffice.kit.DirectBufferAllocator; + +import java.nio.ByteBuffer; + +/** + * A Cairo image that simply saves a buffer of pixel data. + */ +public class BufferedCairoImage extends CairoImage { + private static String LOGTAG = "GeckoBufferedCairoImage"; + private ByteBuffer mBuffer; + private IntSize mSize; + private int mFormat; + + /** + * Creates a buffered Cairo image from a byte buffer. + */ + public BufferedCairoImage(ByteBuffer inBuffer, int inWidth, int inHeight, int inFormat) { + setBuffer(inBuffer, inWidth, inHeight, inFormat); + } + + /** + * Creates a buffered Cairo image from an Android bitmap. + */ + public BufferedCairoImage(Bitmap bitmap) { + setBitmap(bitmap); + } + + private synchronized void freeBuffer() { + mBuffer = DirectBufferAllocator.free(mBuffer); + } + + @Override + public void destroy() { + try { + freeBuffer(); + } catch (Exception ex) { + Log.e(LOGTAG, "error clearing buffer: ", ex); + } + } + + @Override + public ByteBuffer getBuffer() { + return mBuffer; + } + + @Override + public IntSize getSize() { + return mSize; + } + + @Override + public int getFormat() { + return mFormat; + } + + + public void setBuffer(ByteBuffer buffer, int width, int height, int format) { + freeBuffer(); + mBuffer = buffer; + mSize = new IntSize(width, height); + mFormat = format; + } + + public void setBitmap(Bitmap bitmap) { + mFormat = CairoUtils.bitmapConfigToCairoFormat(bitmap.getConfig()); + mSize = new IntSize(bitmap.getWidth(), bitmap.getHeight()); + + int bpp = CairoUtils.bitsPerPixelForCairoFormat(mFormat) / 8; + mBuffer = DirectBufferAllocator.allocate(mSize.getArea() * bpp); + bitmap.copyPixelsToBuffer(mBuffer.asIntBuffer()); + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/CairoGLInfo.java b/android/source/src/java/org/mozilla/gecko/gfx/CairoGLInfo.java new file mode 100644 index 0000000000..078aa41bae --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/CairoGLInfo.java @@ -0,0 +1,35 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import javax.microedition.khronos.opengles.GL10; + +/** Information needed to render Cairo bitmaps using OpenGL ES. */ +public class CairoGLInfo { + public final int internalFormat; + public final int format; + public final int type; + + public CairoGLInfo(int cairoFormat) { + switch (cairoFormat) { + case CairoImage.FORMAT_ARGB32: + internalFormat = format = GL10.GL_RGBA; type = GL10.GL_UNSIGNED_BYTE; + break; + case CairoImage.FORMAT_RGB24: + internalFormat = format = GL10.GL_RGB; type = GL10.GL_UNSIGNED_BYTE; + break; + case CairoImage.FORMAT_RGB16_565: + internalFormat = format = GL10.GL_RGB; type = GL10.GL_UNSIGNED_SHORT_5_6_5; + break; + case CairoImage.FORMAT_A8: + case CairoImage.FORMAT_A1: + throw new RuntimeException("Cairo FORMAT_A1 and FORMAT_A8 unsupported"); + default: + throw new RuntimeException("Unknown Cairo format"); + } + } +} + diff --git a/android/source/src/java/org/mozilla/gecko/gfx/CairoImage.java b/android/source/src/java/org/mozilla/gecko/gfx/CairoImage.java new file mode 100644 index 0000000000..5a18a4bb19 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/CairoImage.java @@ -0,0 +1,28 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import java.nio.ByteBuffer; + +/* + * A bitmap with pixel data in one of the formats that Cairo understands. + */ +public abstract class CairoImage { + public abstract ByteBuffer getBuffer(); + + public abstract void destroy(); + + public abstract IntSize getSize(); + public abstract int getFormat(); + + public static final int FORMAT_INVALID = -1; + public static final int FORMAT_ARGB32 = 0; + public static final int FORMAT_RGB24 = 1; + public static final int FORMAT_A8 = 2; + public static final int FORMAT_A1 = 3; + public static final int FORMAT_RGB16_565 = 4; +} + diff --git a/android/source/src/java/org/mozilla/gecko/gfx/CairoUtils.java b/android/source/src/java/org/mozilla/gecko/gfx/CairoUtils.java new file mode 100644 index 0000000000..e0db6530d5 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/CairoUtils.java @@ -0,0 +1,51 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Bitmap; + +/** + * Utility methods useful when displaying Cairo bitmaps using OpenGL ES. + */ +public class CairoUtils { + private CairoUtils() { /* Don't call me. */ } + + public static int bitsPerPixelForCairoFormat(int cairoFormat) { + switch (cairoFormat) { + case CairoImage.FORMAT_A1: return 1; + case CairoImage.FORMAT_A8: return 8; + case CairoImage.FORMAT_RGB16_565: return 16; + case CairoImage.FORMAT_RGB24: return 24; + case CairoImage.FORMAT_ARGB32: return 32; + default: + throw new RuntimeException("Unknown Cairo format"); + } + } + + public static int bitmapConfigToCairoFormat(Bitmap.Config config) { + if (config == null) + return CairoImage.FORMAT_ARGB32; /* Droid Pro fix. */ + + switch (config) { + case ALPHA_8: return CairoImage.FORMAT_A8; + case ARGB_4444: throw new RuntimeException("ARGB_444 unsupported"); + case ARGB_8888: return CairoImage.FORMAT_ARGB32; + case RGB_565: return CairoImage.FORMAT_RGB16_565; + default: throw new RuntimeException("Unknown Skia bitmap config"); + } + } + + public static Bitmap.Config cairoFormatTobitmapConfig(int format) { + switch (format) { + case CairoImage.FORMAT_A8: return Bitmap.Config.ALPHA_8; + case CairoImage.FORMAT_ARGB32: return Bitmap.Config.ARGB_8888; + case CairoImage.FORMAT_RGB16_565: return Bitmap.Config.RGB_565; + default: + throw new RuntimeException("Unknown CairoImage format"); + } + } +} + diff --git a/android/source/src/java/org/mozilla/gecko/gfx/ComposedTileLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/ComposedTileLayer.java new file mode 100644 index 0000000000..bdef702218 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/ComposedTileLayer.java @@ -0,0 +1,290 @@ +package org.mozilla.gecko.gfx; + +import android.content.ComponentCallbacks2; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.RectF; +import android.graphics.Region; +import android.util.Log; + +import org.libreoffice.LOKitShell; +import org.libreoffice.TileIdentifier; +import org.mozilla.gecko.util.FloatUtils; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public abstract class ComposedTileLayer extends Layer implements ComponentCallbacks2 { + private static final String LOGTAG = ComposedTileLayer.class.getSimpleName(); + + protected final List<SubTile> tiles = new ArrayList<SubTile>(); + + protected final IntSize tileSize; + private final ReadWriteLock tilesReadWriteLock = new ReentrantReadWriteLock(); + private final Lock tilesReadLock = tilesReadWriteLock.readLock(); + private final Lock tilesWriteLock = tilesReadWriteLock.writeLock(); + + protected RectF currentViewport = new RectF(); + protected float currentZoom = 1.0f; + protected RectF currentPageRect = new RectF(); + + private long reevaluationNanoTime = 0; + + public ComposedTileLayer(Context context) { + context.registerComponentCallbacks(this); + this.tileSize = new IntSize(256, 256); + } + + protected static RectF roundToTileSize(RectF input, IntSize tileSize) { + float minX = ((int) (input.left / tileSize.width)) * tileSize.width; + float minY = ((int) (input.top / tileSize.height)) * tileSize.height; + float maxX = ((int) (input.right / tileSize.width) + 1) * tileSize.width; + float maxY = ((int) (input.bottom / tileSize.height) + 1) * tileSize.height; + return new RectF(minX, minY, maxX, maxY); + } + + protected static RectF inflate(RectF rect, IntSize inflateSize) { + RectF newRect = new RectF(rect); + newRect.left -= inflateSize.width; + newRect.left = newRect.left < 0.0f ? 0.0f : newRect.left; + + newRect.top -= inflateSize.height; + newRect.top = newRect.top < 0.0f ? 0.0f : newRect.top; + + newRect.right += inflateSize.width; + newRect.bottom += inflateSize.height; + + return newRect; + } + + protected static RectF normalizeRect(RectF rect, float sourceFactor, float targetFactor) { + return new RectF( + (rect.left / sourceFactor) * targetFactor, + (rect.top / sourceFactor) * targetFactor, + (rect.right / sourceFactor) * targetFactor, + (rect.bottom / sourceFactor) * targetFactor); + } + + public void invalidate() { + tilesReadLock.lock(); + for (SubTile tile : tiles) { + tile.invalidate(); + } + tilesReadLock.unlock(); + } + + @Override + public void beginTransaction() { + super.beginTransaction(); + tilesReadLock.lock(); + for (SubTile tile : tiles) { + tile.beginTransaction(); + } + tilesReadLock.unlock(); + } + + @Override + public void endTransaction() { + tilesReadLock.lock(); + for (SubTile tile : tiles) { + tile.endTransaction(); + } + tilesReadLock.unlock(); + super.endTransaction(); + } + + @Override + public void draw(RenderContext context) { + tilesReadLock.lock(); + for (SubTile tile : tiles) { + if (RectF.intersects(tile.getBounds(context), context.viewport)) { + tile.draw(context); + } + } + tilesReadLock.unlock(); + } + + @Override + protected void performUpdates(RenderContext context) { + super.performUpdates(context); + + tilesReadLock.lock(); + for (SubTile tile : tiles) { + tile.beginTransaction(); + tile.refreshTileMetrics(); + tile.endTransaction(); + tile.performUpdates(context); + } + tilesReadLock.unlock(); + } + + @Override + public Region getValidRegion(RenderContext context) { + Region validRegion = new Region(); + tilesReadLock.lock(); + for (SubTile tile : tiles) { + validRegion.op(tile.getValidRegion(context), Region.Op.UNION); + } + tilesReadLock.unlock(); + return validRegion; + } + + @Override + public void setResolution(float newResolution) { + super.setResolution(newResolution); + tilesReadLock.lock(); + for (SubTile tile : tiles) { + tile.setResolution(newResolution); + } + tilesReadLock.unlock(); + } + + public void reevaluateTiles(ImmutableViewportMetrics viewportMetrics, DisplayPortMetrics mDisplayPort) { + RectF newViewPort = getViewPort(viewportMetrics); + float newZoom = getZoom(viewportMetrics); + + // When + if (newZoom <= 0.0 || Float.isNaN(newZoom)) { + return; + } + + if (currentViewport.equals(newViewPort) && FloatUtils.fuzzyEquals(currentZoom, newZoom)) { + return; + } + + long currentReevaluationNanoTime = System.nanoTime(); + if ((currentReevaluationNanoTime - reevaluationNanoTime) < 25 * 1000000) { + return; + } + + reevaluationNanoTime = currentReevaluationNanoTime; + + currentViewport = newViewPort; + currentZoom = newZoom; + currentPageRect = viewportMetrics.getPageRect(); + + LOKitShell.sendTileReevaluationRequest(this); + } + + protected abstract RectF getViewPort(ImmutableViewportMetrics viewportMetrics); + + protected abstract float getZoom(ImmutableViewportMetrics viewportMetrics); + + protected abstract int getTilePriority(); + + private boolean containsTilesMatching(float x, float y, float currentZoom) { + tilesReadLock.lock(); + try { + for (SubTile tile : tiles) { + if (tile.id.x == x && tile.id.y == y && tile.id.zoom == currentZoom) { + return true; + } + } + return false; + } finally { + tilesReadLock.unlock(); + } + } + + public void addNewTiles(List<SubTile> newTiles) { + for (float y = currentViewport.top; y < currentViewport.bottom; y += tileSize.height) { + if (y > currentPageRect.height()) { + continue; + } + for (float x = currentViewport.left; x < currentViewport.right; x += tileSize.width) { + if (x > currentPageRect.width()) { + continue; + } + if (!containsTilesMatching(x, y, currentZoom)) { + TileIdentifier tileId = new TileIdentifier((int) x, (int) y, currentZoom, tileSize); + SubTile tile = createNewTile(tileId); + newTiles.add(tile); + } + } + } + } + + public void clearMarkedTiles() { + tilesWriteLock.lock(); + Iterator<SubTile> iterator = tiles.iterator(); + while (iterator.hasNext()) { + SubTile tile = iterator.next(); + if (tile.markedForRemoval) { + tile.destroy(); + iterator.remove(); + } + } + tilesWriteLock.unlock(); + } + + public void markTiles() { + tilesReadLock.lock(); + for (SubTile tile : tiles) { + if (FloatUtils.fuzzyEquals(tile.id.zoom, currentZoom)) { + RectF tileRect = tile.id.getRectF(); + if (!RectF.intersects(currentViewport, tileRect)) { + tile.markForRemoval(); + } + } else { + tile.markForRemoval(); + } + } + tilesReadLock.unlock(); + } + + public void clearAndReset() { + tilesWriteLock.lock(); + tiles.clear(); + tilesWriteLock.unlock(); + currentViewport = new RectF(); + } + + private SubTile createNewTile(TileIdentifier tileId) { + SubTile tile = new SubTile(tileId); + tile.beginTransaction(); + tilesWriteLock.lock(); + tiles.add(tile); + tilesWriteLock.unlock(); + return tile; + } + + public boolean isStillValid(TileIdentifier tileId) { + return RectF.intersects(currentViewport, tileId.getRectF()) || currentViewport.contains(tileId.getRectF()); + } + + /** + * Invalidate tiles which intersect the input rect + */ + public void invalidateTiles(List<SubTile> tilesToInvalidate, RectF cssRect) { + RectF zoomedRect = RectUtils.scale(cssRect, currentZoom); + tilesReadLock.lock(); + for (SubTile tile : tiles) { + if (!tile.markedForRemoval && RectF.intersects(zoomedRect, tile.id.getRectF())) { + tilesToInvalidate.add(tile); + } + } + tilesReadLock.unlock(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + } + + @Override + public void onLowMemory() { + Log.i(LOGTAG, "onLowMemory"); + } + + @Override + public void onTrimMemory(int level) { + if (level >= 15 /*TRIM_MEMORY_RUNNING_CRITICAL*/) { + Log.i(LOGTAG, "Trimming memory - TRIM_MEMORY_RUNNING_CRITICAL"); + } else if (level >= 10 /*TRIM_MEMORY_RUNNING_LOW*/) { + Log.i(LOGTAG, "Trimming memory - TRIM_MEMORY_RUNNING_LOW"); + } + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java b/android/source/src/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java new file mode 100644 index 0000000000..d98efa2d50 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java @@ -0,0 +1,760 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.PointF; +import android.graphics.RectF; +import android.util.Log; + +import org.json.JSONArray; +import org.libreoffice.LOKitShell; +import org.libreoffice.LibreOfficeMainActivity; +import org.mozilla.gecko.util.FloatUtils; + +import java.util.Map; + +final class DisplayPortCalculator { + private static final String LOGTAG = DisplayPortCalculator.class.getSimpleName(); + private static final PointF ZERO_VELOCITY = new PointF(0, 0); + + // Keep this in sync with the TILEDLAYERBUFFER_TILE_SIZE defined in gfx/layers/TiledLayerBuffer.h + private static final int TILE_SIZE = 256; + + private static final String PREF_DISPLAYPORT_STRATEGY = "gfx.displayport.strategy"; + private static final String PREF_DISPLAYPORT_FM_MULTIPLIER = "gfx.displayport.strategy_fm.multiplier"; + private static final String PREF_DISPLAYPORT_FM_DANGER_X = "gfx.displayport.strategy_fm.danger_x"; + private static final String PREF_DISPLAYPORT_FM_DANGER_Y = "gfx.displayport.strategy_fm.danger_y"; + private static final String PREF_DISPLAYPORT_VB_MULTIPLIER = "gfx.displayport.strategy_vb.multiplier"; + private static final String PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_vb.threshold"; + private static final String PREF_DISPLAYPORT_VB_REVERSE_BUFFER = "gfx.displayport.strategy_vb.reverse_buffer"; + private static final String PREF_DISPLAYPORT_VB_DANGER_X_BASE = "gfx.displayport.strategy_vb.danger_x_base"; + private static final String PREF_DISPLAYPORT_VB_DANGER_Y_BASE = "gfx.displayport.strategy_vb.danger_y_base"; + private static final String PREF_DISPLAYPORT_VB_DANGER_X_INCR = "gfx.displayport.strategy_vb.danger_x_incr"; + private static final String PREF_DISPLAYPORT_VB_DANGER_Y_INCR = "gfx.displayport.strategy_vb.danger_y_incr"; + private static final String PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD = "gfx.displayport.strategy_pb.threshold"; + + private DisplayPortStrategy sStrategy; + private final LibreOfficeMainActivity mMainActivity; + + DisplayPortCalculator(LibreOfficeMainActivity context) { + this.mMainActivity = context; + sStrategy = new VelocityBiasStrategy(mMainActivity, null); + } + + DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { + return sStrategy.calculate(metrics, (velocity == null ? ZERO_VELOCITY : velocity)); + } + + boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { + if (displayPort == null) { + return true; + } + return sStrategy.aboutToCheckerboard(metrics, (velocity == null ? ZERO_VELOCITY : velocity), displayPort); + } + + boolean drawTimeUpdate(long millis, int pixels) { + return sStrategy.drawTimeUpdate(millis, pixels); + } + + void resetPageState() { + sStrategy.resetPageState(); + } + + static void addPrefNames(JSONArray prefs) { + prefs.put(PREF_DISPLAYPORT_STRATEGY); + prefs.put(PREF_DISPLAYPORT_FM_MULTIPLIER); + prefs.put(PREF_DISPLAYPORT_FM_DANGER_X); + prefs.put(PREF_DISPLAYPORT_FM_DANGER_Y); + prefs.put(PREF_DISPLAYPORT_VB_MULTIPLIER); + prefs.put(PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD); + prefs.put(PREF_DISPLAYPORT_VB_REVERSE_BUFFER); + prefs.put(PREF_DISPLAYPORT_VB_DANGER_X_BASE); + prefs.put(PREF_DISPLAYPORT_VB_DANGER_Y_BASE); + prefs.put(PREF_DISPLAYPORT_VB_DANGER_X_INCR); + prefs.put(PREF_DISPLAYPORT_VB_DANGER_Y_INCR); + prefs.put(PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD); + } + + /** + * Set the active strategy to use. + * See the gfx.displayport.strategy pref in mobile/android/app/mobile.js to see the + * mapping between ints and strategies. + */ + boolean setStrategy(Map<String, Integer> prefs) { + Integer strategy = prefs.get(PREF_DISPLAYPORT_STRATEGY); + if (strategy == null) { + return false; + } + + switch (strategy) { + case 0: + sStrategy = new FixedMarginStrategy(prefs); + break; + case 1: + sStrategy = new VelocityBiasStrategy(mMainActivity, prefs); + break; + case 2: + sStrategy = new DynamicResolutionStrategy(mMainActivity, prefs); + break; + case 3: + sStrategy = new NoMarginStrategy(prefs); + break; + case 4: + sStrategy = new PredictionBiasStrategy(mMainActivity, prefs); + break; + default: + Log.e(LOGTAG, "Invalid strategy index specified"); + return false; + } + Log.i(LOGTAG, "Set strategy " + sStrategy.toString()); + return true; + } + + private static float getFloatPref(Map<String, Integer> prefs, String prefName, int defaultValue) { + Integer value = (prefs == null ? null : prefs.get(prefName)); + return (float)(value == null || value < 0 ? defaultValue : value) / 1000f; + } + + private static abstract class DisplayPortStrategy { + /** Calculates a displayport given a viewport and panning velocity. */ + public abstract DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity); + /** Returns true if a checkerboard is about to be visible and we should not throttle drawing. */ + public abstract boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort); + /** Notify the strategy of a new recorded draw time. Return false to turn off draw time recording. */ + public boolean drawTimeUpdate(long millis, int pixels) { return false; } + /** Reset any page-specific state stored, as the page being displayed has changed. */ + public void resetPageState() {} + } + + /** + * Return the dimensions for a rect that has area (width*height) that does not exceed the page size in the + * given metrics object. The area in the returned FloatSize may be less than width*height if the page is + * small, but it will never be larger than width*height. + * Note that this process may change the relative aspect ratio of the given dimensions. + */ + private static FloatSize reshapeForPage(float width, float height, ImmutableViewportMetrics metrics) { + // figure out how much of the desired buffer amount we can actually use on the horizontal axis + float usableWidth = Math.min(width, metrics.getPageWidth()); + // if we reduced the buffer amount on the horizontal axis, we should take that saved memory and + // use it on the vertical axis + float extraUsableHeight = (float)Math.floor(((width - usableWidth) * height) / usableWidth); + float usableHeight = Math.min(height + extraUsableHeight, metrics.getPageHeight()); + if (usableHeight < height && usableWidth == width) { + // and the reverse - if we shrunk the buffer on the vertical axis we can add it to the horizontal + float extraUsableWidth = (float)Math.floor(((height - usableHeight) * width) / usableHeight); + usableWidth = Math.min(width + extraUsableWidth, metrics.getPageWidth()); + } + return new FloatSize(usableWidth, usableHeight); + } + + /** + * Expand the given rect in all directions by a "danger zone". The size of the danger zone on an axis + * is the size of the view on that axis multiplied by the given multiplier. The expanded rect is then + * clamped to page bounds and returned. + */ + private static RectF expandByDangerZone(RectF rect, float dangerZoneXMultiplier, float dangerZoneYMultiplier, ImmutableViewportMetrics metrics) { + // calculate the danger zone amounts in pixels + float dangerZoneX = metrics.getWidth() * dangerZoneXMultiplier; + float dangerZoneY = metrics.getHeight() * dangerZoneYMultiplier; + rect = RectUtils.expand(rect, dangerZoneX, dangerZoneY); + // clamp to page bounds + return clampToPageBounds(rect, metrics); + } + + /** + * Expand the given margins such that when they are applied on the viewport, the resulting rect + * does not have any partial tiles, except when it is clipped by the page bounds. This assumes + * the tiles are TILE_SIZE by TILE_SIZE and start at the origin, such that there will always be + * a tile at (0,0)-(TILE_SIZE,TILE_SIZE)). + */ + private static DisplayPortMetrics getTileAlignedDisplayPortMetrics(RectF margins, float zoom, ImmutableViewportMetrics metrics) { + float left = metrics.viewportRectLeft - margins.left; + float top = metrics.viewportRectTop - margins.top; + float right = metrics.viewportRectRight + margins.right; + float bottom = metrics.viewportRectBottom + margins.bottom; + left = (float) Math.max(metrics.pageRectLeft, TILE_SIZE * Math.floor(left / TILE_SIZE)); + top = (float) Math.max(metrics.pageRectTop, TILE_SIZE * Math.floor(top / TILE_SIZE)); + right = (float) Math.min(metrics.pageRectRight, TILE_SIZE * Math.ceil(right / TILE_SIZE)); + bottom = (float) Math.min(metrics.pageRectBottom, TILE_SIZE * Math.ceil(bottom / TILE_SIZE)); + return new DisplayPortMetrics(left, top, right, bottom, zoom); + } + + /** + * Adjust the given margins so if they are applied on the viewport in the metrics, the resulting rect + * does not exceed the page bounds. This code will maintain the total margin amount for a given axis; + * it assumes that margins.left + metrics.getWidth() + margins.right is less than or equal to + * metrics.getPageWidth(); and the same for the y axis. + */ + private static RectF shiftMarginsForPageBounds(RectF margins, ImmutableViewportMetrics metrics) { + // check how much we're overflowing in each direction. note that at most one of leftOverflow + // and rightOverflow can be greater than zero, and at most one of topOverflow and bottomOverflow + // can be greater than zero, because of the assumption described in the method javadoc. + float leftOverflow = metrics.pageRectLeft - (metrics.viewportRectLeft - margins.left); + float rightOverflow = (metrics.viewportRectRight + margins.right) - metrics.pageRectRight; + float topOverflow = metrics.pageRectTop - (metrics.viewportRectTop - margins.top); + float bottomOverflow = (metrics.viewportRectBottom + margins.bottom) - metrics.pageRectBottom; + + // if the margins overflow the page bounds, shift them to other side on the same axis + if (leftOverflow > 0) { + margins.left -= leftOverflow; + margins.right += leftOverflow; + } else if (rightOverflow > 0) { + margins.right -= rightOverflow; + margins.left += rightOverflow; + } + if (topOverflow > 0) { + margins.top -= topOverflow; + margins.bottom += topOverflow; + } else if (bottomOverflow > 0) { + margins.bottom -= bottomOverflow; + margins.top += bottomOverflow; + } + return margins; + } + + /** + * Clamp the given rect to the page bounds and return it. + */ + private static RectF clampToPageBounds(RectF rect, ImmutableViewportMetrics metrics) { + if (rect.top < metrics.pageRectTop) rect.top = metrics.pageRectTop; + if (rect.left < metrics.pageRectLeft) rect.left = metrics.pageRectLeft; + if (rect.right > metrics.pageRectRight) rect.right = metrics.pageRectRight; + if (rect.bottom > metrics.pageRectBottom) rect.bottom = metrics.pageRectBottom; + return rect; + } + + /** + * This class implements the variation where we basically don't bother with a display port. + */ + private static class NoMarginStrategy extends DisplayPortStrategy { + NoMarginStrategy(Map<String, Integer> prefs) { + // no prefs in this strategy + } + + public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { + return new DisplayPortMetrics(metrics.viewportRectLeft, + metrics.viewportRectTop, + metrics.viewportRectRight, + metrics.viewportRectBottom, + metrics.zoomFactor); + } + + public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { + return true; + } + + @Override + public String toString() { + return "NoMarginStrategy"; + } + } + + /** + * This class implements the variation where we use a fixed-size margin on the display port. + * The margin is always 300 pixels in all directions, except when we are (a) approaching a page + * boundary, and/or (b) if we are limited by the page size. In these cases we try to maintain + * the area of the display port by (a) shifting the buffer to the other side on the same axis, + * and/or (b) increasing the buffer on the other axis to compensate for the reduced buffer on + * one axis. + */ + private static class FixedMarginStrategy extends DisplayPortStrategy { + // The length of each axis of the display port will be the corresponding view length + // multiplied by this factor. + private final float SIZE_MULTIPLIER; + + // If the visible rect is within the danger zone (measured as a fraction of the view size + // from the edge of the displayport) we start redrawing to minimize checkerboarding. + private final float DANGER_ZONE_X_MULTIPLIER; + private final float DANGER_ZONE_Y_MULTIPLIER; + + FixedMarginStrategy(Map<String, Integer> prefs) { + SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_MULTIPLIER, 2000); + DANGER_ZONE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_X, 100); + DANGER_ZONE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_FM_DANGER_Y, 200); + } + + public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { + float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER; + float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER; + + // we need to avoid having a display port that is larger than the page, or we will end up + // painting things outside the page bounds (bug 729169). we simultaneously need to make + // the display port as large as possible so that we redraw less. reshape the display + // port dimensions to accomplish this. + FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics); + float horizontalBuffer = usableSize.width - metrics.getWidth(); + float verticalBuffer = usableSize.height - metrics.getHeight(); + + // and now calculate the display port margins based on how much buffer we've decided to use and + // the page bounds, ensuring we use all of the available buffer amounts on one side or the other + // on any given axis. (i.e. if we're scrolled to the top of the page, the vertical buffer is + // entirely below the visible viewport, but if we're halfway down the page, the vertical buffer + // is split). + RectF margins = new RectF(); + margins.left = horizontalBuffer / 2.0f; + margins.right = horizontalBuffer - margins.left; + margins.top = verticalBuffer / 2.0f; + margins.bottom = verticalBuffer - margins.top; + margins = shiftMarginsForPageBounds(margins, metrics); + + return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics); + } + + public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { + // Increase the size of the viewport based on the danger zone multiplier (and clamp to page + // boundaries), and intersect it with the current displayport to determine whether we're + // close to checkerboarding. + RectF adjustedViewport = expandByDangerZone(metrics.getViewport(), DANGER_ZONE_X_MULTIPLIER, DANGER_ZONE_Y_MULTIPLIER, metrics); + return !displayPort.contains(adjustedViewport); + } + + @Override + public String toString() { + return "FixedMarginStrategy mult=" + SIZE_MULTIPLIER + ", dangerX=" + DANGER_ZONE_X_MULTIPLIER + ", dangerY=" + DANGER_ZONE_Y_MULTIPLIER; + } + } + + /** + * This class implements the variation with a small fixed-size margin with velocity bias. + * In this variation, the default margins are pretty small relative to the view size, but + * they are affected by the panning velocity. Specifically, if we are panning on one axis, + * we remove the margins on the other axis because we are likely axis-locked. Also once + * we are panning in one direction above a certain threshold velocity, we shift the buffer + * so that it is almost entirely in the direction of the pan, with a little bit in the + * reverse direction. + */ + private static class VelocityBiasStrategy extends DisplayPortStrategy { + // The length of each axis of the display port will be the corresponding view length + // multiplied by this factor. + private final float SIZE_MULTIPLIER; + // The velocity above which we apply the velocity bias + private final float VELOCITY_THRESHOLD; + // How much of the buffer to keep in the reverse direction of the velocity + private final float REVERSE_BUFFER; + // If the visible rect is within the danger zone we start redrawing to minimize + // checkerboarding. the danger zone amount is a linear function of the form: + // viewportsize * (base + velocity * incr) + // where base and incr are configurable values. + private final float DANGER_ZONE_BASE_X_MULTIPLIER; + private final float DANGER_ZONE_BASE_Y_MULTIPLIER; + private final float DANGER_ZONE_INCR_X_MULTIPLIER; + private final float DANGER_ZONE_INCR_Y_MULTIPLIER; + + VelocityBiasStrategy(LibreOfficeMainActivity context, Map<String, Integer> prefs) { + SIZE_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_MULTIPLIER, 2000); + VELOCITY_THRESHOLD = LOKitShell.getDpi(context) * getFloatPref(prefs, PREF_DISPLAYPORT_VB_VELOCITY_THRESHOLD, 32); + REVERSE_BUFFER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_REVERSE_BUFFER, 200); + DANGER_ZONE_BASE_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_BASE, 1000); + DANGER_ZONE_BASE_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_BASE, 1000); + DANGER_ZONE_INCR_X_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_X_INCR, 0); + DANGER_ZONE_INCR_Y_MULTIPLIER = getFloatPref(prefs, PREF_DISPLAYPORT_VB_DANGER_Y_INCR, 0); + } + + /** + * Split the given amounts into margins based on the VELOCITY_THRESHOLD and REVERSE_BUFFER values. + * If the velocity is above the VELOCITY_THRESHOLD on an axis, split the amount into REVERSE_BUFFER + * and 1.0 - REVERSE_BUFFER fractions. The REVERSE_BUFFER fraction is set as the margin in the + * direction opposite to the velocity, and the remaining fraction is set as the margin in the direction + * of the velocity. If the velocity is lower than VELOCITY_THRESHOLD, split the amount evenly into the + * two margins on that axis. + */ + private RectF velocityBiasedMargins(float xAmount, float yAmount, PointF velocity) { + RectF margins = new RectF(); + + if (velocity.x > VELOCITY_THRESHOLD) { + margins.left = xAmount * REVERSE_BUFFER; + } else if (velocity.x < -VELOCITY_THRESHOLD) { + margins.left = xAmount * (1.0f - REVERSE_BUFFER); + } else { + margins.left = xAmount / 2.0f; + } + margins.right = xAmount - margins.left; + + if (velocity.y > VELOCITY_THRESHOLD) { + margins.top = yAmount * REVERSE_BUFFER; + } else if (velocity.y < -VELOCITY_THRESHOLD) { + margins.top = yAmount * (1.0f - REVERSE_BUFFER); + } else { + margins.top = yAmount / 2.0f; + } + margins.bottom = yAmount - margins.top; + + return margins; + } + + public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { + float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER; + float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER; + + // but if we're panning on one axis, set the margins for the other axis to zero since we are likely + // axis locked and won't be displaying that extra area. + if (Math.abs(velocity.x) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.y, 0)) { + displayPortHeight = metrics.getHeight(); + } else if (Math.abs(velocity.y) > VELOCITY_THRESHOLD && FloatUtils.fuzzyEquals(velocity.x, 0)) { + displayPortWidth = metrics.getWidth(); + } + + // we need to avoid having a display port that is larger than the page, or we will end up + // painting things outside the page bounds (bug 729169). + displayPortWidth = Math.min(displayPortWidth, metrics.getPageWidth()); + displayPortHeight = Math.min(displayPortHeight, metrics.getPageHeight()); + float horizontalBuffer = displayPortWidth - metrics.getWidth(); + float verticalBuffer = displayPortHeight - metrics.getHeight(); + + // split the buffer amounts into margins based on velocity, and shift it to + // take into account the page bounds + RectF margins = velocityBiasedMargins(horizontalBuffer, verticalBuffer, velocity); + margins = shiftMarginsForPageBounds(margins, metrics); + + return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics); + } + + public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { + // calculate the danger zone amounts based on the prefs + float dangerZoneX = metrics.getWidth() * (DANGER_ZONE_BASE_X_MULTIPLIER + (velocity.x * DANGER_ZONE_INCR_X_MULTIPLIER)); + float dangerZoneY = metrics.getHeight() * (DANGER_ZONE_BASE_Y_MULTIPLIER + (velocity.y * DANGER_ZONE_INCR_Y_MULTIPLIER)); + // clamp it such that when added to the viewport, they don't exceed page size. + // this is a prerequisite to calling shiftMarginsForPageBounds as we do below. + dangerZoneX = Math.min(dangerZoneX, metrics.getPageWidth() - metrics.getWidth()); + dangerZoneY = Math.min(dangerZoneY, metrics.getPageHeight() - metrics.getHeight()); + + // split the danger zone into margins based on velocity, and ensure it doesn't exceed + // page bounds. + RectF dangerMargins = velocityBiasedMargins(dangerZoneX, dangerZoneY, velocity); + dangerMargins = shiftMarginsForPageBounds(dangerMargins, metrics); + + // we're about to checkerboard if the current viewport area + the danger zone margins + // fall out of the current displayport anywhere. + RectF adjustedViewport = new RectF( + metrics.viewportRectLeft - dangerMargins.left, + metrics.viewportRectTop - dangerMargins.top, + metrics.viewportRectRight + dangerMargins.right, + metrics.viewportRectBottom + dangerMargins.bottom); + return !displayPort.contains(adjustedViewport); + } + + @Override + public String toString() { + return "VelocityBiasStrategy mult=" + SIZE_MULTIPLIER + ", threshold=" + VELOCITY_THRESHOLD + ", reverse=" + REVERSE_BUFFER + + ", dangerBaseX=" + DANGER_ZONE_BASE_X_MULTIPLIER + ", dangerBaseY=" + DANGER_ZONE_BASE_Y_MULTIPLIER + + ", dangerIncrX=" + DANGER_ZONE_INCR_Y_MULTIPLIER + ", dangerIncrY=" + DANGER_ZONE_INCR_Y_MULTIPLIER; + } + } + + /** + * This class implements the variation where we draw more of the page at low resolution while panning. + * In this variation, as we pan faster, we increase the page area we are drawing, but reduce the draw + * resolution to compensate. This results in the same device-pixel area drawn; the compositor then + * scales this up to the viewport zoom level. This results in a large area of the page drawn but it + * looks blurry. The assumption is that drawing extra that we never display is better than checkerboarding, + * where we draw less but never even show it on the screen. + */ + private static class DynamicResolutionStrategy extends DisplayPortStrategy { + + // The velocity above which we start zooming out the display port to keep up + // with the panning. + private final float VELOCITY_EXPANSION_THRESHOLD; + + + DynamicResolutionStrategy(LibreOfficeMainActivity context, Map<String, Integer> prefs) { + // ignore prefs for now + VELOCITY_EXPANSION_THRESHOLD = LOKitShell.getDpi(context) / 16f; + VELOCITY_FAST_THRESHOLD = VELOCITY_EXPANSION_THRESHOLD * 2.0f; + } + + // The length of each axis of the display port will be the corresponding view length + // multiplied by this factor. + private static final float SIZE_MULTIPLIER = 1.5f; + + // How much we increase the display port based on velocity. Assuming no friction and + // splitting (see below), this should be the number of frames (@60fps) between us + // calculating the display port and the draw of the *next* display port getting composited + // and displayed on the screen. This is because the timeline looks like this: + // Java: pan pan pan pan pan pan ! pan pan pan pan pan pan ! + // Gecko: \-> draw -> composite / \-> draw -> composite / + // The display port calculated on the first "pan" gets composited to the screen at the + // first exclamation mark, and remains on the screen until the second exclamation mark. + // In order to avoid checkerboarding, that display port must be able to contain all of + // the panning until the second exclamation mark, which encompasses two entire draw/composite + // cycles. + // If we take into account friction, our velocity multiplier should be reduced as the + // amount of pan will decrease each time. If we take into account display port splitting, + // it should be increased as the splitting means some of the display port will be used to + // draw in the opposite direction of the velocity. For now I'm assuming these two cancel + // each other out. + private static final float VELOCITY_MULTIPLIER = 60.0f; + + // The following constants adjust how biased the display port is in the direction of panning. + // When panning fast (above the FAST_THRESHOLD) we use the fast split factor to split the + // display port "buffer" area, otherwise we use the slow split factor. This is based on the + // assumption that if the user is panning fast, they are less likely to reverse directions + // and go backwards, so we should spend more of our display port buffer in the direction of + // panning. + private final float VELOCITY_FAST_THRESHOLD; + private static final float FAST_SPLIT_FACTOR = 0.95f; + private static final float SLOW_SPLIT_FACTOR = 0.8f; + + // The following constants are used for viewport prediction; we use them to estimate where + // the viewport will be soon and whether or not we should trigger a draw right now. "soon" + // in the previous sentence really refers to the amount of time it would take to draw and + // composite from the point at which we do the calculation, and that is not really a known + // quantity. The velocity multiplier is how much we multiply the velocity by; it has the + // same caveats as the VELOCITY_MULTIPLIER above except that it only needs to take into account + // one draw/composite cycle instead of two. The danger zone multiplier is a multiplier of the + // viewport size that we use as an extra "danger zone" around the viewport; if this danger + // zone falls outside the display port then we are approaching the point at which we will + // checkerboard, and hence should start drawing. Note that if DANGER_ZONE_MULTIPLIER is + // greater than (SIZE_MULTIPLIER - 1.0f), then at zero velocity we will always be in the + // danger zone, and thus will be constantly drawing. + private static final float PREDICTION_VELOCITY_MULTIPLIER = 30.0f; + private static final float DANGER_ZONE_MULTIPLIER = 0.20f; // must be less than (SIZE_MULTIPLIER - 1.0f) + + public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { + float displayPortWidth = metrics.getWidth() * SIZE_MULTIPLIER; + float displayPortHeight = metrics.getHeight() * SIZE_MULTIPLIER; + + // for resolution calculation purposes, we need to know what the adjusted display port dimensions + // would be if we had zero velocity, so calculate that here before we increase the display port + // based on velocity. + FloatSize reshapedSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics); + + // increase displayPortWidth and displayPortHeight based on the velocity, but maintaining their + // relative aspect ratio. + if (velocity.length() > VELOCITY_EXPANSION_THRESHOLD) { + float velocityFactor = Math.max(Math.abs(velocity.x) / displayPortWidth, + Math.abs(velocity.y) / displayPortHeight); + velocityFactor *= VELOCITY_MULTIPLIER; + + displayPortWidth += (displayPortWidth * velocityFactor); + displayPortHeight += (displayPortHeight * velocityFactor); + } + + // at this point, displayPortWidth and displayPortHeight are how much of the page (in device pixels) + // we want to be rendered by Gecko. Note here "device pixels" is equivalent to CSS pixels multiplied + // by metrics.zoomFactor + + // we need to avoid having a display port that is larger than the page, or we will end up + // painting things outside the page bounds (bug 729169). we simultaneously need to make + // the display port as large as possible so that we redraw less. reshape the display + // port dimensions to accomplish this. this may change the aspect ratio of the display port, + // but we are assuming that this is desirable because the advantages from pre-drawing will + // outweigh the disadvantages from any buffer reallocations that might occur. + FloatSize usableSize = reshapeForPage(displayPortWidth, displayPortHeight, metrics); + float horizontalBuffer = usableSize.width - metrics.getWidth(); + float verticalBuffer = usableSize.height - metrics.getHeight(); + + // at this point, horizontalBuffer and verticalBuffer are the dimensions of the buffer area we have. + // the buffer area is the off-screen area that is part of the display port and will be pre-drawn in case + // the user scrolls there. we now need to split the buffer area on each axis so that we know + // what the exact margins on each side will be. first we split the buffer amount based on the direction + // we're moving, so that we have a larger buffer in the direction of travel. + RectF margins = new RectF(); + margins.left = splitBufferByVelocity(horizontalBuffer, velocity.x); + margins.right = horizontalBuffer - margins.left; + margins.top = splitBufferByVelocity(verticalBuffer, velocity.y); + margins.bottom = verticalBuffer - margins.top; + + // then, we account for running into the page bounds - so that if we hit the top of the page, we need + // to drop the top margin and move that amount to the bottom margin. + margins = shiftMarginsForPageBounds(margins, metrics); + + // finally, we calculate the resolution we want to render the display port area at. We do this + // so that as we expand the display port area (because of velocity), we reduce the resolution of + // the painted area so as to maintain the size of the buffer Gecko is painting into. we calculate + // the reduction in resolution by comparing the display port size with and without the velocity + // changes applied. + // this effectively means that as we pan faster and faster, the display port grows, but we paint + // at lower resolutions. this paints more area to reduce checkerboard at the cost of increasing + // compositor-scaling and blurriness. Once we stop panning, the blurriness must be entirely gone. + // Note that usable* could be less than base* if we are pinch-zoomed out into overscroll, so we + // clamp it to make sure this doesn't increase our display resolution past metrics.zoomFactor. + float scaleFactor = Math.min(reshapedSize.width / usableSize.width, reshapedSize.height / usableSize.height); + float displayResolution = metrics.zoomFactor * Math.min(1.0f, scaleFactor); + + return new DisplayPortMetrics( + metrics.viewportRectLeft - margins.left, + metrics.viewportRectTop - margins.top, + metrics.viewportRectRight + margins.right, + metrics.viewportRectBottom + margins.bottom, + displayResolution); + } + + /** + * Split the given buffer amount into two based on the velocity. + * Given an amount of total usable buffer on an axis, this will + * return the amount that should be used on the left/top side of + * the axis (the side which a negative velocity vector corresponds + * to). + */ + private float splitBufferByVelocity(float amount, float velocity) { + // if no velocity, so split evenly + if (FloatUtils.fuzzyEquals(velocity, 0)) { + return amount / 2.0f; + } + // if we're moving quickly, assign more of the amount in that direction + // since is less likely that we will reverse direction immediately + if (velocity < -VELOCITY_FAST_THRESHOLD) { + return amount * FAST_SPLIT_FACTOR; + } + if (velocity > VELOCITY_FAST_THRESHOLD) { + return amount * (1.0f - FAST_SPLIT_FACTOR); + } + // if we're moving slowly, then assign less of the amount in that direction + if (velocity < 0) { + return amount * SLOW_SPLIT_FACTOR; + } else { + return amount * (1.0f - SLOW_SPLIT_FACTOR); + } + } + + public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { + // Expand the viewport based on our velocity (and clamp it to page boundaries). + // Then intersect it with the last-requested displayport to determine whether we're + // close to checkerboarding. + + RectF predictedViewport = metrics.getViewport(); + + // first we expand the viewport in the direction we're moving based on some + // multiple of the current velocity. + if (velocity.length() > 0) { + if (velocity.x < 0) { + predictedViewport.left += velocity.x * PREDICTION_VELOCITY_MULTIPLIER; + } else if (velocity.x > 0) { + predictedViewport.right += velocity.x * PREDICTION_VELOCITY_MULTIPLIER; + } + + if (velocity.y < 0) { + predictedViewport.top += velocity.y * PREDICTION_VELOCITY_MULTIPLIER; + } else if (velocity.y > 0) { + predictedViewport.bottom += velocity.y * PREDICTION_VELOCITY_MULTIPLIER; + } + } + + // then we expand the viewport evenly in all directions just to have an extra + // safety zone. this also clamps it to page bounds. + predictedViewport = expandByDangerZone(predictedViewport, DANGER_ZONE_MULTIPLIER, DANGER_ZONE_MULTIPLIER, metrics); + return !displayPort.contains(predictedViewport); + } + + @Override + public String toString() { + return "DynamicResolutionStrategy"; + } + } + + /** + * This class implements the variation where we use the draw time to predict where we will be when + * a draw completes, and draw that instead of where we are now. In this variation, when our panning + * speed drops below a certain threshold, we draw 9 viewports' worth of content so that the user can + * pan in any direction without encountering checkerboarding. + * Once the user is panning, we modify the displayport to encompass an area range of where we think + * the user will be when the draw completes. This heuristic relies on both the estimated draw time + * the panning velocity; unexpected changes in either of these values will cause the heuristic to + * fail and show checkerboard. + */ + private static class PredictionBiasStrategy extends DisplayPortStrategy { + private static float VELOCITY_THRESHOLD; + + private int mPixelArea; // area of the viewport, used in draw time calculations + private int mMinFramesToDraw; // minimum number of frames we take to draw + private int mMaxFramesToDraw; // maximum number of frames we take to draw + + PredictionBiasStrategy(LibreOfficeMainActivity context, Map<String, Integer> prefs) { + VELOCITY_THRESHOLD = LOKitShell.getDpi(context) * getFloatPref(prefs, PREF_DISPLAYPORT_PB_VELOCITY_THRESHOLD, 16); + resetPageState(); + } + + public DisplayPortMetrics calculate(ImmutableViewportMetrics metrics, PointF velocity) { + float width = metrics.getWidth(); + float height = metrics.getHeight(); + mPixelArea = (int)(width * height); + + if (velocity.length() < VELOCITY_THRESHOLD) { + // if we're going slow, expand the displayport to 9x viewport size + RectF margins = new RectF(width, height, width, height); + return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics); + } + + // figure out how far we expect to be + float minDx = velocity.x * mMinFramesToDraw; + float minDy = velocity.y * mMinFramesToDraw; + float maxDx = velocity.x * mMaxFramesToDraw; + float maxDy = velocity.y * mMaxFramesToDraw; + + // figure out how many pixels we will be drawing when we draw the above-calculated range. + // this will be larger than the viewport area. + float pixelsToDraw = (width + Math.abs(maxDx - minDx)) * (height + Math.abs(maxDy - minDy)); + // adjust how far we will get because of the time spent drawing all these extra pixels. this + // will again increase the number of pixels drawn so really we could keep iterating this over + // and over, but once seems enough for now. + maxDx = maxDx * pixelsToDraw / mPixelArea; + maxDy = maxDy * pixelsToDraw / mPixelArea; + + // and finally generate the displayport. the min/max stuff takes care of + // negative velocities as well as positive. + RectF margins = new RectF( + -Math.min(minDx, maxDx), + -Math.min(minDy, maxDy), + Math.max(minDx, maxDx), + Math.max(minDy, maxDy)); + return getTileAlignedDisplayPortMetrics(margins, metrics.zoomFactor, metrics); + } + + public boolean aboutToCheckerboard(ImmutableViewportMetrics metrics, PointF velocity, DisplayPortMetrics displayPort) { + // the code below is the same as in calculate() but is awkward to refactor since it has multiple outputs. + // refer to the comments in calculate() to understand what this is doing. + float minDx = velocity.x * mMinFramesToDraw; + float minDy = velocity.y * mMinFramesToDraw; + float maxDx = velocity.x * mMaxFramesToDraw; + float maxDy = velocity.y * mMaxFramesToDraw; + float pixelsToDraw = (metrics.getWidth() + Math.abs(maxDx - minDx)) * (metrics.getHeight() + Math.abs(maxDy - minDy)); + maxDx = maxDx * pixelsToDraw / mPixelArea; + maxDy = maxDy * pixelsToDraw / mPixelArea; + + // now that we have an idea of how far we will be when the draw completes, take the farthest + // end of that range and see if it falls outside the displayport bounds. if it does, allow + // the draw to go through + RectF predictedViewport = metrics.getViewport(); + predictedViewport.left += maxDx; + predictedViewport.top += maxDy; + predictedViewport.right += maxDx; + predictedViewport.bottom += maxDy; + + predictedViewport = clampToPageBounds(predictedViewport, metrics); + return !displayPort.contains(predictedViewport); + } + + @Override + public boolean drawTimeUpdate(long millis, int pixels) { + // calculate the number of frames it took to draw a viewport-sized area + float normalizedTime = (float)mPixelArea * (float)millis / (float)pixels; + int normalizedFrames = (int)Math.ceil(normalizedTime * 60f / 1000f); + // broaden our range on how long it takes to draw if the draw falls outside + // the range. this allows it to grow gradually. this heuristic may need to + // be tweaked into more of a floating window average or something. + if (normalizedFrames <= mMinFramesToDraw) { + mMinFramesToDraw--; + } else if (normalizedFrames > mMaxFramesToDraw) { + mMaxFramesToDraw++; + } else { + return true; + } + Log.d(LOGTAG, "Widened draw range to [" + mMinFramesToDraw + ", " + mMaxFramesToDraw + "]"); + return true; + } + + @Override + public void resetPageState() { + mMinFramesToDraw = 0; + mMaxFramesToDraw = 2; + } + + @Override + public String toString() { + return "PredictionBiasStrategy threshold=" + VELOCITY_THRESHOLD; + } + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java b/android/source/src/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java new file mode 100644 index 0000000000..f622c44ff9 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java @@ -0,0 +1,67 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.RectF; + +import org.mozilla.gecko.util.FloatUtils; + +/* + * This class keeps track of the area we request Gecko to paint, as well + * as the resolution of the paint. The area may be different from the visible + * area of the page, and the resolution may be different from the resolution + * used in the compositor to render the page. This is so that we can ask Gecko + * to paint a much larger area without using extra memory, and then render some + * subsection of that with compositor scaling. + */ +public final class DisplayPortMetrics { + private final RectF mPosition; + private final float mResolution; + + public RectF getPosition() { + return mPosition; + } + + public float getResolution() { + return mResolution; + } + + public DisplayPortMetrics() { + this(0, 0, 0, 0, 1); + } + + public DisplayPortMetrics(float left, float top, float right, float bottom, float resolution) { + mPosition = new RectF(left, top, right, bottom); + mResolution = resolution; + } + + public boolean contains(RectF rect) { + return mPosition.contains(rect); + } + + public boolean fuzzyEquals(DisplayPortMetrics metrics) { + return RectUtils.fuzzyEquals(mPosition, metrics.mPosition) + && FloatUtils.fuzzyEquals(mResolution, metrics.mResolution); + } + + public String toJSON() { + StringBuffer sb = new StringBuffer(256); + sb.append("{ \"left\": ").append(mPosition.left) + .append(", \"top\": ").append(mPosition.top) + .append(", \"right\": ").append(mPosition.right) + .append(", \"bottom\": ").append(mPosition.bottom) + .append(", \"resolution\": ").append(mResolution) + .append('}'); + return sb.toString(); + } + + @Override + public String toString() { + return "DisplayPortMetrics v=(" + mPosition.left + "," + + mPosition.top + "," + mPosition.right + "," + + mPosition.bottom + ") z=" + mResolution; + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/DynamicTileLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/DynamicTileLayer.java new file mode 100644 index 0000000000..ea95c032e8 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/DynamicTileLayer.java @@ -0,0 +1,30 @@ +package org.mozilla.gecko.gfx; + +import android.content.Context; +import android.graphics.RectF; + +public class DynamicTileLayer extends ComposedTileLayer { + public DynamicTileLayer(Context context) { + super(context); + } + + @Override + protected RectF getViewPort(ImmutableViewportMetrics viewportMetrics) { + RectF rect = viewportMetrics.getViewport(); + return inflate(roundToTileSize(rect, tileSize), getInflateFactor()); + } + + @Override + protected float getZoom(ImmutableViewportMetrics viewportMetrics) { + return viewportMetrics.zoomFactor; + } + + @Override + protected int getTilePriority() { + return 0; + } + + private IntSize getInflateFactor() { + return new IntSize(tileSize.width*2, tileSize.height*4); + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/FixedZoomTileLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/FixedZoomTileLayer.java new file mode 100644 index 0000000000..e86494c20b --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/FixedZoomTileLayer.java @@ -0,0 +1,31 @@ +package org.mozilla.gecko.gfx; + +import android.content.Context; +import android.graphics.RectF; + +public class FixedZoomTileLayer extends ComposedTileLayer { + public FixedZoomTileLayer(Context context) { + super(context); + } + + @Override + protected RectF getViewPort(ImmutableViewportMetrics viewportMetrics) { + float zoom = getZoom(viewportMetrics); + RectF rect = normalizeRect(viewportMetrics.getViewport(), viewportMetrics.zoomFactor, zoom); + return inflate(roundToTileSize(rect, tileSize), getInflateFactor()); + } + + @Override + protected float getZoom(ImmutableViewportMetrics viewportMetrics) { + return 1.0f / 16.0f; + } + + @Override + protected int getTilePriority() { + return -1; + } + + private IntSize getInflateFactor() { + return new IntSize(tileSize.width, tileSize.height*6); + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/FloatSize.java b/android/source/src/java/org/mozilla/gecko/gfx/FloatSize.java new file mode 100644 index 0000000000..7b18373115 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/FloatSize.java @@ -0,0 +1,53 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.util.FloatUtils; + +public class FloatSize { + public final float width, height; + + public FloatSize(FloatSize size) { width = size.width; height = size.height; } + public FloatSize(IntSize size) { width = size.width; height = size.height; } + public FloatSize(float aWidth, float aHeight) { width = aWidth; height = aHeight; } + + public FloatSize(JSONObject json) { + try { + width = (float)json.getDouble("width"); + height = (float)json.getDouble("height"); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { return "(" + width + "," + height + ")"; } + + public boolean isPositive() { + return (width > 0 && height > 0); + } + + public boolean fuzzyEquals(FloatSize size) { + return (FloatUtils.fuzzyEquals(size.width, width) && + FloatUtils.fuzzyEquals(size.height, height)); + } + + public FloatSize scale(float factor) { + return new FloatSize(width * factor, height * factor); + } + + /* + * Returns the size that represents a linear transition between this size and `to` at time `t`, + * which is on the scale [0, 1). + */ + public FloatSize interpolate(FloatSize to, float t) { + return new FloatSize(FloatUtils.interpolate(width, to.width, t), + FloatUtils.interpolate(height, to.height, t)); + } +} + diff --git a/android/source/src/java/org/mozilla/gecko/gfx/GLController.java b/android/source/src/java/org/mozilla/gecko/gfx/GLController.java new file mode 100644 index 0000000000..6a43dd6a87 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/GLController.java @@ -0,0 +1,215 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL; +import javax.microedition.khronos.opengles.GL10; + +public class GLController { + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private static final String LOGTAG = "GeckoGLController"; + + private LayerView mView; + private int mGLVersion; + private int mWidth, mHeight; + + private EGL10 mEGL; + private EGLDisplay mEGLDisplay; + private EGLConfig mEGLConfig; + private EGLContext mEGLContext; + private EGLSurface mEGLSurface; + + private static final int LOCAL_EGL_OPENGL_ES2_BIT = 4; + + private static final int[] CONFIG_SPEC = { + EGL10.EGL_RED_SIZE, 5, + EGL10.EGL_GREEN_SIZE, 6, + EGL10.EGL_BLUE_SIZE, 5, + EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, + EGL10.EGL_RENDERABLE_TYPE, LOCAL_EGL_OPENGL_ES2_BIT, + EGL10.EGL_NONE + }; + + public GLController(LayerView view) { + mView = view; + mGLVersion = 2; + } + + public void setGLVersion(int version) { + mGLVersion = version; + } + + /** You must call this on the same thread you intend to use OpenGL on. */ + public void initGLContext() { + initEGLContext(); + createEGLSurface(); + } + + public void disposeGLContext() { + if (mEGL == null) { + return; + } + + if (!mEGL.eglMakeCurrent(mEGLDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT)) { + throw new GLControllerException("EGL context could not be released! " + + getEGLError()); + } + + if (mEGLSurface != null) { + if (!mEGL.eglDestroySurface(mEGLDisplay, mEGLSurface)) { + throw new GLControllerException("EGL surface could not be destroyed! " + + getEGLError()); + } + + mEGLSurface = null; + } + + if (mEGLContext != null) { + if (!mEGL.eglDestroyContext(mEGLDisplay, mEGLContext)) { + throw new GLControllerException("EGL context could not be destroyed! " + + getEGLError()); + } + + mEGLContext = null; + } + } + + public GL10 getGL() { return (GL10) mEGLContext.getGL(); } + public EGLDisplay getEGLDisplay() { return mEGLDisplay; } + public EGLConfig getEGLConfig() { return mEGLConfig; } + public EGLContext getEGLContext() { return mEGLContext; } + public EGLSurface getEGLSurface() { return mEGLSurface; } + public LayerView getView() { return mView; } + + public boolean hasSurface() { + return mEGLSurface != null; + } + + public boolean swapBuffers() { + return mEGL.eglSwapBuffers(mEGLDisplay, mEGLSurface); + } + + public synchronized int getWidth() { + return mWidth; + } + + public synchronized int getHeight() { + return mHeight; + } + + synchronized void surfaceDestroyed() { + notifyAll(); + } + + synchronized void surfaceChanged(int newWidth, int newHeight) { + mWidth = newWidth; + mHeight = newHeight; + notifyAll(); + } + + private void initEGL() { + mEGL = (EGL10)EGLContext.getEGL(); + + mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) { + throw new GLControllerException("eglGetDisplay() failed"); + } + + int[] version = new int[2]; + if (!mEGL.eglInitialize(mEGLDisplay, version)) { + throw new GLControllerException("eglInitialize() failed " + getEGLError()); + } + + mEGLConfig = chooseConfig(); + } + + private void initEGLContext() { + initEGL(); + + int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, mGLVersion, EGL10.EGL_NONE }; + mEGLContext = mEGL.eglCreateContext(mEGLDisplay, mEGLConfig, EGL10.EGL_NO_CONTEXT, + attribList); + if (mEGLContext == null || mEGLContext == EGL10.EGL_NO_CONTEXT) { + throw new GLControllerException("createContext() failed " + + getEGLError()); + } + + if (mView.getRenderer() != null) { + GL10 gl = (GL10) mEGLContext.getGL(); + mView.getRenderer().onSurfaceCreated(gl, mEGLConfig); + mView.getRenderer().onSurfaceChanged(gl, mWidth, mHeight); + } + } + + private EGLConfig chooseConfig() { + int[] numConfigs = new int[1]; + if (!mEGL.eglChooseConfig(mEGLDisplay, CONFIG_SPEC, null, 0, numConfigs) || + numConfigs[0] <= 0) { + throw new GLControllerException("No available EGL configurations " + + getEGLError()); + } + + EGLConfig[] configs = new EGLConfig[numConfigs[0]]; + if (!mEGL.eglChooseConfig(mEGLDisplay, CONFIG_SPEC, configs, numConfigs[0], numConfigs)) { + throw new GLControllerException("No EGL configuration for that specification " + + getEGLError()); + } + + // Select the first 565 RGB configuration. + int[] red = new int[1], green = new int[1], blue = new int[1]; + for (EGLConfig config : configs) { + mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_RED_SIZE, red); + mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_GREEN_SIZE, green); + mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_BLUE_SIZE, blue); + if (red[0] == 5 && green[0] == 6 && blue[0] == 5) { + return config; + } + } + + // if there's no 565 RGB configuration, select another one that fulfils the specification + return configs[0]; + } + + private void createEGLSurface() { + Object window = mView.getNativeWindow(); + mEGLSurface = mEGL.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, window, null); + if (mEGLSurface == null || mEGLSurface == EGL10.EGL_NO_SURFACE) { + throw new GLControllerException("EGL window surface could not be created! " + + getEGLError()); + } + + if (!mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) { + throw new GLControllerException("EGL surface could not be made into the current " + + "surface! " + getEGLError()); + } + + if (mView.getRenderer() != null) { + GL10 gl = (GL10) mEGLContext.getGL(); + mView.getRenderer().onSurfaceCreated(gl, mEGLConfig); + mView.getRenderer().onSurfaceChanged(gl, mView.getWidth(), mView.getHeight()); + } + } + + private String getEGLError() { + return "Error " + mEGL.eglGetError(); + } + + public static class GLControllerException extends RuntimeException { + public static final long serialVersionUID = 1L; + + GLControllerException(String e) { + super(e); + } + } +} + diff --git a/android/source/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java b/android/source/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java new file mode 100644 index 0000000000..72a96f0bb0 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java @@ -0,0 +1,356 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import org.libreoffice.LibreOfficeMainActivity; +import android.graphics.PointF; +import android.graphics.RectF; +import android.util.DisplayMetrics; + +import org.libreoffice.LOKitShell; +import org.mozilla.gecko.ZoomConstraints; + +import java.util.List; + +public class GeckoLayerClient implements PanZoomTarget { + private static final String LOGTAG = GeckoLayerClient.class.getSimpleName(); + + private LayerRenderer mLayerRenderer; + + private LibreOfficeMainActivity mContext; + private IntSize mScreenSize; + private DisplayPortMetrics mDisplayPort; + + private ComposedTileLayer mLowResLayer; + private ComposedTileLayer mRootLayer; + + private boolean mForceRedraw; + + /* The current viewport metrics. + * This is volatile so that we can read and write to it from different threads. + * We avoid synchronization to make getting the viewport metrics from + * the compositor as cheap as possible. The viewport is immutable so + * we don't need to worry about anyone mutating it while we're reading from it. + * Specifically: + * 1) reading mViewportMetrics from any thread is fine without synchronization + * 2) writing to mViewportMetrics requires synchronizing on the layer controller object + * 3) whenever reading multiple fields from mViewportMetrics without synchronization (i.e. in + * case 1 above) you should always first grab a local copy of the reference, and then use + * that because mViewportMetrics might get reassigned in between reading the different + * fields. */ + private volatile ImmutableViewportMetrics mViewportMetrics; + + private ZoomConstraints mZoomConstraints; + + private boolean mIsReady; + + private PanZoomController mPanZoomController; + private LayerView mView; + private final DisplayPortCalculator mDisplayPortCalculator; + + public GeckoLayerClient(LibreOfficeMainActivity context) { + // we can fill these in with dummy values because they are always written + // to before being read + mContext = context; + mScreenSize = new IntSize(0, 0); + mDisplayPort = new DisplayPortMetrics(); + mDisplayPortCalculator = new DisplayPortCalculator(mContext); + + mForceRedraw = true; + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + mViewportMetrics = new ImmutableViewportMetrics(displayMetrics); + } + + public void setView(LayerView view) { + mView = view; + mPanZoomController = PanZoomController.Factory.create(mContext, this, view); + mView.connect(this); + } + + public void notifyReady() { + mIsReady = true; + + mRootLayer = new DynamicTileLayer(mContext); + mLowResLayer = new FixedZoomTileLayer(mContext); + + mLayerRenderer = new LayerRenderer(mView); + + mView.setLayerRenderer(mLayerRenderer); + + sendResizeEventIfNecessary(false); + mView.requestRender(); + } + + public void destroy() { + mPanZoomController.destroy(); + } + + Layer getRoot() { + return mIsReady ? mRootLayer : null; + } + + Layer getLowResLayer() { + return mIsReady ? mLowResLayer : null; + } + + public LayerView getView() { + return mView; + } + + /** + * Returns true if this controller is fine with performing a redraw operation or false if it + * would prefer that the action didn't take place. + */ + private boolean getRedrawHint() { + if (mForceRedraw) { + mForceRedraw = false; + return true; + } + + if (!mPanZoomController.getRedrawHint()) { + return false; + } + return mDisplayPortCalculator.aboutToCheckerboard(mViewportMetrics, mPanZoomController.getVelocityVector(), getDisplayPort()); + } + + /** + * The view calls this function to indicate that the viewport changed size. It must hold the + * monitor while calling it. + * + * TODO: Refactor this to use an interface. Expose that interface only to the view and not + * to the layer client. That way, the layer client won't be tempted to call this, which might + * result in an infinite loop. + */ + void setViewportSize(FloatSize size, boolean forceResizeEvent) { + mViewportMetrics = mViewportMetrics.setViewportSize(size.width, size.height); + sendResizeEventIfNecessary(forceResizeEvent); + } + + PanZoomController getPanZoomController() { + return mPanZoomController; + } + + /* Informs Gecko that the screen size has changed. + * @param force: If true, a resize event will always be sent, otherwise + * it is only sent if size has changed. */ + private void sendResizeEventIfNecessary(boolean force) { + DisplayMetrics metrics = mContext.getResources().getDisplayMetrics(); + IntSize newScreenSize = new IntSize(metrics.widthPixels, metrics.heightPixels); + + if (!force && mScreenSize.equals(newScreenSize)) { + return; + } + + mScreenSize = newScreenSize; + + LOKitShell.sendSizeChangedEvent(mScreenSize.width, mScreenSize.height); + } + + /** + * Sets the current page rect. You must hold the monitor while calling this. + */ + private void setPageRect(RectF rect, RectF cssRect) { + // Since the "rect" is always just a multiple of "cssRect" we don't need to + // check both; this function assumes that both "rect" and "cssRect" are relative + // the zoom factor in mViewportMetrics. + if (mViewportMetrics.getCssPageRect().equals(cssRect)) + return; + + mViewportMetrics = mViewportMetrics.setPageRect(rect, cssRect); + + // Page size is owned by the layer client, so no need to notify it of + // this change. + + post(new Runnable() { + public void run() { + mPanZoomController.pageRectUpdated(); + mView.requestRender(); + } + }); + } + + private void adjustViewport(DisplayPortMetrics displayPort) { + ImmutableViewportMetrics metrics = getViewportMetrics(); + + ImmutableViewportMetrics clampedMetrics = metrics.clamp(); + + if (displayPort == null) { + displayPort = mDisplayPortCalculator.calculate(metrics, mPanZoomController.getVelocityVector()); + } + + mDisplayPort = displayPort; + + reevaluateTiles(); + } + + /** + * Aborts any pan/zoom animation that is currently in progress. + */ + public void abortPanZoomAnimation() { + if (mPanZoomController != null) { + mView.post(new Runnable() { + public void run() { + mPanZoomController.abortAnimation(); + } + }); + } + } + + public void setZoomConstraints(ZoomConstraints constraints) { + mZoomConstraints = constraints; + } + + /** The compositor invokes this function whenever it determines that the page rect + * has changed (based on the information it gets from layout). If setFirstPaintViewport + * is invoked on a frame, then this function will not be. For any given frame, this + * function will be invoked before syncViewportInfo. + */ + public void setPageRect(float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom) { + synchronized (getLock()) { + RectF cssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom); + float ourZoom = getViewportMetrics().zoomFactor; + setPageRect(RectUtils.scale(cssPageRect, ourZoom), cssPageRect); + // Here the page size of the document has changed, but the document being displayed + // is still the same. Therefore, we don't need to send anything to browser.js; any + // changes we need to make to the display port will get sent the next time we call + // adjustViewport(). + } + } + + private DisplayPortMetrics getDisplayPort() { + return mDisplayPort; + } + + public void beginDrawing() { + mLowResLayer.beginTransaction(); + mRootLayer.beginTransaction(); + } + + public void endDrawing() { + mLowResLayer.endTransaction(); + mRootLayer.endTransaction(); + } + + private void geometryChanged() { + sendResizeEventIfNecessary(false); + if (getRedrawHint()) { + adjustViewport(null); + } + } + + /** Implementation of PanZoomTarget */ + @Override + public ImmutableViewportMetrics getViewportMetrics() { + return mViewportMetrics; + } + + /** Implementation of PanZoomTarget */ + @Override + public ZoomConstraints getZoomConstraints() { + return mZoomConstraints; + } + + /** Implementation of PanZoomTarget */ + @Override + public void setAnimationTarget(ImmutableViewportMetrics viewport) { + if (mIsReady) { + // We know what the final viewport of the animation is going to be, so + // immediately request a draw of that area by setting the display port + // accordingly. This way we should have the content pre-rendered by the + // time the animation is done. + DisplayPortMetrics displayPort = mDisplayPortCalculator.calculate(viewport, null); + adjustViewport(displayPort); + } + } + + /** Implementation of PanZoomTarget + * You must hold the monitor while calling this. + */ + @Override + public void setViewportMetrics(ImmutableViewportMetrics viewport) { + mViewportMetrics = viewport; + mView.requestRender(); + if (mIsReady) { + geometryChanged(); + } + } + + /** Implementation of PanZoomTarget */ + @Override + public void forceRedraw() { + mForceRedraw = true; + if (mIsReady) { + geometryChanged(); + } + } + + /** Implementation of PanZoomTarget */ + @Override + public boolean post(Runnable action) { + return mView.post(action); + } + + /** Implementation of PanZoomTarget */ + @Override + public Object getLock() { + return this; + } + + public PointF convertViewPointToLayerPoint(PointF viewPoint) { + ImmutableViewportMetrics viewportMetrics = mViewportMetrics; + PointF origin = viewportMetrics.getOrigin(); + float zoom = viewportMetrics.zoomFactor; + + return new PointF( + ((viewPoint.x + origin.x) / zoom), + ((viewPoint.y + origin.y) / zoom)); + } + + /** Implementation of PanZoomTarget */ + @Override + public boolean isFullScreen() { + return false; + } + + public void zoomTo(RectF rect) { + if (mPanZoomController instanceof JavaPanZoomController) { + ((JavaPanZoomController) mPanZoomController).animatedZoomTo(rect); + } + } + + /** + * Move the viewport to the desired point, and change the zoom level. + */ + public void moveTo(PointF point, Float zoom) { + if (mPanZoomController instanceof JavaPanZoomController) { + ((JavaPanZoomController) mPanZoomController).animatedMove(point, zoom); + } + } + + public void zoomTo(float pageWidth, float pageHeight) { + zoomTo(new RectF(0, 0, pageWidth, pageHeight)); + } + + public void forceRender() { + mView.requestRender(); + } + + /* Root Layer Access */ + private void reevaluateTiles() { + mLowResLayer.reevaluateTiles(mViewportMetrics, mDisplayPort); + mRootLayer.reevaluateTiles(mViewportMetrics, mDisplayPort); + } + + public void clearAndResetlayers() { + mLowResLayer.clearAndReset(); + mRootLayer.clearAndReset(); + } + + public void invalidateTiles(List<SubTile> tilesToInvalidate, RectF rect) { + mLowResLayer.invalidateTiles(tilesToInvalidate, rect); + mRootLayer.invalidateTiles(tilesToInvalidate, rect); + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java b/android/source/src/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java new file mode 100644 index 0000000000..f90580fbee --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java @@ -0,0 +1,241 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.PointF; +import android.graphics.RectF; +import android.util.DisplayMetrics; + +import org.mozilla.gecko.util.FloatUtils; + +/** + * ImmutableViewportMetrics are used to store the viewport metrics + * in way that we can access a version of them from multiple threads + * without having to take a lock + */ +public class ImmutableViewportMetrics { + + // We need to flatten the RectF and FloatSize structures + // because Java doesn't have the concept of const classes + public final float pageRectLeft; + public final float pageRectTop; + public final float pageRectRight; + public final float pageRectBottom; + public final float cssPageRectLeft; + public final float cssPageRectTop; + public final float cssPageRectRight; + public final float cssPageRectBottom; + public final float viewportRectLeft; + public final float viewportRectTop; + public final float viewportRectRight; + public final float viewportRectBottom; + public final float zoomFactor; + + public ImmutableViewportMetrics(DisplayMetrics metrics) { + viewportRectLeft = pageRectLeft = cssPageRectLeft = 0; + viewportRectTop = pageRectTop = cssPageRectTop = 0; + viewportRectRight = pageRectRight = cssPageRectRight = metrics.widthPixels; + viewportRectBottom = pageRectBottom = cssPageRectBottom = metrics.heightPixels; + zoomFactor = 1.0f; + } + + private ImmutableViewportMetrics(float aPageRectLeft, float aPageRectTop, + float aPageRectRight, float aPageRectBottom, float aCssPageRectLeft, + float aCssPageRectTop, float aCssPageRectRight, float aCssPageRectBottom, + float aViewportRectLeft, float aViewportRectTop, float aViewportRectRight, + float aViewportRectBottom, float aZoomFactor) + { + pageRectLeft = aPageRectLeft; + pageRectTop = aPageRectTop; + pageRectRight = aPageRectRight; + pageRectBottom = aPageRectBottom; + cssPageRectLeft = aCssPageRectLeft; + cssPageRectTop = aCssPageRectTop; + cssPageRectRight = aCssPageRectRight; + cssPageRectBottom = aCssPageRectBottom; + viewportRectLeft = aViewportRectLeft; + viewportRectTop = aViewportRectTop; + viewportRectRight = aViewportRectRight; + viewportRectBottom = aViewportRectBottom; + zoomFactor = aZoomFactor; + } + + public float getWidth() { + return viewportRectRight - viewportRectLeft; + } + + public float getHeight() { + return viewportRectBottom - viewportRectTop; + } + + public PointF getOrigin() { + return new PointF(viewportRectLeft, viewportRectTop); + } + + public FloatSize getSize() { + return new FloatSize(viewportRectRight - viewportRectLeft, viewportRectBottom - viewportRectTop); + } + + public RectF getViewport() { + return new RectF(viewportRectLeft, + viewportRectTop, + viewportRectRight, + viewportRectBottom); + } + + public RectF getCssViewport() { + return RectUtils.scale(getViewport(), 1/zoomFactor); + } + + public RectF getPageRect() { + return new RectF(pageRectLeft, pageRectTop, pageRectRight, pageRectBottom); + } + + public float getPageWidth() { + return pageRectRight - pageRectLeft; + } + + public float getPageHeight() { + return pageRectBottom - pageRectTop; + } + + public RectF getCssPageRect() { + return new RectF(cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom); + } + + public float getZoomFactor() { + return zoomFactor; + } + + /* + * Returns the viewport metrics that represent a linear transition between "this" and "to" at + * time "t", which is on the scale [0, 1). This function interpolates all values stored in + * the viewport metrics. + */ + public ImmutableViewportMetrics interpolate(ImmutableViewportMetrics to, float t) { + return new ImmutableViewportMetrics( + FloatUtils.interpolate(pageRectLeft, to.pageRectLeft, t), + FloatUtils.interpolate(pageRectTop, to.pageRectTop, t), + FloatUtils.interpolate(pageRectRight, to.pageRectRight, t), + FloatUtils.interpolate(pageRectBottom, to.pageRectBottom, t), + FloatUtils.interpolate(cssPageRectLeft, to.cssPageRectLeft, t), + FloatUtils.interpolate(cssPageRectTop, to.cssPageRectTop, t), + FloatUtils.interpolate(cssPageRectRight, to.cssPageRectRight, t), + FloatUtils.interpolate(cssPageRectBottom, to.cssPageRectBottom, t), + FloatUtils.interpolate(viewportRectLeft, to.viewportRectLeft, t), + FloatUtils.interpolate(viewportRectTop, to.viewportRectTop, t), + FloatUtils.interpolate(viewportRectRight, to.viewportRectRight, t), + FloatUtils.interpolate(viewportRectBottom, to.viewportRectBottom, t), + FloatUtils.interpolate(zoomFactor, to.zoomFactor, t)); + } + + public ImmutableViewportMetrics setViewportSize(float width, float height) { + return new ImmutableViewportMetrics( + pageRectLeft, pageRectTop, pageRectRight, pageRectBottom, + cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom, + viewportRectLeft, viewportRectTop, viewportRectLeft + width, viewportRectTop + height, + zoomFactor); + } + + public ImmutableViewportMetrics setViewportOrigin(float newOriginX, float newOriginY) { + return new ImmutableViewportMetrics( + pageRectLeft, pageRectTop, pageRectRight, pageRectBottom, + cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom, + newOriginX, newOriginY, newOriginX + getWidth(), newOriginY + getHeight(), + zoomFactor); + } + + public ImmutableViewportMetrics setZoomFactor(float newZoomFactor) { + return new ImmutableViewportMetrics( + pageRectLeft, pageRectTop, pageRectRight, pageRectBottom, + cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom, + viewportRectLeft, viewportRectTop, viewportRectRight, viewportRectBottom, + newZoomFactor); + } + + public ImmutableViewportMetrics offsetViewportBy(float dx, float dy) { + return setViewportOrigin(viewportRectLeft + dx, viewportRectTop + dy); + } + + public ImmutableViewportMetrics setPageRect(RectF pageRect, RectF cssPageRect) { + return new ImmutableViewportMetrics( + pageRect.left, pageRect.top, pageRect.right, pageRect.bottom, + cssPageRect.left, cssPageRect.top, cssPageRect.right, cssPageRect.bottom, + viewportRectLeft, viewportRectTop, viewportRectRight, viewportRectBottom, + zoomFactor); + } + + /* This will set the zoom factor and re-scale page-size and viewport offset + * accordingly. The given focus will remain at the same point on the screen + * after scaling. + */ + public ImmutableViewportMetrics scaleTo(float newZoomFactor, PointF focus) { + // cssPageRect* is invariant, since we're setting the scale factor + // here. The page rect is based on the CSS page rect. + float newPageRectLeft = cssPageRectLeft * newZoomFactor; + float newPageRectTop = cssPageRectTop * newZoomFactor; + float newPageRectRight = cssPageRectLeft + ((cssPageRectRight - cssPageRectLeft) * newZoomFactor); + float newPageRectBottom = cssPageRectTop + ((cssPageRectBottom - cssPageRectTop) * newZoomFactor); + + PointF origin = getOrigin(); + origin.offset(focus.x, focus.y); + origin = PointUtils.scale(origin, newZoomFactor / zoomFactor); + origin.offset(-focus.x, -focus.y); + + return new ImmutableViewportMetrics( + newPageRectLeft, newPageRectTop, newPageRectRight, newPageRectBottom, + cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom, + origin.x, origin.y, origin.x + getWidth(), origin.y + getHeight(), + newZoomFactor); + } + + /** Clamps the viewport to remain within the page rect. */ + public ImmutableViewportMetrics clamp() { + RectF newViewport = getViewport(); + + // The viewport bounds ought to never exceed the page bounds. + if (newViewport.right > pageRectRight) + newViewport.offset(pageRectRight - newViewport.right, 0); + if (newViewport.left < pageRectLeft) + newViewport.offset(pageRectLeft - newViewport.left, 0); + + if (newViewport.bottom > pageRectBottom) + newViewport.offset(0, pageRectBottom - newViewport.bottom); + if (newViewport.top < pageRectTop) + newViewport.offset(0, pageRectTop - newViewport.top); + + return new ImmutableViewportMetrics( + pageRectLeft, pageRectTop, pageRectRight, pageRectBottom, + cssPageRectLeft, cssPageRectTop, cssPageRectRight, cssPageRectBottom, + newViewport.left, newViewport.top, newViewport.right, newViewport.bottom, + zoomFactor); + } + + public boolean fuzzyEquals(ImmutableViewportMetrics other) { + return FloatUtils.fuzzyEquals(pageRectLeft, other.pageRectLeft) + && FloatUtils.fuzzyEquals(pageRectTop, other.pageRectTop) + && FloatUtils.fuzzyEquals(pageRectRight, other.pageRectRight) + && FloatUtils.fuzzyEquals(pageRectBottom, other.pageRectBottom) + && FloatUtils.fuzzyEquals(cssPageRectLeft, other.cssPageRectLeft) + && FloatUtils.fuzzyEquals(cssPageRectTop, other.cssPageRectTop) + && FloatUtils.fuzzyEquals(cssPageRectRight, other.cssPageRectRight) + && FloatUtils.fuzzyEquals(cssPageRectBottom, other.cssPageRectBottom) + && FloatUtils.fuzzyEquals(viewportRectLeft, other.viewportRectLeft) + && FloatUtils.fuzzyEquals(viewportRectTop, other.viewportRectTop) + && FloatUtils.fuzzyEquals(viewportRectRight, other.viewportRectRight) + && FloatUtils.fuzzyEquals(viewportRectBottom, other.viewportRectBottom) + && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor); + } + + @Override + public String toString() { + return "ImmutableViewportMetrics v=(" + viewportRectLeft + "," + viewportRectTop + "," + + viewportRectRight + "," + viewportRectBottom + ") p=(" + pageRectLeft + "," + + pageRectTop + "," + pageRectRight + "," + pageRectBottom + ") c=(" + + cssPageRectLeft + "," + cssPageRectTop + "," + cssPageRectRight + "," + + cssPageRectBottom + ") z=" + zoomFactor; + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/InputConnectionHandler.java b/android/source/src/java/org/mozilla/gecko/gfx/InputConnectionHandler.java new file mode 100644 index 0000000000..d460c19e1c --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/InputConnectionHandler.java @@ -0,0 +1,15 @@ +package org.mozilla.gecko.gfx; + +import android.view.KeyEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +public interface InputConnectionHandler +{ + InputConnection onCreateInputConnection(EditorInfo outAttrs); + boolean onKeyPreIme(int keyCode, KeyEvent event); + boolean onKeyDown(int keyCode, KeyEvent event); + boolean onKeyLongPress(int keyCode, KeyEvent event); + boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event); + boolean onKeyUp(int keyCode, KeyEvent event); +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/IntSize.java b/android/source/src/java/org/mozilla/gecko/gfx/IntSize.java new file mode 100644 index 0000000000..b0741d2f68 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/IntSize.java @@ -0,0 +1,73 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import org.json.JSONException; +import org.json.JSONObject; + +public class IntSize { + public final int width, height; + + public IntSize(IntSize size) { width = size.width; height = size.height; } + public IntSize(int inWidth, int inHeight) { width = inWidth; height = inHeight; } + + public IntSize(FloatSize size) { + width = Math.round(size.width); + height = Math.round(size.height); + } + + public IntSize(JSONObject json) { + try { + width = json.getInt("width"); + height = json.getInt("height"); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + public int getArea() { + return width * height; + } + + public boolean equals(IntSize size) { + return ((size.width == width) && (size.height == height)); + } + + public boolean isPositive() { + return (width > 0 && height > 0); + } + + @Override + public String toString() { return "(" + width + "," + height + ")"; } + + public IntSize scale(float factor) { + return new IntSize(Math.round(width * factor), + Math.round(height * factor)); + } + + /* Returns the power of two that is greater than or equal to value */ + public static int nextPowerOfTwo(int value) { + // code taken from http://acius2.blogspot.com/2007/11/calculating-next-power-of-2.html + if (0 == value--) { + return 1; + } + value = (value >> 1) | value; + value = (value >> 2) | value; + value = (value >> 4) | value; + value = (value >> 8) | value; + value = (value >> 16) | value; + return value + 1; + } + + public static int nextPowerOfTwo(float value) { + return nextPowerOfTwo((int) value); + } + + public IntSize nextPowerOfTwo() { + return new IntSize(nextPowerOfTwo(width), nextPowerOfTwo(height)); + } +} + diff --git a/android/source/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java b/android/source/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java new file mode 100644 index 0000000000..b20d602a21 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java @@ -0,0 +1,1087 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.PointF; +import android.graphics.RectF; +import android.os.Build; +import android.util.Log; +import android.view.GestureDetector; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.View; + +import org.libreoffice.LOKitShell; +import org.libreoffice.LibreOfficeMainActivity; +import org.mozilla.gecko.ZoomConstraints; +import org.mozilla.gecko.util.FloatUtils; + +import java.util.Timer; +import java.util.TimerTask; +import java.lang.StrictMath; + +/* + * Handles the kinetic scrolling and zooming physics for a layer controller. + * + * Many ideas are from Joe Hewitt's Scrollability: + * https://github.com/joehewitt/scrollability/ + */ +class JavaPanZoomController + extends GestureDetector.SimpleOnGestureListener + implements PanZoomController, SimpleScaleGestureDetector.SimpleScaleGestureListener +{ + private static final String LOGTAG = "GeckoPanZoomController"; + + // Animation stops if the velocity is below this value when overscrolled or panning. + private static final float STOPPED_THRESHOLD = 4.0f; + + // Animation stops is the velocity is below this threshold when flinging. + private static final float FLING_STOPPED_THRESHOLD = 0.1f; + + // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans + // between the touch-down and touch-up of a click). In units of density-independent pixels. + private final float PAN_THRESHOLD; + + // Angle from axis within which we stay axis-locked + private static final double AXIS_LOCK_ANGLE = Math.PI / 6.0; // 30 degrees + + // The maximum amount we allow you to zoom into a page + private static final float MAX_ZOOM = 4.0f; + + // The threshold zoom factor of whether a double tap triggers zoom-in or zoom-out + private static final float DOUBLE_TAP_THRESHOLD = 1.0f; + + // The maximum amount we would like to scroll with the mouse + private final float MAX_SCROLL; + + private enum PanZoomState { + NOTHING, /* no touch-start events received */ + FLING, /* all touches removed, but we're still scrolling page */ + TOUCHING, /* one touch-start event received */ + PANNING_LOCKED, /* touch-start followed by move (i.e. panning with axis lock) */ + PANNING, /* panning without axis lock */ + PANNING_HOLD, /* in panning, but not moving. + * similar to TOUCHING but after starting a pan */ + PANNING_HOLD_LOCKED, /* like PANNING_HOLD, but axis lock still in effect */ + PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */ + ANIMATED_ZOOM, /* animated zoom to a new rect */ + BOUNCE, /* in a bounce animation */ + + WAITING_LISTENERS, /* a state halfway between NOTHING and TOUCHING - the user has + put a finger down, but we don't yet know if a touch listener has + prevented the default actions yet. we still need to abort animations. */ + } + + private final PanZoomTarget mTarget; + private final SubdocumentScrollHelper mSubscroller; + private final Axis mX; + private final Axis mY; + private final TouchEventHandler mTouchEventHandler; + private Thread mMainThread; + private LibreOfficeMainActivity mContext; + + /* The timer that handles flings or bounces. */ + private Timer mAnimationTimer; + /* The runnable being scheduled by the animation timer. */ + private AnimationRunnable mAnimationRunnable; + /* The zoom focus at the first zoom event (in page coordinates). */ + private PointF mLastZoomFocus; + /* The time the last motion event took place. */ + private long mLastEventTime; + /* Current state the pan/zoom UI is in. */ + private PanZoomState mState; + /* Whether or not to wait for a double-tap before dispatching a single-tap */ + private boolean mWaitForDoubleTap; + + JavaPanZoomController(LibreOfficeMainActivity context, PanZoomTarget target, View view) { + mContext = context; + PAN_THRESHOLD = 1/16f * LOKitShell.getDpi(view.getContext()); + MAX_SCROLL = 0.075f * LOKitShell.getDpi(view.getContext()); + mTarget = target; + mSubscroller = new SubdocumentScrollHelper(); + mX = new AxisX(mSubscroller); + mY = new AxisY(mSubscroller); + mTouchEventHandler = new TouchEventHandler(view.getContext(), view, this); + + mMainThread = mContext.getMainLooper().getThread(); + checkMainThread(); + + setState(PanZoomState.NOTHING); + } + + public void destroy() { + mSubscroller.destroy(); + mTouchEventHandler.destroy(); + } + + private static float easeOut(float t) { + // ease-out approx. + // -(t-1)^2+1 + t = t-1; + return -t*t+1; + } + + private void setState(PanZoomState state) { + if (state != mState) { + mState = state; + } + } + + private ImmutableViewportMetrics getMetrics() { + return mTarget.getViewportMetrics(); + } + + // for debugging bug 713011; it can be taken out once that is resolved. + private void checkMainThread() { + if (mMainThread != Thread.currentThread()) { + // log with full stack trace + Log.e(LOGTAG, "Uh-oh, we're running on the wrong thread!", new Exception()); + } + } + + /** This function MUST be called on the UI thread */ + public boolean onMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_CLASS_MASK) == InputDevice.SOURCE_CLASS_POINTER + && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_SCROLL) { + return handlePointerScroll(event); + } + return false; + } + + /** This function MUST be called on the UI thread */ + public boolean onTouchEvent(MotionEvent event) { + return mTouchEventHandler.handleEvent(event); + } + + boolean handleEvent(MotionEvent event) { + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: return handleTouchStart(event); + case MotionEvent.ACTION_MOVE: return handleTouchMove(event); + case MotionEvent.ACTION_UP: return handleTouchEnd(event); + case MotionEvent.ACTION_CANCEL: return handleTouchCancel(event); + } + return false; + } + + /** This function MUST be called on the UI thread */ + public void notifyDefaultActionPrevented(boolean prevented) { + mTouchEventHandler.handleEventListenerAction(!prevented); + } + + /** This function must be called from the UI thread. */ + public void abortAnimation() { + checkMainThread(); + // this happens when gecko changes the viewport on us or if the device is rotated. + // if that's the case, abort any animation in progress and re-zoom so that the page + // snaps to edges. for other cases (where the user's finger(s) are down) don't do + // anything special. + switch (mState) { + case FLING: + mX.stopFling(); + mY.stopFling(); + // fall through + case BOUNCE: + case ANIMATED_ZOOM: + // the zoom that's in progress likely makes no sense any more (such as if + // the screen orientation changed) so abort it + setState(PanZoomState.NOTHING); + // fall through + case NOTHING: + // Don't do animations here; they're distracting and can cause flashes on page + // transitions. + synchronized (mTarget.getLock()) { + mTarget.setViewportMetrics(getValidViewportMetrics()); + mTarget.forceRedraw(); + } + break; + } + } + + /** This function must be called on the UI thread. */ + void startingNewEventBlock(MotionEvent event, boolean waitingForTouchListeners) { + checkMainThread(); + mSubscroller.cancel(); + if (waitingForTouchListeners && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { + // this is the first touch point going down, so we enter the pending state + // setting the state will kill any animations in progress, possibly leaving + // the page in overscroll + setState(PanZoomState.WAITING_LISTENERS); + } + } + + /** This function must be called on the UI thread. */ + void preventedTouchFinished() { + checkMainThread(); + if (mState == PanZoomState.WAITING_LISTENERS) { + // if we enter here, we just finished a block of events whose default actions + // were prevented by touch listeners. Now there are no touch points left, so + // we need to reset our state and re-bounce because we might be in overscroll + bounce(); + } + } + + /** This must be called on the UI thread. */ + public void pageRectUpdated() { + if (mState == PanZoomState.NOTHING) { + synchronized (mTarget.getLock()) { + ImmutableViewportMetrics validated = getValidViewportMetrics(); + if (!getMetrics().fuzzyEquals(validated)) { + // page size changed such that we are now in overscroll. snap to + // the nearest valid viewport + mTarget.setViewportMetrics(validated); + } + } + } + } + + /* + * Panning/scrolling + */ + + private boolean handleTouchStart(MotionEvent event) { + // user is taking control of movement, so stop + // any auto-movement we have going + stopAnimationTimer(); + + switch (mState) { + case ANIMATED_ZOOM: + // We just interrupted a double-tap animation, so force a redraw in + // case this touchstart is just a tap that doesn't end up triggering + // a redraw + mTarget.forceRedraw(); + // fall through + case FLING: + case BOUNCE: + case NOTHING: + case WAITING_LISTENERS: + startTouch(event.getX(0), event.getY(0), event.getEventTime()); + return false; + case TOUCHING: + case PANNING: + case PANNING_LOCKED: + case PANNING_HOLD: + case PANNING_HOLD_LOCKED: + case PINCHING: + Log.e(LOGTAG, "Received impossible touch down while in " + mState); + return false; + } + Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchStart"); + return false; + } + + private boolean handleTouchMove(MotionEvent event) { + if (mState == PanZoomState.PANNING_LOCKED || mState == PanZoomState.PANNING) { + if (getVelocity() > 18.0f) { + mContext.hideSoftKeyboard(); + } + } + + switch (mState) { + case FLING: + case BOUNCE: + case WAITING_LISTENERS: + // should never happen + Log.e(LOGTAG, "Received impossible touch move while in " + mState); + // fall through + case ANIMATED_ZOOM: + case NOTHING: + // may happen if user double-taps and drags without lifting after the + // second tap. ignore the move if this happens. + return false; + + case TOUCHING: + // Don't allow panning if there is an element in full-screen mode. See bug 775511. + if (mTarget.isFullScreen() || panDistance(event) < PAN_THRESHOLD) { + return false; + } + cancelTouch(); + startPanning(event.getX(0), event.getY(0), event.getEventTime()); + track(event); + return true; + + case PANNING_HOLD_LOCKED: + setState(PanZoomState.PANNING_LOCKED); + // fall through + case PANNING_LOCKED: + track(event); + return true; + + case PANNING_HOLD: + setState(PanZoomState.PANNING); + // fall through + case PANNING: + track(event); + return true; + + case PINCHING: + // scale gesture listener will handle this + return false; + } + Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchMove"); + return false; + } + + private boolean handleTouchEnd(MotionEvent event) { + + switch (mState) { + case FLING: + case BOUNCE: + case WAITING_LISTENERS: + // should never happen + Log.e(LOGTAG, "Received impossible touch end while in " + mState); + // fall through + case ANIMATED_ZOOM: + case NOTHING: + // may happen if user double-taps and drags without lifting after the + // second tap. ignore if this happens. + return false; + + case TOUCHING: + // the switch into TOUCHING might have happened while the page was + // snapping back after overscroll. we need to finish the snap if that + // was the case + bounce(); + return false; + + case PANNING: + case PANNING_LOCKED: + case PANNING_HOLD: + case PANNING_HOLD_LOCKED: + setState(PanZoomState.FLING); + fling(); + return true; + + case PINCHING: + setState(PanZoomState.NOTHING); + return true; + } + Log.e(LOGTAG, "Unhandled case " + mState + " in handleTouchEnd"); + return false; + } + + private boolean handleTouchCancel(MotionEvent event) { + cancelTouch(); + + if (mState == PanZoomState.WAITING_LISTENERS) { + // we might get a cancel event from the TouchEventHandler while in the + // WAITING_LISTENERS state if the touch listeners prevent-default the + // block of events. at this point being in WAITING_LISTENERS is equivalent + // to being in NOTHING with the exception of possibly being in overscroll. + // so here we don't want to do anything right now; the overscroll will be + // corrected in preventedTouchFinished(). + return false; + } + + // ensure we snap back if we're overscrolled + bounce(); + return false; + } + + private boolean handlePointerScroll(MotionEvent event) { + if (mState == PanZoomState.NOTHING || mState == PanZoomState.FLING) { + float scrollX = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + float scrollY = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + + scrollBy(scrollX * MAX_SCROLL, scrollY * MAX_SCROLL); + bounce(); + return true; + } + return false; + } + + private void startTouch(float x, float y, long time) { + mX.startTouch(x); + mY.startTouch(y); + setState(PanZoomState.TOUCHING); + mLastEventTime = time; + } + + private void startPanning(float x, float y, long time) { + float dx = mX.panDistance(x); + float dy = mY.panDistance(y); + double angle = Math.atan2(dy, dx); // range [-pi, pi] + angle = Math.abs(angle); // range [0, pi] + + // When the touch move breaks through the pan threshold, reposition the touch down origin + // so the page won't jump when we start panning. + mX.startTouch(x); + mY.startTouch(y); + mLastEventTime = time; + + if (!mX.scrollable() || !mY.scrollable()) { + setState(PanZoomState.PANNING); + } else if (angle < AXIS_LOCK_ANGLE || angle > (Math.PI - AXIS_LOCK_ANGLE)) { + mY.setScrollingDisabled(true); + setState(PanZoomState.PANNING_LOCKED); + } else if (Math.abs(angle - (Math.PI / 2)) < AXIS_LOCK_ANGLE) { + mX.setScrollingDisabled(true); + setState(PanZoomState.PANNING_LOCKED); + } else { + setState(PanZoomState.PANNING); + } + } + + private float panDistance(MotionEvent move) { + float dx = mX.panDistance(move.getX(0)); + float dy = mY.panDistance(move.getY(0)); + return (float) Math.hypot(dx , dy); + } + + private void track(float x, float y, long time) { + float timeDelta = (float)(time - mLastEventTime); + if (FloatUtils.fuzzyEquals(timeDelta, 0)) { + // probably a duplicate event, ignore it. using a zero timeDelta will mess + // up our velocity + return; + } + mLastEventTime = time; + + mX.updateWithTouchAt(x, timeDelta); + mY.updateWithTouchAt(y, timeDelta); + } + + private void track(MotionEvent event) { + mX.saveTouchPos(); + mY.saveTouchPos(); + + for (int i = 0; i < event.getHistorySize(); i++) { + track(event.getHistoricalX(0, i), + event.getHistoricalY(0, i), + event.getHistoricalEventTime(i)); + } + track(event.getX(0), event.getY(0), event.getEventTime()); + + if (stopped()) { + if (mState == PanZoomState.PANNING) { + setState(PanZoomState.PANNING_HOLD); + } else if (mState == PanZoomState.PANNING_LOCKED) { + setState(PanZoomState.PANNING_HOLD_LOCKED); + } else { + // should never happen, but handle anyway for robustness + Log.e(LOGTAG, "Impossible case " + mState + " when stopped in track"); + setState(PanZoomState.PANNING_HOLD_LOCKED); + } + } + + mX.startPan(); + mY.startPan(); + updatePosition(); + } + + private void scrollBy(float dx, float dy) { + ImmutableViewportMetrics scrolled = getMetrics().offsetViewportBy(dx, dy); + mTarget.setViewportMetrics(scrolled); + } + + private void fling() { + updatePosition(); + + stopAnimationTimer(); + + boolean stopped = stopped(); + mX.startFling(stopped); + mY.startFling(stopped); + + startAnimationTimer(new FlingRunnable()); + } + + /* Performs a bounce-back animation to the given viewport metrics. */ + private void bounce(ImmutableViewportMetrics metrics, PanZoomState state) { + stopAnimationTimer(); + + ImmutableViewportMetrics bounceStartMetrics = getMetrics(); + if (bounceStartMetrics.fuzzyEquals(metrics)) { + setState(PanZoomState.NOTHING); + finishAnimation(); + return; + } + + setState(state); + + // At this point we have already set mState to BOUNCE or ANIMATED_ZOOM, so + // getRedrawHint() is returning false. This means we can safely call + // setAnimationTarget to set the new final display port and not have it get + // clobbered by display ports from intermediate animation frames. + mTarget.setAnimationTarget(metrics); + startAnimationTimer(new BounceRunnable(bounceStartMetrics, metrics)); + } + + /* Performs a bounce-back animation to the nearest valid viewport metrics. */ + private void bounce() { + bounce(getValidViewportMetrics(), PanZoomState.BOUNCE); + } + + /* Starts the fling or bounce animation. */ + private void startAnimationTimer(final AnimationRunnable runnable) { + if (mAnimationTimer != null) { + Log.e(LOGTAG, "Attempted to start a new fling without canceling the old one!"); + stopAnimationTimer(); + } + + mAnimationTimer = new Timer("Animation Timer"); + mAnimationRunnable = runnable; + mAnimationTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { mTarget.post(runnable); } + }, 0, (int)Axis.MS_PER_FRAME); + } + + /* Stops the fling or bounce animation. */ + private void stopAnimationTimer() { + if (mAnimationTimer != null) { + mAnimationTimer.cancel(); + mAnimationTimer = null; + } + if (mAnimationRunnable != null) { + mAnimationRunnable.terminate(); + mAnimationRunnable = null; + } + } + + private float getVelocity() { + float xvel = mX.getRealVelocity(); + float yvel = mY.getRealVelocity(); + return (float) StrictMath.hypot(xvel, yvel); + } + + public PointF getVelocityVector() { + return new PointF(mX.getRealVelocity(), mY.getRealVelocity()); + } + + private boolean stopped() { + return getVelocity() < STOPPED_THRESHOLD; + } + + private PointF resetDisplacement() { + return new PointF(mX.resetDisplacement(), mY.resetDisplacement()); + } + + private void updatePosition() { + mX.displace(); + mY.displace(); + PointF displacement = resetDisplacement(); + if (FloatUtils.fuzzyEquals(displacement.x, 0.0f) && FloatUtils.fuzzyEquals(displacement.y, 0.0f)) { + return; + } + if (! mSubscroller.scrollBy(displacement)) { + synchronized (mTarget.getLock()) { + scrollBy(displacement.x, displacement.y); + } + } + } + + private abstract class AnimationRunnable implements Runnable { + private boolean mAnimationTerminated; + + /* This should always run on the UI thread */ + public final void run() { + /* + * Since the animation timer queues this runnable on the UI thread, it + * is possible that even when the animation timer is cancelled, there + * are multiple instances of this queued, so we need to have another + * mechanism to abort. This is done by using the mAnimationTerminated flag. + */ + if (mAnimationTerminated) { + return; + } + animateFrame(); + } + + protected abstract void animateFrame(); + + /* This should always run on the UI thread */ + final void terminate() { + mAnimationTerminated = true; + } + } + + /* The callback that performs the bounce animation. */ + private class BounceRunnable extends AnimationRunnable { + /* The current frame of the bounce-back animation */ + private int mBounceFrame; + /* + * The viewport metrics that represent the start and end of the bounce-back animation, + * respectively. + */ + private ImmutableViewportMetrics mBounceStartMetrics; + private ImmutableViewportMetrics mBounceEndMetrics; + + BounceRunnable(ImmutableViewportMetrics startMetrics, ImmutableViewportMetrics endMetrics) { + mBounceStartMetrics = startMetrics; + mBounceEndMetrics = endMetrics; + } + + protected void animateFrame() { + /* + * The pan/zoom controller might have signaled to us that it wants to abort the + * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail + * out. + */ + if (!(mState == PanZoomState.BOUNCE || mState == PanZoomState.ANIMATED_ZOOM)) { + finishAnimation(); + return; + } + + /* Perform the next frame of the bounce-back animation. */ + if (mBounceFrame < (int)(256f/Axis.MS_PER_FRAME)) { + advanceBounce(); + return; + } + + /* Finally, if there's nothing else to do, complete the animation and go to sleep. */ + finishBounce(); + finishAnimation(); + setState(PanZoomState.NOTHING); + } + + /* Performs one frame of a bounce animation. */ + private void advanceBounce() { + synchronized (mTarget.getLock()) { + float t = easeOut(mBounceFrame * Axis.MS_PER_FRAME / 256f); + ImmutableViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t); + mTarget.setViewportMetrics(newMetrics); + mBounceFrame++; + } + } + + /* Concludes a bounce animation and snaps the viewport into place. */ + private void finishBounce() { + synchronized (mTarget.getLock()) { + mTarget.setViewportMetrics(mBounceEndMetrics); + mBounceFrame = -1; + } + } + } + + // The callback that performs the fling animation. + private class FlingRunnable extends AnimationRunnable { + protected void animateFrame() { + /* + * The pan/zoom controller might have signaled to us that it wants to abort the + * animation by setting the state to PanZoomState.NOTHING. Handle this case and bail + * out. + */ + if (mState != PanZoomState.FLING) { + finishAnimation(); + return; + } + + /* Advance flings, if necessary. */ + boolean flingingX = mX.advanceFling(); + boolean flingingY = mY.advanceFling(); + + boolean overscrolled = (mX.overscrolled() || mY.overscrolled()); + + /* If we're still flinging in any direction, update the origin. */ + if (flingingX || flingingY) { + updatePosition(); + + /* + * Check to see if we're still flinging with an appreciable velocity. The threshold is + * higher in the case of overscroll, so we bounce back eagerly when overscrolling but + * coast smoothly to a stop when not. In other words, require a greater velocity to + * maintain the fling once we enter overscroll. + */ + float threshold = (overscrolled && !mSubscroller.scrolling() ? STOPPED_THRESHOLD : FLING_STOPPED_THRESHOLD); + if (getVelocity() >= threshold) { + mContext.getDocumentOverlay().showPageNumberRect(); + // we're still flinging + return; + } + + mX.stopFling(); + mY.stopFling(); + } + + /* Perform a bounce-back animation if overscrolled. */ + if (overscrolled) { + bounce(); + } else { + finishAnimation(); + setState(PanZoomState.NOTHING); + } + } + } + + private void finishAnimation() { + checkMainThread(); + + stopAnimationTimer(); + + mContext.getDocumentOverlay().hidePageNumberRect(); + + // Force a viewport synchronisation + mTarget.forceRedraw(); + } + + /* Returns the nearest viewport metrics with no overscroll visible. */ + private ImmutableViewportMetrics getValidViewportMetrics() { + return getValidViewportMetrics(getMetrics()); + } + + private ImmutableViewportMetrics getValidViewportMetrics(ImmutableViewportMetrics viewportMetrics) { + /* First, we adjust the zoom factor so that we can make no overscrolled area visible. */ + float zoomFactor = viewportMetrics.zoomFactor; + RectF pageRect = viewportMetrics.getPageRect(); + RectF viewport = viewportMetrics.getViewport(); + + float focusX = viewport.width() / 2.0f; + float focusY = viewport.height() / 2.0f; + + float minZoomFactor = 0.0f; + float maxZoomFactor = MAX_ZOOM; + + ZoomConstraints constraints = mTarget.getZoomConstraints(); + if (null == constraints) { + Log.e(LOGTAG, "ZoomConstraints not available - too impatient?"); + return viewportMetrics; + + } + if (constraints.getMinZoom() > 0) + minZoomFactor = constraints.getMinZoom(); + if (constraints.getMaxZoom() > 0) + maxZoomFactor = constraints.getMaxZoom(); + + maxZoomFactor = Math.max(maxZoomFactor, minZoomFactor); + + if (zoomFactor < minZoomFactor) { + // if one (or both) of the page dimensions is smaller than the viewport, + // zoom using the top/left as the focus on that axis. this prevents the + // scenario where, if both dimensions are smaller than the viewport, but + // by different scale factors, we end up scrolled to the end on one axis + // after applying the scale + PointF center = new PointF(focusX, focusY); + viewportMetrics = viewportMetrics.scaleTo(minZoomFactor, center); + } else if (zoomFactor > maxZoomFactor) { + PointF center = new PointF(viewport.width() / 2.0f, viewport.height() / 2.0f); + viewportMetrics = viewportMetrics.scaleTo(maxZoomFactor, center); + } + + /* Now we pan to the right origin. */ + viewportMetrics = viewportMetrics.clamp(); + + viewportMetrics = pushPageToCenterOfViewport(viewportMetrics); + + return viewportMetrics; + } + + private ImmutableViewportMetrics pushPageToCenterOfViewport(ImmutableViewportMetrics viewportMetrics) { + RectF pageRect = viewportMetrics.getPageRect(); + RectF viewportRect = viewportMetrics.getViewport(); + + if (pageRect.width() < viewportRect.width()) { + float originX = (viewportRect.width() - pageRect.width()) / 2.0f; + viewportMetrics = viewportMetrics.setViewportOrigin(-originX, viewportMetrics.getOrigin().y); + } + + if (pageRect.height() < viewportRect.height()) { + float originY = (viewportRect.height() - pageRect.height()) / 2.0f; + viewportMetrics = viewportMetrics.setViewportOrigin(viewportMetrics.getOrigin().x, -originY); + } + + return viewportMetrics; + } + + private class AxisX extends Axis { + AxisX(SubdocumentScrollHelper subscroller) { super(subscroller); } + @Override + public float getOrigin() { return getMetrics().viewportRectLeft; } + @Override + protected float getViewportLength() { return getMetrics().getWidth(); } + @Override + protected float getPageStart() { return getMetrics().pageRectLeft; } + @Override + protected float getPageLength() { return getMetrics().getPageWidth(); } + } + + private class AxisY extends Axis { + AxisY(SubdocumentScrollHelper subscroller) { super(subscroller); } + @Override + public float getOrigin() { return getMetrics().viewportRectTop; } + @Override + protected float getViewportLength() { return getMetrics().getHeight(); } + @Override + protected float getPageStart() { return getMetrics().pageRectTop; } + @Override + protected float getPageLength() { return getMetrics().getPageHeight(); } + } + + /* + * Zooming + */ + @Override + public boolean onScaleBegin(SimpleScaleGestureDetector detector) { + if (mState == PanZoomState.ANIMATED_ZOOM) + return false; + + if (null == mTarget.getZoomConstraints()) + return false; + + setState(PanZoomState.PINCHING); + mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY()); + cancelTouch(); + + return true; + } + + @Override + public boolean onScale(SimpleScaleGestureDetector detector) { + if (mTarget.isFullScreen()) + return false; + + if (mState != PanZoomState.PINCHING) + return false; + + float prevSpan = detector.getPreviousSpan(); + if (FloatUtils.fuzzyEquals(prevSpan, 0.0f)) { + // let's eat this one to avoid setting the new zoom to infinity (bug 711453) + return true; + } + + float spanRatio = detector.getCurrentSpan() / prevSpan; + + synchronized (mTarget.getLock()) { + float newZoomFactor = getMetrics().zoomFactor * spanRatio; + float minZoomFactor = 0.0f; // deliberately set to zero to allow big zoom out effect + float maxZoomFactor = MAX_ZOOM; + + ZoomConstraints constraints = mTarget.getZoomConstraints(); + + if (constraints.getMaxZoom() > 0) + maxZoomFactor = constraints.getMaxZoom(); + + if (newZoomFactor < minZoomFactor) { + // apply resistance when zooming past minZoomFactor, + // such that it asymptotically reaches minZoomFactor / 2.0 + // but never exceeds that + final float rate = 0.5f; // controls how quickly we approach the limit + float excessZoom = minZoomFactor - newZoomFactor; + excessZoom = 1.0f - (float)Math.exp(-excessZoom * rate); + newZoomFactor = minZoomFactor * (1.0f - excessZoom / 2.0f); + } + + if (newZoomFactor > maxZoomFactor) { + // apply resistance when zooming past maxZoomFactor, + // such that it asymptotically reaches maxZoomFactor + 1.0 + // but never exceeds that + float excessZoom = newZoomFactor - maxZoomFactor; + excessZoom = 1.0f - (float)Math.exp(-excessZoom); + newZoomFactor = maxZoomFactor + excessZoom; + } + + scrollBy(mLastZoomFocus.x - detector.getFocusX(), + mLastZoomFocus.y - detector.getFocusY()); + PointF focus = new PointF(detector.getFocusX(), detector.getFocusY()); + scaleWithFocus(newZoomFactor, focus); + } + + mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY()); + + return true; + } + + @Override + public void onScaleEnd(SimpleScaleGestureDetector detector) { + if (mState == PanZoomState.ANIMATED_ZOOM) + return; + + // switch back to the touching state + startTouch(detector.getFocusX(), detector.getFocusY(), detector.getEventTime()); + + // Force a viewport synchronisation + mTarget.forceRedraw(); + + } + + /** + * Scales the viewport, keeping the given focus point in the same place before and after the + * scale operation. You must hold the monitor while calling this. + */ + private void scaleWithFocus(float zoomFactor, PointF focus) { + ImmutableViewportMetrics viewportMetrics = getMetrics(); + viewportMetrics = viewportMetrics.scaleTo(zoomFactor, focus); + mTarget.setViewportMetrics(viewportMetrics); + } + + public boolean getRedrawHint() { + switch (mState) { + case PINCHING: + case ANIMATED_ZOOM: + case BOUNCE: + // don't redraw during these because the zoom is (or might be, in the case + // of BOUNCE) be changing rapidly and gecko will have to redraw the entire + // display port area. we trigger a force-redraw upon exiting these states. + return false; + default: + // allow redrawing in other states + return true; + } + } + + @Override + public boolean onDown(MotionEvent motionEvent) { + mWaitForDoubleTap = mTarget.getZoomConstraints() != null; + return false; + } + + @Override + public void onShowPress(MotionEvent motionEvent) { + // If we get this, it will be followed either by a call to + // onSingleTapUp (if the user lifts their finger before the + // long-press timeout) or a call to onLongPress (if the user + // does not). In the former case, we want to make sure it is + // treated as a click. (Note that if this is called, we will + // not get a call to onDoubleTap). + mWaitForDoubleTap = false; + } + + private PointF getMotionInDocumentCoordinates(MotionEvent motionEvent) { + RectF viewport = getValidViewportMetrics().getViewport(); + PointF viewPoint = new PointF(motionEvent.getX(0), motionEvent.getY(0)); + return mTarget.convertViewPointToLayerPoint(viewPoint); + } + + @Override + public void onLongPress(MotionEvent motionEvent) { + LOKitShell.sendTouchEvent("LongPress", getMotionInDocumentCoordinates(motionEvent)); + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + mContext.getDocumentOverlay().showPageNumberRect(); + return super.onScroll(e1, e2, distanceX, distanceY); + } + + @Override + public boolean onSingleTapUp(MotionEvent motionEvent) { + // When double-tapping is allowed, we have to wait to see if this is + // going to be a double-tap. + if (!mWaitForDoubleTap) { + LOKitShell.sendTouchEvent("SingleTap", getMotionInDocumentCoordinates(motionEvent)); + } + // return false because we still want to get the ACTION_UP event that triggers this + return false; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent motionEvent) { + // In cases where we don't wait for double-tap, we handle this in onSingleTapUp. + if (mWaitForDoubleTap) { + LOKitShell.sendTouchEvent("SingleTap", getMotionInDocumentCoordinates(motionEvent)); + } + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent motionEvent) { + if (null == mTarget.getZoomConstraints()) { + return true; + } + // Double tap zooms in or out depending on the current zoom factor + PointF pointOfTap = getMotionInDocumentCoordinates(motionEvent); + ImmutableViewportMetrics metrics = getMetrics(); + float newZoom = metrics.getZoomFactor() >= + DOUBLE_TAP_THRESHOLD ? mTarget.getZoomConstraints().getDefaultZoom() : DOUBLE_TAP_THRESHOLD; + // calculate new top_left point from the point of tap + float ratio = newZoom/metrics.getZoomFactor(); + float newLeft = pointOfTap.x - 1/ratio * (pointOfTap.x - metrics.getOrigin().x / metrics.getZoomFactor()); + float newTop = pointOfTap.y - 1/ratio * (pointOfTap.y - metrics.getOrigin().y / metrics.getZoomFactor()); + // animate move to the new view + animatedMove(new PointF(newLeft, newTop), newZoom); + + LOKitShell.sendTouchEvent("DoubleTap", pointOfTap); + return true; + } + + private void cancelTouch() { + //GeckoEvent e = GeckoEvent.createBroadcastEvent("Gesture:CancelTouch", ""); + //GeckoAppShell.sendEventToGecko(e); + } + + /** + * Zoom to a specified rect IN CSS PIXELS. + * + * While we usually use device pixels, zoomToRect must be specified in CSS + * pixels. + */ + boolean animatedZoomTo(RectF zoomToRect) { + final float startZoom = getMetrics().zoomFactor; + + RectF viewport = getMetrics().getViewport(); + // 1. adjust the aspect ratio of zoomToRect to match that of the current viewport, + // enlarging as necessary (if it gets too big, it will get shrunk in the next step). + // while enlarging make sure we enlarge equally on both sides to keep the target rect + // centered. + float targetRatio = viewport.width() / viewport.height(); + float rectRatio = zoomToRect.width() / zoomToRect.height(); + if (FloatUtils.fuzzyEquals(targetRatio, rectRatio)) { + // all good, do nothing + } else if (targetRatio < rectRatio) { + // need to increase zoomToRect height + float newHeight = zoomToRect.width() / targetRatio; + zoomToRect.top -= (newHeight - zoomToRect.height()) / 2; + zoomToRect.bottom = zoomToRect.top + newHeight; + } else { // targetRatio > rectRatio) { + // need to increase zoomToRect width + float newWidth = targetRatio * zoomToRect.height(); + zoomToRect.left -= (newWidth - zoomToRect.width()) / 2; + zoomToRect.right = zoomToRect.left + newWidth; + } + + float finalZoom = viewport.width() / zoomToRect.width(); + + ImmutableViewportMetrics finalMetrics = getMetrics(); + finalMetrics = finalMetrics.setViewportOrigin( + zoomToRect.left * finalMetrics.zoomFactor, + zoomToRect.top * finalMetrics.zoomFactor); + finalMetrics = finalMetrics.scaleTo(finalZoom, new PointF(0.0f, 0.0f)); + + // 2. now run getValidViewportMetrics on it, so that the target viewport is + // clamped down to prevent overscroll, over-zoom, and other bad conditions. + finalMetrics = getValidViewportMetrics(finalMetrics); + + bounce(finalMetrics, PanZoomState.ANIMATED_ZOOM); + return true; + } + + /** + * Move the viewport to the top-left point to and zoom to the desired + * zoom factor. Input zoom factor can be null, in this case leave the zoom unchanged. + */ + boolean animatedMove(PointF topLeft, Float zoom) { + RectF moveToRect = getMetrics().getCssViewport(); + moveToRect.offsetTo(topLeft.x, topLeft.y); + + ImmutableViewportMetrics finalMetrics = getMetrics(); + + finalMetrics = finalMetrics.setViewportOrigin( + moveToRect.left * finalMetrics.zoomFactor, + moveToRect.top * finalMetrics.zoomFactor); + + if (zoom != null) { + finalMetrics = finalMetrics.scaleTo(zoom, new PointF(0.0f, 0.0f)); + } + finalMetrics = getValidViewportMetrics(finalMetrics); + + bounce(finalMetrics, PanZoomState.ANIMATED_ZOOM); + return true; + } + + /** This function must be called from the UI thread. */ + public void abortPanning() { + checkMainThread(); + bounce(); + } + + public void setOverScrollMode(int overscrollMode) { + mX.setOverScrollMode(overscrollMode); + mY.setOverScrollMode(overscrollMode); + } + + public int getOverScrollMode() { + return mX.getOverScrollMode(); + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/Layer.java b/android/source/src/java/org/mozilla/gecko/gfx/Layer.java new file mode 100644 index 0000000000..b7fee29fc9 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/Layer.java @@ -0,0 +1,218 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; + +import org.mozilla.gecko.util.FloatUtils; + +import java.nio.FloatBuffer; +import java.util.concurrent.locks.ReentrantLock; + +public abstract class Layer { + private final ReentrantLock mTransactionLock; + private boolean mInTransaction; + private Rect mNewPosition; + private float mNewResolution; + + protected Rect mPosition; + protected float mResolution; + protected boolean mUsesDefaultProgram = true; + + public Layer() { + this(null); + } + + public Layer(IntSize size) { + mTransactionLock = new ReentrantLock(); + if (size == null) { + mPosition = new Rect(); + } else { + mPosition = new Rect(0, 0, size.width, size.height); + } + mResolution = 1.0f; + } + + /** + * Updates the layer. This returns false if there is still work to be done + * after this update. + */ + public final boolean update(RenderContext context) { + if (mTransactionLock.isHeldByCurrentThread()) { + throw new RuntimeException("draw() called while transaction lock held by this " + + "thread?!"); + } + + if (mTransactionLock.tryLock()) { + try { + performUpdates(context); + return true; + } finally { + mTransactionLock.unlock(); + } + } + + return false; + } + + /** Subclasses override this function to draw the layer. */ + public abstract void draw(RenderContext context); + + /** Given the intrinsic size of the layer, returns the pixel boundaries of the layer rect. */ + protected RectF getBounds(RenderContext context) { + return RectUtils.scale(new RectF(mPosition), context.zoomFactor / mResolution); + } + + /** + * Returns the region of the layer that is considered valid. The default + * implementation of this will return the bounds of the layer, but this + * may be overridden. + */ + public Region getValidRegion(RenderContext context) { + return new Region(RectUtils.round(getBounds(context))); + } + + /** + * Call this before modifying the layer. Note that, for TileLayers, "modifying the layer" + * includes altering the underlying CairoImage in any way. Thus you must call this function + * before modifying the byte buffer associated with this layer. + * + * This function may block, so you should never call this on the main UI thread. + */ + public void beginTransaction() { + if (mTransactionLock.isHeldByCurrentThread()) + throw new RuntimeException("Nested transactions are not supported"); + mTransactionLock.lock(); + mInTransaction = true; + mNewResolution = mResolution; + } + + /** Call this when you're done modifying the layer. */ + public void endTransaction() { + if (!mInTransaction) + throw new RuntimeException("endTransaction() called outside a transaction"); + mInTransaction = false; + mTransactionLock.unlock(); + } + + /** Returns true if the layer is currently in a transaction and false otherwise. */ + protected boolean inTransaction() { + return mInTransaction; + } + + /** Returns the current layer position. */ + public Rect getPosition() { + return mPosition; + } + + /** Sets the position. Only valid inside a transaction. */ + public void setPosition(Rect newPosition) { + if (!mInTransaction) + throw new RuntimeException("setPosition() is only valid inside a transaction"); + mNewPosition = newPosition; + } + + /** Returns the current layer's resolution. */ + public float getResolution() { + return mResolution; + } + + /** + * Sets the layer resolution. This value is used to determine how many pixels per + * device pixel this layer was rendered at. This will be reflected by scaling by + * the reciprocal of the resolution in the layer's transform() function. + * Only valid inside a transaction. */ + public void setResolution(float newResolution) { + if (!mInTransaction) + throw new RuntimeException("setResolution() is only valid inside a transaction"); + mNewResolution = newResolution; + } + + public boolean usesDefaultProgram() { + return mUsesDefaultProgram; + } + + /** + * Subclasses may override this method to perform custom layer updates. This will be called + * with the transaction lock held. Subclass implementations of this method must call the + * superclass implementation. Returns false if there is still work to be done after this + * update is complete. + */ + protected void performUpdates(RenderContext context) { + if (mNewPosition != null) { + mPosition = mNewPosition; + mNewPosition = null; + } + if (mNewResolution != 0.0f) { + mResolution = mNewResolution; + mNewResolution = 0.0f; + } + } + + /** + * This function fills in the provided <tt>dest</tt> array with values to render a texture. + * The array is filled with 4 sets of {x, y, z, texture_x, texture_y} values (so 20 values + * in total) corresponding to the corners of the rect. + */ + protected final void fillRectCoordBuffer(float[] dest, RectF rect, float viewWidth, float viewHeight, + Rect cropRect, float texWidth, float texHeight) { + //x, y, z, texture_x, texture_y + dest[0] = rect.left / viewWidth; + dest[1] = rect.bottom / viewHeight; + dest[2] = 0; + dest[3] = cropRect.left / texWidth; + dest[4] = cropRect.top / texHeight; + + dest[5] = rect.left / viewWidth; + dest[6] = rect.top / viewHeight; + dest[7] = 0; + dest[8] = cropRect.left / texWidth; + dest[9] = cropRect.bottom / texHeight; + + dest[10] = rect.right / viewWidth; + dest[11] = rect.bottom / viewHeight; + dest[12] = 0; + dest[13] = cropRect.right / texWidth; + dest[14] = cropRect.top / texHeight; + + dest[15] = rect.right / viewWidth; + dest[16] = rect.top / viewHeight; + dest[17] = 0; + dest[18] = cropRect.right / texWidth; + dest[19] = cropRect.bottom / texHeight; + } + + public static class RenderContext { + public final RectF viewport; + public final RectF pageRect; + public final float zoomFactor; + public final int positionHandle; + public final int textureHandle; + public final FloatBuffer coordBuffer; + + public RenderContext(RectF aViewport, RectF aPageRect, float aZoomFactor, + int aPositionHandle, int aTextureHandle, FloatBuffer aCoordBuffer) { + viewport = aViewport; + pageRect = aPageRect; + zoomFactor = aZoomFactor; + positionHandle = aPositionHandle; + textureHandle = aTextureHandle; + coordBuffer = aCoordBuffer; + } + + public boolean fuzzyEquals(RenderContext other) { + if (other == null) { + return false; + } + return RectUtils.fuzzyEquals(viewport, other.viewport) + && RectUtils.fuzzyEquals(pageRect, other.pageRect) + && FloatUtils.fuzzyEquals(zoomFactor, other.zoomFactor); + } + } +} + diff --git a/android/source/src/java/org/mozilla/gecko/gfx/LayerRenderer.java b/android/source/src/java/org/mozilla/gecko/gfx/LayerRenderer.java new file mode 100644 index 0000000000..6ea7dd0edc --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/LayerRenderer.java @@ -0,0 +1,453 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.os.SystemClock; +import android.util.Log; + +import org.libreoffice.kit.DirectBufferAllocator; +import org.mozilla.gecko.gfx.Layer.RenderContext; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * The layer renderer implements the rendering logic for a layer view. + */ +public class LayerRenderer implements GLSurfaceView.Renderer { + private static final String LOGTAG = "GeckoLayerRenderer"; + + /* + * The amount of time a frame is allowed to take to render before we declare it a dropped + * frame. + */ + private static final int MAX_FRAME_TIME = 16; /* 1000 ms / 60 FPS */ + + private final LayerView mView; + private final SingleTileLayer mBackgroundLayer; + private final NinePatchTileLayer mShadowLayer; + private final ScrollbarLayer mHorizScrollLayer; + private final ScrollbarLayer mVertScrollLayer; + private final FadeRunnable mFadeRunnable; + private ByteBuffer mCoordByteBuffer; + private FloatBuffer mCoordBuffer; + private RenderContext mLastPageContext; + private int mMaxTextureSize; + + private CopyOnWriteArrayList<Layer> mExtraLayers = new CopyOnWriteArrayList<Layer>(); + + // Used by GLES 2.0 + private int mProgram; + private int mPositionHandle; + private int mTextureHandle; + private int mSampleHandle; + private int mTMatrixHandle; + + // column-major matrix applied to each vertex to shift the viewport from + // one ranging from (-1, -1),(1,1) to (0,0),(1,1) and to scale all sizes by + // a factor of 2 to fill up the screen + public static final float[] DEFAULT_TEXTURE_MATRIX = { + 2.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 2.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 2.0f, 0.0f, + -1.0f, -1.0f, 0.0f, 1.0f + }; + + private static final int COORD_BUFFER_SIZE = 20; + + // The shaders run on the GPU directly, the vertex shader is only applying the + // matrix transform detailed above + + // Note we flip the y-coordinate in the vertex shader from a + // coordinate system with (0,0) in the top left to one with (0,0) in + // the bottom left. + + public static final String DEFAULT_VERTEX_SHADER = + "uniform mat4 uTMatrix;\n" + + "attribute vec4 vPosition;\n" + + "attribute vec2 aTexCoord;\n" + + "varying vec2 vTexCoord;\n" + + "void main() {\n" + + " gl_Position = uTMatrix * vPosition;\n" + + " vTexCoord.x = aTexCoord.x;\n" + + " vTexCoord.y = 1.0 - aTexCoord.y;\n" + + "}\n"; + + // We use highp because the screenshot textures + // we use are large and we stretch them a lot + // so we need all the precision we can get. + // Unfortunately, highp is not required by ES 2.0 + // so on GPU's like Mali we end up getting mediump + public static final String DEFAULT_FRAGMENT_SHADER = + "precision highp float;\n" + + "varying vec2 vTexCoord;\n" + + "uniform sampler2D sTexture;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(sTexture, vTexCoord);\n" + + "}\n"; + + public LayerRenderer(LayerView view) { + mView = view; + + CairoImage backgroundImage = new BufferedCairoImage(view.getBackgroundPattern()); + mBackgroundLayer = new SingleTileLayer(true, backgroundImage); + + CairoImage shadowImage = new BufferedCairoImage(view.getShadowPattern()); + mShadowLayer = new NinePatchTileLayer(shadowImage); + + mHorizScrollLayer = ScrollbarLayer.create(this, false); + mVertScrollLayer = ScrollbarLayer.create(this, true); + mFadeRunnable = new FadeRunnable(); + + // Initialize the FloatBuffer that will be used to store all vertices and texture + // coordinates in draw() commands. + mCoordByteBuffer = DirectBufferAllocator.allocate(COORD_BUFFER_SIZE * 4); + mCoordByteBuffer.order(ByteOrder.nativeOrder()); + mCoordBuffer = mCoordByteBuffer.asFloatBuffer(); + } + + @Override + protected void finalize() throws Throwable { + try { + DirectBufferAllocator.free(mCoordByteBuffer); + mCoordByteBuffer = null; + mCoordBuffer = null; + } finally { + super.finalize(); + } + } + + public void destroy() { + DirectBufferAllocator.free(mCoordByteBuffer); + mCoordByteBuffer = null; + mCoordBuffer = null; + mBackgroundLayer.destroy(); + mShadowLayer.destroy(); + mHorizScrollLayer.destroy(); + mVertScrollLayer.destroy(); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + createDefaultProgram(); + activateDefaultProgram(); + } + + public void createDefaultProgram() { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DEFAULT_VERTEX_SHADER); + int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DEFAULT_FRAGMENT_SHADER); + + mProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program + GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program + GLES20.glLinkProgram(mProgram); // creates OpenGL program executables + + // Get handles to the vertex shader's vPosition, aTexCoord, sTexture, and uTMatrix members. + mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); + mTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord"); + mSampleHandle = GLES20.glGetUniformLocation(mProgram, "sTexture"); + mTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTMatrix"); + + int maxTextureSizeResult[] = new int[1]; + GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeResult, 0); + mMaxTextureSize = maxTextureSizeResult[0]; + } + + // Activates the shader program. + public void activateDefaultProgram() { + // Add the program to the OpenGL environment + GLES20.glUseProgram(mProgram); + + // Set the transformation matrix + GLES20.glUniformMatrix4fv(mTMatrixHandle, 1, false, DEFAULT_TEXTURE_MATRIX, 0); + + // Enable the arrays from which we get the vertex and texture coordinates + GLES20.glEnableVertexAttribArray(mPositionHandle); + GLES20.glEnableVertexAttribArray(mTextureHandle); + + GLES20.glUniform1i(mSampleHandle, 0); + } + + // Deactivates the shader program. This must be done to avoid crashes after returning to the + // Gecko C++ compositor from Java. + public void deactivateDefaultProgram() { + GLES20.glDisableVertexAttribArray(mTextureHandle); + GLES20.glDisableVertexAttribArray(mPositionHandle); + GLES20.glUseProgram(0); + } + + public int getMaxTextureSize() { + return mMaxTextureSize; + } + + public void addLayer(Layer layer) { + synchronized (mExtraLayers) { + if (mExtraLayers.contains(layer)) { + mExtraLayers.remove(layer); + } + + mExtraLayers.add(layer); + } + } + + public void removeLayer(Layer layer) { + synchronized (mExtraLayers) { + mExtraLayers.remove(layer); + } + } + + /** + * Called whenever a new frame is about to be drawn. + */ + @Override + public void onDrawFrame(GL10 gl) { + Frame frame = new Frame(mView.getLayerClient().getViewportMetrics()); + synchronized (mView.getLayerClient()) { + frame.beginDrawing(); + frame.drawBackground(); + frame.drawRootLayer(); + frame.drawForeground(); + frame.endDrawing(); + } + } + + private RenderContext createScreenContext(ImmutableViewportMetrics metrics) { + RectF viewport = new RectF(0.0f, 0.0f, metrics.getWidth(), metrics.getHeight()); + RectF pageRect = new RectF(metrics.getPageRect()); + return createContext(viewport, pageRect, 1.0f); + } + + private RenderContext createPageContext(ImmutableViewportMetrics metrics) { + Rect viewport = RectUtils.round(metrics.getViewport()); + RectF pageRect = metrics.getPageRect(); + float zoomFactor = metrics.zoomFactor; + return createContext(new RectF(viewport), pageRect, zoomFactor); + } + + private RenderContext createContext(RectF viewport, RectF pageRect, float zoomFactor) { + return new RenderContext(viewport, pageRect, zoomFactor, mPositionHandle, mTextureHandle, + mCoordBuffer); + } + + @Override + public void onSurfaceChanged(GL10 gl, final int width, final int height) { + GLES20.glViewport(0, 0, width, height); + } + + /* + * create a vertex shader type (GLES20.GL_VERTEX_SHADER) + * or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) + */ + public static int loadShader(int type, String shaderCode) { + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, shaderCode); + GLES20.glCompileShader(shader); + return shader; + } + + class FadeRunnable implements Runnable { + private boolean mStarted; + private long mRunAt; + + void scheduleStartFade(long delay) { + mRunAt = SystemClock.elapsedRealtime() + delay; + if (!mStarted) { + mView.postDelayed(this, delay); + mStarted = true; + } + } + + void scheduleNextFadeFrame() { + if (mStarted) { + Log.e(LOGTAG, "scheduleNextFadeFrame() called while scheduled for starting fade"); + } + mView.postDelayed(this, 1000L / 60L); // request another frame at 60fps + } + + boolean timeToFade() { + return !mStarted; + } + + public void run() { + long timeDelta = mRunAt - SystemClock.elapsedRealtime(); + if (timeDelta > 0) { + // the run-at time was pushed back, so reschedule + mView.postDelayed(this, timeDelta); + } else { + // reached the run-at time, execute + mStarted = false; + mView.requestRender(); + } + } + } + + public class Frame { + // A fixed snapshot of the viewport metrics that this frame is using to render content. + private ImmutableViewportMetrics mFrameMetrics; + // A rendering context for page-positioned layers, and one for screen-positioned layers. + private RenderContext mPageContext, mScreenContext; + // Whether a layer was updated. + private boolean mUpdated; + private final Rect mPageRect; + + public Frame(ImmutableViewportMetrics metrics) { + mFrameMetrics = metrics; + mPageContext = createPageContext(metrics); + mScreenContext = createScreenContext(metrics); + mPageRect = getPageRect(); + } + + private void setScissorRect() { + Rect scissorRect = transformToScissorRect(mPageRect); + GLES20.glEnable(GLES20.GL_SCISSOR_TEST); + GLES20.glScissor(scissorRect.left, scissorRect.top, + scissorRect.width(), scissorRect.height()); + } + + private Rect transformToScissorRect(Rect rect) { + IntSize screenSize = new IntSize(mFrameMetrics.getSize()); + + int left = Math.max(0, rect.left); + int top = Math.max(0, rect.top); + int right = Math.min(screenSize.width, rect.right); + int bottom = Math.min(screenSize.height, rect.bottom); + + return new Rect(left, screenSize.height - bottom, right, + (screenSize.height - bottom) + (bottom - top)); + } + + private Rect getPageRect() { + Point origin = PointUtils.round(mFrameMetrics.getOrigin()); + Rect pageRect = RectUtils.round(mFrameMetrics.getPageRect()); + pageRect.offset(-origin.x, -origin.y); + return pageRect; + } + + public void beginDrawing() { + TextureReaper.get().reap(); + TextureGenerator.get().fill(); + + mUpdated = true; + + Layer rootLayer = mView.getLayerClient().getRoot(); + Layer lowResLayer = mView.getLayerClient().getLowResLayer(); + + if (!mPageContext.fuzzyEquals(mLastPageContext)) { + // the viewport or page changed, so show the scrollbars again + // as per UX decision + mVertScrollLayer.unfade(); + mHorizScrollLayer.unfade(); + mFadeRunnable.scheduleStartFade(ScrollbarLayer.FADE_DELAY); + } else if (mFadeRunnable.timeToFade()) { + boolean stillFading = mVertScrollLayer.fade() | mHorizScrollLayer.fade(); + if (stillFading) { + mFadeRunnable.scheduleNextFadeFrame(); + } + } + mLastPageContext = mPageContext; + + /* Update layers. */ + if (rootLayer != null) mUpdated &= rootLayer.update(mPageContext); // called on compositor thread + if (lowResLayer != null) mUpdated &= lowResLayer.update(mPageContext); // called on compositor thread + mUpdated &= mBackgroundLayer.update(mScreenContext); // called on compositor thread + mUpdated &= mShadowLayer.update(mPageContext); // called on compositor thread + mUpdated &= mVertScrollLayer.update(mPageContext); // called on compositor thread + mUpdated &= mHorizScrollLayer.update(mPageContext); // called on compositor thread + + for (Layer layer : mExtraLayers) + mUpdated &= layer.update(mPageContext); // called on compositor thread + } + + public void drawBackground() { + GLES20.glDisable(GLES20.GL_SCISSOR_TEST); + + /* Update background color. */ + final int backgroundColor = Color.WHITE; + + /* Clear to the page background colour. The bits set here need to + * match up with those used in gfx/layers/opengl/LayerManagerOGL.cpp. + */ + GLES20.glClearColor(((backgroundColor >> 16) & 0xFF) / 255.0f, + ((backgroundColor >> 8) & 0xFF) / 255.0f, + (backgroundColor & 0xFF) / 255.0f, + 0.0f); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | + GLES20.GL_DEPTH_BUFFER_BIT); + + /* Draw the background. */ + mBackgroundLayer.setMask(mPageRect); + mBackgroundLayer.draw(mScreenContext); + + /* Draw the drop shadow, if we need to. */ + RectF untransformedPageRect = new RectF(0.0f, 0.0f, mPageRect.width(), + mPageRect.height()); + if (!untransformedPageRect.contains(mFrameMetrics.getViewport())) + mShadowLayer.draw(mPageContext); + + /* Scissor around the page-rect, in case the page has shrunk + * since the screenshot layer was last updated. + */ + setScissorRect(); // Calls glEnable(GL_SCISSOR_TEST)) + } + + // Draws the layer the client added to us. + void drawRootLayer() { + Layer lowResLayer = mView.getLayerClient().getLowResLayer(); + if (lowResLayer == null) { + return; + } + lowResLayer.draw(mPageContext); + + Layer rootLayer = mView.getLayerClient().getRoot(); + if (rootLayer == null) { + return; + } + + rootLayer.draw(mPageContext); + } + + public void drawForeground() { + /* Draw any extra layers that were added (likely plugins) */ + if (mExtraLayers.size() > 0) { + for (Layer layer : mExtraLayers) { + if (!layer.usesDefaultProgram()) + deactivateDefaultProgram(); + + layer.draw(mPageContext); + + if (!layer.usesDefaultProgram()) + activateDefaultProgram(); + } + } + + /* Draw the vertical scrollbar. */ + if (mPageRect.height() > mFrameMetrics.getHeight()) + mVertScrollLayer.draw(mPageContext); + + /* Draw the horizontal scrollbar. */ + if (mPageRect.width() > mFrameMetrics.getWidth()) + mHorizScrollLayer.draw(mPageContext); + } + + public void endDrawing() { + // If a layer update requires further work, schedule another redraw + if (!mUpdated) + mView.requestRender(); + } + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/LayerView.java b/android/source/src/java/org/mozilla/gecko/gfx/LayerView.java new file mode 100644 index 0000000000..29049f9291 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/LayerView.java @@ -0,0 +1,337 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.PixelFormat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.widget.FrameLayout; + +import org.libreoffice.LOEvent; +import org.libreoffice.LOKitShell; +import org.libreoffice.LibreOfficeMainActivity; +import org.libreoffice.R; +import org.mozilla.gecko.OnInterceptTouchListener; +import org.mozilla.gecko.OnSlideSwipeListener; + +/** + * A view rendered by the layer compositor. + * + * This view delegates to LayerRenderer to actually do the drawing. Its role is largely that of a + * mediator between the LayerRenderer and the LayerController. + */ +public class LayerView extends FrameLayout { + private static String LOGTAG = LayerView.class.getName(); + + private GeckoLayerClient mLayerClient; + private PanZoomController mPanZoomController; + private GLController mGLController; + private InputConnectionHandler mInputConnectionHandler; + private LayerRenderer mRenderer; + + private SurfaceView mSurfaceView; + + private Listener mListener; + private OnInterceptTouchListener mTouchIntercepter; + private LibreOfficeMainActivity mContext; + + public LayerView(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = (LibreOfficeMainActivity) context; + + mSurfaceView = new SurfaceView(context); + addView(mSurfaceView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + + SurfaceHolder holder = mSurfaceView.getHolder(); + holder.addCallback(new SurfaceListener()); + holder.setFormat(PixelFormat.RGB_565); + + mGLController = new GLController(this); + } + + void connect(GeckoLayerClient layerClient) { + mLayerClient = layerClient; + mPanZoomController = mLayerClient.getPanZoomController(); + mRenderer = new LayerRenderer(this); + mInputConnectionHandler = null; + + setFocusable(true); + setFocusableInTouchMode(true); + + createGLThread(); + setOnTouchListener(new OnSlideSwipeListener(getContext(), mLayerClient)); + } + + public void show() { + // Fix this if TextureView support is turned back on above + mSurfaceView.setVisibility(View.VISIBLE); + } + + public void hide() { + // Fix this if TextureView support is turned back on above + mSurfaceView.setVisibility(View.INVISIBLE); + } + + public void destroy() { + if (mLayerClient != null) { + mLayerClient.destroy(); + } + if (mRenderer != null) { + mRenderer.destroy(); + } + } + + public void setTouchIntercepter(final OnInterceptTouchListener touchIntercepter) { + // this gets run on the gecko thread, but for thread safety we want the assignment + // on the UI thread. + post(new Runnable() { + public void run() { + mTouchIntercepter = touchIntercepter; + } + }); + } + + public void setInputConnectionHandler(InputConnectionHandler inputConnectionHandler) { + mInputConnectionHandler = inputConnectionHandler; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + requestFocus(); + } + + if (mTouchIntercepter != null && mTouchIntercepter.onInterceptTouchEvent(this, event)) { + return true; + } + if (mPanZoomController != null && mPanZoomController.onTouchEvent(event)) { + return true; + } + if (mTouchIntercepter != null && mTouchIntercepter.onTouch(this, event)) { + return true; + } + return false; + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + return mTouchIntercepter != null && mTouchIntercepter.onTouch(this, event); + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + return mPanZoomController != null && mPanZoomController.onMotionEvent(event); + } + + public GeckoLayerClient getLayerClient() { return mLayerClient; } + public PanZoomController getPanZoomController() { return mPanZoomController; } + + public ImmutableViewportMetrics getViewportMetrics() { + return mLayerClient.getViewportMetrics(); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + if (mInputConnectionHandler != null) + return mInputConnectionHandler.onCreateInputConnection(outAttrs); + return null; + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + return mInputConnectionHandler != null && mInputConnectionHandler.onKeyPreIme(keyCode, event); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return mInputConnectionHandler != null && mInputConnectionHandler.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return mInputConnectionHandler != null && mInputConnectionHandler.onKeyLongPress(keyCode, event); + } + + @Override + public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + return mInputConnectionHandler != null && mInputConnectionHandler.onKeyMultiple(keyCode, repeatCount, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return mInputConnectionHandler != null && mInputConnectionHandler.onKeyUp(keyCode, event); + } + + public void requestRender() { + if (mListener != null) { + mListener.renderRequested(); + } + } + + public void addLayer(Layer layer) { + mRenderer.addLayer(layer); + } + + public void removeLayer(Layer layer) { + mRenderer.removeLayer(layer); + } + + public int getMaxTextureSize() { + return mRenderer.getMaxTextureSize(); + } + + public void setLayerRenderer(LayerRenderer renderer) { + mRenderer = renderer; + } + + public LayerRenderer getLayerRenderer() { + return mRenderer; + } + + public LayerRenderer getRenderer() { + return mRenderer; + } + + public void setListener(Listener listener) { + mListener = listener; + } + + Listener getListener() { + return mListener; + } + + public GLController getGLController() { + return mGLController; + } + + public Bitmap getDrawable(String name) { + Context context = getContext(); + Resources resources = context.getResources(); + String packageName = resources.getResourcePackageName(R.id.dummy_id_for_package_name_resolution); + int resourceID = resources.getIdentifier(name, "drawable", packageName); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inScaled = false; + return BitmapFactory.decodeResource(context.getResources(), resourceID, options); + } + + Bitmap getBackgroundPattern() { + return getDrawable("background"); + } + + Bitmap getShadowPattern() { + return getDrawable("shadow"); + } + + private void onSizeChanged(int width, int height) { + mGLController.surfaceChanged(width, height); + + mLayerClient.setViewportSize(new FloatSize(width, height), false); + + if (mListener != null) { + mListener.surfaceChanged(width, height); + } + + LOKitShell.sendEvent(new LOEvent(LOEvent.UPDATE_ZOOM_CONSTRAINTS)); + } + + private void onDestroyed() { + mGLController.surfaceDestroyed(); + + if (mListener != null) { + mListener.compositionPauseRequested(); + } + } + + public Object getNativeWindow() { + return mSurfaceView.getHolder(); + } + + public interface Listener { + void compositorCreated(); + void renderRequested(); + void compositionPauseRequested(); + void surfaceChanged(int width, int height); + } + + private class SurfaceListener implements SurfaceHolder.Callback { + public void surfaceChanged(SurfaceHolder holder, int format, int width, + int height) { + onSizeChanged(width, height); + } + + public void surfaceCreated(SurfaceHolder holder) { + if (mRenderControllerThread != null) { + mRenderControllerThread.surfaceCreated(); + } + } + + public void surfaceDestroyed(SurfaceHolder holder) { + onDestroyed(); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (changed) { + mLayerClient.setViewportSize(new FloatSize(right - left, bottom - top), true); + } + } + + private RenderControllerThread mRenderControllerThread; + + public synchronized void createGLThread() { + if (mRenderControllerThread != null) { + throw new LayerViewException ("createGLThread() called with a GL thread already in place!"); + } + + Log.e(LOGTAG, "### Creating GL thread!"); + mRenderControllerThread = new RenderControllerThread(mGLController); + mRenderControllerThread.start(); + setListener(mRenderControllerThread); + notifyAll(); + } + + public synchronized Thread destroyGLThread() { + // Wait for the GL thread to be started. + Log.e(LOGTAG, "### Waiting for GL thread to be created..."); + while (mRenderControllerThread == null) { + try { + wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + Log.e(LOGTAG, "### Destroying GL thread!"); + Thread thread = mRenderControllerThread; + mRenderControllerThread.shutdown(); + setListener(null); + mRenderControllerThread = null; + return thread; + } + + public static class LayerViewException extends RuntimeException { + public static final long serialVersionUID = 1L; + + LayerViewException(String e) { + super(e); + } + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/NinePatchTileLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/NinePatchTileLayer.java new file mode 100644 index 0000000000..99f203961a --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/NinePatchTileLayer.java @@ -0,0 +1,131 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.RectF; +import android.opengl.GLES20; + +import java.nio.FloatBuffer; + +/** + * Encapsulates the logic needed to draw a nine-patch bitmap using OpenGL ES. + * + * For more information on nine-patch bitmaps, see the following document: + * http://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch + */ +public class NinePatchTileLayer extends TileLayer { + private static final int PATCH_SIZE = 16; + private static final int TEXTURE_SIZE = 64; + + public NinePatchTileLayer(CairoImage image) { + super(image, PaintMode.NORMAL); + } + + @Override + public void draw(RenderContext context) { + if (!initialized()) + return; + + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + GLES20.glEnable(GLES20.GL_BLEND); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureID()); + drawPatches(context); + } + + private void drawPatches(RenderContext context) { + /* + * We divide the nine-patch bitmap up as follows: + * + * +---+---+---+ + * | 0 | 1 | 2 | + * +---+---+---+ + * | 3 | | 4 | + * +---+---+---+ + * | 5 | 6 | 7 | + * +---+---+---+ + */ + + // page is the rect of the "missing" center spot in the picture above + RectF page = context.pageRect; + + drawPatch(context, 0, PATCH_SIZE * 3, /* 0 */ + page.left - PATCH_SIZE, page.top - PATCH_SIZE, PATCH_SIZE, PATCH_SIZE); + drawPatch(context, PATCH_SIZE, PATCH_SIZE * 3, /* 1 */ + page.left, page.top - PATCH_SIZE, page.width(), PATCH_SIZE); + drawPatch(context, PATCH_SIZE * 2, PATCH_SIZE * 3, /* 2 */ + page.right, page.top - PATCH_SIZE, PATCH_SIZE, PATCH_SIZE); + drawPatch(context, 0, PATCH_SIZE * 2, /* 3 */ + page.left - PATCH_SIZE, page.top, PATCH_SIZE, page.height()); + drawPatch(context, PATCH_SIZE * 2, PATCH_SIZE * 2, /* 4 */ + page.right, page.top, PATCH_SIZE, page.height()); + drawPatch(context, 0, PATCH_SIZE, /* 5 */ + page.left - PATCH_SIZE, page.bottom, PATCH_SIZE, PATCH_SIZE); + drawPatch(context, PATCH_SIZE, PATCH_SIZE, /* 6 */ + page.left, page.bottom, page.width(), PATCH_SIZE); + drawPatch(context, PATCH_SIZE * 2, PATCH_SIZE, /* 7 */ + page.right, page.bottom, PATCH_SIZE, PATCH_SIZE); + } + + private void drawPatch(RenderContext context, int textureX, int textureY, + float tileX, float tileY, float tileWidth, float tileHeight) { + RectF viewport = context.viewport; + float viewportHeight = viewport.height(); + float drawX = tileX - viewport.left; + float drawY = viewportHeight - (tileY + tileHeight - viewport.top); + + float[] coords = { + //x, y, z, texture_x, texture_y + drawX/viewport.width(), drawY/viewport.height(), 0, + textureX/(float)TEXTURE_SIZE, textureY/(float)TEXTURE_SIZE, + + drawX/viewport.width(), (drawY+tileHeight)/viewport.height(), 0, + textureX/(float)TEXTURE_SIZE, (textureY+PATCH_SIZE)/(float)TEXTURE_SIZE, + + (drawX+tileWidth)/viewport.width(), drawY/viewport.height(), 0, + (textureX+PATCH_SIZE)/(float)TEXTURE_SIZE, textureY/(float)TEXTURE_SIZE, + + (drawX+tileWidth)/viewport.width(), (drawY+tileHeight)/viewport.height(), 0, + (textureX+PATCH_SIZE)/(float)TEXTURE_SIZE, (textureY+PATCH_SIZE)/(float)TEXTURE_SIZE + + }; + + // Get the buffer and handles from the context + FloatBuffer coordBuffer = context.coordBuffer; + int positionHandle = context.positionHandle; + int textureHandle = context.textureHandle; + + // Make sure we are at position zero in the buffer in case other draw methods did not clean + // up after themselves + coordBuffer.position(0); + coordBuffer.put(coords); + + // Unbind any the current array buffer so we can use client side buffers + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, coordBuffer); + + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); + + // Use bilinear filtering for both magnification and minimization of the texture. This + // applies only to the shadow layer so we do not incur a high overhead. + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_LINEAR); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/PanZoomController.java b/android/source/src/java/org/mozilla/gecko/gfx/PanZoomController.java new file mode 100644 index 0000000000..ebcd641f21 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/PanZoomController.java @@ -0,0 +1,36 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.PointF; +import android.view.MotionEvent; +import android.view.View; +import org.libreoffice.LibreOfficeMainActivity; + +interface PanZoomController { + + class Factory { + static PanZoomController create(LibreOfficeMainActivity context, PanZoomTarget target, View view) { + return new JavaPanZoomController(context, target, view); + } + } + + void destroy(); + + boolean onTouchEvent(MotionEvent event); + boolean onMotionEvent(MotionEvent event); + void notifyDefaultActionPrevented(boolean prevented); + + boolean getRedrawHint(); + PointF getVelocityVector(); + + void pageRectUpdated(); + void abortPanning(); + void abortAnimation(); + + void setOverScrollMode(int overscrollMode); + int getOverScrollMode(); +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/PanZoomTarget.java b/android/source/src/java/org/mozilla/gecko/gfx/PanZoomTarget.java new file mode 100644 index 0000000000..88e1b216c6 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/PanZoomTarget.java @@ -0,0 +1,26 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.PointF; + +import org.mozilla.gecko.ZoomConstraints; + +public interface PanZoomTarget { + public ImmutableViewportMetrics getViewportMetrics(); + public ZoomConstraints getZoomConstraints(); + + public void setAnimationTarget(ImmutableViewportMetrics viewport); + public void setViewportMetrics(ImmutableViewportMetrics viewport); + /** This triggers an (asynchronous) viewport update/redraw. */ + public void forceRedraw(); + + public boolean post(Runnable action); + public Object getLock(); + public PointF convertViewPointToLayerPoint(PointF viewPoint); + + boolean isFullScreen(); +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/PointUtils.java b/android/source/src/java/org/mozilla/gecko/gfx/PointUtils.java new file mode 100644 index 0000000000..4eff380527 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/PointUtils.java @@ -0,0 +1,53 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Point; +import android.graphics.PointF; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.StrictMath; + +public final class PointUtils { + public static PointF add(PointF one, PointF two) { + return new PointF(one.x + two.x, one.y + two.y); + } + + public static PointF subtract(PointF one, PointF two) { + return new PointF(one.x - two.x, one.y - two.y); + } + + public static PointF scale(PointF point, float factor) { + return new PointF(point.x * factor, point.y * factor); + } + + public static Point round(PointF point) { + return new Point(Math.round(point.x), Math.round(point.y)); + } + + /* Computes the magnitude of the given vector. */ + public static float distance(PointF point) { + return (float)StrictMath.hypot(point.x, point.y); + } + + /** Computes the scalar distance between two points. */ + public static float distance(PointF one, PointF two) { + return PointF.length(one.x - two.x, one.y - two.y); + } + + public static JSONObject toJSON(PointF point) throws JSONException { + // Ensure we put ints, not longs, because Gecko message handlers call getInt(). + int x = Math.round(point.x); + int y = Math.round(point.y); + JSONObject json = new JSONObject(); + json.put("x", x); + json.put("y", y); + return json; + } +} + diff --git a/android/source/src/java/org/mozilla/gecko/gfx/RectUtils.java b/android/source/src/java/org/mozilla/gecko/gfx/RectUtils.java new file mode 100644 index 0000000000..e7fa540a39 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/RectUtils.java @@ -0,0 +1,110 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; + +import org.mozilla.gecko.util.FloatUtils; + +public final class RectUtils { + private RectUtils() {} + + public static RectF expand(RectF rect, float moreWidth, float moreHeight) { + float halfMoreWidth = moreWidth / 2; + float halfMoreHeight = moreHeight / 2; + return new RectF(rect.left - halfMoreWidth, + rect.top - halfMoreHeight, + rect.right + halfMoreWidth, + rect.bottom + halfMoreHeight); + } + + public static RectF contract(RectF rect, float lessWidth, float lessHeight) { + float halfLessWidth = lessWidth / 2.0f; + float halfLessHeight = lessHeight / 2.0f; + return new RectF(rect.left + halfLessWidth, + rect.top + halfLessHeight, + rect.right - halfLessWidth, + rect.bottom - halfLessHeight); + } + + public static RectF intersect(RectF one, RectF two) { + float left = Math.max(one.left, two.left); + float top = Math.max(one.top, two.top); + float right = Math.min(one.right, two.right); + float bottom = Math.min(one.bottom, two.bottom); + return new RectF(left, top, Math.max(right, left), Math.max(bottom, top)); + } + + public static RectF scale(RectF rect, float scale) { + float x = rect.left * scale; + float y = rect.top * scale; + return new RectF(x, y, + x + (rect.width() * scale), + y + (rect.height() * scale)); + } + + public static RectF inverseScale(RectF rect, float scale) { + float x = rect.left / scale; + float y = rect.top / scale; + return new RectF(x, y, + x + (rect.width() / scale), + y + (rect.height() / scale)); + } + + /** Returns the nearest integer rect of the given rect. */ + public static Rect round(RectF rect) { + Rect r = new Rect(); + round(rect, r); + return r; + } + + public static void round(RectF rect, Rect dest) { + dest.set(Math.round(rect.left), Math.round(rect.top), + Math.round(rect.right), Math.round(rect.bottom)); + } + + public static Rect roundIn(RectF rect) { + return new Rect((int)Math.ceil(rect.left), (int)Math.ceil(rect.top), + (int)Math.floor(rect.right), (int)Math.floor(rect.bottom)); + } + + public static IntSize getSize(Rect rect) { + return new IntSize(rect.width(), rect.height()); + } + + public static Point getOrigin(Rect rect) { + return new Point(rect.left, rect.top); + } + + public static PointF getOrigin(RectF rect) { + return new PointF(rect.left, rect.top); + } + + public static boolean fuzzyEquals(RectF a, RectF b) { + if (a == null && b == null) + return true; + else + return a != null && b != null + && FloatUtils.fuzzyEquals(a.top, b.top) + && FloatUtils.fuzzyEquals(a.left, b.left) + && FloatUtils.fuzzyEquals(a.right, b.right) + && FloatUtils.fuzzyEquals(a.bottom, b.bottom); + } + + /** + * Assign rectangle values from source to target. + */ + public static void assign(final RectF target, final RectF source) + { + target.left = source.left; + target.top = source.top; + target.right = source.right; + target.bottom = source.bottom; + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/RenderControllerThread.java b/android/source/src/java/org/mozilla/gecko/gfx/RenderControllerThread.java new file mode 100644 index 0000000000..5c74d56a00 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/RenderControllerThread.java @@ -0,0 +1,143 @@ +package org.mozilla.gecko.gfx; + +import android.opengl.GLSurfaceView; + +import java.util.concurrent.LinkedBlockingQueue; + +import javax.microedition.khronos.opengles.GL10; + +/** + * Thread which controls the rendering to OpenGL context. Render commands are queued and + * processed and delegated by this thread. + */ +public class RenderControllerThread extends Thread implements LayerView.Listener { + private LinkedBlockingQueue<RenderCommand> queue = new LinkedBlockingQueue<RenderCommand>(); + private GLController controller; + private boolean renderQueued = false; + private int width; + private int height; + + public RenderControllerThread(GLController controller) { + this.controller = controller; + } + + @Override + public void run() { + while (true) { + RenderCommand command; + try { + command = queue.take(); + execute(command); + if (command == RenderCommand.SHUTDOWN) { + return; + } + } catch (InterruptedException exception) { + throw new RuntimeException(exception); + } + } + } + + void execute(RenderCommand command) { + switch (command) { + case SHUTDOWN: + doShutdown(); + break; + case RENDER_FRAME: + doRenderFrame(); + break; + case SIZE_CHANGED: + doSizeChanged(); + break; + case SURFACE_CREATED: + doSurfaceCreated(); + break; + case SURFACE_DESTROYED: + doSurfaceDestroyed(); + break; + } + } + + public void shutdown() { + queue.add(RenderCommand.SHUTDOWN); + } + + @Override + public void compositorCreated() { + + } + + @Override + public void renderRequested() { + synchronized (this) { + if (!renderQueued) { + queue.add(RenderCommand.RENDER_FRAME); + renderQueued = true; + } + } + } + + @Override + public void compositionPauseRequested() { + queue.add(RenderCommand.SURFACE_DESTROYED); + } + + @Override + public void surfaceChanged(int width, int height) { + this.width = width; + this.height = height; + queue.add(RenderCommand.SIZE_CHANGED); + } + + public void surfaceCreated() { + queue.add(RenderCommand.SURFACE_CREATED); + } + + private GLSurfaceView.Renderer getRenderer() { + return controller.getView().getRenderer(); + } + + private void doShutdown() { + controller.disposeGLContext(); + controller = null; + } + + private void doRenderFrame() { + synchronized (this) { + renderQueued = false; + } + if (controller.getEGLSurface() == null) { + return; + } + GLSurfaceView.Renderer renderer = getRenderer(); + if (renderer != null) { + renderer.onDrawFrame(controller.getGL()); + } + controller.swapBuffers(); + } + + private void doSizeChanged() { + GLSurfaceView.Renderer renderer = getRenderer(); + if (renderer != null) { + renderer.onSurfaceChanged(controller.getGL(), width, height); + } + } + + private void doSurfaceCreated() { + if (!controller.hasSurface()) { + controller.initGLContext(); + } + } + + private void doSurfaceDestroyed() { + controller.disposeGLContext(); + } + + public enum RenderCommand { + SHUTDOWN, + RECREATE_SURFACE, + RENDER_FRAME, + SIZE_CHANGED, + SURFACE_CREATED, + SURFACE_DESTROYED, + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/ScrollbarLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/ScrollbarLayer.java new file mode 100644 index 0000000000..7ef8ff0206 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/ScrollbarLayer.java @@ -0,0 +1,451 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.RectF; +import android.opengl.GLES20; + +import org.libreoffice.kit.DirectBufferAllocator; +import org.mozilla.gecko.util.FloatUtils; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; + +/** + * Draws a small rect. This is scaled to become a scrollbar. + */ +public class ScrollbarLayer extends TileLayer { + private static String LOGTAG = LayerView.class.getName(); + public static final long FADE_DELAY = 500; // milliseconds before fade-out starts + private static final float FADE_AMOUNT = 0.03f; // how much (as a percent) the scrollbar should fade per frame + + private static final int PADDING = 1; // gap between scrollbar and edge of viewport + private static final int BAR_SIZE = 6; + private static final int CAP_RADIUS = (BAR_SIZE / 2); + + private final boolean mVertical; + private final Bitmap mBitmap; + private final Canvas mCanvas; + private float mOpacity; + + private LayerRenderer mRenderer; + private int mProgram; + private int mPositionHandle; + private int mTextureHandle; + private int mSampleHandle; + private int mTMatrixHandle; + private int mOpacityHandle; + + // Fragment shader used to draw the scroll-bar with opacity + private static final String FRAGMENT_SHADER = + "precision mediump float;\n" + + "varying vec2 vTexCoord;\n" + + "uniform sampler2D sTexture;\n" + + "uniform float uOpacity;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(sTexture, vec2(vTexCoord.x, 1.0 - vTexCoord.y));\n" + + " gl_FragColor.a *= uOpacity;\n" + + "}\n"; + + // Dimensions of the texture image + private static final float TEX_HEIGHT = 8.0f; + private static final float TEX_WIDTH = 8.0f; + + // Texture coordinates for the scrollbar's body + // We take a 1x1 pixel from the center of the image and scale it to become the bar + private static final float[] BODY_TEX_COORDS = { + // x, y + CAP_RADIUS/TEX_WIDTH, CAP_RADIUS/TEX_HEIGHT, + CAP_RADIUS/TEX_WIDTH, (CAP_RADIUS+1)/TEX_HEIGHT, + (CAP_RADIUS+1)/TEX_WIDTH, CAP_RADIUS/TEX_HEIGHT, + (CAP_RADIUS+1)/TEX_WIDTH, (CAP_RADIUS+1)/TEX_HEIGHT + }; + + // Texture coordinates for the top cap of the scrollbar + private static final float[] TOP_CAP_TEX_COORDS = { + // x, y + 0 , 1.0f - CAP_RADIUS/TEX_HEIGHT, + 0 , 1.0f, + BAR_SIZE/TEX_WIDTH, 1.0f - CAP_RADIUS/TEX_HEIGHT, + BAR_SIZE/TEX_WIDTH, 1.0f + }; + + // Texture coordinates for the bottom cap of the scrollbar + private static final float[] BOT_CAP_TEX_COORDS = { + // x, y + 0 , 1.0f - BAR_SIZE/TEX_HEIGHT, + 0 , 1.0f - CAP_RADIUS/TEX_HEIGHT, + BAR_SIZE/TEX_WIDTH, 1.0f - BAR_SIZE/TEX_HEIGHT, + BAR_SIZE/TEX_WIDTH, 1.0f - CAP_RADIUS/TEX_HEIGHT + }; + + // Texture coordinates for the left cap of the scrollbar + private static final float[] LEFT_CAP_TEX_COORDS = { + // x, y + 0 , 1.0f - BAR_SIZE/TEX_HEIGHT, + 0 , 1.0f, + CAP_RADIUS/TEX_WIDTH, 1.0f - BAR_SIZE/TEX_HEIGHT, + CAP_RADIUS/TEX_WIDTH, 1.0f + }; + + // Texture coordinates for the right cap of the scrollbar + private static final float[] RIGHT_CAP_TEX_COORDS = { + // x, y + CAP_RADIUS/TEX_WIDTH, 1.0f - BAR_SIZE/TEX_HEIGHT, + CAP_RADIUS/TEX_WIDTH, 1.0f, + BAR_SIZE/TEX_WIDTH , 1.0f - BAR_SIZE/TEX_HEIGHT, + BAR_SIZE/TEX_WIDTH , 1.0f + }; + + private ScrollbarLayer(LayerRenderer renderer, CairoImage image, boolean vertical, ByteBuffer buffer) { + super(image, TileLayer.PaintMode.NORMAL); + mVertical = vertical; + mRenderer = renderer; + + IntSize size = image.getSize(); + mBitmap = Bitmap.createBitmap(size.width, size.height, Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(mBitmap); + + // Paint a spot to use as the scroll indicator + Paint foregroundPaint = new Paint(); + foregroundPaint.setAntiAlias(true); + foregroundPaint.setStyle(Paint.Style.FILL); + foregroundPaint.setColor(Color.argb(127, 0, 0, 0)); + + mCanvas.drawColor(Color.argb(0, 0, 0, 0), PorterDuff.Mode.CLEAR); + mCanvas.drawCircle(CAP_RADIUS, CAP_RADIUS, CAP_RADIUS, foregroundPaint); + + mBitmap.copyPixelsToBuffer(buffer.asIntBuffer()); + } + + public static ScrollbarLayer create(LayerRenderer renderer, boolean vertical) { + // just create an empty image for now, it will get drawn + // on demand anyway + int imageSize = IntSize.nextPowerOfTwo(BAR_SIZE); + ByteBuffer buffer = DirectBufferAllocator.allocate(imageSize * imageSize * 4); + CairoImage image = new BufferedCairoImage(buffer, imageSize, imageSize, + CairoImage.FORMAT_ARGB32); + return new ScrollbarLayer(renderer, image, vertical, buffer); + } + + private void createProgram() { + int vertexShader = LayerRenderer.loadShader(GLES20.GL_VERTEX_SHADER, + LayerRenderer.DEFAULT_VERTEX_SHADER); + int fragmentShader = LayerRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER, + FRAGMENT_SHADER); + + mProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program + GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program + GLES20.glLinkProgram(mProgram); // creates OpenGL program executables + + // Get handles to the shaders' vPosition, aTexCoord, sTexture, and uTMatrix members. + mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); + mTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord"); + mSampleHandle = GLES20.glGetUniformLocation(mProgram, "sTexture"); + mTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTMatrix"); + mOpacityHandle = GLES20.glGetUniformLocation(mProgram, "uOpacity"); + } + + private void activateProgram() { + // Add the program to the OpenGL environment + GLES20.glUseProgram(mProgram); + + // Set the transformation matrix + GLES20.glUniformMatrix4fv(mTMatrixHandle, 1, false, + LayerRenderer.DEFAULT_TEXTURE_MATRIX, 0); + + // Enable the arrays from which we get the vertex and texture coordinates + GLES20.glEnableVertexAttribArray(mPositionHandle); + GLES20.glEnableVertexAttribArray(mTextureHandle); + + GLES20.glUniform1i(mSampleHandle, 0); + GLES20.glUniform1f(mOpacityHandle, mOpacity); + } + + private void deactivateProgram() { + GLES20.glDisableVertexAttribArray(mTextureHandle); + GLES20.glDisableVertexAttribArray(mPositionHandle); + GLES20.glUseProgram(0); + } + + /** + * Decrease the opacity of the scrollbar by one frame's worth. + * Return true if the opacity was decreased, or false if the scrollbars + * are already fully faded out. + */ + public boolean fade() { + if (FloatUtils.fuzzyEquals(mOpacity, 0.0f)) { + return false; + } + beginTransaction(); // called on compositor thread + mOpacity = Math.max(mOpacity - FADE_AMOUNT, 0.0f); + endTransaction(); + return true; + } + + /** + * Restore the opacity of the scrollbar to fully opaque. + * Return true if the opacity was changed, or false if the scrollbars + * are already fully opaque. + */ + public boolean unfade() { + if (FloatUtils.fuzzyEquals(mOpacity, 1.0f)) { + return false; + } + beginTransaction(); // called on compositor thread + mOpacity = 1.0f; + endTransaction(); + + return true; + } + + @Override + public void draw(RenderContext context) { + if (!initialized()) + return; + + // Create the shader program, if necessary + if (mProgram == 0) { + createProgram(); + } + + // Enable the shader program + mRenderer.deactivateDefaultProgram(); + activateProgram(); + + GLES20.glEnable(GLES20.GL_BLEND); + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + + Rect rect = RectUtils.round(mVertical + ? getVerticalRect(context) + : getHorizontalRect(context)); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureID()); + + float viewWidth = context.viewport.width(); + float viewHeight = context.viewport.height(); + + float top = viewHeight - rect.top; + float bot = viewHeight - rect.bottom; + + // Coordinates for the scrollbar's body combined with the texture coordinates + float[] bodyCoords = { + // x, y, z, texture_x, texture_y + rect.left/viewWidth, bot/viewHeight, 0, + BODY_TEX_COORDS[0], BODY_TEX_COORDS[1], + + rect.left/viewWidth, (bot+rect.height())/viewHeight, 0, + BODY_TEX_COORDS[2], BODY_TEX_COORDS[3], + + (rect.left+rect.width())/viewWidth, bot/viewHeight, 0, + BODY_TEX_COORDS[4], BODY_TEX_COORDS[5], + + (rect.left+rect.width())/viewWidth, (bot+rect.height())/viewHeight, 0, + BODY_TEX_COORDS[6], BODY_TEX_COORDS[7] + }; + + // Get the buffer and handles from the context + FloatBuffer coordBuffer = context.coordBuffer; + int positionHandle = mPositionHandle; + int textureHandle = mTextureHandle; + + // Make sure we are at position zero in the buffer in case other draw methods did not + // clean up after themselves + coordBuffer.position(0); + coordBuffer.put(bodyCoords); + + // Unbind any the current array buffer so we can use client side buffers + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + // Reset the position in the buffer for the next set of vertex and texture coordinates. + coordBuffer.position(0); + + if (mVertical) { + // top endcap + float[] topCap = { + // x, y, z, texture_x, texture_y + rect.left/viewWidth, top/viewHeight, 0, + TOP_CAP_TEX_COORDS[0], TOP_CAP_TEX_COORDS[1], + + rect.left/viewWidth, (top+CAP_RADIUS)/viewHeight, 0, + TOP_CAP_TEX_COORDS[2], TOP_CAP_TEX_COORDS[3], + + (rect.left+BAR_SIZE)/viewWidth, top/viewHeight, 0, + TOP_CAP_TEX_COORDS[4], TOP_CAP_TEX_COORDS[5], + + (rect.left+BAR_SIZE)/viewWidth, (top+CAP_RADIUS)/viewHeight, 0, + TOP_CAP_TEX_COORDS[6], TOP_CAP_TEX_COORDS[7] + }; + + coordBuffer.put(topCap); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the + // buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + // Reset the position in the buffer for the next set of vertex and texture + // coordinates. + coordBuffer.position(0); + + // bottom endcap + float[] botCap = { + // x, y, z, texture_x, texture_y + rect.left/viewWidth, (bot-CAP_RADIUS)/viewHeight, 0, + BOT_CAP_TEX_COORDS[0], BOT_CAP_TEX_COORDS[1], + + rect.left/viewWidth, (bot)/viewHeight, 0, + BOT_CAP_TEX_COORDS[2], BOT_CAP_TEX_COORDS[3], + + (rect.left+BAR_SIZE)/viewWidth, (bot-CAP_RADIUS)/viewHeight, 0, + BOT_CAP_TEX_COORDS[4], BOT_CAP_TEX_COORDS[5], + + (rect.left+BAR_SIZE)/viewWidth, (bot)/viewHeight, 0, + BOT_CAP_TEX_COORDS[6], BOT_CAP_TEX_COORDS[7] + }; + + coordBuffer.put(botCap); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the + // buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + // Reset the position in the buffer for the next set of vertex and texture + // coordinates. + coordBuffer.position(0); + } else { + // left endcap + float[] leftCap = { + // x, y, z, texture_x, texture_y + (rect.left-CAP_RADIUS)/viewWidth, bot/viewHeight, 0, + LEFT_CAP_TEX_COORDS[0], LEFT_CAP_TEX_COORDS[1], + (rect.left-CAP_RADIUS)/viewWidth, (bot+BAR_SIZE)/viewHeight, 0, + LEFT_CAP_TEX_COORDS[2], LEFT_CAP_TEX_COORDS[3], + (rect.left)/viewWidth, bot/viewHeight, 0, LEFT_CAP_TEX_COORDS[4], + LEFT_CAP_TEX_COORDS[5], + (rect.left)/viewWidth, (bot+BAR_SIZE)/viewHeight, 0, + LEFT_CAP_TEX_COORDS[6], LEFT_CAP_TEX_COORDS[7] + }; + + coordBuffer.put(leftCap); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the + // buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + // Reset the position in the buffer for the next set of vertex and texture + // coordinates. + coordBuffer.position(0); + + // right endcap + float[] rightCap = { + // x, y, z, texture_x, texture_y + rect.right/viewWidth, (bot)/viewHeight, 0, + RIGHT_CAP_TEX_COORDS[0], RIGHT_CAP_TEX_COORDS[1], + + rect.right/viewWidth, (bot+BAR_SIZE)/viewHeight, 0, + RIGHT_CAP_TEX_COORDS[2], RIGHT_CAP_TEX_COORDS[3], + + (rect.right+CAP_RADIUS)/viewWidth, (bot)/viewHeight, 0, + RIGHT_CAP_TEX_COORDS[4], RIGHT_CAP_TEX_COORDS[5], + + (rect.right+CAP_RADIUS)/viewWidth, (bot+BAR_SIZE)/viewHeight, 0, + RIGHT_CAP_TEX_COORDS[6], RIGHT_CAP_TEX_COORDS[7] + }; + + coordBuffer.put(rightCap); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the + // buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, + coordBuffer); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } + + // Enable the default shader program again + deactivateProgram(); + mRenderer.activateDefaultProgram(); + } + + private RectF getVerticalRect(RenderContext context) { + RectF viewport = context.viewport; + RectF pageRect = context.pageRect; + float barStart = ((viewport.top - pageRect.top) * (viewport.height() / pageRect.height())) + CAP_RADIUS; + float barEnd = ((viewport.bottom - pageRect.top) * (viewport.height() / pageRect.height())) - CAP_RADIUS; + if (barStart > barEnd) { + float middle = (barStart + barEnd) / 2.0f; + barStart = barEnd = middle; + } + float right = viewport.width() - PADDING; + return new RectF(right - BAR_SIZE, barStart, right, barEnd); + } + + private RectF getHorizontalRect(RenderContext context) { + RectF viewport = context.viewport; + RectF pageRect = context.pageRect; + float barStart = ((viewport.left - pageRect.left) * (viewport.width() / pageRect.width())) + CAP_RADIUS; + float barEnd = ((viewport.right - pageRect.left) * (viewport.width() / pageRect.width())) - CAP_RADIUS; + if (barStart > barEnd) { + float middle = (barStart + barEnd) / 2.0f; + barStart = barEnd = middle; + } + float bottom = viewport.height() - PADDING; + return new RectF(barStart, bottom - BAR_SIZE, barEnd, bottom); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/android/source/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java b/android/source/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java new file mode 100644 index 0000000000..e89015b5ed --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java @@ -0,0 +1,322 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.PointF; +import android.util.Log; +import android.view.MotionEvent; + +import org.json.JSONException; + +import java.util.LinkedList; +import java.util.ListIterator; +import java.util.Stack; + +/** + * A less buggy, and smoother, replacement for the built-in Android ScaleGestureDetector. + * + * This gesture detector is more reliable than the built-in ScaleGestureDetector because: + * + * - It doesn't assume that pointer IDs are numbered 0 and 1. + * + * - It doesn't attempt to correct for "slop" when resting one's hand on the device. On some + * devices (e.g. the Droid X) this can cause the ScaleGestureDetector to lose track of how many + * pointers are down, with disastrous results (bug 706684). + * + * - Cancelling a zoom into a pan is handled correctly. + * + * - Starting with three or more fingers down, releasing fingers so that only two are down, and + * then performing a scale gesture is handled correctly. + * + * - It doesn't take pressure into account, which results in smoother scaling. + */ +public class SimpleScaleGestureDetector { + private static final String LOGTAG = "ScaleGestureDetector"; + + private SimpleScaleGestureListener mListener; + private long mLastEventTime; + private boolean mScaleResult; + + /* Information about all pointers that are down. */ + private LinkedList<PointerInfo> mPointerInfo; + + /** Creates a new gesture detector with the given listener. */ + public SimpleScaleGestureDetector(SimpleScaleGestureListener listener) { + mListener = listener; + mPointerInfo = new LinkedList<PointerInfo>(); + } + + /** Forward touch events to this function. */ + public void onTouchEvent(MotionEvent event) { + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + // If we get ACTION_DOWN while still tracking any pointers, + // something is wrong. Cancel the current gesture and start over. + if (getPointersDown() > 0) + onTouchEnd(event); + onTouchStart(event); + break; + case MotionEvent.ACTION_POINTER_DOWN: + onTouchStart(event); + break; + case MotionEvent.ACTION_MOVE: + onTouchMove(event); + break; + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + onTouchEnd(event); + break; + } + } + + private int getPointersDown() { + return mPointerInfo.size(); + } + + private int getActionIndex(MotionEvent event) { + return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) + >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; + } + + private void onTouchStart(MotionEvent event) { + mLastEventTime = event.getEventTime(); + mPointerInfo.addFirst(PointerInfo.create(event, getActionIndex(event))); + if (getPointersDown() == 2) { + sendScaleGesture(EventType.BEGIN); + } + } + + private void onTouchMove(MotionEvent event) { + mLastEventTime = event.getEventTime(); + for (int i = 0; i < event.getPointerCount(); i++) { + PointerInfo pointerInfo = pointerInfoForEventIndex(event, i); + if (pointerInfo != null) { + pointerInfo.populate(event, i); + } + } + + if (getPointersDown() == 2) { + sendScaleGesture(EventType.CONTINUE); + } + } + + private void onTouchEnd(MotionEvent event) { + mLastEventTime = event.getEventTime(); + + int action = event.getAction() & MotionEvent.ACTION_MASK; + boolean isCancel = (action == MotionEvent.ACTION_CANCEL || + action == MotionEvent.ACTION_DOWN); + + int id = event.getPointerId(getActionIndex(event)); + ListIterator<PointerInfo> iterator = mPointerInfo.listIterator(); + while (iterator.hasNext()) { + PointerInfo pointerInfo = iterator.next(); + if (!(isCancel || pointerInfo.getId() == id)) { + continue; + } + + // One of the pointers we were tracking was lifted. Remove its info object from the + // list, recycle it to avoid GC pauses, and send an onScaleEnd() notification if this + // ended the gesture. + iterator.remove(); + pointerInfo.recycle(); + if (getPointersDown() == 1) { + sendScaleGesture(EventType.END); + } + } + } + + /** + * Returns the X coordinate of the focus location (the midpoint of the two fingers). If only + * one finger is down, returns the location of that finger. + */ + public float getFocusX() { + switch (getPointersDown()) { + case 1: + return mPointerInfo.getFirst().getCurrent().x; + case 2: + PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); + return (pointerA.getCurrent().x + pointerB.getCurrent().x) / 2.0f; + } + + Log.e(LOGTAG, "No gesture taking place in getFocusX()!"); + return 0.0f; + } + + /** + * Returns the Y coordinate of the focus location (the midpoint of the two fingers). If only + * one finger is down, returns the location of that finger. + */ + public float getFocusY() { + switch (getPointersDown()) { + case 1: + return mPointerInfo.getFirst().getCurrent().y; + case 2: + PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); + return (pointerA.getCurrent().y + pointerB.getCurrent().y) / 2.0f; + } + + Log.e(LOGTAG, "No gesture taking place in getFocusY()!"); + return 0.0f; + } + + /** Returns the most recent distance between the two pointers. */ + public float getCurrentSpan() { + if (getPointersDown() != 2) { + Log.e(LOGTAG, "No gesture taking place in getCurrentSpan()!"); + return 0.0f; + } + + PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); + return PointUtils.distance(pointerA.getCurrent(), pointerB.getCurrent()); + } + + /** Returns the second most recent distance between the two pointers. */ + public float getPreviousSpan() { + if (getPointersDown() != 2) { + Log.e(LOGTAG, "No gesture taking place in getPreviousSpan()!"); + return 0.0f; + } + + PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); + PointF a = pointerA.getPrevious(), b = pointerB.getPrevious(); + if (a == null || b == null) { + a = pointerA.getCurrent(); + b = pointerB.getCurrent(); + } + + return PointUtils.distance(a, b); + } + + /** Returns the time of the last event related to the gesture. */ + public long getEventTime() { + return mLastEventTime; + } + + /** Returns true if the scale gesture is in progress and false otherwise. */ + public boolean isInProgress() { + return getPointersDown() == 2; + } + + /* Sends the requested scale gesture notification to the listener. */ + private void sendScaleGesture(EventType eventType) { + switch (eventType) { + case BEGIN: + mScaleResult = mListener.onScaleBegin(this); + break; + case CONTINUE: + if (mScaleResult) { + mListener.onScale(this); + } + break; + case END: + if (mScaleResult) { + mListener.onScaleEnd(this); + } + break; + } + } + + /* + * Returns the pointer info corresponding to the given pointer index, or null if the pointer + * isn't one that's being tracked. + */ + private PointerInfo pointerInfoForEventIndex(MotionEvent event, int index) { + int id = event.getPointerId(index); + for (PointerInfo pointerInfo : mPointerInfo) { + if (pointerInfo.getId() == id) { + return pointerInfo; + } + } + return null; + } + + private enum EventType { + BEGIN, + CONTINUE, + END, + } + + /* Encapsulates information about one of the two fingers involved in the gesture. */ + private static class PointerInfo { + /* A free list that recycles pointer info objects, to reduce GC pauses. */ + private static Stack<PointerInfo> sPointerInfoFreeList; + + private int mId; + private PointF mCurrent, mPrevious; + + private PointerInfo() { + // External users should use create() instead. + } + + /* Creates or recycles a new PointerInfo instance from an event and a pointer index. */ + public static PointerInfo create(MotionEvent event, int index) { + if (sPointerInfoFreeList == null) { + sPointerInfoFreeList = new Stack<PointerInfo>(); + } + + PointerInfo pointerInfo; + if (sPointerInfoFreeList.empty()) { + pointerInfo = new PointerInfo(); + } else { + pointerInfo = sPointerInfoFreeList.pop(); + } + + pointerInfo.populate(event, index); + return pointerInfo; + } + + /* + * Fills in the fields of this instance from the given motion event and pointer index + * within that event. + */ + public void populate(MotionEvent event, int index) { + mId = event.getPointerId(index); + mPrevious = mCurrent; + mCurrent = new PointF(event.getX(index), event.getY(index)); + } + + public void recycle() { + mId = -1; + mPrevious = mCurrent = null; + sPointerInfoFreeList.push(this); + } + + public int getId() { return mId; } + public PointF getCurrent() { return mCurrent; } + public PointF getPrevious() { return mPrevious; } + + @Override + public String toString() { + if (mId == -1) { + return "(up)"; + } + + try { + String prevString; + if (mPrevious == null) { + prevString = "n/a"; + } else { + prevString = PointUtils.toJSON(mPrevious).toString(); + } + + // The current position should always be non-null. + String currentString = PointUtils.toJSON(mCurrent).toString(); + return "id=" + mId + " cur=" + currentString + " prev=" + prevString; + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + } + + public static interface SimpleScaleGestureListener { + public boolean onScale(SimpleScaleGestureDetector detector); + public boolean onScaleBegin(SimpleScaleGestureDetector detector); + public void onScaleEnd(SimpleScaleGestureDetector detector); + } +} + diff --git a/android/source/src/java/org/mozilla/gecko/gfx/SingleTileLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/SingleTileLayer.java new file mode 100644 index 0000000000..0bc2716783 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/SingleTileLayer.java @@ -0,0 +1,154 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.RegionIterator; +import android.opengl.GLES20; + +import java.nio.FloatBuffer; + +/** + * Encapsulates the logic needed to draw a single textured tile. + * + * TODO: Repeating textures really should be their own type of layer. + */ +public class SingleTileLayer extends TileLayer { + private static final String LOGTAG = "GeckoSingleTileLayer"; + + private Rect mMask; + + // To avoid excessive GC, declare some objects here that would otherwise + // be created and destroyed frequently during draw(). + private final RectF mBounds; + private final RectF mTextureBounds; + private final RectF mViewport; + private final Rect mIntBounds; + private final Rect mSubRect; + private final RectF mSubRectF; + private final Region mMaskedBounds; + private final Rect mCropRect; + private final RectF mObjRectF; + private final float[] mCoords; + + public SingleTileLayer(CairoImage image) { + this(false, image); + } + + public SingleTileLayer(boolean repeat, CairoImage image) { + this(image, repeat ? TileLayer.PaintMode.REPEAT : TileLayer.PaintMode.NORMAL); + } + + public SingleTileLayer(CairoImage image, TileLayer.PaintMode paintMode) { + super(image, paintMode); + + mBounds = new RectF(); + mTextureBounds = new RectF(); + mViewport = new RectF(); + mIntBounds = new Rect(); + mSubRect = new Rect(); + mSubRectF = new RectF(); + mMaskedBounds = new Region(); + mCropRect = new Rect(); + mObjRectF = new RectF(); + mCoords = new float[20]; + } + + /** + * Set an area to mask out when rendering. + */ + public void setMask(Rect aMaskRect) { + mMask = aMaskRect; + } + + @Override + public void draw(RenderContext context) { + // mTextureIDs may be null here during startup if Layer.java's draw method + // failed to acquire the transaction lock and call performUpdates. + if (!initialized()) + return; + + mViewport.set(context.viewport); + + if (repeats()) { + // If we're repeating, we want to adjust the texture bounds so that + // the texture repeats the correct number of times when drawn at + // the size of the viewport. + mBounds.set(getBounds(context)); + mTextureBounds.set(0.0f, 0.0f, mBounds.width(), mBounds.height()); + mBounds.set(0.0f, 0.0f, mViewport.width(), mViewport.height()); + } else if (stretches()) { + // If we're stretching, we just want the bounds and texture bounds + // to fit to the page. + mBounds.set(context.pageRect); + mTextureBounds.set(mBounds); + } else { + mBounds.set(getBounds(context)); + mTextureBounds.set(mBounds); + } + + mBounds.roundOut(mIntBounds); + mMaskedBounds.set(mIntBounds); + if (mMask != null) { + mMaskedBounds.op(mMask, Region.Op.DIFFERENCE); + if (mMaskedBounds.isEmpty()) + return; + } + + // XXX Possible optimisation here, form this array so we can draw it in + // a single call. + RegionIterator i = new RegionIterator(mMaskedBounds); + while (i.next(mSubRect)) { + // Compensate for rounding errors at the edge of the tile caused by + // the roundOut above + mSubRectF.set(Math.max(mBounds.left, (float)mSubRect.left), + Math.max(mBounds.top, (float)mSubRect.top), + Math.min(mBounds.right, (float)mSubRect.right), + Math.min(mBounds.bottom, (float)mSubRect.bottom)); + + // This is the left/top/right/bottom of the rect, relative to the + // bottom-left of the layer, to use for texture coordinates. + mCropRect.set(Math.round(mSubRectF.left - mBounds.left), + Math.round(mBounds.bottom - mSubRectF.top), + Math.round(mSubRectF.right - mBounds.left), + Math.round(mBounds.bottom - mSubRectF.bottom)); + + mObjRectF.set(mSubRectF.left - mViewport.left, + mViewport.bottom - mSubRectF.bottom, + mSubRectF.right - mViewport.left, + mViewport.bottom - mSubRectF.top); + + fillRectCoordBuffer(mCoords, mObjRectF, mViewport.width(), mViewport.height(), + mCropRect, mTextureBounds.width(), mTextureBounds.height()); + + FloatBuffer coordBuffer = context.coordBuffer; + int positionHandle = context.positionHandle; + int textureHandle = context.textureHandle; + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureID()); + + // Make sure we are at position zero in the buffer + coordBuffer.position(0); + coordBuffer.put(mCoords); + + // Unbind any the current array buffer so we can use client side buffers + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, coordBuffer); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } + } +} + diff --git a/android/source/src/java/org/mozilla/gecko/gfx/SubTile.java b/android/source/src/java/org/mozilla/gecko/gfx/SubTile.java new file mode 100644 index 0000000000..bdad37195d --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/SubTile.java @@ -0,0 +1,254 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.RegionIterator; +import android.opengl.GLES20; +import android.util.Log; + +import org.libreoffice.TileIdentifier; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; + +public class SubTile extends Layer { + private static String LOGTAG = SubTile.class.getSimpleName(); + public final TileIdentifier id; + + private final RectF mBounds; + private final RectF mTextureBounds; + private final RectF mViewport; + private final Rect mIntBounds; + private final Rect mSubRect; + private final RectF mSubRectF; + private final Region mMaskedBounds; + private final Rect mCropRect; + private final RectF mObjRectF; + private final float[] mCoords; + + public boolean markedForRemoval = false; + + private CairoImage mImage; + private IntSize mSize; + private int[] mTextureIDs; + private boolean mDirtyTile; + + public SubTile(TileIdentifier id) { + super(); + this.id = id; + + mBounds = new RectF(); + mTextureBounds = new RectF(); + mViewport = new RectF(); + mIntBounds = new Rect(); + mSubRect = new Rect(); + mSubRectF = new RectF(); + mMaskedBounds = new Region(); + mCropRect = new Rect(); + mObjRectF = new RectF(); + mCoords = new float[20]; + + mImage = null; + mTextureIDs = null; + mSize = new IntSize(0, 0); + mDirtyTile = false; + } + + public void setImage(CairoImage image) { + if (image.getSize().isPositive()) { + this.mImage = image; + } + } + + public void refreshTileMetrics() { + setPosition(id.getCSSRect()); + } + + public void markForRemoval() { + markedForRemoval = true; + } + + protected int getTextureID() { + return mTextureIDs[0]; + } + + protected boolean initialized() { + return mTextureIDs != null; + } + + @Override + protected void finalize() throws Throwable { + try { + destroyImage(); + cleanTexture(); + } finally { + super.finalize(); + } + } + + private void cleanTexture() { + if (mTextureIDs != null) { + TextureReaper.get().add(mTextureIDs); + mTextureIDs = null; + TextureReaper.get().reap(); + } + } + + public void destroy() { + try { + destroyImage(); + cleanTexture(); + } catch (Exception ex) { + Log.e(LOGTAG, "Error clearing buffers: ", ex); + } + } + + public void destroyImage() { + if (mImage != null) { + mImage.destroy(); + mImage = null; + } + } + + /** + * Invalidates the entire buffer so that it will be uploaded again. Only valid inside a + * transaction. + */ + public void invalidate() { + if (!inTransaction()) { + throw new RuntimeException("invalidate() is only valid inside a transaction"); + } + if (mImage == null) { + return; + } + mDirtyTile = true; + } + + /** + * Remove the texture if the image is of different size than the current uploaded texture. + */ + private void validateTexture() { + IntSize textureSize = mImage.getSize().nextPowerOfTwo(); + + if (!textureSize.equals(mSize)) { + mSize = textureSize; + cleanTexture(); + } + } + + @Override + protected void performUpdates(RenderContext context) { + super.performUpdates(context); + if (mImage == null && !mDirtyTile) { + return; + } + validateTexture(); + uploadNewTexture(); + mDirtyTile = false; + } + + private void uploadNewTexture() { + ByteBuffer imageBuffer = mImage.getBuffer(); + if (imageBuffer == null) { + return; + } + + if (mTextureIDs == null) { + mTextureIDs = new int[1]; + GLES20.glGenTextures(mTextureIDs.length, mTextureIDs, 0); + } + + int cairoFormat = mImage.getFormat(); + CairoGLInfo glInfo = new CairoGLInfo(cairoFormat); + + bindAndSetGLParameters(); + + IntSize bufferSize = mImage.getSize(); + + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, glInfo.internalFormat, + mSize.width, mSize.height, 0, glInfo.format, glInfo.type, imageBuffer); + + destroyImage(); + } + + private void bindAndSetGLParameters() { + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIDs[0]); + + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + } + + @Override + public void draw(RenderContext context) { + // mTextureIDs may be null here during startup if Layer.java's draw method + // failed to acquire the transaction lock and call performUpdates. + if (!initialized()) + return; + + mViewport.set(context.viewport); + + mBounds.set(getBounds(context)); + mTextureBounds.set(mBounds); + + mBounds.roundOut(mIntBounds); + mMaskedBounds.set(mIntBounds); + + // XXX Possible optimisation here, form this array so we can draw it in + // a single call. + RegionIterator iterator = new RegionIterator(mMaskedBounds); + while (iterator.next(mSubRect)) { + // Compensate for rounding errors at the edge of the tile caused by + // the roundOut above + mSubRectF.set(Math.max(mBounds.left, (float) mSubRect.left), + Math.max(mBounds.top, (float) mSubRect.top), + Math.min(mBounds.right, (float) mSubRect.right), + Math.min(mBounds.bottom, (float) mSubRect.bottom)); + + // This is the left/top/right/bottom of the rect, relative to the + // bottom-left of the layer, to use for texture coordinates. + mCropRect.set(Math.round(mSubRectF.left - mBounds.left), + Math.round(mBounds.bottom - mSubRectF.top), + Math.round(mSubRectF.right - mBounds.left), + Math.round(mBounds.bottom - mSubRectF.bottom)); + + mObjRectF.set(mSubRectF.left - mViewport.left, + mViewport.bottom - mSubRectF.bottom, + mSubRectF.right - mViewport.left, + mViewport.bottom - mSubRectF.top); + + fillRectCoordBuffer(mCoords, mObjRectF, mViewport.width(), mViewport.height(), mCropRect, mTextureBounds.width(), mTextureBounds.height()); + + FloatBuffer coordBuffer = context.coordBuffer; + int positionHandle = context.positionHandle; + int textureHandle = context.textureHandle; + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureID()); + + // Make sure we are at position zero in the buffer + coordBuffer.position(0); + coordBuffer.put(mCoords); + + // Unbind any the current array buffer so we can use client side buffers + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + // Vertex coordinates are x,y,z starting at position 0 into the buffer. + coordBuffer.position(0); + GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 20, coordBuffer); + + // Texture coordinates are texture_x, texture_y starting at position 3 into the buffer. + coordBuffer.position(3); + GLES20.glVertexAttribPointer(textureHandle, 2, GLES20.GL_FLOAT, false, 20, coordBuffer); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java b/android/source/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java new file mode 100644 index 0000000000..5a752e3c71 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java @@ -0,0 +1,78 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.PointF; +import android.os.Handler; + +class SubdocumentScrollHelper { + private static final String LOGTAG = "GeckoSubdocumentScrollHelper"; + + private final Handler mUiHandler; + + /* This is the amount of displacement we have accepted but not yet sent to JS; this is + * only valid when mOverrideScrollPending is true. */ + private final PointF mPendingDisplacement; + + /* When this is true, we're sending scroll events to JS to scroll the active subdocument. */ + private boolean mOverridePanning; + + /* When this is true, we have received an ack for the last scroll event we sent to JS, and + * are ready to send the next scroll event. Note we only ever have one scroll event inflight + * at a time. */ + private boolean mOverrideScrollAck; + + /* When this is true, we have a pending scroll that we need to send to JS; we were unable + * to send it when it was initially requested because mOverrideScrollAck was not true. */ + private boolean mOverrideScrollPending; + + /* When this is true, the last scroll event we sent actually did some amount of scrolling on + * the subdocument; we use this to decide when we have reached the end of the subdocument. */ + private boolean mScrollSucceeded; + + SubdocumentScrollHelper() { + // mUiHandler will be bound to the UI thread since that's where this constructor runs + mUiHandler = new Handler(); + mPendingDisplacement = new PointF(); + } + + void destroy() { + } + + boolean scrollBy(PointF displacement) { + if (! mOverridePanning) { + return false; + } + + if (! mOverrideScrollAck) { + mOverrideScrollPending = true; + mPendingDisplacement.x += displacement.x; + mPendingDisplacement.y += displacement.y; + return true; + } + + mOverrideScrollAck = false; + mOverrideScrollPending = false; + // clear the |mPendingDisplacement| after serializing |displacement| to + // JSON because they might be the same object + mPendingDisplacement.x = 0; + mPendingDisplacement.y = 0; + + return true; + } + + void cancel() { + mOverridePanning = false; + } + + boolean scrolling() { + return mOverridePanning; + } + + boolean lastScrollSucceeded() { + return mScrollSucceeded; + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/TextLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/TextLayer.java new file mode 100644 index 0000000000..023433a888 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/TextLayer.java @@ -0,0 +1,69 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Typeface; + +import org.libreoffice.kit.DirectBufferAllocator; + +import java.nio.ByteBuffer; + +/** + * Draws text on a layer. This is used for the frame rate meter. + */ +public class TextLayer extends SingleTileLayer { + private final ByteBuffer mBuffer; // this buffer is owned by the BufferedCairoImage + private final IntSize mSize; + + /* + * This awkward pattern is necessary due to Java's restrictions on when one can call superclass + * constructors. + */ + private TextLayer(ByteBuffer buffer, BufferedCairoImage image, IntSize size, String text) { + super(false, image); + mBuffer = buffer; + mSize = size; + renderText(text); + } + + public static TextLayer create(IntSize size, String text) { + ByteBuffer buffer = DirectBufferAllocator.allocate(size.width * size.height * 4); + BufferedCairoImage image = new BufferedCairoImage(buffer, size.width, size.height, + CairoImage.FORMAT_ARGB32); + return new TextLayer(buffer, image, size, text); + } + + public void setText(String text) { + renderText(text); + invalidate(); + } + + private void renderText(String text) { + Bitmap bitmap = Bitmap.createBitmap(mSize.width, mSize.height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + Paint textPaint = new Paint(); + textPaint.setAntiAlias(true); + textPaint.setColor(Color.WHITE); + textPaint.setFakeBoldText(true); + textPaint.setTextSize(18.0f); + textPaint.setTypeface(Typeface.DEFAULT_BOLD); + float width = textPaint.measureText(text) + 18.0f; + + Paint backgroundPaint = new Paint(); + backgroundPaint.setColor(Color.argb(127, 0, 0, 0)); + canvas.drawRect(0.0f, 0.0f, width, 18.0f + 6.0f, backgroundPaint); + + canvas.drawText(text, 6.0f, 18.0f, textPaint); + + bitmap.copyPixelsToBuffer(mBuffer.asIntBuffer()); + } +} + diff --git a/android/source/src/java/org/mozilla/gecko/gfx/TextureGenerator.java b/android/source/src/java/org/mozilla/gecko/gfx/TextureGenerator.java new file mode 100644 index 0000000000..bccd8968c8 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/TextureGenerator.java @@ -0,0 +1,77 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.opengl.GLES20; +import android.util.Log; + +import java.util.concurrent.ArrayBlockingQueue; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLContext; + +public class TextureGenerator { + private static final String LOGTAG = "TextureGenerator"; + private static final int POOL_SIZE = 5; + + private static TextureGenerator sSharedInstance; + + private ArrayBlockingQueue<Integer> mTextureIds; + private EGLContext mContext; + + private TextureGenerator() { + mTextureIds = new ArrayBlockingQueue<Integer>(POOL_SIZE); + } + + public static TextureGenerator get() { + if (sSharedInstance == null) + sSharedInstance = new TextureGenerator(); + return sSharedInstance; + } + + public synchronized int take() { + try { + // Will block until one becomes available + return mTextureIds.take(); + } catch (InterruptedException e) { + return 0; + } + } + + public synchronized void fill() { + EGL10 egl = (EGL10) EGLContext.getEGL(); + EGLContext context = egl.eglGetCurrentContext(); + + if (mContext != null && mContext != context) { + mTextureIds.clear(); + } + + mContext = context; + + int numNeeded = mTextureIds.remainingCapacity(); + if (numNeeded == 0) + return; + + // Clear existing GL errors + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + Log.w(LOGTAG, String.format("Clearing GL error: %#x", error)); + } + + int[] textures = new int[numNeeded]; + GLES20.glGenTextures(numNeeded, textures, 0); + + error = GLES20.glGetError(); + if (error != GLES20.GL_NO_ERROR) { + Log.e(LOGTAG, String.format("Failed to generate textures: %#x", error), new Exception()); + return; + } + + for (int i = 0; i < numNeeded; i++) { + mTextureIds.offer(textures[i]); + } + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/TextureReaper.java b/android/source/src/java/org/mozilla/gecko/gfx/TextureReaper.java new file mode 100644 index 0000000000..1a8a504597 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/TextureReaper.java @@ -0,0 +1,62 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.opengl.GLES20; +import android.util.Log; + +import java.util.ArrayList; + +/** + * Manages a list of dead tiles, so we don't leak resources. + */ +public class TextureReaper { + private static TextureReaper sSharedInstance; + private ArrayList<Integer> mDeadTextureIDs = new ArrayList<Integer>(); + private static final String LOGTAG = TextureReaper.class.getSimpleName(); + + private TextureReaper() { + } + + public static TextureReaper get() { + if (sSharedInstance == null) { + sSharedInstance = new TextureReaper(); + } + return sSharedInstance; + } + + public void add(int[] textureIDs) { + for (int textureID : textureIDs) { + add(textureID); + } + } + + public synchronized void add(int textureID) { + mDeadTextureIDs.add(textureID); + } + + public synchronized void reap() { + int numTextures = mDeadTextureIDs.size(); + // Adreno 200 will generate INVALID_VALUE if len == 0 is passed to glDeleteTextures, + // even though it's not supposed to. + if (numTextures == 0) + return; + + int[] deadTextureIDs = new int[numTextures]; + for (int i = 0; i < numTextures; i++) { + Integer id = mDeadTextureIDs.get(i); + if (id == null) { + deadTextureIDs[i] = 0; + Log.e(LOGTAG, "Dead texture id is null"); + } else { + deadTextureIDs[i] = mDeadTextureIDs.get(i); + } + } + mDeadTextureIDs.clear(); + + GLES20.glDeleteTextures(deadTextureIDs.length, deadTextureIDs, 0); + } +}
\ No newline at end of file diff --git a/android/source/src/java/org/mozilla/gecko/gfx/TileLayer.java b/android/source/src/java/org/mozilla/gecko/gfx/TileLayer.java new file mode 100644 index 0000000000..3d0ff1fede --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/TileLayer.java @@ -0,0 +1,176 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.Rect; +import android.opengl.GLES20; +import android.util.Log; + +import java.nio.ByteBuffer; + +/** + * Base class for tile layers, which encapsulate the logic needed to draw textured tiles in OpenGL + * ES. + */ +public abstract class TileLayer extends Layer { + private static final String LOGTAG = "GeckoTileLayer"; + + private final Rect mDirtyRect; + private IntSize mSize; + private int[] mTextureIDs; + + protected final CairoImage mImage; + + public CairoImage getImage() { + return mImage; + } + + public enum PaintMode { NORMAL, REPEAT, STRETCH }; + private PaintMode mPaintMode; + + public TileLayer(CairoImage image, PaintMode paintMode) { + super(image == null ? null : image.getSize()); + + mPaintMode = paintMode; + mImage = image; + mSize = new IntSize(0, 0); + mDirtyRect = new Rect(); + } + + protected boolean repeats() { return mPaintMode == PaintMode.REPEAT; } + protected boolean stretches() { return mPaintMode == PaintMode.STRETCH; } + protected int getTextureID() { return mTextureIDs[0]; } + protected boolean initialized() { return mImage != null && mTextureIDs != null; } + + @Override + protected void finalize() throws Throwable { + try { + if (mTextureIDs != null) + TextureReaper.get().add(mTextureIDs); + } finally { + super.finalize(); + } + } + + public void destroy() { + try { + if (mImage != null) { + mImage.destroy(); + } + } catch (Exception ex) { + Log.e(LOGTAG, "error clearing buffers: ", ex); + } + } + + public void setPaintMode(PaintMode mode) { + mPaintMode = mode; + } + + /** + * Invalidates the entire buffer so that it will be uploaded again. Only valid inside a + * transaction. + */ + public void invalidate() { + if (!inTransaction()) + throw new RuntimeException("invalidate() is only valid inside a transaction"); + IntSize bufferSize = mImage.getSize(); + mDirtyRect.set(0, 0, bufferSize.width, bufferSize.height); + } + + private void validateTexture() { + /* Calculate the ideal texture size. This must be a power of two if + * the texture is repeated or OpenGL ES 2.0 isn't supported, as + * OpenGL ES 2.0 is required for NPOT texture support (without + * extensions), but doesn't support repeating NPOT textures. + * + * XXX Currently, we don't pick a GLES 2.0 context, so always round. + */ + IntSize textureSize = mImage.getSize().nextPowerOfTwo(); + + if (!textureSize.equals(mSize)) { + mSize = textureSize; + + // Delete the old texture + if (mTextureIDs != null) { + TextureReaper.get().add(mTextureIDs); + mTextureIDs = null; + + // Free the texture immediately, so we don't incur a + // temporarily increased memory usage. + TextureReaper.get().reap(); + } + } + } + + @Override + protected void performUpdates(RenderContext context) { + super.performUpdates(context); + + // Reallocate the texture if the size has changed + validateTexture(); + + // Don't do any work if the image has an invalid size. + if (!mImage.getSize().isPositive()) + return; + + // If we haven't allocated a texture, assume the whole region is dirty + if (mTextureIDs == null) { + uploadFullTexture(); + } else { + uploadDirtyRect(mDirtyRect); + } + + mDirtyRect.setEmpty(); + } + + private void uploadFullTexture() { + IntSize bufferSize = mImage.getSize(); + uploadDirtyRect(new Rect(0, 0, bufferSize.width, bufferSize.height)); + } + + private void uploadDirtyRect(Rect dirtyRect) { + // If we have nothing to upload, just return for now + if (dirtyRect.isEmpty()) + return; + + // It's possible that the buffer will be null, check for that and return + ByteBuffer imageBuffer = mImage.getBuffer(); + if (imageBuffer == null) + return; + + if (mTextureIDs == null) { + mTextureIDs = new int[1]; + GLES20.glGenTextures(mTextureIDs.length, mTextureIDs, 0); + } + + int cairoFormat = mImage.getFormat(); + CairoGLInfo glInfo = new CairoGLInfo(cairoFormat); + + bindAndSetGLParameters(); + + // XXX TexSubImage2D is too broken to rely on Adreno, and very slow + // on other chipsets, so we always upload the entire buffer. + IntSize bufferSize = mImage.getSize(); + + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, glInfo.internalFormat, mSize.width, + mSize.height, 0, glInfo.format, glInfo.type, imageBuffer); + + } + + private void bindAndSetGLParameters() { + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureIDs[0]); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR); + + int repeatMode = repeats() ? GLES20.GL_REPEAT : GLES20.GL_CLAMP_TO_EDGE; + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, repeatMode); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, repeatMode); + } +} + diff --git a/android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java b/android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java new file mode 100644 index 0000000000..1c227de20b --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java @@ -0,0 +1,306 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.content.Context; +import android.os.SystemClock; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * This class handles incoming touch events from the user and sends them to + * listeners in Gecko and/or performs the "default action" (asynchronous pan/zoom + * behaviour. EVERYTHING IN THIS CLASS MUST RUN ON THE UI THREAD. + * + * In the following code/comments, a "block" of events refers to a contiguous + * sequence of events that starts with a DOWN or POINTER_DOWN and goes up to + * but not including the next DOWN or POINTER_DOWN event. + * + * "Dispatching" an event refers to performing the default actions for the event, + * which at our level of abstraction just means sending it off to the gesture + * detectors and the pan/zoom controller. + * + * If an event is "default-prevented" that means one or more listeners in Gecko + * has called preventDefault() on the event, which means that the default action + * for that event should not occur. Usually we care about a "block" of events being + * default-prevented, which means that the DOWN/POINTER_DOWN event that started + * the block, or the first MOVE event following that, were prevent-defaulted. + * + * A "default-prevented notification" is when we here in Java-land receive a notification + * from gecko as to whether or not a block of events was default-prevented. This happens + * at some point after the first or second event in the block is processed in Gecko. + * This code assumes we get EXACTLY ONE default-prevented notification for each block + * of events. + * + * Note that even if all events are default-prevented, we still send specific types + * of notifications to the pan/zoom controller. The notifications are needed + * to respond to user actions a timely manner regardless of default-prevention, + * and fix issues like bug 749384. + */ +public final class TouchEventHandler { + private static final String LOGTAG = "GeckoTouchEventHandler"; + + // The time limit for listeners to respond with preventDefault on touchevents + // before we begin panning the page + private final int EVENT_LISTENER_TIMEOUT = 200; + + private final View mView; + private final GestureDetector mGestureDetector; + private final SimpleScaleGestureDetector mScaleGestureDetector; + private final JavaPanZoomController mPanZoomController; + + // the queue of events that we are holding on to while waiting for a preventDefault + // notification + private final Queue<MotionEvent> mEventQueue; + private final ListenerTimeoutProcessor mListenerTimeoutProcessor; + + // whether or not we should wait for touch listeners to respond (this state is + // per-tab and is updated when we switch tabs). + private boolean mWaitForTouchListeners; + + // true if we should hold incoming events in our queue. this is re-set for every + // block of events, this is cleared once we find out if the block has been + // default-prevented or not (or we time out waiting for that). + private boolean mHoldInQueue; + + // true if we should dispatch incoming events to the gesture detector and the pan/zoom + // controller. if this is false, then the current block of events has been + // default-prevented, and we should not dispatch these events (although we'll still send + // them to gecko listeners). + private boolean mDispatchEvents; + + // this next variable requires some explanation. strap yourself in. + // + // for each block of events, we do two things: (1) send the events to gecko and expect + // exactly one default-prevented notification in return, and (2) kick off a delayed + // ListenerTimeoutProcessor that triggers in case we don't hear from the listener in + // a timely fashion. + // since events are constantly coming in, we need to be able to handle more than one + // block of events in the queue. + // + // this means that there are ordering restrictions on these that we can take advantage of, + // and need to abide by. blocks of events in the queue will always be in the order that + // the user generated them. default-prevented notifications we get from gecko will be in + // the same order as the blocks of events in the queue. the ListenerTimeoutProcessors that + // have been posted will also fire in the same order as the blocks of events in the queue. + // HOWEVER, we may get multiple default-prevented notifications interleaved with multiple + // ListenerTimeoutProcessor firings, and that interleaving is not predictable. + // + // therefore, we need to make sure that for each block of events, we process the queued + // events exactly once, either when we get the default-prevented notification, or when the + // timeout expires (whichever happens first). there is no way to associate the + // default-prevented notification with a particular block of events other than via ordering, + // + // so what we do to accomplish this is to track a "processing balance", which is the number + // of default-prevented notifications that we have received, minus the number of ListenerTimeoutProcessors + // that have fired. (think "balance" as in teeter-totter balance). this value is: + // - zero when we are in a state where the next default-prevented notification we expect + // to receive and the next ListenerTimeoutProcessor we expect to fire both correspond to + // the next block of events in the queue. + // - positive when we are in a state where we have received more default-prevented notifications + // than ListenerTimeoutProcessors. This means that the next default-prevented notification + // does correspond to the block at the head of the queue, but the next n ListenerTimeoutProcessors + // need to be ignored as they are for blocks we have already processed. (n is the absolute value + // of the balance.) + // - negative when we are in a state where we have received more ListenerTimeoutProcessors than + // default-prevented notifications. This means that the next ListenerTimeoutProcessor that + // we receive does correspond to the block at the head of the queue, but the next n + // default-prevented notifications need to be ignored as they are for blocks we have already + // processed. (n is the absolute value of the balance.) + private int mProcessingBalance; + + TouchEventHandler(Context context, View view, JavaPanZoomController panZoomController) { + mView = view; + + mEventQueue = new LinkedList<MotionEvent>(); + mPanZoomController = panZoomController; + mGestureDetector = new GestureDetector(context, mPanZoomController); + mScaleGestureDetector = new SimpleScaleGestureDetector(mPanZoomController); + mListenerTimeoutProcessor = new ListenerTimeoutProcessor(); + mDispatchEvents = true; + + mGestureDetector.setOnDoubleTapListener(mPanZoomController); + } + + void destroy() { + } + + /* This function MUST be called on the UI thread */ + public boolean handleEvent(MotionEvent event) { + if (isDownEvent(event)) { + // this is the start of a new block of events! whee! + mHoldInQueue = mWaitForTouchListeners; + + // Set mDispatchEvents to true so that we are guaranteed to either queue these + // events or dispatch them. The only time we should not do either is once we've + // heard back from content to preventDefault this block. + mDispatchEvents = true; + if (mHoldInQueue) { + // if the new block we are starting is the current block (i.e. there are no + // other blocks waiting in the queue, then we should let the pan/zoom controller + // know we are waiting for the touch listeners to run + if (mEventQueue.isEmpty()) { + mPanZoomController.startingNewEventBlock(event, true); + } + } else { + // we're not going to be holding this block of events in the queue, but we need + // a marker of some sort so that the processEventBlock loop deals with the blocks + // in the right order as notifications come in. we use a single null event in + // the queue as a placeholder for a block of events that has already been dispatched. + mEventQueue.add(null); + mPanZoomController.startingNewEventBlock(event, false); + } + + // set the timeout so that we dispatch these events and update mProcessingBalance + // if we don't get a default-prevented notification + mView.postDelayed(mListenerTimeoutProcessor, EVENT_LISTENER_TIMEOUT); + } + + // if we need to hold the events, add it to the queue. if we need to dispatch + // it directly, do that. it is possible that both mHoldInQueue and mDispatchEvents + // are false, in which case we are processing a block of events that we know + // has been default-prevented. in that case we don't keep the events as we don't + // need them (but we still pass them to the gecko listener). + if (mHoldInQueue) { + mEventQueue.add(MotionEvent.obtain(event)); + } else if (mDispatchEvents) { + dispatchEvent(event); + } else if (touchFinished(event)) { + mPanZoomController.preventedTouchFinished(); + } + + return true; + } + + /** + * This function is how gecko sends us a default-prevented notification. It is called + * once gecko knows definitively whether the block of events has had preventDefault + * called on it (either on the initial down event that starts the block, or on + * the first event following that down event). + * + * This function MUST be called on the UI thread. + */ + public void handleEventListenerAction(boolean allowDefaultAction) { + if (mProcessingBalance > 0) { + // this event listener that triggered this took too long, and the corresponding + // ListenerTimeoutProcessor runnable already ran for the event in question. the + // block of events this is for has already been processed, so we don't need to + // do anything here. + } else { + processEventBlock(allowDefaultAction); + } + mProcessingBalance--; + } + + /* This function MUST be called on the UI thread. */ + public void setWaitForTouchListeners(boolean aValue) { + mWaitForTouchListeners = aValue; + } + + private boolean isDownEvent(MotionEvent event) { + int action = (event.getAction() & MotionEvent.ACTION_MASK); + return (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN); + } + + private boolean touchFinished(MotionEvent event) { + int action = (event.getAction() & MotionEvent.ACTION_MASK); + return (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL); + } + + /** + * Dispatch the event to the gesture detectors and the pan/zoom controller. + */ + private void dispatchEvent(MotionEvent event) { + if (mGestureDetector.onTouchEvent(event)) { + return; + } + mScaleGestureDetector.onTouchEvent(event); + if (mScaleGestureDetector.isInProgress()) { + return; + } + mPanZoomController.handleEvent(event); + } + + /** + * Process the block of events at the head of the queue now that we know + * whether it has been default-prevented or not. + */ + private void processEventBlock(boolean allowDefaultAction) { + if (!allowDefaultAction) { + // if the block has been default-prevented, cancel whatever stuff we had in + // progress in the gesture detector and pan zoom controller + long now = SystemClock.uptimeMillis(); + dispatchEvent(MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0)); + } + + if (mEventQueue.isEmpty()) { + Log.e(LOGTAG, "Unexpected empty event queue in processEventBlock!", new Exception()); + return; + } + + // the odd loop condition is because the first event in the queue will + // always be a DOWN or POINTER_DOWN event, and we want to process all + // the events in the queue starting at that one, up to but not including + // the next DOWN or POINTER_DOWN event. + + MotionEvent event = mEventQueue.poll(); + while (true) { + // event being null here is valid and represents a block of events + // that has already been dispatched. + + if (event != null) { + // for each event we process, only dispatch it if the block hasn't been + // default-prevented. + if (allowDefaultAction) { + dispatchEvent(event); + } else if (touchFinished(event)) { + mPanZoomController.preventedTouchFinished(); + } + } + if (mEventQueue.isEmpty()) { + // we have processed the backlog of events, and are all caught up. + // now we can set clear the hold flag and set the dispatch flag so + // that the handleEvent() function can do the right thing for all + // remaining events in this block (which is still ongoing) without + // having to put them in the queue. + mHoldInQueue = false; + mDispatchEvents = allowDefaultAction; + break; + } + event = mEventQueue.peek(); + if (event == null || isDownEvent(event)) { + // we have finished processing the block we were interested in. + // now we wait for the next call to processEventBlock + if (event != null) { + mPanZoomController.startingNewEventBlock(event, true); + } + break; + } + // pop the event we peeked above, as it is still part of the block and + // we want to keep processing + mEventQueue.remove(); + } + } + + private class ListenerTimeoutProcessor implements Runnable { + /* This MUST be run on the UI thread */ + public void run() { + if (mProcessingBalance < 0) { + // gecko already responded with default-prevented notification, and so + // the block of events this ListenerTimeoutProcessor corresponds to have + // already been removed from the queue. + } else { + processEventBlock(true); + } + mProcessingBalance++; + } + } +} diff --git a/android/source/src/java/org/mozilla/gecko/gfx/ViewportMetrics.java b/android/source/src/java/org/mozilla/gecko/gfx/ViewportMetrics.java new file mode 100644 index 0000000000..f8b5c2e055 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/gfx/ViewportMetrics.java @@ -0,0 +1,173 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.gfx; + +import android.graphics.PointF; +import android.graphics.RectF; +import android.util.DisplayMetrics; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * ViewportMetrics manages state and contains some utility functions related to + * the page viewport for the Gecko layer client to use. + */ +public class ViewportMetrics { + private static final String LOGTAG = "GeckoViewportMetrics"; + + private RectF mPageRect; + private RectF mCssPageRect; + private RectF mViewportRect; + private float mZoomFactor; + + public ViewportMetrics(DisplayMetrics metrics) { + mPageRect = new RectF(0, 0, metrics.widthPixels, metrics.heightPixels); + mCssPageRect = new RectF(0, 0, metrics.widthPixels, metrics.heightPixels); + mViewportRect = new RectF(0, 0, metrics.widthPixels, metrics.heightPixels); + mZoomFactor = 1.0f; + } + + public ViewportMetrics(ViewportMetrics viewport) { + mPageRect = new RectF(viewport.getPageRect()); + mCssPageRect = new RectF(viewport.getCssPageRect()); + mViewportRect = new RectF(viewport.getViewport()); + mZoomFactor = viewport.getZoomFactor(); + } + + public ViewportMetrics(ImmutableViewportMetrics viewport) { + mPageRect = new RectF(viewport.pageRectLeft, + viewport.pageRectTop, + viewport.pageRectRight, + viewport.pageRectBottom); + mCssPageRect = new RectF(viewport.cssPageRectLeft, + viewport.cssPageRectTop, + viewport.cssPageRectRight, + viewport.cssPageRectBottom); + mViewportRect = new RectF(viewport.viewportRectLeft, + viewport.viewportRectTop, + viewport.viewportRectRight, + viewport.viewportRectBottom); + mZoomFactor = viewport.zoomFactor; + } + + public ViewportMetrics(JSONObject json) throws JSONException { + float x = (float)json.getDouble("x"); + float y = (float)json.getDouble("y"); + float width = (float)json.getDouble("width"); + float height = (float)json.getDouble("height"); + float pageLeft = (float)json.getDouble("pageLeft"); + float pageTop = (float)json.getDouble("pageTop"); + float pageRight = (float)json.getDouble("pageRight"); + float pageBottom = (float)json.getDouble("pageBottom"); + float cssPageLeft = (float)json.getDouble("cssPageLeft"); + float cssPageTop = (float)json.getDouble("cssPageTop"); + float cssPageRight = (float)json.getDouble("cssPageRight"); + float cssPageBottom = (float)json.getDouble("cssPageBottom"); + float zoom = (float)json.getDouble("zoom"); + + mPageRect = new RectF(pageLeft, pageTop, pageRight, pageBottom); + mCssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom); + mViewportRect = new RectF(x, y, x + width, y + height); + mZoomFactor = zoom; + } + + public ViewportMetrics(float x, float y, float width, float height, + float pageLeft, float pageTop, float pageRight, float pageBottom, + float cssPageLeft, float cssPageTop, float cssPageRight, float cssPageBottom, + float zoom) { + mPageRect = new RectF(pageLeft, pageTop, pageRight, pageBottom); + mCssPageRect = new RectF(cssPageLeft, cssPageTop, cssPageRight, cssPageBottom); + mViewportRect = new RectF(x, y, x + width, y + height); + mZoomFactor = zoom; + } + + public PointF getOrigin() { + return new PointF(mViewportRect.left, mViewportRect.top); + } + + public FloatSize getSize() { + return new FloatSize(mViewportRect.width(), mViewportRect.height()); + } + + public RectF getViewport() { + return mViewportRect; + } + + public RectF getCssViewport() { + return RectUtils.scale(mViewportRect, 1/mZoomFactor); + } + + public RectF getPageRect() { + return mPageRect; + } + + public RectF getCssPageRect() { + return mCssPageRect; + } + + public float getZoomFactor() { + return mZoomFactor; + } + + public void setPageRect(RectF pageRect, RectF cssPageRect) { + mPageRect = pageRect; + mCssPageRect = cssPageRect; + } + + public void setViewport(RectF viewport) { + mViewportRect = viewport; + } + + public void setOrigin(PointF origin) { + mViewportRect.set(origin.x, origin.y, + origin.x + mViewportRect.width(), + origin.y + mViewportRect.height()); + } + + public void setSize(FloatSize size) { + mViewportRect.right = mViewportRect.left + size.width; + mViewportRect.bottom = mViewportRect.top + size.height; + } + + public void setZoomFactor(float zoomFactor) { + mZoomFactor = zoomFactor; + } + + public String toJSON() { + // Round off height and width. Since the height and width are the size of the screen, it + // makes no sense to send non-integer coordinates to Gecko. + int height = Math.round(mViewportRect.height()); + int width = Math.round(mViewportRect.width()); + + StringBuffer sb = new StringBuffer(512); + sb.append("{ \"x\" : ").append(mViewportRect.left) + .append(", \"y\" : ").append(mViewportRect.top) + .append(", \"width\" : ").append(width) + .append(", \"height\" : ").append(height) + .append(", \"pageLeft\" : ").append(mPageRect.left) + .append(", \"pageTop\" : ").append(mPageRect.top) + .append(", \"pageRight\" : ").append(mPageRect.right) + .append(", \"pageBottom\" : ").append(mPageRect.bottom) + .append(", \"cssPageLeft\" : ").append(mCssPageRect.left) + .append(", \"cssPageTop\" : ").append(mCssPageRect.top) + .append(", \"cssPageRight\" : ").append(mCssPageRect.right) + .append(", \"cssPageBottom\" : ").append(mCssPageRect.bottom) + .append(", \"zoom\" : ").append(mZoomFactor) + .append(" }"); + return sb.toString(); + } + + @Override + public String toString() { + StringBuffer buff = new StringBuffer(256); + buff.append("v=").append(mViewportRect.toString()) + .append(" p=").append(mPageRect.toString()) + .append(" c=").append(mCssPageRect.toString()) + .append(" z=").append(mZoomFactor); + return buff.toString(); + } +} diff --git a/android/source/src/java/org/mozilla/gecko/util/FloatUtils.java b/android/source/src/java/org/mozilla/gecko/util/FloatUtils.java new file mode 100644 index 0000000000..a48266c573 --- /dev/null +++ b/android/source/src/java/org/mozilla/gecko/util/FloatUtils.java @@ -0,0 +1,41 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * 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/. */ + +package org.mozilla.gecko.util; + +import android.graphics.PointF; + +public final class FloatUtils { + private FloatUtils() {} + + public static boolean fuzzyEquals(float a, float b) { + return (Math.abs(a - b) < 1e-6); + } + + public static boolean fuzzyEquals(PointF a, PointF b) { + return fuzzyEquals(a.x, b.x) && fuzzyEquals(a.y, b.y); + } + + /* + * Returns the value that represents a linear transition between `from` and `to` at time `t`, + * which is on the scale [0, 1). Thus with t = 0.0f, this returns `from`; with t = 1.0f, this + * returns `to`; with t = 0.5f, this returns the value halfway from `from` to `to`. + */ + public static float interpolate(float from, float to, float t) { + return from + (to - from) * t; + } + + /** + * Returns 'value', clamped so that it isn't any lower than 'low', and it + * isn't any higher than 'high'. + */ + public static float clamp(float value, float low, float high) { + if (high < low) { + throw new IllegalArgumentException( + "clamp called with invalid parameters (" + high + " < " + low + ")" ); + } + return Math.max(low, Math.min(high, value)); + } +} |