summaryrefslogtreecommitdiffstats
path: root/widget/headless
diff options
context:
space:
mode:
Diffstat (limited to 'widget/headless')
-rw-r--r--widget/headless/HeadlessClipboard.cpp137
-rw-r--r--widget/headless/HeadlessClipboard.h33
-rw-r--r--widget/headless/HeadlessClipboardData.cpp19
-rw-r--r--widget/headless/HeadlessClipboardData.h34
-rw-r--r--widget/headless/HeadlessCompositorWidget.cpp46
-rw-r--r--widget/headless/HeadlessCompositorWidget.h54
-rw-r--r--widget/headless/HeadlessKeyBindings.cpp37
-rw-r--r--widget/headless/HeadlessKeyBindings.h42
-rw-r--r--widget/headless/HeadlessKeyBindingsCocoa.mm51
-rw-r--r--widget/headless/HeadlessLookAndFeel.h58
-rw-r--r--widget/headless/HeadlessLookAndFeelGTK.cpp226
-rw-r--r--widget/headless/HeadlessScreenHelper.cpp43
-rw-r--r--widget/headless/HeadlessScreenHelper.h27
-rw-r--r--widget/headless/HeadlessSound.cpp36
-rw-r--r--widget/headless/HeadlessSound.h31
-rw-r--r--widget/headless/HeadlessWidget.cpp618
-rw-r--r--widget/headless/HeadlessWidget.h181
-rw-r--r--widget/headless/HeadlessWidgetTypes.ipdlh20
-rw-r--r--widget/headless/moz.build50
-rw-r--r--widget/headless/tests/headless.html6
-rw-r--r--widget/headless/tests/headless_button.html6
-rw-r--r--widget/headless/tests/moz.build7
-rw-r--r--widget/headless/tests/test_headless.js214
-rw-r--r--widget/headless/tests/test_headless_clipboard.js45
-rw-r--r--widget/headless/tests/xpcshell.ini10
25 files changed, 2031 insertions, 0 deletions
diff --git a/widget/headless/HeadlessClipboard.cpp b/widget/headless/HeadlessClipboard.cpp
new file mode 100644
index 0000000000..b2c94cb48c
--- /dev/null
+++ b/widget/headless/HeadlessClipboard.cpp
@@ -0,0 +1,137 @@
+/* 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/. */
+
+#include "HeadlessClipboard.h"
+
+#include "nsISupportsPrimitives.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla {
+namespace widget {
+
+NS_IMPL_ISUPPORTS(HeadlessClipboard, nsIClipboard)
+
+HeadlessClipboard::HeadlessClipboard()
+ : mClipboard(MakeUnique<HeadlessClipboardData>()) {}
+
+NS_IMETHODIMP
+HeadlessClipboard::SetData(nsITransferable* aTransferable,
+ nsIClipboardOwner* anOwner,
+ int32_t aWhichClipboard) {
+ if (aWhichClipboard != kGlobalClipboard) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ // Clear out the clipboard in order to set the new data.
+ EmptyClipboard(aWhichClipboard);
+
+ // Only support plain text for now.
+ nsCOMPtr<nsISupports> clip;
+ nsresult rv =
+ aTransferable->GetTransferData(kUnicodeMime, getter_AddRefs(clip));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ nsCOMPtr<nsISupportsString> wideString = do_QueryInterface(clip);
+ if (!wideString) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ nsAutoString utf16string;
+ wideString->GetData(utf16string);
+ mClipboard->SetText(utf16string);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HeadlessClipboard::GetData(nsITransferable* aTransferable,
+ int32_t aWhichClipboard) {
+ if (aWhichClipboard != kGlobalClipboard) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsISupportsString> dataWrapper =
+ do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv);
+ rv = dataWrapper->SetData(mClipboard->GetText());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ nsCOMPtr<nsISupports> genericDataWrapper = do_QueryInterface(dataWrapper);
+ rv = aTransferable->SetTransferData(kUnicodeMime, genericDataWrapper);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HeadlessClipboard::EmptyClipboard(int32_t aWhichClipboard) {
+ if (aWhichClipboard != kGlobalClipboard) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ mClipboard->Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HeadlessClipboard::HasDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard,
+ bool* aHasType) {
+ *aHasType = false;
+ if (aWhichClipboard != kGlobalClipboard) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ // Retrieve the union of all aHasType in aFlavorList
+ for (auto& flavor : aFlavorList) {
+ if (flavor.EqualsLiteral(kUnicodeMime) && mClipboard->HasText()) {
+ *aHasType = true;
+ break;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HeadlessClipboard::SupportsSelectionClipboard(bool* aIsSupported) {
+ *aIsSupported = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HeadlessClipboard::SupportsFindClipboard(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ *_retval = false;
+ return NS_OK;
+}
+
+RefPtr<GenericPromise> HeadlessClipboard::AsyncGetData(
+ nsITransferable* aTransferable, int32_t aWhichClipboard) {
+ nsresult rv = GetData(aTransferable, aWhichClipboard);
+ if (NS_FAILED(rv)) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+
+ return GenericPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<DataFlavorsPromise> HeadlessClipboard::AsyncHasDataMatchingFlavors(
+ const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) {
+ nsTArray<nsCString> results;
+ for (const auto& flavor : aFlavorList) {
+ bool hasMatchingFlavor = false;
+ nsresult rv = HasDataMatchingFlavors(AutoTArray<nsCString, 1>{flavor},
+ aWhichClipboard, &hasMatchingFlavor);
+ if (NS_SUCCEEDED(rv) && hasMatchingFlavor) {
+ results.AppendElement(flavor);
+ }
+ }
+
+ return DataFlavorsPromise::CreateAndResolve(std::move(results), __func__);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessClipboard.h b/widget/headless/HeadlessClipboard.h
new file mode 100644
index 0000000000..da7819e195
--- /dev/null
+++ b/widget/headless/HeadlessClipboard.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+#ifndef mozilla_widget_HeadlessClipboard_h
+#define mozilla_widget_HeadlessClipboard_h
+
+#include "nsIClipboard.h"
+#include "mozilla/UniquePtr.h"
+#include "HeadlessClipboardData.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessClipboard final : public nsIClipboard {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICLIPBOARD
+
+ HeadlessClipboard();
+
+ protected:
+ ~HeadlessClipboard() = default;
+
+ private:
+ UniquePtr<HeadlessClipboardData> mClipboard;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/headless/HeadlessClipboardData.cpp b/widget/headless/HeadlessClipboardData.cpp
new file mode 100644
index 0000000000..3722605854
--- /dev/null
+++ b/widget/headless/HeadlessClipboardData.cpp
@@ -0,0 +1,19 @@
+/* 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/. */
+
+#include "HeadlessClipboardData.h"
+
+namespace mozilla {
+namespace widget {
+
+void HeadlessClipboardData::SetText(const nsAString& aText) { mPlain = aText; }
+
+bool HeadlessClipboardData::HasText() const { return !mPlain.IsEmpty(); }
+
+const nsAString& HeadlessClipboardData::GetText() const { return mPlain; }
+
+void HeadlessClipboardData::Clear() { mPlain.Truncate(0); }
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessClipboardData.h b/widget/headless/HeadlessClipboardData.h
new file mode 100644
index 0000000000..14cb84820a
--- /dev/null
+++ b/widget/headless/HeadlessClipboardData.h
@@ -0,0 +1,34 @@
+/* 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/. */
+
+#ifndef mozilla_widget_HeadlessClipboardData_h
+#define mozilla_widget_HeadlessClipboardData_h
+
+#include "mozilla/RefPtr.h"
+#include "nsString.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessClipboardData final {
+ public:
+ explicit HeadlessClipboardData() = default;
+ ~HeadlessClipboardData() = default;
+
+ // For text/plain
+ void SetText(const nsAString& aText);
+ bool HasText() const;
+ const nsAString& GetText() const;
+
+ // For other APIs
+ void Clear();
+
+ private:
+ nsAutoString mPlain;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_HeadlessClipboardData_h
diff --git a/widget/headless/HeadlessCompositorWidget.cpp b/widget/headless/HeadlessCompositorWidget.cpp
new file mode 100644
index 0000000000..bb4ee9175e
--- /dev/null
+++ b/widget/headless/HeadlessCompositorWidget.cpp
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mozilla/widget/PlatformWidgetTypes.h"
+#include "HeadlessCompositorWidget.h"
+#include "VsyncDispatcher.h"
+
+namespace mozilla {
+namespace widget {
+
+HeadlessCompositorWidget::HeadlessCompositorWidget(
+ const HeadlessCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions, HeadlessWidget* aWindow)
+ : CompositorWidget(aOptions),
+ mWidget(aWindow),
+ mClientSize(LayoutDeviceIntSize(aInitData.InitialClientSize()),
+ "HeadlessCompositorWidget::mClientSize") {}
+
+void HeadlessCompositorWidget::ObserveVsync(VsyncObserver* aObserver) {
+ if (RefPtr<CompositorVsyncDispatcher> cvd =
+ mWidget->GetCompositorVsyncDispatcher()) {
+ cvd->SetCompositorVsyncObserver(aObserver);
+ }
+}
+
+nsIWidget* HeadlessCompositorWidget::RealWidget() { return mWidget; }
+
+void HeadlessCompositorWidget::NotifyClientSizeChanged(
+ const LayoutDeviceIntSize& aClientSize) {
+ auto size = mClientSize.Lock();
+ *size = aClientSize;
+}
+
+LayoutDeviceIntSize HeadlessCompositorWidget::GetClientSize() {
+ auto size = mClientSize.Lock();
+ return *size;
+}
+
+uintptr_t HeadlessCompositorWidget::GetWidgetKey() {
+ return reinterpret_cast<uintptr_t>(mWidget);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessCompositorWidget.h b/widget/headless/HeadlessCompositorWidget.h
new file mode 100644
index 0000000000..facd2bc65a
--- /dev/null
+++ b/widget/headless/HeadlessCompositorWidget.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef widget_headless_HeadlessCompositorWidget_h
+#define widget_headless_HeadlessCompositorWidget_h
+
+#include "mozilla/widget/CompositorWidget.h"
+
+#include "HeadlessWidget.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessCompositorWidgetInitData;
+
+class HeadlessCompositorWidget final : public CompositorWidget,
+ public CompositorWidgetDelegate {
+ public:
+ HeadlessCompositorWidget(const HeadlessCompositorWidgetInitData& aInitData,
+ const layers::CompositorOptions& aOptions,
+ HeadlessWidget* aWindow);
+
+ void NotifyClientSizeChanged(const LayoutDeviceIntSize& aClientSize);
+
+ // CompositorWidget Overrides
+
+ uintptr_t GetWidgetKey() override;
+
+ LayoutDeviceIntSize GetClientSize() override;
+
+ nsIWidget* RealWidget() override;
+ CompositorWidgetDelegate* AsDelegate() override { return this; }
+
+ void ObserveVsync(VsyncObserver* aObserver) override;
+
+ // CompositorWidgetDelegate Overrides
+
+ HeadlessCompositorWidget* AsHeadlessCompositorWidget() override {
+ return this;
+ }
+
+ private:
+ HeadlessWidget* mWidget;
+
+ // See GtkCompositorWidget for the justification for this mutex.
+ DataMutex<LayoutDeviceIntSize> mClientSize;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // widget_headless_HeadlessCompositor_h
diff --git a/widget/headless/HeadlessKeyBindings.cpp b/widget/headless/HeadlessKeyBindings.cpp
new file mode 100644
index 0000000000..13fcd4c6ca
--- /dev/null
+++ b/widget/headless/HeadlessKeyBindings.cpp
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+#include "HeadlessKeyBindings.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/WritingModes.h"
+
+namespace mozilla {
+namespace widget {
+
+HeadlessKeyBindings& HeadlessKeyBindings::GetInstance() {
+ static UniquePtr<HeadlessKeyBindings> sInstance;
+ if (!sInstance) {
+ sInstance.reset(new HeadlessKeyBindings());
+ ClearOnShutdown(&sInstance);
+ }
+ return *sInstance;
+}
+
+nsresult HeadlessKeyBindings::AttachNativeKeyEvent(
+ WidgetKeyboardEvent& aEvent) {
+ // Stub for non-mac platforms.
+ return NS_OK;
+}
+
+void HeadlessKeyBindings::GetEditCommands(
+ NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode, nsTArray<CommandInt>& aCommands) {
+ // Stub for non-mac platforms.
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessKeyBindings.h b/widget/headless/HeadlessKeyBindings.h
new file mode 100644
index 0000000000..5eff30c4f5
--- /dev/null
+++ b/widget/headless/HeadlessKeyBindings.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+#ifndef mozilla_widget_HeadlessKeyBindings_h
+#define mozilla_widget_HeadlessKeyBindings_h
+
+#include "mozilla/TextEvents.h"
+#include "nsIWidget.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+
+class WritingMode;
+template <typename T>
+class Maybe;
+
+namespace widget {
+
+/**
+ * Helper to emulate native key bindings. Currently only MacOS is supported.
+ */
+
+class HeadlessKeyBindings final {
+ public:
+ HeadlessKeyBindings() = default;
+
+ static HeadlessKeyBindings& GetInstance();
+
+ void GetEditCommands(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands);
+ [[nodiscard]] nsresult AttachNativeKeyEvent(WidgetKeyboardEvent& aEvent);
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_HeadlessKeyBindings_h
diff --git a/widget/headless/HeadlessKeyBindingsCocoa.mm b/widget/headless/HeadlessKeyBindingsCocoa.mm
new file mode 100644
index 0000000000..881bff50f4
--- /dev/null
+++ b/widget/headless/HeadlessKeyBindingsCocoa.mm
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+#include "HeadlessKeyBindings.h"
+#import <Cocoa/Cocoa.h>
+#include "nsCocoaUtils.h"
+#include "NativeKeyBindings.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/WritingModes.h"
+
+namespace mozilla {
+namespace widget {
+
+HeadlessKeyBindings& HeadlessKeyBindings::GetInstance() {
+ static UniquePtr<HeadlessKeyBindings> sInstance;
+ if (!sInstance) {
+ sInstance.reset(new HeadlessKeyBindings());
+ ClearOnShutdown(&sInstance);
+ }
+ return *sInstance;
+}
+
+nsresult HeadlessKeyBindings::AttachNativeKeyEvent(WidgetKeyboardEvent& aEvent) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ aEvent.mNativeKeyEvent = nsCocoaUtils::MakeNewCococaEventFromWidgetEvent(aEvent, 0, nil);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+void HeadlessKeyBindings::GetEditCommands(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ const Maybe<WritingMode>& aWritingMode,
+ nsTArray<CommandInt>& aCommands) {
+ // Convert the widget keyboard into a cocoa event so it can be translated
+ // into commands in the NativeKeyBindings.
+ WidgetKeyboardEvent modifiedEvent(aEvent);
+ modifiedEvent.mNativeKeyEvent = nsCocoaUtils::MakeNewCococaEventFromWidgetEvent(aEvent, 0, nil);
+
+ NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
+ keyBindings->GetEditCommands(modifiedEvent, aWritingMode, aCommands);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessLookAndFeel.h b/widget/headless/HeadlessLookAndFeel.h
new file mode 100644
index 0000000000..4e61b9e95d
--- /dev/null
+++ b/widget/headless/HeadlessLookAndFeel.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_HeadlessLookAndFeel_h
+#define mozilla_widget_HeadlessLookAndFeel_h
+
+#include "nsXPLookAndFeel.h"
+#include "nsLookAndFeel.h"
+
+namespace mozilla {
+namespace widget {
+
+#if defined(MOZ_WIDGET_GTK)
+
+// Our nsLookAndFeel for Gtk relies on APIs that aren't available in headless
+// mode, so for processes that are unable to connect to a display server, we use
+// an implementation with hardcoded values.
+//
+// HeadlessLookAndFeel is used:
+//
+// * in the parent process, when full headless mode (MOZ_HEADLESS=1) is
+// enabled
+//
+// The result of this is that when headless content mode is enabled, content
+// processes use values derived from the parent's nsLookAndFeel (i.e., values
+// derived from Gtk APIs) while still refraining from making any display server
+// connections.
+
+class HeadlessLookAndFeel : public nsXPLookAndFeel {
+ public:
+ explicit HeadlessLookAndFeel();
+ virtual ~HeadlessLookAndFeel();
+
+ void NativeInit() final{};
+ nsresult NativeGetInt(IntID, int32_t& aResult) override;
+ nsresult NativeGetFloat(FloatID, float& aResult) override;
+ nsresult NativeGetColor(ColorID, ColorScheme, nscolor& aResult) override;
+ bool NativeGetFont(FontID, nsString& aFontName, gfxFontStyle&) override;
+
+ char16_t GetPasswordCharacterImpl() override;
+};
+
+#else
+
+// When possible, we simply reuse the platform's existing nsLookAndFeel
+// implementation in headless mode.
+
+typedef nsLookAndFeel HeadlessLookAndFeel;
+
+#endif
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/headless/HeadlessLookAndFeelGTK.cpp b/widget/headless/HeadlessLookAndFeelGTK.cpp
new file mode 100644
index 0000000000..171d39df3f
--- /dev/null
+++ b/widget/headless/HeadlessLookAndFeelGTK.cpp
@@ -0,0 +1,226 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "HeadlessLookAndFeel.h"
+#include "mozilla/FontPropertyTypes.h"
+#include "nsIContent.h"
+
+namespace mozilla::widget {
+
+static const char16_t UNICODE_BULLET = 0x2022;
+
+HeadlessLookAndFeel::HeadlessLookAndFeel() = default;
+
+HeadlessLookAndFeel::~HeadlessLookAndFeel() = default;
+
+nsresult HeadlessLookAndFeel::NativeGetColor(ColorID aID, ColorScheme aScheme,
+ nscolor& aResult) {
+ aResult = GetStandinForNativeColor(aID, aScheme);
+ return NS_OK;
+}
+
+nsresult HeadlessLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
+ nsresult res = NS_OK;
+ // These values should be sane defaults for headless mode under GTK.
+ switch (aID) {
+ case IntID::CaretBlinkTime:
+ aResult = 567;
+ break;
+ case IntID::CaretWidth:
+ aResult = 1;
+ break;
+ case IntID::ShowCaretDuringSelection:
+ aResult = 0;
+ break;
+ case IntID::SelectTextfieldsOnKeyFocus:
+ aResult = 1;
+ break;
+ case IntID::SubmenuDelay:
+ aResult = 200;
+ break;
+ case IntID::MenusCanOverlapOSBar:
+ aResult = 0;
+ break;
+ case IntID::UseOverlayScrollbars:
+ aResult = 0;
+ break;
+ case IntID::AllowOverlayScrollbarsOverlap:
+ aResult = 0;
+ break;
+ case IntID::SkipNavigatingDisabledMenuItem:
+ aResult = 1;
+ break;
+ case IntID::DragThresholdX:
+ case IntID::DragThresholdY:
+ aResult = 4;
+ break;
+ case IntID::UseAccessibilityTheme:
+ aResult = 0;
+ break;
+ case IntID::ScrollArrowStyle:
+ aResult = eScrollArrow_None;
+ break;
+ case IntID::ScrollButtonLeftMouseButtonAction:
+ aResult = 0;
+ return NS_OK;
+ case IntID::ScrollButtonMiddleMouseButtonAction:
+ aResult = 3;
+ return NS_OK;
+ case IntID::ScrollButtonRightMouseButtonAction:
+ aResult = 3;
+ return NS_OK;
+ case IntID::TreeOpenDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeCloseDelay:
+ aResult = 1000;
+ break;
+ case IntID::TreeLazyScrollDelay:
+ aResult = 150;
+ break;
+ case IntID::TreeScrollDelay:
+ aResult = 100;
+ break;
+ case IntID::TreeScrollLinesMax:
+ aResult = 3;
+ break;
+ case IntID::TabFocusModel:
+ aResult = nsIContent::eTabFocus_textControlsMask;
+ break;
+ case IntID::ChosenMenuItemsShouldBlink:
+ aResult = 1;
+ break;
+ case IntID::WindowsAccentColorInTitlebar:
+ case IntID::WindowsDefaultTheme:
+ case IntID::DWMCompositor:
+ aResult = 0;
+ res = NS_ERROR_NOT_IMPLEMENTED;
+ break;
+ case IntID::WindowsClassic:
+ case IntID::WindowsGlass:
+ aResult = 0;
+ res = NS_ERROR_FAILURE;
+ break;
+ case IntID::AlertNotificationOrigin:
+ aResult = NS_ALERT_TOP;
+ break;
+ case IntID::ScrollToClick:
+ aResult = 0;
+ break;
+ case IntID::IMERawInputUnderlineStyle:
+ case IntID::IMESelectedRawTextUnderlineStyle:
+ case IntID::IMEConvertedTextUnderlineStyle:
+ case IntID::IMESelectedConvertedTextUnderline:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Solid);
+ break;
+ case IntID::SpellCheckerUnderlineStyle:
+ aResult = static_cast<int32_t>(StyleTextDecorationStyle::Dotted);
+ break;
+ case IntID::MenuBarDrag:
+ aResult = 0;
+ break;
+ case IntID::ScrollbarButtonAutoRepeatBehavior:
+ aResult = 0;
+ break;
+ case IntID::TooltipDelay:
+ aResult = 500;
+ break;
+ case IntID::SwipeAnimationEnabled:
+ aResult = 0;
+ break;
+ case IntID::ScrollbarDisplayOnMouseMove:
+ aResult = 0;
+ break;
+ case IntID::ScrollbarFadeBeginDelay:
+ aResult = 0;
+ break;
+ case IntID::ScrollbarFadeDuration:
+ aResult = 0;
+ break;
+ case IntID::ContextMenuOffsetVertical:
+ aResult = -6;
+ break;
+ case IntID::ContextMenuOffsetHorizontal:
+ aResult = 1;
+ break;
+ case IntID::GTKCSDAvailable:
+ aResult = 0;
+ break;
+ case IntID::GTKCSDMinimizeButton:
+ aResult = 0;
+ break;
+ case IntID::GTKCSDMaximizeButton:
+ aResult = 0;
+ break;
+ case IntID::GTKCSDCloseButton:
+ aResult = 1;
+ break;
+ case IntID::GTKCSDReversedPlacement:
+ aResult = 0;
+ break;
+ case IntID::SystemUsesDarkTheme:
+ aResult = 0;
+ break;
+ case IntID::PrefersReducedMotion:
+ aResult = 0;
+ break;
+ case IntID::PrimaryPointerCapabilities:
+ aResult = 0;
+ break;
+ case IntID::AllPointerCapabilities:
+ aResult = 0;
+ break;
+ default:
+ aResult = 0;
+ res = NS_ERROR_FAILURE;
+ break;
+ }
+ return res;
+}
+
+nsresult HeadlessLookAndFeel::NativeGetFloat(FloatID aID, float& aResult) {
+ nsresult res = NS_OK;
+
+ // Hardcoded values for GTK.
+ switch (aID) {
+ case FloatID::IMEUnderlineRelativeSize:
+ aResult = 1.0f;
+ break;
+ case FloatID::SpellCheckerUnderlineRelativeSize:
+ aResult = 1.0f;
+ break;
+ case FloatID::CaretAspectRatio:
+ // Intentionally failing to quietly indicate lack of support.
+ aResult = -1.0;
+ res = NS_ERROR_FAILURE;
+ break;
+ default:
+ aResult = -1.0;
+ res = NS_ERROR_FAILURE;
+ break;
+ }
+
+ return res;
+}
+
+bool HeadlessLookAndFeel::NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) {
+ // Default to san-serif for everything.
+ aFontStyle.style = FontSlantStyle::NORMAL;
+ aFontStyle.weight = FontWeight::NORMAL;
+ aFontStyle.stretch = FontStretch::NORMAL;
+ aFontStyle.size = 14;
+ aFontStyle.systemFont = true;
+
+ aFontName.AssignLiteral("sans-serif");
+ return true;
+}
+
+char16_t HeadlessLookAndFeel::GetPasswordCharacterImpl() {
+ return UNICODE_BULLET;
+}
+
+} // namespace mozilla::widget
diff --git a/widget/headless/HeadlessScreenHelper.cpp b/widget/headless/HeadlessScreenHelper.cpp
new file mode 100644
index 0000000000..4d8dbfa0d1
--- /dev/null
+++ b/widget/headless/HeadlessScreenHelper.cpp
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "HeadlessScreenHelper.h"
+
+#include "prenv.h"
+#include "mozilla/dom/DOMTypes.h"
+#include "mozilla/RefPtr.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace widget {
+
+/* static */
+LayoutDeviceIntRect HeadlessScreenHelper::GetScreenRect() {
+ char* ev = PR_GetEnv("MOZ_HEADLESS_WIDTH");
+ int width = 1366;
+ if (ev) {
+ width = atoi(ev);
+ }
+ ev = PR_GetEnv("MOZ_HEADLESS_HEIGHT");
+ int height = 768;
+ if (ev) {
+ height = atoi(ev);
+ }
+ return LayoutDeviceIntRect(0, 0, width, height);
+}
+
+HeadlessScreenHelper::HeadlessScreenHelper() {
+ AutoTArray<RefPtr<Screen>, 1> screenList;
+ LayoutDeviceIntRect rect = GetScreenRect();
+ auto ret = MakeRefPtr<Screen>(
+ rect, rect, 24, 24, 0, DesktopToLayoutDeviceScale(),
+ CSSToLayoutDeviceScale(), 96.0f, Screen::IsPseudoDisplay::No);
+ screenList.AppendElement(ret.forget());
+ ScreenManager::Refresh(std::move(screenList));
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessScreenHelper.h b/widget/headless/HeadlessScreenHelper.h
new file mode 100644
index 0000000000..f86677af52
--- /dev/null
+++ b/widget/headless/HeadlessScreenHelper.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_HeadlessScreenHelper_h
+#define mozilla_widget_HeadlessScreenHelper_h
+
+#include "mozilla/widget/ScreenManager.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessScreenHelper final : public ScreenManager::Helper {
+ public:
+ HeadlessScreenHelper();
+ ~HeadlessScreenHelper() override = default;
+
+ private:
+ static LayoutDeviceIntRect GetScreenRect();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_HeadlessScreenHelper_h
diff --git a/widget/headless/HeadlessSound.cpp b/widget/headless/HeadlessSound.cpp
new file mode 100644
index 0000000000..278d31b355
--- /dev/null
+++ b/widget/headless/HeadlessSound.cpp
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "HeadlessSound.h"
+
+namespace mozilla {
+namespace widget {
+
+NS_IMPL_ISUPPORTS(HeadlessSound, nsISound, nsIStreamLoaderObserver)
+
+HeadlessSound::HeadlessSound() = default;
+
+HeadlessSound::~HeadlessSound() = default;
+
+NS_IMETHODIMP
+HeadlessSound::Init() { return NS_OK; }
+
+NS_IMETHODIMP HeadlessSound::OnStreamComplete(nsIStreamLoader* aLoader,
+ nsISupports* context,
+ nsresult aStatus,
+ uint32_t dataLen,
+ const uint8_t* data) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP HeadlessSound::Beep() { return NS_OK; }
+
+NS_IMETHODIMP HeadlessSound::Play(nsIURL* aURL) { return NS_OK; }
+
+NS_IMETHODIMP HeadlessSound::PlayEventSound(uint32_t aEventId) { return NS_OK; }
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessSound.h b/widget/headless/HeadlessSound.h
new file mode 100644
index 0000000000..a481acc61e
--- /dev/null
+++ b/widget/headless/HeadlessSound.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_widget_HeadlessSound_h
+#define mozilla_widget_HeadlessSound_h
+
+#include "nsISound.h"
+#include "nsIStreamLoader.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessSound : public nsISound, public nsIStreamLoaderObserver {
+ public:
+ HeadlessSound();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISOUND
+ NS_DECL_NSISTREAMLOADEROBSERVER
+
+ private:
+ virtual ~HeadlessSound();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_HeadlessSound_h
diff --git a/widget/headless/HeadlessWidget.cpp b/widget/headless/HeadlessWidget.cpp
new file mode 100644
index 0000000000..54d9625e5e
--- /dev/null
+++ b/widget/headless/HeadlessWidget.cpp
@@ -0,0 +1,618 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+#include "HeadlessWidget.h"
+#include "ErrorList.h"
+#include "HeadlessCompositorWidget.h"
+#include "BasicEvents.h"
+#include "MouseEvents.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NativeKeyBindingsType.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TextEventDispatcher.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/WritingModes.h"
+#include "mozilla/widget/HeadlessWidgetTypes.h"
+#include "mozilla/widget/PlatformWidgetTypes.h"
+#include "nsIScreen.h"
+#include "HeadlessKeyBindings.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+
+using mozilla::LogLevel;
+
+#ifdef MOZ_LOGGING
+
+# include "mozilla/Logging.h"
+static mozilla::LazyLogModule sWidgetLog("Widget");
+static mozilla::LazyLogModule sWidgetFocusLog("WidgetFocus");
+# define LOG(args) MOZ_LOG(sWidgetLog, mozilla::LogLevel::Debug, args)
+# define LOGFOCUS(args) \
+ MOZ_LOG(sWidgetFocusLog, mozilla::LogLevel::Debug, args)
+
+#else
+
+# define LOG(args)
+# define LOGFOCUS(args)
+
+#endif /* MOZ_LOGGING */
+
+/*static*/
+already_AddRefed<nsIWidget> nsIWidget::CreateHeadlessWidget() {
+ nsCOMPtr<nsIWidget> widget = new mozilla::widget::HeadlessWidget();
+ return widget.forget();
+}
+
+namespace mozilla {
+namespace widget {
+
+StaticAutoPtr<nsTArray<HeadlessWidget*>> HeadlessWidget::sActiveWindows;
+
+already_AddRefed<HeadlessWidget> HeadlessWidget::GetActiveWindow() {
+ if (!sActiveWindows) {
+ return nullptr;
+ }
+ auto length = sActiveWindows->Length();
+ if (length == 0) {
+ return nullptr;
+ }
+ RefPtr<HeadlessWidget> widget = sActiveWindows->ElementAt(length - 1);
+ return widget.forget();
+}
+
+HeadlessWidget::HeadlessWidget()
+ : mEnabled(true),
+ mVisible(false),
+ mDestroyed(false),
+ mTopLevel(nullptr),
+ mCompositorWidget(nullptr),
+ mSizeMode(nsSizeMode_Normal),
+ mLastSizeMode(nsSizeMode_Normal),
+ mEffectiveSizeMode(nsSizeMode_Normal),
+ mRestoreBounds(0, 0, 0, 0) {
+ if (!sActiveWindows) {
+ sActiveWindows = new nsTArray<HeadlessWidget*>();
+ ClearOnShutdown(&sActiveWindows);
+ }
+}
+
+HeadlessWidget::~HeadlessWidget() {
+ LOG(("HeadlessWidget::~HeadlessWidget() [%p]\n", (void*)this));
+
+ Destroy();
+}
+
+void HeadlessWidget::Destroy() {
+ if (mDestroyed) {
+ return;
+ }
+ LOG(("HeadlessWidget::Destroy [%p]\n", (void*)this));
+ mDestroyed = true;
+
+ if (sActiveWindows) {
+ int32_t index = sActiveWindows->IndexOf(this);
+ if (index != -1) {
+ RefPtr<HeadlessWidget> activeWindow = GetActiveWindow();
+ sActiveWindows->RemoveElementAt(index);
+ // If this is the currently active widget and there's a previously active
+ // widget, activate the previous widget.
+ RefPtr<HeadlessWidget> previousActiveWindow = GetActiveWindow();
+ if (this == activeWindow && previousActiveWindow &&
+ previousActiveWindow->mWidgetListener) {
+ previousActiveWindow->mWidgetListener->WindowActivated();
+ }
+ }
+ }
+
+ nsBaseWidget::OnDestroy();
+
+ nsBaseWidget::Destroy();
+}
+
+nsresult HeadlessWidget::Create(nsIWidget* aParent,
+ nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ nsWidgetInitData* aInitData) {
+ MOZ_ASSERT(!aNativeParent, "No native parents for headless widgets.");
+
+ BaseCreate(nullptr, aInitData);
+
+ mBounds = aRect;
+ mRestoreBounds = aRect;
+
+ if (aParent) {
+ mTopLevel = aParent->GetTopLevelWidget();
+ } else {
+ mTopLevel = this;
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<nsIWidget> HeadlessWidget::CreateChild(
+ const LayoutDeviceIntRect& aRect, nsWidgetInitData* aInitData,
+ bool aForceUseIWidgetParent) {
+ nsCOMPtr<nsIWidget> widget = nsIWidget::CreateHeadlessWidget();
+ if (!widget) {
+ return nullptr;
+ }
+ if (NS_FAILED(widget->Create(this, nullptr, aRect, aInitData))) {
+ return nullptr;
+ }
+ return widget.forget();
+}
+
+void HeadlessWidget::GetCompositorWidgetInitData(
+ mozilla::widget::CompositorWidgetInitData* aInitData) {
+ *aInitData =
+ mozilla::widget::HeadlessCompositorWidgetInitData(GetClientSize());
+}
+
+nsIWidget* HeadlessWidget::GetTopLevelWidget() { return mTopLevel; }
+
+void HeadlessWidget::RaiseWindow() {
+ MOZ_ASSERT(mTopLevel == this || mWindowType == eWindowType_dialog ||
+ mWindowType == eWindowType_sheet,
+ "Raising a non-toplevel window.");
+
+ // Do nothing if this is the currently active window.
+ RefPtr<HeadlessWidget> activeWindow = GetActiveWindow();
+ if (activeWindow == this) {
+ return;
+ }
+
+ // Raise the window to the top of the stack.
+ nsWindowZ placement = nsWindowZTop;
+ nsCOMPtr<nsIWidget> actualBelow;
+ if (mWidgetListener)
+ mWidgetListener->ZLevelChanged(true, &placement, nullptr,
+ getter_AddRefs(actualBelow));
+
+ // Deactivate the last active window.
+ if (activeWindow && activeWindow->mWidgetListener) {
+ activeWindow->mWidgetListener->WindowDeactivated();
+ }
+
+ // Remove this window if it's already tracked.
+ int32_t index = sActiveWindows->IndexOf(this);
+ if (index != -1) {
+ sActiveWindows->RemoveElementAt(index);
+ }
+
+ // Activate this window.
+ sActiveWindows->AppendElement(this);
+ if (mWidgetListener) mWidgetListener->WindowActivated();
+}
+
+void HeadlessWidget::Show(bool aState) {
+ mVisible = aState;
+
+ LOG(("HeadlessWidget::Show [%p] state %d\n", (void*)this, aState));
+
+ // Top-level window and dialogs are activated/raised when shown.
+ if (aState && (mTopLevel == this || mWindowType == eWindowType_dialog ||
+ mWindowType == eWindowType_sheet)) {
+ RaiseWindow();
+ }
+
+ ApplySizeModeSideEffects();
+}
+
+bool HeadlessWidget::IsVisible() const { return mVisible; }
+
+void HeadlessWidget::SetFocus(Raise aRaise,
+ mozilla::dom::CallerType aCallerType) {
+ LOGFOCUS((" SetFocus %d [%p]\n", aRaise == Raise::Yes, (void*)this));
+
+ // This means we request activation of our toplevel window.
+ if (aRaise == Raise::Yes) {
+ HeadlessWidget* topLevel = (HeadlessWidget*)GetTopLevelWidget();
+
+ // The toplevel only becomes active if it's currently visible; otherwise, it
+ // will be activated anyway when it's shown.
+ if (topLevel->IsVisible()) topLevel->RaiseWindow();
+ }
+}
+
+void HeadlessWidget::Enable(bool aState) { mEnabled = aState; }
+
+bool HeadlessWidget::IsEnabled() const { return mEnabled; }
+
+void HeadlessWidget::Move(double aX, double aY) {
+ LOG(("HeadlessWidget::Move [%p] %f %f\n", (void*)this, aX, aY));
+
+ double scale =
+ BoundsUseDesktopPixels() ? GetDesktopToDeviceScale().scale : 1.0;
+ int32_t x = NSToIntRound(aX * scale);
+ int32_t y = NSToIntRound(aY * scale);
+
+ if (mWindowType == eWindowType_toplevel ||
+ mWindowType == eWindowType_dialog) {
+ SetSizeMode(nsSizeMode_Normal);
+ }
+
+ MoveInternal(x, y);
+}
+
+void HeadlessWidget::MoveInternal(int32_t aX, int32_t aY) {
+ // Since a popup window's x/y coordinates are in relation to
+ // the parent, the parent might have moved so we always move a
+ // popup window.
+ if (mBounds.IsEqualXY(aX, aY) && mWindowType != eWindowType_popup) {
+ return;
+ }
+
+ mBounds.MoveTo(aX, aY);
+ NotifyWindowMoved(aX, aY);
+}
+
+LayoutDeviceIntPoint HeadlessWidget::WidgetToScreenOffset() {
+ return mTopLevel->GetBounds().TopLeft();
+}
+
+WindowRenderer* HeadlessWidget::GetWindowRenderer() {
+ return nsBaseWidget::GetWindowRenderer();
+}
+
+void HeadlessWidget::SetCompositorWidgetDelegate(
+ CompositorWidgetDelegate* delegate) {
+ if (delegate) {
+ mCompositorWidget = delegate->AsHeadlessCompositorWidget();
+ MOZ_ASSERT(mCompositorWidget,
+ "HeadlessWidget::SetCompositorWidgetDelegate called with a "
+ "non-HeadlessCompositorWidget");
+ } else {
+ mCompositorWidget = nullptr;
+ }
+}
+
+void HeadlessWidget::Resize(double aWidth, double aHeight, bool aRepaint) {
+ int32_t width = NSToIntRound(aWidth);
+ int32_t height = NSToIntRound(aHeight);
+ ResizeInternal(width, height, aRepaint);
+}
+
+void HeadlessWidget::ResizeInternal(int32_t aWidth, int32_t aHeight,
+ bool aRepaint) {
+ ConstrainSize(&aWidth, &aHeight);
+ mBounds.SizeTo(LayoutDeviceIntSize(aWidth, aHeight));
+
+ if (mCompositorWidget) {
+ mCompositorWidget->NotifyClientSizeChanged(
+ LayoutDeviceIntSize(mBounds.Width(), mBounds.Height()));
+ }
+ if (mWidgetListener) {
+ mWidgetListener->WindowResized(this, mBounds.Width(), mBounds.Height());
+ }
+ if (mAttachedWidgetListener) {
+ mAttachedWidgetListener->WindowResized(this, mBounds.Width(),
+ mBounds.Height());
+ }
+}
+
+void HeadlessWidget::Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) {
+ MoveInternal(NSToIntRound(aX), NSToIntRound(aY));
+ Resize(aWidth, aHeight, aRepaint);
+}
+
+void HeadlessWidget::SetSizeMode(nsSizeMode aMode) {
+ LOG(("HeadlessWidget::SetSizeMode [%p] %d\n", (void*)this, aMode));
+
+ if (aMode == mSizeMode) {
+ return;
+ }
+
+ if (aMode == nsSizeMode_Normal && mSizeMode == nsSizeMode_Fullscreen) {
+ MakeFullScreen(false);
+ return;
+ }
+
+ mSizeMode = aMode;
+
+ // Normally in real widget backends a window event would be triggered that
+ // would cause the window manager to handle resizing the window. In headless
+ // the window must manually be resized.
+ ApplySizeModeSideEffects();
+}
+
+void HeadlessWidget::ApplySizeModeSideEffects() {
+ if (!mVisible || mEffectiveSizeMode == mSizeMode) {
+ return;
+ }
+
+ if (mEffectiveSizeMode == nsSizeMode_Normal) {
+ // Store the last normal size bounds so it can be restored when entering
+ // normal mode again.
+ mRestoreBounds = mBounds;
+ }
+
+ switch (mSizeMode) {
+ case nsSizeMode_Normal: {
+ MoveInternal(mRestoreBounds.X(), mRestoreBounds.Y());
+ ResizeInternal(mRestoreBounds.Width(), mRestoreBounds.Height(), false);
+ break;
+ }
+ case nsSizeMode_Minimized:
+ break;
+ case nsSizeMode_Maximized: {
+ nsCOMPtr<nsIScreen> screen = GetWidgetScreen();
+ if (screen) {
+ int32_t left, top, width, height;
+ if (NS_SUCCEEDED(
+ screen->GetRectDisplayPix(&left, &top, &width, &height))) {
+ MoveInternal(0, 0);
+ ResizeInternal(width, height, true);
+ }
+ }
+ break;
+ }
+ case nsSizeMode_Fullscreen:
+ // This will take care of resizing the window.
+ nsBaseWidget::InfallibleMakeFullScreen(true);
+ break;
+ default:
+ break;
+ }
+
+ mEffectiveSizeMode = mSizeMode;
+ if (mWidgetListener) {
+ mWidgetListener->SizeModeChanged(mSizeMode);
+ }
+}
+
+nsresult HeadlessWidget::MakeFullScreen(bool aFullScreen) {
+ // Directly update the size mode here so a later call SetSizeMode does
+ // nothing.
+ if (aFullScreen) {
+ if (mSizeMode != nsSizeMode_Fullscreen) {
+ mLastSizeMode = mSizeMode;
+ }
+ mSizeMode = nsSizeMode_Fullscreen;
+ } else {
+ mSizeMode = mLastSizeMode;
+ }
+
+ // Notify the listener first so size mode change events are triggered before
+ // resize events.
+ if (mWidgetListener) {
+ mWidgetListener->SizeModeChanged(mSizeMode);
+ mWidgetListener->FullscreenChanged(aFullScreen);
+ }
+
+ // Real widget backends don't seem to follow a common approach for
+ // when and how many resize events are triggered during fullscreen
+ // transitions. InfallibleMakeFullScreen will trigger a resize, but it
+ // will be ignored if still transitioning to fullscreen, so it must be
+ // triggered on the next tick.
+ RefPtr<HeadlessWidget> self(this);
+ NS_DispatchToCurrentThread(NS_NewRunnableFunction(
+ "HeadlessWidget::MakeFullScreen", [self, aFullScreen]() -> void {
+ self->InfallibleMakeFullScreen(aFullScreen);
+ }));
+
+ return NS_OK;
+}
+
+nsresult HeadlessWidget::AttachNativeKeyEvent(WidgetKeyboardEvent& aEvent) {
+ HeadlessKeyBindings& bindings = HeadlessKeyBindings::GetInstance();
+ return bindings.AttachNativeKeyEvent(aEvent);
+}
+
+bool HeadlessWidget::GetEditCommands(NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ nsTArray<CommandInt>& aCommands) {
+ // Validate the arguments.
+ if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
+ return false;
+ }
+
+ Maybe<WritingMode> writingMode;
+ if (aEvent.NeedsToRemapNavigationKey()) {
+ if (RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher()) {
+ writingMode = dispatcher->MaybeQueryWritingModeAtSelection();
+ }
+ }
+
+ HeadlessKeyBindings& bindings = HeadlessKeyBindings::GetInstance();
+ bindings.GetEditCommands(aType, aEvent, writingMode, aCommands);
+ return true;
+}
+
+nsresult HeadlessWidget::DispatchEvent(WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) {
+#ifdef DEBUG
+ debug_DumpEvent(stdout, aEvent->mWidget, aEvent, "HeadlessWidget", 0);
+#endif
+
+ aStatus = nsEventStatus_eIgnore;
+
+ if (mAttachedWidgetListener) {
+ aStatus = mAttachedWidgetListener->HandleEvent(aEvent, mUseAttachedEvents);
+ } else if (mWidgetListener) {
+ aStatus = mWidgetListener->HandleEvent(aEvent, mUseAttachedEvents);
+ }
+
+ return NS_OK;
+}
+
+nsresult HeadlessWidget::SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "mouseevent");
+ EventMessage msg;
+ switch (aNativeMessage) {
+ case NativeMouseMessage::Move:
+ msg = eMouseMove;
+ break;
+ case NativeMouseMessage::ButtonDown:
+ msg = eMouseDown;
+ break;
+ case NativeMouseMessage::ButtonUp:
+ msg = eMouseUp;
+ break;
+ case NativeMouseMessage::EnterWindow:
+ case NativeMouseMessage::LeaveWindow:
+ MOZ_ASSERT_UNREACHABLE("Unsupported synthesized mouse event");
+ return NS_ERROR_UNEXPECTED;
+ }
+ WidgetMouseEvent event(true, msg, this, WidgetMouseEvent::eReal);
+ event.mRefPoint = aPoint - WidgetToScreenOffset();
+ if (msg == eMouseDown || msg == eMouseUp) {
+ event.mButton = aButton;
+ }
+ if (msg == eMouseDown) {
+ event.mClickCount = 1;
+ }
+ event.AssignEventTime(WidgetEventTime());
+ DispatchInputEvent(&event);
+ return NS_OK;
+}
+
+nsresult HeadlessWidget::SynthesizeNativeMouseScrollEvent(
+ mozilla::LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage,
+ double aDeltaX, double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "mousescrollevent");
+ printf(">>> DEBUG_ME: Synth: aDeltaY=%f\n", aDeltaY);
+ // The various platforms seem to handle scrolling deltas differently,
+ // but the following seems to emulate it well enough.
+ WidgetWheelEvent event(true, eWheel, this);
+ event.mDeltaMode = MOZ_HEADLESS_SCROLL_DELTA_MODE;
+ event.mIsNoLineOrPageDelta = true;
+ event.mDeltaX = -aDeltaX * MOZ_HEADLESS_SCROLL_MULTIPLIER;
+ event.mDeltaY = -aDeltaY * MOZ_HEADLESS_SCROLL_MULTIPLIER;
+ event.mDeltaZ = -aDeltaZ * MOZ_HEADLESS_SCROLL_MULTIPLIER;
+ event.mRefPoint = aPoint - WidgetToScreenOffset();
+ event.AssignEventTime(WidgetEventTime());
+ DispatchInputEvent(&event);
+ return NS_OK;
+}
+
+nsresult HeadlessWidget::SynthesizeNativeTouchPoint(
+ uint32_t aPointerId, TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint, double aPointerPressure,
+ uint32_t aPointerOrientation, nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "touchpoint");
+
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aPointerState == TOUCH_HOVER) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (!mSynthesizedTouchInput) {
+ mSynthesizedTouchInput = MakeUnique<MultiTouchInput>();
+ }
+
+ LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset();
+ MultiTouchInput inputToDispatch = UpdateSynthesizedTouchState(
+ mSynthesizedTouchInput.get(), TimeStamp::Now(), aPointerId, aPointerState,
+ pointInWindow, aPointerPressure, aPointerOrientation);
+ DispatchTouchInput(inputToDispatch);
+ return NS_OK;
+}
+
+nsresult HeadlessWidget::SynthesizeNativeTouchPadPinch(
+ TouchpadGesturePhase aEventPhase, float aScale, LayoutDeviceIntPoint aPoint,
+ int32_t aModifierFlags) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ PinchGestureInput::PinchGestureType pinchGestureType =
+ PinchGestureInput::PINCHGESTURE_SCALE;
+ ScreenCoord CurrentSpan;
+ ScreenCoord PreviousSpan;
+ switch (aEventPhase) {
+ case PHASE_BEGIN:
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_START;
+ CurrentSpan = aScale;
+ PreviousSpan = 0.999;
+ break;
+
+ case PHASE_UPDATE:
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_SCALE;
+ if (aScale == mLastPinchSpan) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ CurrentSpan = aScale;
+ PreviousSpan = mLastPinchSpan;
+ break;
+
+ case PHASE_END:
+ pinchGestureType = PinchGestureInput::PINCHGESTURE_END;
+ CurrentSpan = aScale;
+ PreviousSpan = mLastPinchSpan;
+ break;
+
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ScreenPoint touchpadPoint = ViewAs<ScreenPixel>(
+ aPoint - WidgetToScreenOffset(),
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+ // The headless widget does not support modifiers.
+ // Do not pass `aModifierFlags` because it contains native modifier values.
+ PinchGestureInput inputToDispatch(
+ pinchGestureType, PinchGestureInput::TRACKPAD, TimeStamp::Now(),
+ ExternalPoint(0, 0), touchpadPoint,
+ 100.0 * ((aEventPhase == PHASE_END) ? ScreenCoord(1.f) : CurrentSpan),
+ 100.0 * ((aEventPhase == PHASE_END) ? ScreenCoord(1.f) : PreviousSpan),
+ 0);
+
+ if (!inputToDispatch.SetLineOrPageDeltaY(this)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mLastPinchSpan = aScale;
+ DispatchPinchGestureInput(inputToDispatch);
+ return NS_OK;
+}
+
+nsresult HeadlessWidget::SynthesizeNativeTouchpadPan(
+ TouchpadGesturePhase aEventPhase, LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY, int32_t aModifierFlags,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "touchpadpanevent");
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ PanGestureInput::PanGestureType eventType = PanGestureInput::PANGESTURE_PAN;
+ switch (aEventPhase) {
+ case PHASE_BEGIN:
+ eventType = PanGestureInput::PANGESTURE_START;
+ break;
+ case PHASE_UPDATE:
+ eventType = PanGestureInput::PANGESTURE_PAN;
+ break;
+ case PHASE_END:
+ eventType = PanGestureInput::PANGESTURE_END;
+ break;
+ default:
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ ScreenPoint touchpadPoint = ViewAs<ScreenPixel>(
+ aPoint - WidgetToScreenOffset(),
+ PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent);
+ PanGestureInput input(eventType, TimeStamp::Now(), touchpadPoint,
+ ScreenPoint(float(aDeltaX), float(aDeltaY)),
+ // Same as SynthesizeNativeTouchPadPinch case we ignore
+ // aModifierFlags.
+ 0);
+
+ input.mSimulateMomentum =
+ Preferences::GetBool("apz.test.headless.simulate_momentum");
+
+ DispatchPanGestureInput(input);
+
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessWidget.h b/widget/headless/HeadlessWidget.h
new file mode 100644
index 0000000000..f5b1088686
--- /dev/null
+++ b/widget/headless/HeadlessWidget.h
@@ -0,0 +1,181 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * 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/. */
+
+#ifndef HEADLESSWIDGET_H
+#define HEADLESSWIDGET_H
+
+#include "mozilla/widget/InProcessCompositorWidget.h"
+#include "nsBaseWidget.h"
+#include "CompositorWidget.h"
+#include "mozilla/dom/WheelEventBinding.h"
+
+// The various synthesized event values are hardcoded to avoid pulling
+// in the platform specific widget code.
+#if defined(MOZ_WIDGET_GTK)
+# define MOZ_HEADLESS_SCROLL_MULTIPLIER 3
+# define MOZ_HEADLESS_SCROLL_DELTA_MODE \
+ mozilla::dom::WheelEvent_Binding::DOM_DELTA_LINE
+#elif defined(XP_WIN)
+# define MOZ_HEADLESS_SCROLL_MULTIPLIER \
+ .025 // default scroll lines (3) / WHEEL_DELTA (120)
+# define MOZ_HEADLESS_SCROLL_DELTA_MODE \
+ mozilla::dom::WheelEvent_Binding::DOM_DELTA_LINE
+#elif defined(XP_MACOSX)
+# define MOZ_HEADLESS_SCROLL_MULTIPLIER 1
+# define MOZ_HEADLESS_SCROLL_DELTA_MODE \
+ mozilla::dom::WheelEvent_Binding::DOM_DELTA_PIXEL
+#elif defined(ANDROID)
+# define MOZ_HEADLESS_SCROLL_MULTIPLIER 1
+# define MOZ_HEADLESS_SCROLL_DELTA_MODE \
+ mozilla::dom::WheelEvent_Binding::DOM_DELTA_LINE
+#else
+# define MOZ_HEADLESS_SCROLL_MULTIPLIER -1
+# define MOZ_HEADLESS_SCROLL_DELTA_MODE -1
+#endif
+
+namespace mozilla {
+enum class NativeKeyBindingsType : uint8_t;
+namespace widget {
+
+class HeadlessWidget : public nsBaseWidget {
+ public:
+ HeadlessWidget();
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(HeadlessWidget, nsBaseWidget)
+
+ void* GetNativeData(uint32_t aDataType) override {
+ // Headless widgets have no native data.
+ return nullptr;
+ }
+
+ virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent,
+ const LayoutDeviceIntRect& aRect,
+ nsWidgetInitData* aInitData = nullptr) override;
+ using nsBaseWidget::Create; // for Create signature not overridden here
+ virtual already_AddRefed<nsIWidget> CreateChild(
+ const LayoutDeviceIntRect& aRect, nsWidgetInitData* aInitData = nullptr,
+ bool aForceUseIWidgetParent = false) override;
+
+ virtual nsIWidget* GetTopLevelWidget() override;
+
+ virtual void GetCompositorWidgetInitData(
+ mozilla::widget::CompositorWidgetInitData* aInitData) override;
+
+ virtual void Destroy() override;
+ virtual void Show(bool aState) override;
+ virtual bool IsVisible() const override;
+ virtual void Move(double aX, double aY) override;
+ virtual void Resize(double aWidth, double aHeight, bool aRepaint) override;
+ virtual void Resize(double aX, double aY, double aWidth, double aHeight,
+ bool aRepaint) override;
+ virtual nsSizeMode SizeMode() override { return mSizeMode; }
+ virtual void SetSizeMode(nsSizeMode aMode) override;
+ virtual nsresult MakeFullScreen(bool aFullScreen) override;
+ virtual void Enable(bool aState) override;
+ virtual bool IsEnabled() const override;
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+ virtual void Invalidate(const LayoutDeviceIntRect& aRect) override {
+ // TODO: see if we need to do anything here.
+ }
+ virtual nsresult SetTitle(const nsAString& title) override {
+ // Headless widgets have no title, so just ignore it.
+ return NS_OK;
+ }
+ virtual nsresult SetNonClientMargins(
+ LayoutDeviceIntMargin& margins) override {
+ // Headless widgets have no chrome margins, so just ignore the call.
+ return NS_OK;
+ }
+ virtual LayoutDeviceIntPoint WidgetToScreenOffset() override;
+ virtual void SetInputContext(const InputContext& aContext,
+ const InputContextAction& aAction) override {
+ mInputContext = aContext;
+ }
+ virtual InputContext GetInputContext() override { return mInputContext; }
+
+ virtual WindowRenderer* GetWindowRenderer() override;
+
+ void SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) override;
+
+ [[nodiscard]] virtual nsresult AttachNativeKeyEvent(
+ WidgetKeyboardEvent& aEvent) override;
+ MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
+ NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
+ nsTArray<CommandInt>& aCommands) override;
+
+ virtual nsresult DispatchEvent(WidgetGUIEvent* aEvent,
+ nsEventStatus& aStatus) override;
+
+ virtual nsresult SynthesizeNativeMouseEvent(
+ LayoutDeviceIntPoint aPoint, NativeMouseMessage aNativeMessage,
+ mozilla::MouseButton aButton, nsIWidget::Modifiers aModifierFlags,
+ nsIObserver* aObserver) override;
+ virtual nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
+ nsIObserver* aObserver) override {
+ return SynthesizeNativeMouseEvent(
+ aPoint, NativeMouseMessage::Move, mozilla::MouseButton::eNotPressed,
+ nsIWidget::Modifiers::NO_MODIFIERS, aObserver);
+ };
+
+ virtual nsresult SynthesizeNativeMouseScrollEvent(
+ LayoutDeviceIntPoint aPoint, uint32_t aNativeMessage, double aDeltaX,
+ double aDeltaY, double aDeltaZ, uint32_t aModifierFlags,
+ uint32_t aAdditionalFlags, nsIObserver* aObserver) override;
+
+ virtual nsresult SynthesizeNativeTouchPoint(uint32_t aPointerId,
+ TouchPointerState aPointerState,
+ LayoutDeviceIntPoint aPoint,
+ double aPointerPressure,
+ uint32_t aPointerOrientation,
+ nsIObserver* aObserver) override;
+
+ virtual nsresult SynthesizeNativeTouchPadPinch(
+ TouchpadGesturePhase aEventPhase, float aScale,
+ LayoutDeviceIntPoint aPoint, int32_t aModifierFlags) override;
+
+ virtual nsresult SynthesizeNativeTouchpadPan(TouchpadGesturePhase aEventPhase,
+ LayoutDeviceIntPoint aPoint,
+ double aDeltaX, double aDeltaY,
+ int32_t aModifierFlags,
+ nsIObserver* aObserver) override;
+
+ private:
+ ~HeadlessWidget();
+ bool mEnabled;
+ bool mVisible;
+ bool mDestroyed;
+ nsIWidget* mTopLevel;
+ HeadlessCompositorWidget* mCompositorWidget;
+ nsSizeMode mSizeMode;
+ // The size mode before entering fullscreen mode.
+ nsSizeMode mLastSizeMode;
+ // The last size mode set while the window was visible.
+ nsSizeMode mEffectiveSizeMode;
+ mozilla::ScreenCoord mLastPinchSpan;
+ InputContext mInputContext;
+ mozilla::UniquePtr<mozilla::MultiTouchInput> mSynthesizedTouchInput;
+ // In headless there is no window manager to track window bounds
+ // across size mode changes, so we must track it to emulate.
+ LayoutDeviceIntRect mRestoreBounds;
+ void ApplySizeModeSideEffects();
+ // Move while maintaining size mode.
+ void MoveInternal(int32_t aX, int32_t aY);
+ // Resize while maintaining size mode.
+ void ResizeInternal(int32_t aWidth, int32_t aHeight, bool aRepaint);
+ // Similarly, we must track the active window ourselves in order
+ // to dispatch (de)activation events properly.
+ void RaiseWindow();
+ // The top level widgets are tracked for window ordering. They are
+ // stored in order of activation where the last element is always the
+ // currently active widget.
+ static StaticAutoPtr<nsTArray<HeadlessWidget*>> sActiveWindows;
+ // Get the most recently activated widget or null if there are none.
+ static already_AddRefed<HeadlessWidget> GetActiveWindow();
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif
diff --git a/widget/headless/HeadlessWidgetTypes.ipdlh b/widget/headless/HeadlessWidgetTypes.ipdlh
new file mode 100644
index 0000000000..ac7e0e6142
--- /dev/null
+++ b/widget/headless/HeadlessWidgetTypes.ipdlh
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=99: */
+/* 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/. */
+
+include "mozilla/GfxMessageUtils.h";
+
+using mozilla::LayoutDeviceIntSize from "Units.h";
+
+namespace mozilla {
+namespace widget {
+
+struct HeadlessCompositorWidgetInitData
+{
+ LayoutDeviceIntSize InitialClientSize;
+};
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/moz.build b/widget/headless/moz.build
new file mode 100644
index 0000000000..babb5e8290
--- /dev/null
+++ b/widget/headless/moz.build
@@ -0,0 +1,50 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Headless")
+
+DIRS += ["tests"]
+
+LOCAL_INCLUDES += [
+ "/widget",
+ "/widget/headless",
+]
+
+widget_dir = CONFIG["MOZ_WIDGET_TOOLKIT"]
+
+LOCAL_INCLUDES += [
+ "/widget/%s" % widget_dir,
+]
+
+UNIFIED_SOURCES += [
+ "HeadlessClipboard.cpp",
+ "HeadlessClipboardData.cpp",
+ "HeadlessCompositorWidget.cpp",
+ "HeadlessScreenHelper.cpp",
+ "HeadlessSound.cpp",
+ "HeadlessWidget.cpp",
+]
+
+if widget_dir == "gtk":
+ UNIFIED_SOURCES += [
+ "HeadlessLookAndFeelGTK.cpp",
+ ]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ UNIFIED_SOURCES += [
+ "HeadlessKeyBindingsCocoa.mm",
+ ]
+else:
+ UNIFIED_SOURCES += [
+ "HeadlessKeyBindings.cpp",
+ ]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+REQUIRES_UNIFIED_BUILD = True
diff --git a/widget/headless/tests/headless.html b/widget/headless/tests/headless.html
new file mode 100644
index 0000000000..bbde895077
--- /dev/null
+++ b/widget/headless/tests/headless.html
@@ -0,0 +1,6 @@
+<html>
+<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
+<body style="background-color: rgb(0, 255, 0); color: rgb(0, 0, 255)">
+Hi
+</body>
+</html>
diff --git a/widget/headless/tests/headless_button.html b/widget/headless/tests/headless_button.html
new file mode 100644
index 0000000000..5641066bfe
--- /dev/null
+++ b/widget/headless/tests/headless_button.html
@@ -0,0 +1,6 @@
+<html>
+<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
+<body>
+<button id="btn">button</button>
+</body>
+</html>
diff --git a/widget/headless/tests/moz.build b/widget/headless/tests/moz.build
new file mode 100644
index 0000000000..e48fa734e0
--- /dev/null
+++ b/widget/headless/tests/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["xpcshell.ini"]
diff --git a/widget/headless/tests/test_headless.js b/widget/headless/tests/test_headless.js
new file mode 100644
index 0000000000..83c952f44f
--- /dev/null
+++ b/widget/headless/tests/test_headless.js
@@ -0,0 +1,214 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+do_get_profile();
+const server = new HttpServer();
+server.registerDirectory("/", do_get_cwd());
+server.start(-1);
+const ROOT = `http://localhost:${server.identity.primaryPort}`;
+const BASE = `${ROOT}/`;
+const HEADLESS_URL = `${BASE}/headless.html`;
+const HEADLESS_BUTTON_URL = `${BASE}/headless_button.html`;
+registerCleanupFunction(() => {
+ server.stop(() => {});
+});
+
+// Refrences to the progress listeners to keep them from being gc'ed
+// before they are called.
+const progressListeners = new Map();
+
+function loadContentWindow(windowlessBrowser, uri) {
+ return new Promise((resolve, reject) => {
+ let loadURIOptions = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ };
+ windowlessBrowser.loadURI(uri, loadURIOptions);
+ let docShell = windowlessBrowser.docShell;
+ let webProgress = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ let progressListener = {
+ onLocationChange(progress, request, location, flags) {
+ // Ignore inner-frame events
+ if (progress != webProgress) {
+ return;
+ }
+ // Ignore events that don't change the document
+ if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+ return;
+ }
+ let contentWindow = docShell.domWindow;
+ webProgress.removeProgressListener(progressListener);
+ progressListeners.delete(progressListener);
+ contentWindow.addEventListener(
+ "load",
+ event => {
+ resolve(contentWindow);
+ },
+ { once: true }
+ );
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+ progressListeners.set(progressListener, progressListener);
+ webProgress.addProgressListener(
+ progressListener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION
+ );
+ });
+}
+
+add_task(async function test_snapshot() {
+ let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+ let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_URL);
+ const contentWidth = 400;
+ const contentHeight = 300;
+ // Verify dimensions.
+ contentWindow.resizeTo(contentWidth, contentHeight);
+ equal(contentWindow.innerWidth, contentWidth);
+ equal(contentWindow.innerHeight, contentHeight);
+
+ // Snapshot the test page.
+ let canvas = contentWindow.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "html:canvas"
+ );
+ let context = canvas.getContext("2d");
+ let width = contentWindow.innerWidth;
+ let height = contentWindow.innerHeight;
+ canvas.width = width;
+ canvas.height = height;
+ context.drawWindow(contentWindow, 0, 0, width, height, "rgb(255, 255, 255)");
+ let imageData = context.getImageData(0, 0, width, height).data;
+ ok(
+ imageData[0] === 0 &&
+ imageData[1] === 255 &&
+ imageData[2] === 0 &&
+ imageData[3] === 255,
+ "Page is green."
+ );
+
+ // Search for a blue pixel (a quick and dirty check to see if the blue text is
+ // on the page)
+ let found = false;
+ for (let i = 0; i < imageData.length; i += 4) {
+ if (imageData[i + 2] === 255) {
+ found = true;
+ break;
+ }
+ }
+ ok(found, "Found blue text on page.");
+
+ windowlessBrowser.close();
+});
+
+add_task(async function test_snapshot_widget_layers() {
+ let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+ // nsIWindowlessBrowser inherits from nsIWebNavigation.
+ let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_URL);
+ const contentWidth = 1;
+ const contentHeight = 2;
+ // Verify dimensions.
+ contentWindow.resizeTo(contentWidth, contentHeight);
+ equal(contentWindow.innerWidth, contentWidth);
+ equal(contentWindow.innerHeight, contentHeight);
+
+ // Snapshot the test page.
+ let canvas = contentWindow.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "html:canvas"
+ );
+ let context = canvas.getContext("2d");
+ let width = contentWindow.innerWidth;
+ let height = contentWindow.innerHeight;
+ canvas.width = width;
+ canvas.height = height;
+ context.drawWindow(
+ contentWindow,
+ 0,
+ 0,
+ width,
+ height,
+ "rgb(255, 255, 255)",
+ context.DRAWWINDOW_DRAW_CARET |
+ context.DRAWWINDOW_DRAW_VIEW |
+ context.DRAWWINDOW_USE_WIDGET_LAYERS
+ );
+ ok(true, "Snapshot with widget layers didn't crash.");
+
+ windowlessBrowser.close();
+});
+
+// Ensure keydown events are triggered on the windowless browser.
+add_task(async function test_keydown() {
+ let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+ // nsIWindowlessBrowser inherits from nsIWebNavigation.
+ let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_URL);
+
+ let keydown = new Promise(resolve => {
+ contentWindow.addEventListener(
+ "keydown",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+
+ let tip = Cc["@mozilla.org/text-input-processor;1"].createInstance(
+ Ci.nsITextInputProcessor
+ );
+ let begun = tip.beginInputTransactionForTests(contentWindow);
+ ok(
+ begun,
+ "nsITextInputProcessor.beginInputTransactionForTests() should succeed"
+ );
+ tip.keydown(
+ new contentWindow.KeyboardEvent("", {
+ key: "a",
+ code: "KeyA",
+ keyCode: contentWindow.KeyboardEvent.DOM_VK_A,
+ })
+ );
+
+ await keydown;
+ ok(true, "Send keydown didn't crash");
+
+ windowlessBrowser.close();
+});
+
+// Test dragging the mouse on a button to ensure the creation of the drag
+// service doesn't crash in headless.
+add_task(async function test_mouse_drag() {
+ let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+ // nsIWindowlessBrowser inherits from nsIWebNavigation.
+ let contentWindow = await loadContentWindow(
+ windowlessBrowser,
+ HEADLESS_BUTTON_URL
+ );
+ contentWindow.resizeTo(400, 400);
+
+ let target = contentWindow.document.getElementById("btn");
+ let rect = target.getBoundingClientRect();
+ let left = rect.left;
+ let top = rect.top;
+
+ let utils = contentWindow.windowUtils;
+ utils.sendMouseEvent("mousedown", left, top, 0, 1, 0, false, 0, 0);
+ utils.sendMouseEvent("mousemove", left, top, 0, 1, 0, false, 0, 0);
+ // Wait for a turn of the event loop since the synthetic mouse event
+ // that creates the drag service is processed during the refresh driver.
+ await new Promise(r => {
+ executeSoon(r);
+ });
+ utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
+
+ ok(true, "Send mouse event didn't crash");
+
+ windowlessBrowser.close();
+});
diff --git a/widget/headless/tests/test_headless_clipboard.js b/widget/headless/tests/test_headless_clipboard.js
new file mode 100644
index 0000000000..24fb6fc50f
--- /dev/null
+++ b/widget/headless/tests/test_headless_clipboard.js
@@ -0,0 +1,45 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function getString(clipboard) {
+ var str = "";
+
+ // Create transferable that will transfer the text.
+ var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(null);
+ trans.addDataFlavor("text/unicode");
+
+ clipboard.getData(trans, Ci.nsIClipboard.kGlobalClipboard);
+
+ try {
+ var data = {};
+ trans.getTransferData("text/unicode", data);
+
+ if (data) {
+ data = data.value.QueryInterface(Ci.nsISupportsString);
+ str = data.data;
+ }
+ } catch (ex) {
+ // If the clipboard is empty getTransferData will throw.
+ }
+
+ return str;
+}
+
+add_task(async function test_clipboard() {
+ let clipboard = Services.clipboard;
+
+ // Test copy.
+ const data = "random number: " + Math.random();
+ let helper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ helper.copyString(data);
+ equal(getString(clipboard), data, "Data was successfully copied.");
+
+ clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
+ equal(getString(clipboard), "", "Data was successfully cleared.");
+});
diff --git a/widget/headless/tests/xpcshell.ini b/widget/headless/tests/xpcshell.ini
new file mode 100644
index 0000000000..307a12cb0d
--- /dev/null
+++ b/widget/headless/tests/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+skip-if = toolkit != "gtk" && toolkit != "windows"
+headless = true
+
+[test_headless_clipboard.js]
+[test_headless.js]
+skip-if = appname == "thunderbird"
+support-files =
+ headless.html
+ headless_button.html