Line data Source code
1 : /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* This Source Code Form is subject to the terms of the Mozilla Public
3 : * License, v. 2.0. If a copy of the MPL was not distributed with this
4 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 :
6 : #include "MobileViewportManager.h"
7 :
8 : #include "gfxPrefs.h"
9 : #include "LayersLogging.h"
10 : #include "mozilla/PresShell.h"
11 : #include "nsIDOMEvent.h"
12 : #include "nsIFrame.h"
13 : #include "nsLayoutUtils.h"
14 : #include "nsViewManager.h"
15 : #include "nsViewportInfo.h"
16 : #include "UnitTransforms.h"
17 : #include "nsIDocument.h"
18 :
19 : #define MVM_LOG(...)
20 : // #define MVM_LOG(...) printf_stderr("MVM: " __VA_ARGS__)
21 :
22 0 : NS_IMPL_ISUPPORTS(MobileViewportManager, nsIDOMEventListener, nsIObserver)
23 :
24 3 : static const nsLiteralString DOM_META_ADDED = NS_LITERAL_STRING("DOMMetaAdded");
25 3 : static const nsLiteralString DOM_META_CHANGED = NS_LITERAL_STRING("DOMMetaChanged");
26 3 : static const nsLiteralString FULL_ZOOM_CHANGE = NS_LITERAL_STRING("FullZoomChange");
27 3 : static const nsLiteralString LOAD = NS_LITERAL_STRING("load");
28 3 : static const nsLiteralCString BEFORE_FIRST_PAINT = NS_LITERAL_CSTRING("before-first-paint");
29 :
30 : using namespace mozilla;
31 : using namespace mozilla::layers;
32 :
33 0 : MobileViewportManager::MobileViewportManager(nsIPresShell* aPresShell,
34 0 : nsIDocument* aDocument)
35 : : mDocument(aDocument)
36 : , mPresShell(aPresShell)
37 : , mIsFirstPaint(false)
38 0 : , mPainted(false)
39 : {
40 0 : MOZ_ASSERT(mPresShell);
41 0 : MOZ_ASSERT(mDocument);
42 :
43 : MVM_LOG("%p: creating with presShell %p document %p\n", this, mPresShell, aDocument);
44 :
45 0 : if (nsCOMPtr<nsPIDOMWindowOuter> window = mDocument->GetWindow()) {
46 0 : mEventTarget = window->GetChromeEventHandler();
47 : }
48 0 : if (mEventTarget) {
49 0 : mEventTarget->AddEventListener(DOM_META_ADDED, this, false);
50 0 : mEventTarget->AddEventListener(DOM_META_CHANGED, this, false);
51 0 : mEventTarget->AddEventListener(FULL_ZOOM_CHANGE, this, false);
52 0 : mEventTarget->AddEventListener(LOAD, this, true);
53 : }
54 :
55 0 : nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService();
56 0 : if (observerService) {
57 0 : observerService->AddObserver(this, BEFORE_FIRST_PAINT.Data(), false);
58 : }
59 0 : }
60 :
61 0 : MobileViewportManager::~MobileViewportManager()
62 : {
63 0 : }
64 :
65 : void
66 0 : MobileViewportManager::Destroy()
67 : {
68 : MVM_LOG("%p: destroying\n", this);
69 :
70 0 : if (mEventTarget) {
71 0 : mEventTarget->RemoveEventListener(DOM_META_ADDED, this, false);
72 0 : mEventTarget->RemoveEventListener(DOM_META_CHANGED, this, false);
73 0 : mEventTarget->RemoveEventListener(FULL_ZOOM_CHANGE, this, false);
74 0 : mEventTarget->RemoveEventListener(LOAD, this, true);
75 0 : mEventTarget = nullptr;
76 : }
77 :
78 0 : nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService();
79 0 : if (observerService) {
80 0 : observerService->RemoveObserver(this, BEFORE_FIRST_PAINT.Data());
81 : }
82 :
83 0 : mDocument = nullptr;
84 0 : mPresShell = nullptr;
85 0 : }
86 :
87 : void
88 0 : MobileViewportManager::SetRestoreResolution(float aResolution,
89 : LayoutDeviceIntSize aDisplaySize)
90 : {
91 0 : SetRestoreResolution(aResolution);
92 : ScreenIntSize restoreDisplaySize = ViewAs<ScreenPixel>(aDisplaySize,
93 0 : PixelCastJustification::LayoutDeviceIsScreenForBounds);
94 0 : mRestoreDisplaySize = Some(restoreDisplaySize);
95 0 : }
96 :
97 : void
98 0 : MobileViewportManager::SetRestoreResolution(float aResolution)
99 : {
100 0 : mRestoreResolution = Some(aResolution);
101 0 : }
102 :
103 : void
104 0 : MobileViewportManager::RequestReflow()
105 : {
106 : MVM_LOG("%p: got a reflow request\n", this);
107 0 : RefreshViewportSize(false);
108 0 : }
109 :
110 : void
111 0 : MobileViewportManager::ResolutionUpdated()
112 : {
113 : MVM_LOG("%p: resolution updated\n", this);
114 0 : if (!mPainted) {
115 : // Save the value, so our default zoom calculation
116 : // can take it into account later on.
117 0 : SetRestoreResolution(mPresShell->GetResolution());
118 : }
119 0 : RefreshSPCSPS();
120 0 : }
121 :
122 : NS_IMETHODIMP
123 0 : MobileViewportManager::HandleEvent(nsIDOMEvent* event)
124 : {
125 0 : nsAutoString type;
126 0 : event->GetType(type);
127 :
128 0 : if (type.Equals(DOM_META_ADDED)) {
129 : MVM_LOG("%p: got a dom-meta-added event\n", this);
130 0 : RefreshViewportSize(mPainted);
131 0 : } else if (type.Equals(DOM_META_CHANGED)) {
132 : MVM_LOG("%p: got a dom-meta-changed event\n", this);
133 0 : RefreshViewportSize(mPainted);
134 0 : } else if (type.Equals(FULL_ZOOM_CHANGE)) {
135 : MVM_LOG("%p: got a full-zoom-change event\n", this);
136 0 : RefreshViewportSize(false);
137 0 : } else if (type.Equals(LOAD)) {
138 : MVM_LOG("%p: got a load event\n", this);
139 0 : if (!mPainted) {
140 : // Load event got fired before the before-first-paint message
141 0 : SetInitialViewport();
142 : }
143 : }
144 0 : return NS_OK;
145 : }
146 :
147 : NS_IMETHODIMP
148 0 : MobileViewportManager::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData)
149 : {
150 0 : if (SameCOMIdentity(aSubject, mDocument) && BEFORE_FIRST_PAINT.EqualsASCII(aTopic)) {
151 : MVM_LOG("%p: got a before-first-paint event\n", this);
152 0 : if (!mPainted) {
153 : // before-first-paint message arrived before load event
154 0 : SetInitialViewport();
155 : }
156 : }
157 0 : return NS_OK;
158 : }
159 :
160 : void
161 0 : MobileViewportManager::SetInitialViewport()
162 : {
163 : MVM_LOG("%p: setting initial viewport\n", this);
164 0 : mIsFirstPaint = true;
165 0 : mPainted = true;
166 0 : RefreshViewportSize(false);
167 0 : }
168 :
169 : CSSToScreenScale
170 0 : MobileViewportManager::ClampZoom(const CSSToScreenScale& aZoom,
171 : const nsViewportInfo& aViewportInfo)
172 : {
173 0 : CSSToScreenScale zoom = aZoom;
174 0 : if (zoom < aViewportInfo.GetMinZoom()) {
175 0 : zoom = aViewportInfo.GetMinZoom();
176 : MVM_LOG("%p: Clamped to %f\n", this, zoom.scale);
177 : }
178 0 : if (zoom > aViewportInfo.GetMaxZoom()) {
179 0 : zoom = aViewportInfo.GetMaxZoom();
180 : MVM_LOG("%p: Clamped to %f\n", this, zoom.scale);
181 : }
182 0 : return zoom;
183 : }
184 :
185 : LayoutDeviceToLayerScale
186 0 : MobileViewportManager::ScaleResolutionWithDisplayWidth(const LayoutDeviceToLayerScale& aRes,
187 : const float& aDisplayWidthChangeRatio,
188 : const CSSSize& aNewViewport,
189 : const CSSSize& aOldViewport)
190 : {
191 0 : float cssViewportChangeRatio = (aOldViewport.width == 0)
192 0 : ? 1.0f : aNewViewport.width / aOldViewport.width;
193 0 : LayoutDeviceToLayerScale newRes(aRes.scale * aDisplayWidthChangeRatio
194 0 : / cssViewportChangeRatio);
195 : MVM_LOG("%p: Old resolution was %f, changed by %f/%f to %f\n", this, aRes.scale,
196 : aDisplayWidthChangeRatio, cssViewportChangeRatio, newRes.scale);
197 0 : return newRes;
198 : }
199 :
200 : CSSToScreenScale
201 0 : MobileViewportManager::UpdateResolution(const nsViewportInfo& aViewportInfo,
202 : const ScreenIntSize& aDisplaySize,
203 : const CSSSize& aViewport,
204 : const Maybe<float>& aDisplayWidthChangeRatio)
205 : {
206 : CSSToLayoutDeviceScale cssToDev =
207 0 : mPresShell->GetPresContext()->CSSToDevPixelScale();
208 0 : LayoutDeviceToLayerScale res(mPresShell->GetResolution());
209 :
210 0 : if (mIsFirstPaint) {
211 0 : CSSToScreenScale defaultZoom;
212 0 : if (mRestoreResolution) {
213 0 : LayoutDeviceToLayerScale restoreResolution(mRestoreResolution.value());
214 0 : if (mRestoreDisplaySize) {
215 0 : CSSSize prevViewport = mDocument->GetViewportInfo(mRestoreDisplaySize.value()).GetSize();
216 0 : float restoreDisplayWidthChangeRatio = (mRestoreDisplaySize.value().width > 0)
217 0 : ? (float)aDisplaySize.width / (float)mRestoreDisplaySize.value().width : 1.0f;
218 :
219 0 : restoreResolution =
220 0 : ScaleResolutionWithDisplayWidth(restoreResolution,
221 : restoreDisplayWidthChangeRatio,
222 : aViewport,
223 : prevViewport);
224 : }
225 0 : defaultZoom = CSSToScreenScale(restoreResolution.scale * cssToDev.scale);
226 : MVM_LOG("%p: restored zoom is %f\n", this, defaultZoom.scale);
227 0 : defaultZoom = ClampZoom(defaultZoom, aViewportInfo);
228 : } else {
229 0 : defaultZoom = aViewportInfo.GetDefaultZoom();
230 : MVM_LOG("%p: default zoom from viewport is %f\n", this, defaultZoom.scale);
231 0 : if (!aViewportInfo.IsDefaultZoomValid()) {
232 0 : defaultZoom = MaxScaleRatio(ScreenSize(aDisplaySize), aViewport);
233 : MVM_LOG("%p: Intrinsic computed zoom is %f\n", this, defaultZoom.scale);
234 0 : defaultZoom = ClampZoom(defaultZoom, aViewportInfo);
235 : }
236 : }
237 0 : MOZ_ASSERT(aViewportInfo.GetMinZoom() <= defaultZoom &&
238 : defaultZoom <= aViewportInfo.GetMaxZoom());
239 :
240 : CSSToParentLayerScale zoom = ViewTargetAs<ParentLayerPixel>(defaultZoom,
241 0 : PixelCastJustification::ScreenIsParentLayerForRoot);
242 :
243 0 : LayoutDeviceToLayerScale resolution = zoom / cssToDev * ParentLayerToLayerScale(1);
244 : MVM_LOG("%p: setting resolution %f\n", this, resolution.scale);
245 0 : mPresShell->SetResolutionAndScaleTo(resolution.scale);
246 :
247 0 : return defaultZoom;
248 : }
249 :
250 : // If this is not a first paint, then in some cases we want to update the pre-
251 : // existing resolution so as to maintain how much actual content is visible
252 : // within the display width. Note that "actual content" may be different with
253 : // respect to CSS pixels because of the CSS viewport size changing.
254 : //
255 : // aDisplayWidthChangeRatio is non-empty if:
256 : // (a) The meta-viewport tag information changes, and so the CSS viewport
257 : // might change as a result. If this happens after the content has been
258 : // painted, we want to adjust the zoom to compensate. OR
259 : // (b) The display size changed from a nonzero value to another nonzero value.
260 : // This covers the case where e.g. the device was rotated, and again we
261 : // want to adjust the zoom to compensate.
262 : // Note in particular that aDisplayWidthChangeRatio will be None if all that
263 : // happened was a change in the full-zoom. In this case, we still want to
264 : // compute a new CSS viewport, but we don't want to update the resolution.
265 : //
266 : // Given the above, the algorithm below accounts for all types of changes I
267 : // can conceive of:
268 : // 1. screen size changes, CSS viewport does not (pages with no meta viewport
269 : // or a fixed size viewport)
270 : // 2. screen size changes, CSS viewport also does (pages with a device-width
271 : // viewport)
272 : // 3. screen size remains constant, but CSS viewport changes (meta viewport
273 : // tag is added or removed)
274 : // 4. neither screen size nor CSS viewport changes
275 0 : if (aDisplayWidthChangeRatio) {
276 0 : res = ScaleResolutionWithDisplayWidth(res, aDisplayWidthChangeRatio.value(),
277 : aViewport, mMobileViewportSize);
278 0 : mPresShell->SetResolutionAndScaleTo(res.scale);
279 : }
280 :
281 0 : return ViewTargetAs<ScreenPixel>(cssToDev * res / ParentLayerToLayerScale(1),
282 0 : PixelCastJustification::ScreenIsParentLayerForRoot);
283 : }
284 :
285 : void
286 0 : MobileViewportManager::UpdateSPCSPS(const ScreenIntSize& aDisplaySize,
287 : const CSSToScreenScale& aZoom)
288 : {
289 0 : ScreenSize compositionSize(aDisplaySize);
290 : ScreenMargin scrollbars =
291 0 : LayoutDeviceMargin::FromAppUnits(
292 0 : nsLayoutUtils::ScrollbarAreaToExcludeFromCompositionBoundsFor(
293 0 : mPresShell->GetRootScrollFrame()),
294 0 : mPresShell->GetPresContext()->AppUnitsPerDevPixel())
295 : // Scrollbars are not subject to resolution scaling, so LD pixels =
296 : // Screen pixels for them.
297 0 : * LayoutDeviceToScreenScale(1.0f);
298 :
299 0 : compositionSize.width -= scrollbars.LeftRight();
300 0 : compositionSize.height -= scrollbars.TopBottom();
301 0 : CSSSize compSize = compositionSize / aZoom;
302 : MVM_LOG("%p: Setting SPCSPS %s\n", this, Stringify(compSize).c_str());
303 0 : nsLayoutUtils::SetScrollPositionClampingScrollPortSize(mPresShell, compSize);
304 0 : }
305 :
306 : void
307 0 : MobileViewportManager::UpdateDisplayPortMargins()
308 : {
309 0 : if (nsIFrame* root = mPresShell->GetRootScrollFrame()) {
310 0 : bool hasDisplayPort = nsLayoutUtils::HasDisplayPort(root->GetContent());
311 0 : bool hasResolution = mPresShell->ScaleToResolution() &&
312 0 : mPresShell->GetResolution() != 1.0f;
313 0 : if (!hasDisplayPort && !hasResolution) {
314 : // We only want to update the displayport if there is one already, or
315 : // add one if there's a resolution on the document (see bug 1225508
316 : // comment 1).
317 0 : return;
318 : }
319 : nsRect displayportBase =
320 0 : nsRect(nsPoint(0, 0), nsLayoutUtils::CalculateCompositionSizeForFrame(root));
321 : // We only create MobileViewportManager for root content documents. If that ever changes
322 : // we'd need to limit the size of this displayport base rect because non-toplevel documents
323 : // have no limit on their size.
324 0 : MOZ_ASSERT(mPresShell->GetPresContext()->IsRootContentDocument());
325 0 : nsLayoutUtils::SetDisplayPortBaseIfNotSet(root->GetContent(), displayportBase);
326 0 : nsIScrollableFrame* scrollable = do_QueryFrame(root);
327 : nsLayoutUtils::CalculateAndSetDisplayPortMargins(scrollable,
328 0 : nsLayoutUtils::RepaintMode::DoNotRepaint);
329 : }
330 : }
331 :
332 : void
333 0 : MobileViewportManager::RefreshSPCSPS()
334 : {
335 : // This function is a subset of RefreshViewportSize, and only updates the
336 : // SPCSPS.
337 :
338 0 : if (!gfxPrefs::APZAllowZooming()) {
339 0 : return;
340 : }
341 :
342 : ScreenIntSize displaySize = ViewAs<ScreenPixel>(
343 0 : mDisplaySize, PixelCastJustification::LayoutDeviceIsScreenForBounds);
344 :
345 : CSSToLayoutDeviceScale cssToDev =
346 0 : mPresShell->GetPresContext()->CSSToDevPixelScale();
347 0 : LayoutDeviceToLayerScale res(mPresShell->GetResolution());
348 0 : CSSToScreenScale zoom = ViewTargetAs<ScreenPixel>(cssToDev * res / ParentLayerToLayerScale(1),
349 0 : PixelCastJustification::ScreenIsParentLayerForRoot);
350 :
351 0 : UpdateSPCSPS(displaySize, zoom);
352 : }
353 :
354 : void
355 0 : MobileViewportManager::RefreshViewportSize(bool aForceAdjustResolution)
356 : {
357 : // This function gets called by the various triggers that may result in a
358 : // change of the CSS viewport. In some of these cases (e.g. the meta-viewport
359 : // tag changes) we want to update the resolution and in others (e.g. the full
360 : // zoom changing) we don't want to update the resolution. See the comment in
361 : // UpdateResolution for some more detail on this. An important assumption we
362 : // make here is that this RefreshViewportSize function will be called
363 : // separately for each trigger that changes. For instance it should never get
364 : // called such that both the full zoom and the meta-viewport tag have changed;
365 : // instead it would get called twice - once after each trigger changes. This
366 : // assumption is what allows the aForceAdjustResolution parameter to work as
367 : // intended; if this assumption is violated then we will need to add extra
368 : // complicated logic in UpdateResolution to ensure we only do the resolution
369 : // update in the right scenarios.
370 :
371 0 : Maybe<float> displayWidthChangeRatio;
372 0 : LayoutDeviceIntSize newDisplaySize;
373 0 : if (nsLayoutUtils::GetContentViewerSize(mPresShell->GetPresContext(), newDisplaySize)) {
374 : // See the comment in UpdateResolution for why we're doing this.
375 0 : if (mDisplaySize.width > 0) {
376 0 : if (aForceAdjustResolution || mDisplaySize.width != newDisplaySize.width) {
377 0 : displayWidthChangeRatio = Some((float)newDisplaySize.width / (float)mDisplaySize.width);
378 : }
379 0 : } else if (aForceAdjustResolution) {
380 0 : displayWidthChangeRatio = Some(1.0f);
381 : }
382 :
383 : MVM_LOG("%p: Display width change ratio is %f\n", this, displayWidthChangeRatio.valueOr(0.0f));
384 0 : mDisplaySize = newDisplaySize;
385 : }
386 :
387 : MVM_LOG("%p: Computing CSS viewport using %d,%d\n", this,
388 : mDisplaySize.width, mDisplaySize.height);
389 0 : if (mDisplaySize.width == 0 || mDisplaySize.height == 0) {
390 : // We can't do anything useful here, we should just bail out
391 0 : return;
392 : }
393 :
394 : ScreenIntSize displaySize = ViewAs<ScreenPixel>(
395 0 : mDisplaySize, PixelCastJustification::LayoutDeviceIsScreenForBounds);
396 0 : nsViewportInfo viewportInfo = mDocument->GetViewportInfo(displaySize);
397 :
398 0 : CSSSize viewport = viewportInfo.GetSize();
399 : MVM_LOG("%p: Computed CSS viewport %s\n", this, Stringify(viewport).c_str());
400 :
401 0 : if (!mIsFirstPaint && mMobileViewportSize == viewport) {
402 : // Nothing changed, so no need to do a reflow
403 0 : return;
404 : }
405 :
406 : // If it's the first-paint or the viewport changed, we need to update
407 : // various APZ properties (the zoom and some things that might depend on it)
408 : MVM_LOG("%p: Updating properties because %d || %d\n", this,
409 : mIsFirstPaint, mMobileViewportSize != viewport);
410 :
411 0 : if (gfxPrefs::APZAllowZooming()) {
412 : CSSToScreenScale zoom = UpdateResolution(viewportInfo, displaySize, viewport,
413 0 : displayWidthChangeRatio);
414 : MVM_LOG("%p: New zoom is %f\n", this, zoom.scale);
415 0 : UpdateSPCSPS(displaySize, zoom);
416 : }
417 0 : if (gfxPlatform::AsyncPanZoomEnabled()) {
418 0 : UpdateDisplayPortMargins();
419 : }
420 :
421 0 : CSSSize oldSize = mMobileViewportSize;
422 :
423 : // Update internal state.
424 0 : mIsFirstPaint = false;
425 0 : mMobileViewportSize = viewport;
426 :
427 : // Kick off a reflow.
428 0 : mPresShell->ResizeReflowIgnoreOverride(
429 : nsPresContext::CSSPixelsToAppUnits(viewport.width),
430 : nsPresContext::CSSPixelsToAppUnits(viewport.height),
431 : nsPresContext::CSSPixelsToAppUnits(oldSize.width),
432 0 : nsPresContext::CSSPixelsToAppUnits(oldSize.height));
433 : }
|