From 22a6438a2a0a2f706b3276229662f8726dbff69a Mon Sep 17 00:00:00 2001 From: Chris Lord Date: Wed, 23 Nov 2011 19:07:47 +0000 Subject: [PATCH] Bug 703141 - Reinstate zooming and add CSS zooming after viewport change. r=kats This patch reinstates pinch-zooming and adds CSS re-scaling so that after zooming, the page is rendered at the scaled resolution and you get clear text. --HG-- rename : mobile/android/base/gfx/IntSize.java => mobile/android/base/gfx/FloatSize.java --- mobile/android/base/FloatUtils.java | 46 +++++++ mobile/android/base/GeckoApp.java | 46 ++----- mobile/android/base/Makefile.in | 2 + mobile/android/base/gfx/FloatSize.java | 73 ++++++++++ .../base/gfx/GeckoSoftwareLayerClient.java | 46 +++++-- mobile/android/base/gfx/IntSize.java | 7 + mobile/android/base/gfx/Layer.java | 36 ++++- mobile/android/base/gfx/LayerController.java | 49 +++++-- mobile/android/base/gfx/LayerRenderer.java | 69 ++++++---- mobile/android/base/gfx/LayerView.java | 3 +- .../android/base/gfx/NinePatchTileLayer.java | 9 +- .../base/gfx/PlaceholderLayerClient.java | 49 ++++--- mobile/android/base/gfx/PointUtils.java | 6 + mobile/android/base/gfx/RectUtils.java | 5 + mobile/android/base/gfx/ViewportMetrics.java | 128 +++++++++++------- mobile/android/base/ui/PanZoomController.java | 60 +++----- mobile/android/chrome/content/browser.js | 55 ++++++-- 17 files changed, 477 insertions(+), 212 deletions(-) create mode 100644 mobile/android/base/FloatUtils.java create mode 100644 mobile/android/base/gfx/FloatSize.java diff --git a/mobile/android/base/FloatUtils.java b/mobile/android/base/FloatUtils.java new file mode 100644 index 000000000000..7076015b01b8 --- /dev/null +++ b/mobile/android/base/FloatUtils.java @@ -0,0 +1,46 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Chris Lord + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko; + +import java.lang.Math; + +public final class FloatUtils { + public static boolean fuzzyEquals(float a, float b) { + return (Math.abs(a - b) < 1e-6); + } +} diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index c23b04a385c9..98993e8ffc39 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -40,6 +40,7 @@ package org.mozilla.gecko; +import org.mozilla.gecko.gfx.FloatSize; import org.mozilla.gecko.gfx.GeckoSoftwareLayerClient; import org.mozilla.gecko.gfx.IntSize; import org.mozilla.gecko.gfx.LayerController; @@ -534,41 +535,24 @@ abstract public class GeckoApp if (getLayerController().getLayerClient() != mSoftwareLayerClient) return; - IntSize pageSize = getLayerController().getPageSize(); - Rect visibleArea = getLayerController().getViewport(); - Point offset = getLayerController().getViewportMetrics().getViewportOffset(); + ViewportMetrics viewport = mSoftwareLayerClient.getGeckoViewportMetrics(); + String uri = lastHistoryEntry.mUri; + String title = lastHistoryEntry.mTitle; SharedPreferences prefs = getPlaceholderPrefs(); Editor editor = prefs.edit(); - String uri = lastHistoryEntry.mUri; - String title = lastHistoryEntry.mTitle; - - String lastUri = prefs.getString("last-uri", ""); - String lastTitle = prefs.getString("last-title", uri); - - // see if we can bail. - if (uri.equals(lastUri) && title.equals(lastTitle)) - return; - editor.putString("last-uri", uri); editor.putString("last-title", title); - if (pageSize != null) { - editor.putInt("page-width", pageSize.width); - editor.putInt("page-height", pageSize.height); - } - - if (visibleArea != null) { - editor.putInt("viewport-left", visibleArea.left); - editor.putInt("viewport-top", visibleArea.top); - editor.putInt("viewport-right", visibleArea.right); - editor.putInt("viewport-bottom", visibleArea.bottom); - } - - if (offset != null) { - editor.putInt("viewport-offset-x", offset.x); - editor.putInt("viewport-offset-y", offset.y); + if (viewport != null) { + /* XXX Saving this viewport means there may be a slight + * discrepancy between what the user sees when shutting down + * and what they see when starting up, but it oughtn't be much. + * + * The alternative is to do a transformation between the two. + */ + editor.putString("viewport", viewport.toJSON()); } Log.i(LOGTAG, "Saving:: " + uri + " " + title); @@ -1067,7 +1051,7 @@ abstract public class GeckoApp if (mGeckoLayout.indexOfChild(view) == -1) { lp = new PluginLayoutParams((int) w, (int) h, (int)x, (int)y); - lp.repositionFromVisibleRect(mLayerController.getViewport(), 1.0f/*mLayerController.getZoomFactor()*/, true); + lp.repositionFromVisibleRect(RectUtils.round(mLayerController.getViewport()), mLayerController.getZoomFactor(), true); view.setWillNotDraw(false); if (view instanceof SurfaceView) { @@ -1082,7 +1066,7 @@ abstract public class GeckoApp } else { lp = (PluginLayoutParams)view.getLayoutParams(); lp.reset((int)x, (int)y, (int)w, (int)h); - lp.repositionFromVisibleRect(mLayerController.getViewport(), 1.0f/*mLayerController.getZoomFactor()*/, true); + lp.repositionFromVisibleRect(RectUtils.round(mLayerController.getViewport()), mLayerController.getZoomFactor(), true); try { mGeckoLayout.updateViewLayout(view, lp); } catch (IllegalArgumentException e) { @@ -1124,7 +1108,7 @@ abstract public class GeckoApp public void repositionPluginViews(boolean resize, boolean setVisible) { for (View view : mPluginViews) { PluginLayoutParams lp = (PluginLayoutParams)view.getLayoutParams(); - lp.repositionFromVisibleRect(mLayerController.getViewport(), 1.0f/*mLayerController.getZoomFactor()*/, resize); + lp.repositionFromVisibleRect(RectUtils.round(mLayerController.getViewport()), mLayerController.getZoomFactor(), resize); if (setVisible) { view.setVisibility(View.VISIBLE); diff --git a/mobile/android/base/Makefile.in b/mobile/android/base/Makefile.in index 1e85921b4e2d..42c86286a045 100644 --- a/mobile/android/base/Makefile.in +++ b/mobile/android/base/Makefile.in @@ -56,6 +56,7 @@ JAVAFILES = \ DoorHanger.java \ DoorHangerPopup.java \ Favicons.java \ + FloatUtils.java \ GeckoApp.java \ GeckoAppShell.java \ GeckoConnectivityReceiver.java \ @@ -77,6 +78,7 @@ JAVAFILES = \ gfx/CairoGLInfo.java \ gfx/CairoImage.java \ gfx/CairoUtils.java \ + gfx/FloatSize.java \ gfx/GeckoSoftwareLayerClient.java \ gfx/InputConnectionHandler.java \ gfx/IntSize.java \ diff --git a/mobile/android/base/gfx/FloatSize.java b/mobile/android/base/gfx/FloatSize.java new file mode 100644 index 000000000000..111a70a16dee --- /dev/null +++ b/mobile/android/base/gfx/FloatSize.java @@ -0,0 +1,73 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Android code. + * + * The Initial Developer of the Original Code is Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2009-2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Patrick Walton + * Chris Lord + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +package org.mozilla.gecko.gfx; + +import org.mozilla.gecko.FloatUtils; +import org.json.JSONException; +import org.json.JSONObject; + +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 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); + } +} + diff --git a/mobile/android/base/gfx/GeckoSoftwareLayerClient.java b/mobile/android/base/gfx/GeckoSoftwareLayerClient.java index 921212503c90..90f6423c59f5 100644 --- a/mobile/android/base/gfx/GeckoSoftwareLayerClient.java +++ b/mobile/android/base/gfx/GeckoSoftwareLayerClient.java @@ -43,7 +43,9 @@ import org.mozilla.gecko.gfx.IntSize; import org.mozilla.gecko.gfx.LayerClient; import org.mozilla.gecko.gfx.LayerController; import org.mozilla.gecko.gfx.LayerRenderer; +import org.mozilla.gecko.gfx.PointUtils; import org.mozilla.gecko.gfx.SingleTileLayer; +import org.mozilla.gecko.FloatUtils; import org.mozilla.gecko.GeckoApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; @@ -136,24 +138,32 @@ public class GeckoSoftwareLayerClient extends LayerClient { */ public void endDrawing(int x, int y, int width, int height, String metadata) { try { - LayerController controller = getLayerController(); - if (controller == null) - return; - try { JSONObject metadataObject = new JSONObject(metadata); mGeckoViewport = new ViewportMetrics(metadataObject); - mTileLayer.setOrigin(mGeckoViewport.getDisplayportOrigin()); + mTileLayer.setOrigin(PointUtils.round(mGeckoViewport.getDisplayportOrigin())); + mTileLayer.setResolution(mGeckoViewport.getZoomFactor()); + + // Make sure LayerController metrics changes only happen in the + // UI thread. + final LayerController controller = getLayerController(); if (controller != null) { - if (mNewContent) { - mNewContent = false; - controller.setViewportMetrics(mGeckoViewport); - } else { - controller.setPageSize(mGeckoViewport.getPageSize()); - } - + controller.post(new Runnable() { + @Override + public void run() { + if (mNewContent) { + mNewContent = false; + controller.setViewportMetrics(mGeckoViewport); + } else { + // Don't adjust page size when zooming unless zoom levels are + // approximately equal. + if (FloatUtils.fuzzyEquals(controller.getZoomFactor(), mGeckoViewport.getZoomFactor())) + controller.setPageSize(mGeckoViewport.getPageSize()); + } + } + }); } } catch (JSONException e) { throw new RuntimeException(e); @@ -166,6 +176,11 @@ public class GeckoSoftwareLayerClient extends LayerClient { } } + public ViewportMetrics getGeckoViewportMetrics() { + // Return a copy, as we modify this inside the Gecko thread + return new ViewportMetrics(mGeckoViewport); + } + public void geckoLoadedNewContent() { mNewContent = true; } @@ -236,9 +251,12 @@ public class GeckoSoftwareLayerClient extends LayerClient { } private void adjustViewport() { - ViewportMetrics viewportMetrics = getLayerController().getViewportMetrics(); - Point viewportOffset = viewportMetrics.getOptimumViewportOffset(); + ViewportMetrics viewportMetrics = + new ViewportMetrics(getLayerController().getViewportMetrics()); + + PointF viewportOffset = viewportMetrics.getOptimumViewportOffset(); viewportMetrics.setViewportOffset(viewportOffset); + viewportMetrics.setViewport(viewportMetrics.getClampedViewport()); GeckoEvent event = new GeckoEvent("Viewport:Change", viewportMetrics.toJSON()); GeckoAppShell.sendEventToGecko(event); diff --git a/mobile/android/base/gfx/IntSize.java b/mobile/android/base/gfx/IntSize.java index 1367b4de10e0..e22744142d71 100644 --- a/mobile/android/base/gfx/IntSize.java +++ b/mobile/android/base/gfx/IntSize.java @@ -37,8 +37,10 @@ package org.mozilla.gecko.gfx; +import org.mozilla.gecko.gfx.FloatSize; import org.json.JSONException; import org.json.JSONObject; +import java.lang.Math; public class IntSize { public final int width, height; @@ -46,6 +48,11 @@ public class IntSize { 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"); diff --git a/mobile/android/base/gfx/Layer.java b/mobile/android/base/gfx/Layer.java index 04ea9c4512dd..f94aa757610f 100644 --- a/mobile/android/base/gfx/Layer.java +++ b/mobile/android/base/gfx/Layer.java @@ -20,6 +20,7 @@ * * Contributor(s): * Patrick Walton + * Chris Lord * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or @@ -47,14 +48,17 @@ public abstract class Layer { private boolean mInTransaction; private Point mOrigin; private Point mNewOrigin; + private float mResolution; + private float mNewResolution; public Layer() { mTransactionLock = new ReentrantLock(); mOrigin = new Point(0, 0); + mResolution = 1.0f; } - /** Draws the layer. Automatically applies the translation. */ - public final void draw(GL10 gl) { + /** Updates the layer. */ + public final void update(GL10 gl) { if (mTransactionLock.isHeldByCurrentThread()) { throw new RuntimeException("draw() called while transaction lock held by this " + "thread?!"); @@ -67,9 +71,17 @@ public abstract class Layer { mTransactionLock.unlock(); } } + } - gl.glPushMatrix(); + /** Sets the transformation for the layer. */ + public final void transform(GL10 gl) { + gl.glScalef(1.0f / mResolution, 1.0f / mResolution, 1.0f); gl.glTranslatef(mOrigin.x, mOrigin.y, 0.0f); + } + + /** Draws the layer. Automatically applies the transformation. */ + public final void draw(GL10 gl) { + gl.glPushMatrix(); onDraw(gl); @@ -88,6 +100,7 @@ public abstract class Layer { throw new RuntimeException("Nested transactions are not supported"); mTransactionLock.lock(); mInTransaction = true; + mNewResolution = mResolution; } /** Call this when you're done modifying the layer. */ @@ -115,6 +128,22 @@ public abstract class Layer { mNewOrigin = newOrigin; } + /** 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; + } + /** * Subclasses implement this method to perform drawing. * @@ -132,6 +161,7 @@ public abstract class Layer { mOrigin = mNewOrigin; mNewOrigin = null; } + mResolution = mNewResolution; } } diff --git a/mobile/android/base/gfx/LayerController.java b/mobile/android/base/gfx/LayerController.java index c36fddf23a10..77f15a735923 100644 --- a/mobile/android/base/gfx/LayerController.java +++ b/mobile/android/base/gfx/LayerController.java @@ -110,19 +110,26 @@ public class LayerController { public Context getContext() { return mContext; } public ViewportMetrics getViewportMetrics() { return mViewportMetrics; } - public Rect getViewport() { + public RectF getViewport() { return mViewportMetrics.getViewport(); } - public IntSize getViewportSize() { - Rect viewport = getViewport(); - return new IntSize(viewport.width(), viewport.height()); + public FloatSize getViewportSize() { + return mViewportMetrics.getSize(); } - public IntSize getPageSize() { + public FloatSize getPageSize() { return mViewportMetrics.getPageSize(); } + public PointF getOrigin() { + return mViewportMetrics.getOrigin(); + } + + public float getZoomFactor() { + return mViewportMetrics.getZoomFactor(); + } + public Bitmap getCheckerboardPattern() { return getDrawable("checkerboard"); } public Bitmap getShadowPattern() { return getDrawable("shadow"); } @@ -144,7 +151,7 @@ public class LayerController { * to the layer client. That way, the layer client won't be tempted to call this, which might * result in an infinite loop. */ - public void setViewportSize(IntSize size) { + public void setViewportSize(FloatSize size) { mViewportMetrics.setSize(size); notifyLayerClientOfGeometryChange(); @@ -152,21 +159,28 @@ public class LayerController { } public void scrollTo(PointF point) { - mViewportMetrics.setOrigin(new Point(Math.round(point.x), - Math.round(point.y))); + mViewportMetrics.setOrigin(point); + notifyLayerClientOfGeometryChange(); + mPanZoomController.geometryChanged(); + } + + public void scrollBy(PointF point) { + PointF origin = mViewportMetrics.getOrigin(); + origin.offset(point.x, point.y); + mViewportMetrics.setOrigin(origin); notifyLayerClientOfGeometryChange(); mPanZoomController.geometryChanged(); } - public void setViewport(Rect viewport) { + public void setViewport(RectF viewport) { mViewportMetrics.setViewport(viewport); notifyLayerClientOfGeometryChange(); mPanZoomController.geometryChanged(); } - public void setPageSize(IntSize size) { - if (mViewportMetrics.getPageSize().equals(size)) + public void setPageSize(FloatSize size) { + if (mViewportMetrics.getPageSize().fuzzyEquals(size)) return; mViewportMetrics.setPageSize(size); @@ -184,6 +198,14 @@ public class LayerController { mPanZoomController.geometryChanged(); } + public void scaleTo(float zoomFactor, PointF focus) { + mViewportMetrics.scaleTo(zoomFactor, focus); + + // We assume the zoom level will only be modified by the + // PanZoomController, so no need to notify it of this change. + notifyLayerClientOfGeometryChange(); + } + public boolean post(Runnable action) { return mView.post(action); } public void setOnTouchListener(OnTouchListener onTouchListener) { @@ -204,7 +226,7 @@ public class LayerController { * would prefer that the action didn't take place. */ public boolean getRedrawHint() { - return aboutToCheckerboard(); + return true;//aboutToCheckerboard(); } private RectF getTileRect() { @@ -231,7 +253,8 @@ public class LayerController { return null; // Undo the transforms. - PointF newPoint = new PointF(mViewportMetrics.getOrigin()); + PointF origin = mViewportMetrics.getOrigin(); + PointF newPoint = new PointF(origin.x, origin.y); newPoint.offset(viewPoint.x, viewPoint.y); Point rootOrigin = mRootLayer.getOrigin(); diff --git a/mobile/android/base/gfx/LayerRenderer.java b/mobile/android/base/gfx/LayerRenderer.java index 5d0a90a6021b..4d9fc2a759b8 100644 --- a/mobile/android/base/gfx/LayerRenderer.java +++ b/mobile/android/base/gfx/LayerRenderer.java @@ -48,6 +48,8 @@ import org.mozilla.gecko.gfx.TextureReaper; import org.mozilla.gecko.gfx.TextLayer; import org.mozilla.gecko.gfx.TileLayer; import android.content.Context; +import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.opengl.GLSurfaceView; @@ -108,38 +110,48 @@ public class LayerRenderer implements GLSurfaceView.Renderer { TextureReaper.get().reap(gl); LayerController controller = mView.getController(); - Rect pageRect = getPageRect(); + Layer rootLayer = controller.getRoot(); + + /* Update layers */ + if (rootLayer != null) rootLayer.update(gl); + mShadowLayer.update(gl); + mCheckerboardLayer.update(gl); + mFPSLayer.update(gl); + mVertScrollLayer.update(gl); + mHorizScrollLayer.update(gl); /* Draw the background. */ gl.glClearColor(BACKGROUND_COLOR_R, BACKGROUND_COLOR_G, BACKGROUND_COLOR_B, 1.0f); gl.glClear(GL10.GL_COLOR_BUFFER_BIT); /* Draw the drop shadow. */ - setupPageTransform(gl); + setupPageTransform(gl, false); mShadowLayer.draw(gl); /* Draw the checkerboard. */ - Rect clampedPageRect = clampToScreen(pageRect); - IntSize screenSize = controller.getViewportSize(); + Rect pageRect = getPageRect(); + Rect scissorRect = transformToScissorRect(pageRect); gl.glEnable(GL10.GL_SCISSOR_TEST); - gl.glScissor(clampedPageRect.left, screenSize.height - clampedPageRect.bottom, - clampedPageRect.width(), clampedPageRect.height()); + gl.glScissor(scissorRect.left, scissorRect.top, + scissorRect.width(), scissorRect.height()); gl.glLoadIdentity(); mCheckerboardLayer.draw(gl); /* Draw the layer the client added to us. */ - setupPageTransform(gl); - - Layer rootLayer = controller.getRoot(); - if (rootLayer != null) + if (rootLayer != null) { + gl.glLoadIdentity(); + setupPageTransform(gl, true); + rootLayer.transform(gl); rootLayer.draw(gl); + } gl.glDisable(GL10.GL_SCISSOR_TEST); gl.glEnable(GL10.GL_BLEND); /* Draw the vertical scrollbar */ + IntSize screenSize = new IntSize(controller.getViewportSize()); if (pageRect.height() > screenSize.height) { mVertScrollLayer.drawVertical(gl, screenSize, pageRect); } @@ -156,38 +168,41 @@ public class LayerRenderer implements GLSurfaceView.Renderer { gl.glDisable(GL10.GL_BLEND); } - private void setupPageTransform(GL10 gl) { + private void setupPageTransform(GL10 gl, boolean scale) { LayerController controller = mView.getController(); - Rect viewport = controller.getViewport(); - //float zoomFactor = controller.getZoomFactor(); - gl.glLoadIdentity(); - //gl.glScalef(zoomFactor, zoomFactor, 1.0f); - gl.glTranslatef(-viewport.left, -viewport.top, 0.0f); + PointF origin = controller.getOrigin(); + gl.glTranslatef(-origin.x, -origin.y, 0.0f); + + if (scale) { + float zoomFactor = controller.getZoomFactor(); + gl.glScalef(zoomFactor, zoomFactor, 1.0f); + } } private Rect getPageRect() { LayerController controller = mView.getController(); - float zoomFactor = 1.0f;//controller.getZoomFactor(); - Rect viewport = controller.getViewport(); - IntSize pageSize = controller.getPageSize(); - int x = (int)Math.round(-zoomFactor * viewport.left); - int y = (int)Math.round(-zoomFactor * viewport.top); - return new Rect(x, y, - x + (int)Math.round(zoomFactor * pageSize.width), - y + (int)Math.round(zoomFactor * pageSize.height)); + Point origin = PointUtils.round(controller.getOrigin()); + IntSize pageSize = new IntSize(controller.getPageSize()); + + origin.negate(); + + return new Rect(origin.x, origin.y, + origin.x + pageSize.width, origin.y + pageSize.height); } - private Rect clampToScreen(Rect rect) { + private Rect transformToScissorRect(Rect rect) { LayerController controller = mView.getController(); - IntSize screenSize = controller.getViewportSize(); + IntSize screenSize = new IntSize(controller.getViewportSize()); 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, top, right, bottom); + + return new Rect(left, screenSize.height - bottom, right, + (screenSize.height - bottom) + (bottom - top)); } public void onSurfaceChanged(GL10 gl, final int width, final int height) { diff --git a/mobile/android/base/gfx/LayerView.java b/mobile/android/base/gfx/LayerView.java index 48066d300839..f63e786ff6ad 100644 --- a/mobile/android/base/gfx/LayerView.java +++ b/mobile/android/base/gfx/LayerView.java @@ -37,6 +37,7 @@ package org.mozilla.gecko.gfx; +import org.mozilla.gecko.gfx.FloatSize; import org.mozilla.gecko.gfx.InputConnectionHandler; import org.mozilla.gecko.gfx.LayerController; import android.content.Context; @@ -91,7 +92,7 @@ public class LayerView extends GLSurfaceView { /** The LayerRenderer calls this to indicate that the window has changed size. */ public void setViewportSize(IntSize size) { - mController.setViewportSize(size); + mController.setViewportSize(new FloatSize(size)); } public void setInputConnectionHandler(InputConnectionHandler handler) { diff --git a/mobile/android/base/gfx/NinePatchTileLayer.java b/mobile/android/base/gfx/NinePatchTileLayer.java index cc89f52667a5..440613e3d19d 100644 --- a/mobile/android/base/gfx/NinePatchTileLayer.java +++ b/mobile/android/base/gfx/NinePatchTileLayer.java @@ -37,6 +37,7 @@ package org.mozilla.gecko.gfx; +import org.mozilla.gecko.gfx.FloatSize; import javax.microedition.khronos.opengles.GL10; import java.nio.FloatBuffer; @@ -50,7 +51,7 @@ public class NinePatchTileLayer extends TileLayer { private FloatBuffer mSideTexCoordBuffer, mSideVertexBuffer; private FloatBuffer mTopTexCoordBuffer, mTopVertexBuffer; private LayerController mLayerController; - private IntSize mPageSize; + private FloatSize mPageSize; private static final int PATCH_SIZE = 16; private static final int TEXTURE_SIZE = 48; @@ -91,7 +92,7 @@ public class NinePatchTileLayer extends TileLayer { public NinePatchTileLayer(LayerController layerController, CairoImage image) { super(false, image); - mPageSize = new IntSize(1, 1); + mPageSize = new FloatSize(1.0f, 1.0f); mLayerController = layerController; mSideTexCoordBuffer = createBuffer(SIDE_TEX_COORDS); @@ -125,8 +126,8 @@ public class NinePatchTileLayer extends TileLayer { @Override protected void onTileDraw(GL10 gl) { - IntSize pageSize = mLayerController.getPageSize(); - if (!pageSize.equals(mPageSize)) { + FloatSize pageSize = mLayerController.getPageSize(); + if (!pageSize.fuzzyEquals(mPageSize)) { mPageSize = pageSize; recreateVertexBuffers(); } diff --git a/mobile/android/base/gfx/PlaceholderLayerClient.java b/mobile/android/base/gfx/PlaceholderLayerClient.java index 2af5aff962d7..b6c1fa4c7743 100644 --- a/mobile/android/base/gfx/PlaceholderLayerClient.java +++ b/mobile/android/base/gfx/PlaceholderLayerClient.java @@ -39,19 +39,22 @@ package org.mozilla.gecko.gfx; import org.mozilla.gecko.gfx.BufferedCairoImage; import org.mozilla.gecko.gfx.CairoUtils; -import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.FloatSize; import org.mozilla.gecko.gfx.LayerClient; +import org.mozilla.gecko.gfx.PointUtils; import org.mozilla.gecko.gfx.SingleTileLayer; import org.mozilla.gecko.GeckoApp; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.graphics.Point; -import android.graphics.Rect; +import android.graphics.PointF; +import android.graphics.RectF; import android.os.AsyncTask; import android.os.Environment; import android.util.Log; +import org.json.JSONException; +import org.json.JSONObject; import java.io.File; import java.nio.ByteBuffer; @@ -60,8 +63,11 @@ import java.nio.ByteBuffer; * is up, then we hand off control to it. */ public class PlaceholderLayerClient extends LayerClient { + private static final String LOGTAG = "PlaceholderLayerClient"; + private Context mContext; private ViewportMetrics mViewport; + private boolean mViewportUnknown; private int mWidth, mHeight, mFormat; private ByteBuffer mBuffer; private FetchImageTask mTask; @@ -69,19 +75,19 @@ public class PlaceholderLayerClient extends LayerClient { private PlaceholderLayerClient(Context context) { mContext = context; SharedPreferences prefs = GeckoApp.mAppContext.getPlaceholderPrefs(); - IntSize pageSize = new IntSize(prefs.getInt("page-width", 995), - prefs.getInt("page-height", 1250)); - Rect viewport = new Rect(prefs.getInt("viewport-left", 0), - prefs.getInt("viewport-top", 0), - prefs.getInt("viewport-right", 1), - prefs.getInt("viewport-bottom", 1)); - Point offset = new Point(prefs.getInt("viewport-offset-x", 0), - prefs.getInt("viewport-offset-y", 0)); - - mViewport = new ViewportMetrics(); - mViewport.setPageSize(pageSize); - mViewport.setViewport(viewport); - mViewport.setViewportOffset(offset); + mViewportUnknown = true; + if (prefs.contains("viewport")) { + try { + JSONObject viewportObject = new JSONObject(prefs.getString("viewport", null)); + mViewport = new ViewportMetrics(viewportObject); + mViewportUnknown = false; + } catch (JSONException e) { + Log.e(LOGTAG, "Error parsing saved viewport!"); + mViewport = new ViewportMetrics(); + } + } else { + mViewport = new ViewportMetrics(); + } } public static PlaceholderLayerClient createInstance(Context context) { @@ -116,6 +122,13 @@ public class PlaceholderLayerClient extends LayerClient { mBuffer = ByteBuffer.allocateDirect(mWidth * mHeight * bpp); bitmap.copyPixelsToBuffer(mBuffer.asIntBuffer()); + + if (mViewportUnknown) { + mViewport.setPageSize(new FloatSize(mWidth, mHeight)); + if (getLayerController() != null) + getLayerController().setPageSize(mViewport.getPageSize()); + } + return null; } @@ -125,7 +138,7 @@ public class PlaceholderLayerClient extends LayerClient { SingleTileLayer tileLayer = new SingleTileLayer(image); tileLayer.beginTransaction(); - tileLayer.setOrigin(mViewport.getDisplayportOrigin()); + tileLayer.setOrigin(PointUtils.round(mViewport.getDisplayportOrigin())); tileLayer.endTransaction(); getLayerController().setRoot(tileLayer); @@ -141,6 +154,8 @@ public class PlaceholderLayerClient extends LayerClient { public void setLayerController(LayerController layerController) { super.setLayerController(layerController); + if (mViewportUnknown) + mViewport.setViewport(layerController.getViewport()); layerController.setViewportMetrics(mViewport); BufferedCairoImage image = new BufferedCairoImage(mBuffer, mWidth, mHeight, mFormat); diff --git a/mobile/android/base/gfx/PointUtils.java b/mobile/android/base/gfx/PointUtils.java index ed9939933e23..feb18522a8b3 100644 --- a/mobile/android/base/gfx/PointUtils.java +++ b/mobile/android/base/gfx/PointUtils.java @@ -37,7 +37,9 @@ package org.mozilla.gecko.gfx; +import android.graphics.Point; import android.graphics.PointF; +import java.lang.Math; public final class PointUtils { public static PointF add(PointF one, PointF two) { @@ -51,5 +53,9 @@ public final class PointUtils { 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)); + } } diff --git a/mobile/android/base/gfx/RectUtils.java b/mobile/android/base/gfx/RectUtils.java index bad487d75739..15c584a06f4f 100644 --- a/mobile/android/base/gfx/RectUtils.java +++ b/mobile/android/base/gfx/RectUtils.java @@ -88,4 +88,9 @@ public final class RectUtils { x + (rect.width() * scale), y + (rect.height() * scale)); } + + public static Rect round(RectF rect) { + return new Rect(Math.round(rect.left), Math.round(rect.top), + Math.round(rect.right), Math.round(rect.bottom)); + } } diff --git a/mobile/android/base/gfx/ViewportMetrics.java b/mobile/android/base/gfx/ViewportMetrics.java index 912ca5759993..1743de6d5836 100644 --- a/mobile/android/base/gfx/ViewportMetrics.java +++ b/mobile/android/base/gfx/ViewportMetrics.java @@ -39,9 +39,12 @@ package org.mozilla.gecko.gfx; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; -import org.mozilla.gecko.gfx.IntSize; +import android.graphics.RectF; +import org.mozilla.gecko.gfx.FloatSize; import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.gfx.RectUtils; import org.json.JSONException; import org.json.JSONObject; import android.util.Log; @@ -51,51 +54,57 @@ import android.util.Log; * the page viewport for the Gecko layer client to use. */ public class ViewportMetrics { - private IntSize mPageSize; - private Rect mViewportRect; - private Point mViewportOffset; + private FloatSize mPageSize; + private RectF mViewportRect; + private PointF mViewportOffset; + private float mZoomFactor; public ViewportMetrics() { - mPageSize = new IntSize(LayerController.TILE_WIDTH, - LayerController.TILE_HEIGHT); - mViewportRect = new Rect(0, 0, 1, 1); - mViewportOffset = new Point(0, 0); + mPageSize = new FloatSize(LayerController.TILE_WIDTH, + LayerController.TILE_HEIGHT); + mViewportRect = new RectF(0, 0, 1, 1); + mViewportOffset = new PointF(0, 0); + mZoomFactor = 1.0f; } public ViewportMetrics(ViewportMetrics viewport) { - mPageSize = new IntSize(viewport.getPageSize()); - mViewportRect = new Rect(viewport.getViewport()); - mViewportOffset = new Point(viewport.getViewportOffset()); + mPageSize = new FloatSize(viewport.getPageSize()); + mViewportRect = new RectF(viewport.getViewport()); + PointF offset = viewport.getViewportOffset(); + mViewportOffset = new PointF(offset.x, offset.y); + mZoomFactor = viewport.getZoomFactor(); } public ViewportMetrics(JSONObject json) throws JSONException { - int x = json.getInt("x"); - int y = json.getInt("y"); - int width = json.getInt("width"); - int height = json.getInt("height"); - int pageWidth = json.getInt("pageWidth"); - int pageHeight = json.getInt("pageHeight"); - int offsetX = json.getInt("offsetX"); - int offsetY = json.getInt("offsetY"); + 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 pageWidth = (float)json.getDouble("pageWidth"); + float pageHeight = (float)json.getDouble("pageHeight"); + float offsetX = (float)json.getDouble("offsetX"); + float offsetY = (float)json.getDouble("offsetY"); + float zoom = (float)json.getDouble("zoom"); - mPageSize = new IntSize(pageWidth, pageHeight); - mViewportRect = new Rect(x, y, x + width, y + height); - mViewportOffset = new Point(offsetX, offsetY); + mPageSize = new FloatSize(pageWidth, pageHeight); + mViewportRect = new RectF(x, y, x + width, y + height); + mViewportOffset = new PointF(offsetX, offsetY); + mZoomFactor = zoom; } - public Point getOptimumViewportOffset() { + public PointF getOptimumViewportOffset() { // XXX We currently always position the viewport in the centre of the // displayport, but we might want to optimise this during panning // to minimise checkerboarding. Point optimumOffset = - new Point((LayerController.TILE_WIDTH - mViewportRect.width()) / 2, - (LayerController.TILE_HEIGHT - mViewportRect.height()) / 2); + new Point((int)Math.round((LayerController.TILE_WIDTH - mViewportRect.width()) / 2), + (int)Math.round((LayerController.TILE_HEIGHT - mViewportRect.height()) / 2)); /* XXX Until bug #524925 is fixed, changing the viewport origin will * probably cause things to be slower than just having a smaller usable * displayport. */ - Rect viewport = getClampedViewport(); + Rect viewport = RectUtils.round(getClampedViewport()); // Make sure this offset won't cause wasted pixels in the displayport // (i.e. make sure the resultant displayport intersects with the page @@ -110,29 +119,29 @@ public class ViewportMetrics { else if (optimumOffset.y + viewport.bottom > mPageSize.height) optimumOffset.y -= (mPageSize.height - (optimumOffset.y + viewport.bottom)); - return optimumOffset; + return new PointF(optimumOffset); } - public Point getOrigin() { - return new Point(mViewportRect.left, mViewportRect.top); + public PointF getOrigin() { + return new PointF(mViewportRect.left, mViewportRect.top); } - public Point getDisplayportOrigin() { - return new Point(mViewportRect.left - mViewportOffset.x, - mViewportRect.top - mViewportOffset.y); + public PointF getDisplayportOrigin() { + return new PointF(mViewportRect.left - mViewportOffset.x, + mViewportRect.top - mViewportOffset.y); } - public IntSize getSize() { - return new IntSize(mViewportRect.width(), mViewportRect.height()); + public FloatSize getSize() { + return new FloatSize(mViewportRect.width(), mViewportRect.height()); } - public Rect getViewport() { + public RectF getViewport() { return mViewportRect; } /** Returns the viewport rectangle, clamped within the page-size. */ - public Rect getClampedViewport() { - Rect clampedViewport = new Rect(mViewportRect); + public RectF getClampedViewport() { + RectF clampedViewport = new RectF(mViewportRect); // While the viewport size ought to never exceed the page size, we // do the clamping in this order to make sure that the origin is @@ -150,41 +159,61 @@ public class ViewportMetrics { return clampedViewport; } - public Point getViewportOffset() { + public PointF getViewportOffset() { return mViewportOffset; } - public IntSize getPageSize() { + public FloatSize getPageSize() { return mPageSize; } - public void setPageSize(IntSize pageSize) { + public float getZoomFactor() { + return mZoomFactor; + } + + public void setPageSize(FloatSize pageSize) { mPageSize = pageSize; } - public void setViewport(Rect viewport) { + public void setViewport(RectF viewport) { mViewportRect = viewport; } - public void setOrigin(Point origin) { + public void setOrigin(PointF origin) { mViewportRect.set(origin.x, origin.y, origin.x + mViewportRect.width(), origin.y + mViewportRect.height()); } - public void setSize(IntSize size) { + public void setSize(FloatSize size) { mViewportRect.right = mViewportRect.left + size.width; mViewportRect.bottom = mViewportRect.top + size.height; } - public void setViewportOffset(Point offset) { + public void setViewportOffset(PointF offset) { mViewportOffset = offset; } - public boolean equals(ViewportMetrics viewport) { - return mViewportRect.equals(viewport.getViewport()) && - mPageSize.equals(viewport.getPageSize()) && - mViewportOffset.equals(viewport.getViewportOffset()); + public void setZoomFactor(float zoomFactor) { + mZoomFactor = 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 void scaleTo(float newZoomFactor, PointF focus) { + float scaleFactor = newZoomFactor / mZoomFactor; + + mPageSize = mPageSize.scale(scaleFactor); + + PointF origin = getOrigin(); + origin.offset(focus.x, focus.y); + origin = PointUtils.scale(origin, scaleFactor); + origin.offset(-focus.x, -focus.y); + setOrigin(origin); + + mZoomFactor = newZoomFactor; } public String toJSON() { @@ -196,7 +225,8 @@ public class ViewportMetrics { ", \"pageHeight\" : " + mPageSize.height + ", \"offsetX\" : " + mViewportOffset.x + ", \"offsetY\" : " + mViewportOffset.y + - "}"; + ", \"zoom\" : " + mZoomFactor + + " }"; } } diff --git a/mobile/android/base/ui/PanZoomController.java b/mobile/android/base/ui/PanZoomController.java index 995662cd5192..69c2e0e81f97 100644 --- a/mobile/android/base/ui/PanZoomController.java +++ b/mobile/android/base/ui/PanZoomController.java @@ -38,11 +38,13 @@ package org.mozilla.gecko.ui; import org.json.JSONObject; -import org.mozilla.gecko.gfx.IntSize; +import org.mozilla.gecko.gfx.FloatSize; import org.mozilla.gecko.gfx.LayerController; +import org.mozilla.gecko.FloatUtils; import org.mozilla.gecko.GeckoApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; @@ -88,10 +90,8 @@ public class PanZoomController private Timer mFlingTimer; private Axis mX, mY; - /* The span at the first zoom event (in unzoomed page coordinates). */ - private float mInitialZoomSpan; - /* The zoom focus at the first zoom event (in unzoomed page coordinates). */ - private PointF mInitialZoomFocus; + /* The zoom focus at the first zoom event (in page coordinates). */ + private PointF mLastZoomFocus; private enum PanZoomState { NOTHING, /* no touch-start events received */ @@ -322,8 +322,8 @@ public class PanZoomController // Populates the viewport info and length in the axes. private void populatePositionAndLength() { - IntSize pageSize = mController.getPageSize(); - RectF visibleRect = new RectF(mController.getViewport()); + FloatSize pageSize = mController.getPageSize(); + RectF visibleRect = mController.getViewport(); mX.setPageLength(pageSize.width); mX.viewportPos = visibleRect.left; @@ -379,11 +379,6 @@ public class PanZoomController return 1.0f - excess / (viewportLength * SNAP_LIMIT); } - private static boolean floatsApproxEqual(float a, float b) { - // account for floating point rounding errors - return Math.abs(a - b) < 1e-6; - } - // Physics information for one axis (X or Y). private static class Axis { public enum FlingStates { @@ -411,13 +406,13 @@ public class PanZoomController public float viewportPos; private float mViewportLength; private int mScreenLength; - private int mPageLength; + private float mPageLength; public FlingStates getFlingState() { return mFlingState; } public void setViewportLength(float viewportLength) { mViewportLength = viewportLength; } public void setScreenLength(int screenLength) { mScreenLength = screenLength; } - public void setPageLength(int pageLength) { mPageLength = pageLength; } + public void setPageLength(float pageLength) { mPageLength = pageLength; } private float getViewportEnd() { return viewportPos + mViewportLength; } @@ -458,7 +453,7 @@ public class PanZoomController } float excess = getExcess(); - if (floatsApproxEqual(excess, 0.0f)) + if (FloatUtils.fuzzyEquals(excess, 0.0f)) mFlingState = FlingStates.STOPPED; else mFlingState = FlingStates.WAITING_TO_SNAP; @@ -483,7 +478,7 @@ public class PanZoomController private void scroll() { // If we aren't overscrolled, just apply friction. float excess = getExcess(); - if (floatsApproxEqual(excess, 0.0f)) { + if (FloatUtils.fuzzyEquals(excess, 0.0f)) { velocity *= FRICTION; if (Math.abs(velocity) < 0.1f) { velocity = 0.0f; @@ -603,47 +598,34 @@ public class PanZoomController */ @Override public boolean onScale(ScaleGestureDetector detector) { - /* - mState = PanZoomState.PINCHING; - float newZoom = detector.getCurrentSpan() / mInitialZoomSpan; + float newZoomFactor = mController.getZoomFactor() * + (detector.getCurrentSpan() / detector.getPreviousSpan()); + + mController.scrollBy(new PointF(mLastZoomFocus.x - detector.getFocusX(), + mLastZoomFocus.y - detector.getFocusY())); + mController.scaleTo(newZoomFactor, new PointF(detector.getFocusX(), detector.getFocusY())); + + mLastZoomFocus.set(detector.getFocusX(), detector.getFocusY()); - IntSize screenSize = mController.getScreenSize(); - float x = mInitialZoomFocus.x - (detector.getFocusX() / newZoom); - float y = mInitialZoomFocus.y - (detector.getFocusY() / newZoom); - float width = screenSize.width / newZoom; - float height = screenSize.height / newZoom; - mController.setVisibleRect(x, y, width, height); - mController.notifyLayerClientOfGeometryChange(); - populatePositionAndLength(); - */ return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { - /* mState = PanZoomState.PINCHING; - RectF initialZoomRect = mController.getVisibleRect(); - float initialZoom = mController.getZoomFactor(); - - mInitialZoomFocus = new PointF(initialZoomRect.left + (detector.getFocusX() / initialZoom), - initialZoomRect.top + (detector.getFocusY() / initialZoom)); - mInitialZoomSpan = detector.getCurrentSpan() / initialZoom; - + mLastZoomFocus = new PointF(detector.getFocusX(), detector.getFocusY()); GeckoApp.mAppContext.hidePluginViews(); - */ + return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { - /* mState = PanZoomState.PANNING_HOLD_LOCKED; mX.firstTouchPos = mX.touchPos = detector.getFocusX(); mY.firstTouchPos = mY.touchPos = detector.getFocusY(); GeckoApp.mAppContext.showPluginViews(); - */ } @Override diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index e6a97b2565e1..f46a8921a39b 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -901,7 +901,8 @@ function Tab(aURL, aParams) { this.id = 0; this.create(aURL, aParams); this._viewport = { x: 0, y: 0, width: 1, height: 1, offsetX: 0, offsetY: 0, - pageWidth: 1, pageHeight: 1 }; + pageWidth: 1, pageHeight: 1, zoom: 1.0 }; + this.viewportExcess = { x: 0, y: 0 }; } Tab.prototype = { @@ -917,6 +918,7 @@ Tab.prototype = { this.browser.setAttribute("type", "content"); this.browser.style.width = "980px"; this.browser.style.height = "480px"; + this.browser.style.MozTransformOrigin = "0 0"; this.vbox.appendChild(this.browser); // Turn off clipping so we can buffer areas outside of the browser element. @@ -983,11 +985,18 @@ Tab.prototype = { }, set viewport(aViewport) { - // TODO: Zoom? + // Transform coordinates based on zoom + aViewport.x /= aViewport.zoom; + aViewport.y /= aViewport.zoom; // Set scroll position this.browser.contentWindow.scrollTo(aViewport.x, aViewport.y); + // If we've been asked to over-scroll, do it via the transformation + // and store it separately to the viewport. + let excessX = aViewport.x - this.browser.contentWindow.scrollX; + let excessY = aViewport.y - this.browser.contentWindow.scrollY; + // Check if the viewport size/position has changed and set the necessary // attributes on the browser element. if (aViewport.width != this._viewport.width) { @@ -1001,36 +1010,54 @@ Tab.prototype = { let transformChanged = false; - if (aViewport.offsetX != this._viewport.offsetX) { + if ((aViewport.offsetX != this._viewport.offsetX) || + (excessX != this.viewportExcess.x)) { this._viewport.offsetX = aViewport.offsetX; + this.viewportExcess.x = excessX; transformChanged = true; } - if (aViewport.offsetY != this._viewport.offsetY) { + if ((aViewport.offsetY != this._viewport.offsetY) || + (excessY != this.viewportExcess.y)) { this._viewport.offsetY = aViewport.offsetY; + this.viewportExcess.y = excessY; + transformChanged = true; + } + if (aViewport.zoom != this._viewport.zoom) { + this._viewport.zoom = aViewport.zoom; transformChanged = true; } - if (transformChanged) + if (transformChanged) { this.browser.style.MozTransform = - "translate(" + this._viewport.offsetX + "px, " + - this._viewport.offsetY + "px)"; + "translate(" + (this._viewport.offsetX) + "px, " + + (this._viewport.offsetY) + "px) " + + "scale(" + this._viewport.zoom + ") " + + "translate(" + (-excessX) + "px, " + (-excessY) + "px)"; + } }, get viewport() { // Update the viewport to current dimensions - this._viewport.x = this.browser.contentWindow.scrollX; - this._viewport.y = this.browser.contentWindow.scrollY; + this._viewport.x = this.browser.contentWindow.scrollX + + this.viewportExcess.x; + this._viewport.y = this.browser.contentWindow.scrollY + + this.viewportExcess.y; let doc = this.browser.contentDocument.documentElement; + let pageWidth = this._viewport.width; + let pageHeight = this._viewport.height; if (doc != null) { - this._viewport.pageWidth = doc.scrollWidth; - this._viewport.pageHeight = doc.scrollHeight; - } else { - this._viewport.pageWidth = this._viewport.width; - this._viewport.pageHeight = this._viewport.height; + pageWidth = Math.max(pageWidth, doc.scrollWidth); + pageHeight = Math.max(pageHeight, doc.scrollHeight); } + // Transform coordinates based on zoom + this._viewport.x *= this._viewport.zoom; + this._viewport.y *= this._viewport.zoom; + this._viewport.pageWidth = pageWidth * this._viewport.zoom; + this._viewport.pageHeight = pageHeight * this._viewport.zoom; + return this._viewport; },