tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

DocumentPictureInPicture.cpp (13612B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 #include "mozilla/dom/DocumentPictureInPicture.h"
      6 
      7 #include "mozilla/AsyncEventDispatcher.h"
      8 #include "mozilla/WidgetUtils.h"
      9 #include "mozilla/dom/BrowserChild.h"
     10 #include "mozilla/dom/Document.h"
     11 #include "mozilla/dom/DocumentPictureInPictureEvent.h"
     12 #include "mozilla/dom/WindowContext.h"
     13 #include "mozilla/widget/Screen.h"
     14 #include "nsDocShell.h"
     15 #include "nsDocShellLoadState.h"
     16 #include "nsIWindowWatcher.h"
     17 #include "nsNetUtil.h"
     18 #include "nsPIWindowWatcher.h"
     19 #include "nsServiceManagerUtils.h"
     20 #include "nsWindowWatcher.h"
     21 
     22 namespace mozilla::dom {
     23 
     24 static mozilla::LazyLogModule gDPIPLog("DocumentPIP");
     25 
     26 NS_IMPL_CYCLE_COLLECTION_CLASS(DocumentPictureInPicture)
     27 
     28 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocumentPictureInPicture,
     29                                                  DOMEventTargetHelper)
     30  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLastOpenedWindow)
     31 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
     32 
     33 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocumentPictureInPicture,
     34                                                DOMEventTargetHelper)
     35  NS_IMPL_CYCLE_COLLECTION_UNLINK(mLastOpenedWindow)
     36 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
     37 
     38 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocumentPictureInPicture)
     39  NS_INTERFACE_MAP_ENTRY(nsIObserver)
     40  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
     41 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
     42 
     43 NS_IMPL_ADDREF_INHERITED(DocumentPictureInPicture, DOMEventTargetHelper)
     44 NS_IMPL_RELEASE_INHERITED(DocumentPictureInPicture, DOMEventTargetHelper)
     45 
     46 JSObject* DocumentPictureInPicture::WrapObject(
     47    JSContext* cx, JS::Handle<JSObject*> aGivenProto) {
     48  return DocumentPictureInPicture_Binding::Wrap(cx, this, aGivenProto);
     49 }
     50 
     51 DocumentPictureInPicture::DocumentPictureInPicture(nsPIDOMWindowInner* aWindow)
     52    : DOMEventTargetHelper(aWindow) {
     53  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
     54  NS_ENSURE_TRUE_VOID(os);
     55  DebugOnly<nsresult> rv = os->AddObserver(this, "domwindowclosed", false);
     56  MOZ_ASSERT(NS_SUCCEEDED(rv));
     57 }
     58 
     59 DocumentPictureInPicture::~DocumentPictureInPicture() {
     60  nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
     61  NS_ENSURE_TRUE_VOID(os);
     62  DebugOnly<nsresult> rv = os->RemoveObserver(this, "domwindowclosed");
     63  MOZ_ASSERT(NS_SUCCEEDED(rv));
     64 }
     65 
     66 void DocumentPictureInPicture::OnPiPResized() {
     67  if (!mLastOpenedWindow) {
     68    return;
     69  }
     70 
     71  RefPtr<nsGlobalWindowInner> innerWindow =
     72      nsGlobalWindowInner::Cast(mLastOpenedWindow);
     73 
     74  int x = innerWindow->GetScreenLeft(CallerType::System, IgnoreErrors());
     75  int y = innerWindow->GetScreenTop(CallerType::System, IgnoreErrors());
     76  int width = static_cast<int>(innerWindow->GetInnerWidth(IgnoreErrors()));
     77  int height = static_cast<int>(innerWindow->GetInnerHeight(IgnoreErrors()));
     78 
     79  mPreviousExtent = Some(CSSIntRect(x, y, width, height));
     80 
     81  MOZ_LOG(gDPIPLog, LogLevel::Debug,
     82          ("PiP was resized, remembering position %s",
     83           ToString(mPreviousExtent).c_str()));
     84 }
     85 
     86 void DocumentPictureInPicture::OnPiPClosed() {
     87  if (!mLastOpenedWindow) {
     88    return;
     89  }
     90 
     91  RefPtr<nsGlobalWindowInner> pipInnerWindow =
     92      nsGlobalWindowInner::Cast(mLastOpenedWindow);
     93  pipInnerWindow->RemoveSystemEventListener(u"resize"_ns, this, true);
     94 
     95  MOZ_LOG(gDPIPLog, LogLevel::Debug, ("PiP was closed"));
     96 
     97  mLastOpenedWindow = nullptr;
     98 }
     99 
    100 nsGlobalWindowInner* DocumentPictureInPicture::GetWindow() {
    101  if (mLastOpenedWindow && mLastOpenedWindow->GetOuterWindow() &&
    102      !mLastOpenedWindow->GetOuterWindow()->Closed()) {
    103    return nsGlobalWindowInner::Cast(mLastOpenedWindow);
    104  }
    105  return nullptr;
    106 }
    107 
    108 // Some sane default. Maybe we should come up with an heuristic based on screen
    109 // size.
    110 const CSSIntSize DocumentPictureInPicture::sDefaultSize = {700, 650};
    111 const CSSIntSize DocumentPictureInPicture::sMinSize = {240, 50};
    112 
    113 static nsresult OpenPiPWindowUtility(nsPIDOMWindowOuter* aParent,
    114                                     const CSSIntRect& aExtent, bool aPrivate,
    115                                     mozilla::dom::BrowsingContext** aRet) {
    116  MOZ_DIAGNOSTIC_ASSERT(aParent);
    117 
    118  nsresult rv = NS_OK;
    119  nsCOMPtr<nsIWindowWatcher> ww =
    120      do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv);
    121  NS_ENSURE_SUCCESS(rv, rv);
    122 
    123  nsCOMPtr<nsPIWindowWatcher> pww(do_QueryInterface(ww));
    124  NS_ENSURE_TRUE(pww, NS_ERROR_FAILURE);
    125 
    126  nsCOMPtr<nsIURI> uri;
    127  rv = NS_NewURI(getter_AddRefs(uri), "about:blank"_ns, nullptr);
    128  NS_ENSURE_SUCCESS(rv, rv);
    129 
    130  RefPtr<nsDocShellLoadState> loadState =
    131      nsWindowWatcher::CreateLoadState(uri, aParent);
    132 
    133  // pictureinpicture is a non-standard window feature not available from JS
    134  nsPrintfCString features("pictureinpicture,top=%d,left=%d,width=%d,height=%d",
    135                           aExtent.y, aExtent.x, aExtent.width, aExtent.height);
    136 
    137  rv = pww->OpenWindow2(aParent, uri, "_blank"_ns, features,
    138                        mozilla::dom::UserActivation::Modifiers::None(), false,
    139                        false, true, nullptr, false, false, false,
    140                        nsPIWindowWatcher::PrintKind::PRINT_NONE, loadState,
    141                        aRet);
    142  NS_ENSURE_SUCCESS(rv, rv);
    143  NS_ENSURE_TRUE(aRet, NS_ERROR_FAILURE);
    144  return NS_OK;
    145 }
    146 
    147 /* static */
    148 Maybe<CSSIntRect> DocumentPictureInPicture::GetScreenRect(
    149    nsPIDOMWindowOuter* aWindow) {
    150  nsCOMPtr<nsIWidget> widget = widget::WidgetUtils::DOMWindowToWidget(aWindow);
    151  NS_ENSURE_TRUE(widget, Nothing());
    152  RefPtr<widget::Screen> screen = widget->GetWidgetScreen();
    153  NS_ENSURE_TRUE(screen, Nothing());
    154  LayoutDeviceIntRect rect = screen->GetRect();
    155 
    156  nsGlobalWindowOuter* outerWindow = nsGlobalWindowOuter::Cast(aWindow);
    157  NS_ENSURE_TRUE(outerWindow, Nothing());
    158  nsCOMPtr<nsIBaseWindow> treeOwnerAsWin = outerWindow->GetTreeOwnerWindow();
    159  NS_ENSURE_TRUE(treeOwnerAsWin, Nothing());
    160  auto scale = outerWindow->CSSToDevScaleForBaseWindow(treeOwnerAsWin);
    161 
    162  return Some(RoundedToInt(rect / scale));
    163 }
    164 
    165 // Place window in the bottom right of the opener window's screen
    166 static CSSIntPoint CalcInitialPos(const CSSIntRect& screen,
    167                                  const CSSIntSize& aSize) {
    168  // aSize is the inner size not including browser UI. But we need the outer
    169  // size for calculating where the top left corner of the PiP should be
    170  // initially. For now use a guess of ~80px for the browser UI?
    171  return {std::max(screen.X(), screen.XMost() - aSize.width - 100),
    172          std::max(screen.Y(), screen.YMost() - aSize.height - 100 - 80)};
    173 }
    174 
    175 /* static */
    176 CSSIntSize DocumentPictureInPicture::CalcMaxDimensions(
    177    const CSSIntRect& screen) {
    178  // Limit PIP size to 80% (arbitrary number) of screen size
    179  // https://wicg.github.io/document-picture-in-picture/#maximum-size
    180  CSSIntSize size =
    181      RoundedToInt(screen.Size() * gfx::ScaleFactor<CSSPixel, CSSPixel>(0.8));
    182  size.width = std::max(size.width, sMinSize.width);
    183  size.height = std::max(size.height, sMinSize.height);
    184  return size;
    185 }
    186 
    187 CSSIntRect DocumentPictureInPicture::DetermineExtent(
    188    bool aPreferInitialWindowPlacement, int aRequestedWidth,
    189    int aRequestedHeight, const CSSIntRect& screen) {
    190  // If we remembered an extent, don't preferInitialWindowPlacement, and the
    191  // requested size didn't change, then restore the remembered extent.
    192  const bool shouldUseInitialPlacement =
    193      !mPreviousExtent.isSome() || aPreferInitialWindowPlacement ||
    194      (mLastRequestedSize.isSome() &&
    195       (mLastRequestedSize->Width() != aRequestedWidth ||
    196        mLastRequestedSize->Height() != aRequestedHeight));
    197 
    198  CSSIntRect extent;
    199  if (shouldUseInitialPlacement) {
    200    CSSIntSize size = sDefaultSize;
    201    if (aRequestedWidth > 0 && aRequestedHeight > 0) {
    202      size = CSSIntSize(aRequestedWidth, aRequestedHeight);
    203    }
    204    CSSIntPoint initialPos = CalcInitialPos(screen, size);
    205    extent = CSSIntRect(initialPos, size);
    206 
    207    MOZ_LOG(gDPIPLog, LogLevel::Debug,
    208            ("Calculated initial PiP rect %s", ToString(extent).c_str()));
    209  } else {
    210    extent = mPreviousExtent.value();
    211  }
    212 
    213  // https://wicg.github.io/document-picture-in-picture/#maximum-size
    214  CSSIntSize maxSize = CalcMaxDimensions(screen);
    215  extent.width = std::clamp(extent.width, sMinSize.width, maxSize.width);
    216  extent.height = std::clamp(extent.height, sMinSize.height, maxSize.height);
    217 
    218  return extent;
    219 }
    220 
    221 already_AddRefed<Promise> DocumentPictureInPicture::RequestWindow(
    222    const DocumentPictureInPictureOptions& aOptions, ErrorResult& aRv) {
    223  // Not part of the spec, but check the document is active
    224  RefPtr<nsPIDOMWindowInner> ownerWin = GetOwnerWindow();
    225  if (!ownerWin || !ownerWin->IsFullyActive()) {
    226    aRv.ThrowNotAllowedError("Document is not fully active");
    227    return nullptr;
    228  }
    229 
    230  // 2. Throw if not top-level
    231  BrowsingContext* bc = ownerWin->GetBrowsingContext();
    232  if (!bc || !bc->IsTop()) {
    233    aRv.ThrowNotAllowedError(
    234        "Document Picture-in-Picture is only available in top-level contexts");
    235    return nullptr;
    236  }
    237 
    238  // 3. Throw if already in a Document PIP window
    239  if (bc->GetIsDocumentPiP()) {
    240    aRv.ThrowNotAllowedError(
    241        "Cannot open a Picture-in-Picture window from inside one");
    242    return nullptr;
    243  }
    244 
    245  // 4, 7. Require transient activation
    246  WindowContext* wc = ownerWin->GetWindowContext();
    247  if (!wc || !wc->ConsumeTransientUserGestureActivation()) {
    248    aRv.ThrowNotAllowedError(
    249        "Document Picture-in-Picture requires user activation");
    250    return nullptr;
    251  }
    252 
    253  // 5-6. If width or height is given, both must be specified
    254  if ((aOptions.mWidth > 0) != (aOptions.mHeight > 0)) {
    255    aRv.ThrowRangeError(
    256        "requestWindow: width and height must be specified together");
    257    return nullptr;
    258  }
    259 
    260  // 8. Possibly close last opened window
    261  if (RefPtr<nsPIDOMWindowInner> lastOpenedWindow = mLastOpenedWindow) {
    262    lastOpenedWindow->Close();
    263  }
    264 
    265  CSSIntRect screen;
    266  if (Maybe<CSSIntRect> maybeScreen =
    267          GetScreenRect(ownerWin->GetOuterWindow())) {
    268    screen = maybeScreen.value();
    269  } else {
    270    aRv.ThrowRangeError("Could not determine screen for window");
    271    return nullptr;
    272  }
    273 
    274  // 13-15. Determine PiP extent
    275  const int requestedWidth = SaturatingCast<int>(aOptions.mWidth),
    276            requestedHeight = SaturatingCast<int>(aOptions.mHeight);
    277  CSSIntRect extent = DetermineExtent(aOptions.mPreferInitialWindowPlacement,
    278                                      requestedWidth, requestedHeight, screen);
    279  mLastRequestedSize = Some(CSSIntSize(requestedWidth, requestedHeight));
    280 
    281  MOZ_LOG(gDPIPLog, LogLevel::Debug,
    282          ("Will place PiP at rect %s", ToString(extent).c_str()));
    283 
    284  // 9. Optionally, close any existing PIP windows
    285  // I think it's useful to have multiple PiP windows from different top pages.
    286 
    287  // 15. aOptions.mDisallowReturnToOpener
    288  // I think this button is redundant with close and the webpage won't know
    289  // whether close or return was pressed. So let's not have that button at all.
    290 
    291  // 10. Create a new top-level traversable for target _blank
    292  // 16. Configure PIP to float on top via window features
    293  RefPtr<BrowsingContext> pipTraversable;
    294  nsresult rv = OpenPiPWindowUtility(ownerWin->GetOuterWindow(), extent,
    295                                     bc->UsePrivateBrowsing(),
    296                                     getter_AddRefs(pipTraversable));
    297  if (NS_FAILED(rv)) {
    298    aRv.ThrowUnknownError("Failed to create PIP window");
    299    return nullptr;
    300  }
    301 
    302  // 11. Set PIP's active document's mode to this's document's mode
    303  pipTraversable->GetDocument()->SetCompatibilityMode(
    304      ownerWin->GetDoc()->GetCompatibilityMode());
    305 
    306  // 12. Set PIP's IsDocumentPIP flag
    307  rv = pipTraversable->SetIsDocumentPiP(true);
    308  MOZ_ASSERT(NS_SUCCEEDED(rv));
    309 
    310  // 16. Set mLastOpenedWindow
    311  mLastOpenedWindow = pipTraversable->GetDOMWindow()->GetCurrentInnerWindow();
    312  MOZ_ASSERT(mLastOpenedWindow);
    313 
    314  // Keep track of resizes to update mPreviousExtent
    315  RefPtr<nsGlobalWindowInner> pipInnerWindow =
    316      nsGlobalWindowInner::Cast(mLastOpenedWindow);
    317  pipInnerWindow->AddSystemEventListener(u"resize"_ns, this, true, false);
    318 
    319  // 17. Queue a task to fire a DocumentPictureInPictureEvent named "enter" on
    320  // this with pipTraversable as it's window attribute
    321  DocumentPictureInPictureEventInit eventInit;
    322  eventInit.mWindow = pipInnerWindow;
    323  RefPtr<Event> event =
    324      DocumentPictureInPictureEvent::Constructor(this, u"enter"_ns, eventInit);
    325  RefPtr<AsyncEventDispatcher> asyncDispatcher =
    326      new AsyncEventDispatcher(this, event.forget());
    327  asyncDispatcher->PostDOMEvent();
    328 
    329  // 18. Return pipTraversable
    330  RefPtr<Promise> promise = Promise::CreateInfallible(GetOwnerGlobal());
    331  promise->MaybeResolve(pipInnerWindow);
    332  return promise.forget();
    333 }
    334 
    335 NS_IMETHODIMP
    336 DocumentPictureInPicture::HandleEvent(Event* aEvent) {
    337  nsAutoString type;
    338  aEvent->GetType(type);
    339 
    340  if (type.EqualsLiteral("resize")) {
    341    OnPiPResized();
    342    return NS_OK;
    343  }
    344 
    345  return NS_OK;
    346 }
    347 
    348 NS_IMETHODIMP DocumentPictureInPicture::Observe(nsISupports* aSubject,
    349                                                const char* aTopic,
    350                                                const char16_t* aData) {
    351  if (nsCRT::strcmp(aTopic, "domwindowclosed") == 0) {
    352    nsCOMPtr<nsPIDOMWindowOuter> subjectWin = do_QueryInterface(aSubject);
    353    NS_ENSURE_TRUE(!!subjectWin, NS_OK);
    354 
    355    if (subjectWin->GetCurrentInnerWindow() == mLastOpenedWindow) {
    356      OnPiPClosed();
    357    }
    358  }
    359  return NS_OK;
    360 }
    361 
    362 }  // namespace mozilla::dom