summaryrefslogtreecommitdiffstats
path: root/android/source/src/java/org/mozilla
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 05:54:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 05:54:39 +0000
commit267c6f2ac71f92999e969232431ba04678e7437e (patch)
tree358c9467650e1d0a1d7227a21dac2e3d08b622b2 /android/source/src/java/org/mozilla
parentInitial commit. (diff)
downloadlibreoffice-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')
-rw-r--r--android/source/src/java/org/mozilla/gecko/OnInterceptTouchListener.java14
-rw-r--r--android/source/src/java/org/mozilla/gecko/OnSlideSwipeListener.java94
-rw-r--r--android/source/src/java/org/mozilla/gecko/ZoomConstraints.java30
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/Axis.java337
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/BufferedCairoImage.java83
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/CairoGLInfo.java35
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/CairoImage.java28
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/CairoUtils.java51
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/ComposedTileLayer.java290
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/DisplayPortCalculator.java760
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/DisplayPortMetrics.java67
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/DynamicTileLayer.java30
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/FixedZoomTileLayer.java31
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/FloatSize.java53
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/GLController.java215
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/GeckoLayerClient.java356
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java241
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/InputConnectionHandler.java15
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/IntSize.java73
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/JavaPanZoomController.java1087
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/Layer.java218
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/LayerRenderer.java453
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/LayerView.java337
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/NinePatchTileLayer.java131
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/PanZoomController.java36
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/PanZoomTarget.java26
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/PointUtils.java53
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/RectUtils.java110
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/RenderControllerThread.java143
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/ScrollbarLayer.java451
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java322
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/SingleTileLayer.java154
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/SubTile.java254
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java78
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/TextLayer.java69
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/TextureGenerator.java77
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/TextureReaper.java62
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/TileLayer.java176
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/TouchEventHandler.java306
-rw-r--r--android/source/src/java/org/mozilla/gecko/gfx/ViewportMetrics.java173
-rw-r--r--android/source/src/java/org/mozilla/gecko/util/FloatUtils.java41
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));
+ }
+}