summaryrefslogtreecommitdiffstats
path: root/widget/headless
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /widget/headless
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/headless')
-rw-r--r--widget/headless/HeadlessClipboard.cpp112
-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.cpp43
-rw-r--r--widget/headless/HeadlessCompositorWidget.h53
-rw-r--r--widget/headless/HeadlessKeyBindings.cpp34
-rw-r--r--widget/headless/HeadlessKeyBindings.h35
-rw-r--r--widget/headless/HeadlessKeyBindingsCocoa.mm47
-rw-r--r--widget/headless/HeadlessLookAndFeel.h65
-rw-r--r--widget/headless/HeadlessLookAndFeelGTK.cpp359
-rw-r--r--widget/headless/HeadlessScreenHelper.cpp44
-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/HeadlessThemeGTK.cpp417
-rw-r--r--widget/headless/HeadlessThemeGTK.h61
-rw-r--r--widget/headless/HeadlessWidget.cpp503
-rw-r--r--widget/headless/HeadlessWidget.h187
-rw-r--r--widget/headless/HeadlessWidgetTypes.ipdlh18
-rw-r--r--widget/headless/moz.build49
-rw-r--r--widget/headless/tests/.eslintrc.js5
-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.js215
-rw-r--r--widget/headless/tests/test_headless_clipboard.js46
-rw-r--r--widget/headless/tests/xpcshell.ini10
28 files changed, 2502 insertions, 0 deletions
diff --git a/widget/headless/HeadlessClipboard.cpp b/widget/headless/HeadlessClipboard.cpp
new file mode 100644
index 0000000000..0d8c7fe572
--- /dev/null
+++ b/widget/headless/HeadlessClipboard.cpp
@@ -0,0 +1,112 @@
+/* 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;
+}
+
+} // 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..b31a969b7a
--- /dev/null
+++ b/widget/headless/HeadlessCompositorWidget.cpp
@@ -0,0 +1,43 @@
+/* -*- 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 = aInitData.InitialClientSize();
+}
+
+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) {
+ mClientSize = aClientSize;
+}
+
+LayoutDeviceIntSize HeadlessCompositorWidget::GetClientSize() {
+ return mClientSize;
+}
+
+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..7f91de9e67
--- /dev/null
+++ b/widget/headless/HeadlessCompositorWidget.h
@@ -0,0 +1,53 @@
+/* -*- 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;
+
+ 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..1704612426
--- /dev/null
+++ b/widget/headless/HeadlessKeyBindings.cpp
@@ -0,0 +1,34 @@
+/* -*- 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"
+
+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(
+ nsIWidget::NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
+ 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..c658360cf8
--- /dev/null
+++ b/widget/headless/HeadlessKeyBindings.h
@@ -0,0 +1,35 @@
+/* -*- 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 {
+namespace widget {
+
+/**
+ * Helper to emulate native key bindings. Currently only MacOS is supported.
+ */
+
+class HeadlessKeyBindings final {
+ public:
+ HeadlessKeyBindings() = default;
+
+ static HeadlessKeyBindings& GetInstance();
+
+ void GetEditCommands(nsIWidget::NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ 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..356eec0e56
--- /dev/null
+++ b/widget/headless/HeadlessKeyBindingsCocoa.mm
@@ -0,0 +1,47 @@
+/* -*- 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"
+
+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_ABORT_BLOCK_NSRESULT;
+
+ aEvent.mNativeKeyEvent = nsCocoaUtils::MakeNewCococaEventFromWidgetEvent(aEvent, 0, nil);
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
+}
+
+void HeadlessKeyBindings::GetEditCommands(nsIWidget::NativeKeyBindingsType aType,
+ const WidgetKeyboardEvent& aEvent,
+ 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, aCommands);
+}
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessLookAndFeel.h b/widget/headless/HeadlessLookAndFeel.h
new file mode 100644
index 0000000000..8b6cf033fd
--- /dev/null
+++ b/widget/headless/HeadlessLookAndFeel.h
@@ -0,0 +1,65 @@
+/* -*- 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
+// * in content processes, when full headless mode or headless content
+// mode (security.sandbox.content.headless) is enabled, unless
+// widget.remote-look-and-feel is also enabled, in which case
+// RemoteLookAndFeel is used instead.
+//
+// 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(const LookAndFeelCache* aCache);
+ virtual ~HeadlessLookAndFeel();
+
+ void NativeInit() final{};
+ virtual nsresult NativeGetInt(IntID aID, int32_t& aResult) override;
+ virtual nsresult NativeGetFloat(FloatID aID, float& aResult) override;
+ virtual nsresult NativeGetColor(ColorID aID, nscolor& aResult) override;
+ virtual bool NativeGetFont(FontID aID, nsString& aFontName,
+ gfxFontStyle& aFontStyle) override;
+
+ virtual void RefreshImpl() override;
+ virtual char16_t GetPasswordCharacterImpl() override;
+ virtual bool GetEchoPasswordImpl() 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..3d4a135ee1
--- /dev/null
+++ b/widget/headless/HeadlessLookAndFeelGTK.cpp
@@ -0,0 +1,359 @@
+/* -*- 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"
+
+using mozilla::LookAndFeel;
+
+namespace mozilla {
+namespace widget {
+
+static const char16_t UNICODE_BULLET = 0x2022;
+
+HeadlessLookAndFeel::HeadlessLookAndFeel(const LookAndFeelCache* aCache) {}
+
+HeadlessLookAndFeel::~HeadlessLookAndFeel() = default;
+
+nsresult HeadlessLookAndFeel::NativeGetColor(ColorID aID, nscolor& aColor) {
+ // For headless mode, we use GetStandinForNativeColor for everything we can,
+ // and hardcoded values for everything else.
+
+ nsresult res = NS_OK;
+
+ switch (aID) {
+ // Override the solid black that GetStandinForNativeColor provides for
+ // FieldText, to match our behavior under the real GTK.
+ case ColorID::Fieldtext:
+ aColor = NS_RGB(0x21, 0x21, 0x21);
+ break;
+
+ // The rest are not provided by GetStandinForNativeColor.
+ case ColorID::IMESelectedRawTextBackground:
+ case ColorID::IMESelectedConvertedTextBackground:
+ case ColorID::IMERawInputBackground:
+ case ColorID::IMEConvertedTextBackground:
+ aColor = NS_TRANSPARENT;
+ break;
+ case ColorID::IMESelectedRawTextForeground:
+ case ColorID::IMESelectedConvertedTextForeground:
+ case ColorID::IMERawInputForeground:
+ case ColorID::IMEConvertedTextForeground:
+ aColor = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+ case ColorID::IMERawInputUnderline:
+ case ColorID::IMEConvertedTextUnderline:
+ aColor = NS_40PERCENT_FOREGROUND_COLOR;
+ break;
+ case ColorID::IMESelectedRawTextUnderline:
+ case ColorID::IMESelectedConvertedTextUnderline:
+ aColor = NS_SAME_AS_FOREGROUND_COLOR;
+ break;
+ case ColorID::MozEventreerow:
+ aColor = NS_RGB(0xff, 0xff, 0xff);
+ break;
+ case ColorID::MozGtkInfoBarText:
+ aColor = NS_RGB(0x00, 0x00, 0x00);
+ break;
+ case ColorID::MozMacButtonactivetext:
+ case ColorID::MozMacDefaultbuttontext:
+ aColor = NS_RGB(0xff, 0xff, 0xff);
+ break;
+ case ColorID::SpellCheckerUnderline:
+ aColor = NS_RGB(0xff, 0x00, 0x00);
+ break;
+ case ColorID::TextBackground:
+ aColor = NS_RGB(0xff, 0xff, 0xff);
+ break;
+ case ColorID::TextForeground:
+ aColor = NS_RGB(0x00, 0x00, 0x00);
+ break;
+ case ColorID::TextHighlightBackground:
+ aColor = NS_RGB(0xef, 0x0f, 0xff);
+ break;
+ case ColorID::TextHighlightForeground:
+ aColor = NS_RGB(0xff, 0xff, 0xff);
+ break;
+ case ColorID::TextSelectBackground:
+ aColor = NS_RGB(0xaa, 0xaa, 0xaa);
+ break;
+ case ColorID::TextSelectBackgroundAttention:
+ aColor = NS_TRANSPARENT;
+ break;
+ case ColorID::TextSelectBackgroundDisabled:
+ aColor = NS_RGB(0xaa, 0xaa, 0xaa);
+ break;
+ case ColorID::TextSelectForeground:
+ GetColor(ColorID::TextSelectBackground, aColor);
+ if (aColor == 0x000000)
+ aColor = NS_RGB(0xff, 0xff, 0xff);
+ else
+ aColor = NS_DONT_CHANGE_COLOR;
+ break;
+ case ColorID::Widget3DHighlight:
+ aColor = NS_RGB(0xa0, 0xa0, 0xa0);
+ break;
+ case ColorID::Widget3DShadow:
+ aColor = NS_RGB(0x40, 0x40, 0x40);
+ break;
+ case ColorID::WidgetBackground:
+ aColor = NS_RGB(0xdd, 0xdd, 0xdd);
+ break;
+ case ColorID::WidgetForeground:
+ aColor = NS_RGB(0x00, 0x00, 0x00);
+ break;
+ case ColorID::WidgetSelectBackground:
+ aColor = NS_RGB(0x80, 0x80, 0x80);
+ break;
+ case ColorID::WidgetSelectForeground:
+ aColor = NS_RGB(0x00, 0x00, 0x80);
+ break;
+ case ColorID::WindowBackground:
+ aColor = NS_RGB(0xff, 0xff, 0xff);
+ break;
+ case ColorID::WindowForeground:
+ aColor = NS_RGB(0x00, 0x00, 0x00);
+ break;
+ default:
+ aColor = GetStandinForNativeColor(aID);
+ break;
+ }
+
+ return res;
+}
+
+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::ShowHideScrollbars:
+ 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::ScrollSliderStyle:
+ aResult = eScrollThumbStyle_Proportional;
+ 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::TouchEnabled:
+ case IntID::MacGraphiteTheme:
+ case IntID::MacBigSurTheme:
+ aResult = 0;
+ res = NS_ERROR_NOT_IMPLEMENTED;
+ 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 = NS_STYLE_TEXT_DECORATION_STYLE_SOLID;
+ break;
+ case IntID::SpellCheckerUnderlineStyle:
+ aResult = NS_STYLE_TEXT_DECORATION_STYLE_DOTTED;
+ break;
+ case IntID::MenuBarDrag:
+ aResult = 0;
+ break;
+ case IntID::WindowsThemeIdentifier:
+ case IntID::OperatingSystemVersionIdentifier:
+ aResult = 0;
+ res = NS_ERROR_NOT_IMPLEMENTED;
+ 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:
+ case IntID::GTKCSDHideTitlebarByDefault:
+ case IntID::GTKCSDTransparentBackground:
+ 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:
+ NS_WARNING(
+ "HeadlessLookAndFeel::NativeGetInt called with an unrecognized aID");
+ 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:
+ NS_WARNING(
+ "HeadlessLookAndFeel::NativeGetFloat called with an unrecognized "
+ "aID");
+ 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;
+}
+
+void HeadlessLookAndFeel::RefreshImpl() { nsXPLookAndFeel::RefreshImpl(); }
+
+bool HeadlessLookAndFeel::GetEchoPasswordImpl() { return false; }
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessScreenHelper.cpp b/widget/headless/HeadlessScreenHelper.cpp
new file mode 100644
index 0000000000..3800c9c73a
--- /dev/null
+++ b/widget/headless/HeadlessScreenHelper.cpp
@@ -0,0 +1,44 @@
+/* -*- 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();
+ RefPtr<Screen> ret =
+ new Screen(rect, rect, 24, 24, DesktopToLayoutDeviceScale(),
+ CSSToLayoutDeviceScale(), 96.0f);
+ screenList.AppendElement(ret.forget());
+ ScreenManager& screenManager = ScreenManager::GetSingleton();
+ 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/HeadlessThemeGTK.cpp b/widget/headless/HeadlessThemeGTK.cpp
new file mode 100644
index 0000000000..64135202e4
--- /dev/null
+++ b/widget/headless/HeadlessThemeGTK.cpp
@@ -0,0 +1,417 @@
+/* -*- 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 "HeadlessThemeGTK.h"
+
+#include "mozilla/StaticPrefs_layout.h"
+#include "nsStyleConsts.h"
+#include "nsIFrame.h"
+
+namespace mozilla {
+namespace widget {
+
+NS_IMPL_ISUPPORTS_INHERITED(HeadlessThemeGTK, nsNativeTheme, nsITheme)
+
+NS_IMETHODIMP
+HeadlessThemeGTK::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect,
+ const nsRect& aDirtyRect) {
+ return NS_OK;
+}
+
+LayoutDeviceIntMargin HeadlessThemeGTK::GetWidgetBorder(
+ nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) {
+ LayoutDeviceIntMargin result;
+ // The following values are generated from the Ubuntu GTK theme.
+ switch (aAppearance) {
+ case StyleAppearance::Button:
+ case StyleAppearance::Toolbarbutton:
+ result.top = 6;
+ result.right = 7;
+ result.bottom = 6;
+ result.left = 7;
+ break;
+ case StyleAppearance::FocusOutline:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ result.top = 5;
+ result.right = 7;
+ result.bottom = 5;
+ result.left = 7;
+ break;
+ case StyleAppearance::Statusbarpanel:
+ case StyleAppearance::Resizerpanel:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::Treeheadersortarrow:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ case StyleAppearance::SpinnerTextfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Menupopup:
+ case StyleAppearance::MozGtkInfoBar:
+ result.top = 1;
+ result.right = 1;
+ result.bottom = 1;
+ result.left = 1;
+ break;
+ case StyleAppearance::Treeheadercell:
+ result.top = 5;
+ result.right = 7;
+ result.bottom = 6;
+ result.left = 6;
+ break;
+ case StyleAppearance::Tab:
+ result.top = 4;
+ result.right = 7;
+ result.bottom = 2;
+ result.left = 7;
+ break;
+ case StyleAppearance::Tooltip:
+ result.top = 6;
+ result.right = 6;
+ result.bottom = 6;
+ result.left = 6;
+ break;
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Menulist:
+ result.top = 6;
+ result.right = 22;
+ result.bottom = 6;
+ result.left = 7;
+ break;
+ case StyleAppearance::MozMenulistArrowButton:
+ result.top = 1;
+ result.right = 1;
+ result.bottom = 1;
+ result.left = 0;
+ break;
+ case StyleAppearance::Menuitem:
+ case StyleAppearance::Checkmenuitem:
+ case StyleAppearance::Radiomenuitem:
+ if (IsRegularMenuItem(aFrame)) {
+ break;
+ }
+ result.top = 3;
+ result.right = 5;
+ result.bottom = 3;
+ result.left = 5;
+ break;
+ default:
+ break;
+ }
+ return result;
+}
+
+bool HeadlessThemeGTK::GetWidgetPadding(nsDeviceContext* aContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) {
+ // The following values are generated from the Ubuntu GTK theme.
+ switch (aAppearance) {
+ case StyleAppearance::Radio:
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Dualbutton:
+ case StyleAppearance::ToolbarbuttonDropdown:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::ButtonArrowNext:
+ case StyleAppearance::ButtonArrowPrevious:
+ case StyleAppearance::TabScrollArrowBack:
+ case StyleAppearance::TabScrollArrowForward:
+ case StyleAppearance::MozMenulistArrowButton:
+ case StyleAppearance::RangeThumb:
+ case StyleAppearance::ButtonFocus:
+ aResult->top = 0;
+ aResult->right = 0;
+ aResult->bottom = 0;
+ aResult->left = 0;
+ return true;
+ case StyleAppearance::Menuitem:
+ case StyleAppearance::Checkmenuitem:
+ case StyleAppearance::Radiomenuitem:
+ if (!IsRegularMenuItem(aFrame)) {
+ return false;
+ }
+ aResult->top = 3;
+ aResult->right = 5;
+ aResult->bottom = 3;
+ aResult->left = 5;
+ return true;
+ default:
+ break;
+ }
+ return false;
+}
+
+NS_IMETHODIMP
+HeadlessThemeGTK::GetMinimumWidgetSize(nsPresContext* aPresContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntSize* aResult,
+ bool* aIsOverridable) {
+ aResult->width = aResult->height = 0;
+ *aIsOverridable = true;
+
+ // The following values are generated from the Ubuntu GTK theme.
+ switch (aAppearance) {
+ case StyleAppearance::Splitter:
+ if (IsHorizontal(aFrame)) {
+ aResult->width = 6;
+ aResult->height = 0;
+ } else {
+ aResult->width = 0;
+ aResult->height = 6;
+ }
+ *aIsOverridable = false;
+ break;
+ case StyleAppearance::Button:
+ case StyleAppearance::Toolbarbutton:
+ aResult->width = 14;
+ aResult->height = 12;
+ break;
+ case StyleAppearance::Radio:
+ case StyleAppearance::Checkbox:
+ aResult->width = 18;
+ aResult->height = 18;
+ break;
+ case StyleAppearance::ToolbarbuttonDropdown:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::ButtonArrowNext:
+ case StyleAppearance::ButtonArrowPrevious:
+ case StyleAppearance::Resizer:
+ aResult->width = 15;
+ aResult->height = 15;
+ *aIsOverridable = false;
+ break;
+ case StyleAppearance::Separator:
+ aResult->width = 12;
+ aResult->height = 0;
+ break;
+ case StyleAppearance::Treetwisty:
+ case StyleAppearance::Treetwistyopen:
+ aResult->width = 8;
+ aResult->height = 8;
+ *aIsOverridable = false;
+ break;
+ case StyleAppearance::Treeheadercell:
+ aResult->width = 13;
+ aResult->height = 11;
+ break;
+ case StyleAppearance::Treeheadersortarrow:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ aResult->width = 14;
+ aResult->height = 13;
+ break;
+ case StyleAppearance::TabScrollArrowBack:
+ case StyleAppearance::TabScrollArrowForward:
+ aResult->width = 16;
+ aResult->height = 16;
+ *aIsOverridable = false;
+ break;
+ case StyleAppearance::Spinner:
+ aResult->width = 14;
+ aResult->height = 26;
+ break;
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ aResult->width = 0;
+ aResult->height = 12;
+ break;
+ case StyleAppearance::ScrollbarHorizontal:
+ aResult->width = 31;
+ aResult->height = 10;
+ break;
+ case StyleAppearance::ScrollbarVertical:
+ aResult->width = 10;
+ aResult->height = 31;
+ break;
+ case StyleAppearance::ScrollbarbuttonUp:
+ case StyleAppearance::ScrollbarbuttonDown:
+ aResult->width = 10;
+ aResult->height = 13;
+ *aIsOverridable = false;
+ break;
+ case StyleAppearance::ScrollbarbuttonLeft:
+ case StyleAppearance::ScrollbarbuttonRight:
+ aResult->width = 13;
+ aResult->height = 10;
+ *aIsOverridable = false;
+ break;
+ case StyleAppearance::ScrollbarthumbHorizontal:
+ aResult->width = 31;
+ aResult->height = 10;
+ *aIsOverridable = false;
+ break;
+ case StyleAppearance::ScrollbarthumbVertical:
+ aResult->width = 10;
+ aResult->height = 31;
+ *aIsOverridable = false;
+ break;
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Menulist:
+ aResult->width = 44;
+ aResult->height = 27;
+ break;
+ case StyleAppearance::MozMenulistArrowButton:
+ aResult->width = 29;
+ aResult->height = 28;
+ *aIsOverridable = false;
+ break;
+ case StyleAppearance::RangeThumb:
+ aResult->width = 14;
+ aResult->height = 18;
+ *aIsOverridable = false;
+ break;
+ case StyleAppearance::Menuseparator:
+ aResult->width = 0;
+ aResult->height = 8;
+ *aIsOverridable = false;
+ break;
+ default:
+ break;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HeadlessThemeGTK::WidgetStateChanged(nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsAtom* aAttribute, bool* aShouldRepaint,
+ const nsAttrValue* aOldValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HeadlessThemeGTK::ThemeChanged() { return NS_OK; }
+
+static bool IsFrameContentNodeInNamespace(nsIFrame* aFrame,
+ uint32_t aNamespace) {
+ nsIContent* content = aFrame ? aFrame->GetContent() : nullptr;
+ if (!content) return false;
+ return content->IsInNamespace(aNamespace);
+}
+
+NS_IMETHODIMP_(bool)
+HeadlessThemeGTK::ThemeSupportsWidget(nsPresContext* aPresContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::Button:
+ case StyleAppearance::Radio:
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::FocusOutline:
+ case StyleAppearance::Toolbox:
+ case StyleAppearance::Toolbar:
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Dualbutton:
+ case StyleAppearance::ToolbarbuttonDropdown:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::ButtonArrowNext:
+ case StyleAppearance::ButtonArrowPrevious:
+ case StyleAppearance::Separator:
+ case StyleAppearance::Toolbargripper:
+ case StyleAppearance::Splitter:
+ case StyleAppearance::Statusbar:
+ case StyleAppearance::Statusbarpanel:
+ case StyleAppearance::Resizerpanel:
+ case StyleAppearance::Resizer:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Treeview:
+ case StyleAppearance::Treetwisty:
+ case StyleAppearance::Treeheadercell:
+ case StyleAppearance::Treeheadersortarrow:
+ case StyleAppearance::Treetwistyopen:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::Tab:
+ case StyleAppearance::Tabpanels:
+ case StyleAppearance::TabScrollArrowBack:
+ case StyleAppearance::TabScrollArrowForward:
+ case StyleAppearance::Tooltip:
+ case StyleAppearance::Spinner:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ case StyleAppearance::SpinnerTextfield:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::ScrollbarHorizontal:
+ case StyleAppearance::ScrollbarVertical:
+ case StyleAppearance::ScrollbarbuttonUp:
+ case StyleAppearance::ScrollbarbuttonDown:
+ case StyleAppearance::ScrollbarbuttonLeft:
+ case StyleAppearance::ScrollbarbuttonRight:
+ case StyleAppearance::ScrollbartrackHorizontal:
+ case StyleAppearance::ScrollbartrackVertical:
+ case StyleAppearance::ScrollbarthumbHorizontal:
+ case StyleAppearance::ScrollbarthumbVertical:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::MenulistText:
+ case StyleAppearance::Range:
+ case StyleAppearance::RangeThumb:
+ case StyleAppearance::CheckboxContainer:
+ case StyleAppearance::RadioContainer:
+ case StyleAppearance::CheckboxLabel:
+ case StyleAppearance::RadioLabel:
+ case StyleAppearance::ButtonFocus:
+ case StyleAppearance::Window:
+ case StyleAppearance::Dialog:
+ case StyleAppearance::Menubar:
+ case StyleAppearance::Menupopup:
+ case StyleAppearance::Menuitem:
+ case StyleAppearance::Checkmenuitem:
+ case StyleAppearance::Radiomenuitem:
+ case StyleAppearance::Menuseparator:
+ case StyleAppearance::Menuarrow:
+ case StyleAppearance::MozGtkInfoBar:
+ return !IsWidgetStyled(aPresContext, aFrame, aAppearance);
+ case StyleAppearance::MozMenulistArrowButton:
+ return (!aFrame ||
+ IsFrameContentNodeInNamespace(aFrame, kNameSpaceID_XUL)) &&
+ !IsWidgetStyled(aPresContext, aFrame, aAppearance);
+ default:
+ break;
+ }
+ return false;
+}
+
+NS_IMETHODIMP_(bool)
+HeadlessThemeGTK::WidgetIsContainer(StyleAppearance aAppearance) {
+ if (aAppearance == StyleAppearance::MozMenulistArrowButton ||
+ aAppearance == StyleAppearance::Radio ||
+ aAppearance == StyleAppearance::RangeThumb ||
+ aAppearance == StyleAppearance::Checkbox ||
+ aAppearance == StyleAppearance::TabScrollArrowBack ||
+ aAppearance == StyleAppearance::TabScrollArrowForward ||
+ aAppearance == StyleAppearance::ButtonArrowUp ||
+ aAppearance == StyleAppearance::ButtonArrowDown ||
+ aAppearance == StyleAppearance::ButtonArrowNext ||
+ aAppearance == StyleAppearance::ButtonArrowPrevious) {
+ return false;
+ }
+ return true;
+}
+
+bool HeadlessThemeGTK::ThemeDrawsFocusForWidget(StyleAppearance aAppearance) {
+ if (aAppearance == StyleAppearance::Menulist ||
+ aAppearance == StyleAppearance::Button ||
+ aAppearance == StyleAppearance::Treeheadercell) {
+ return true;
+ }
+ return false;
+}
+
+bool HeadlessThemeGTK::ThemeNeedsComboboxDropmarker() { return false; }
+
+} // namespace widget
+} // namespace mozilla
diff --git a/widget/headless/HeadlessThemeGTK.h b/widget/headless/HeadlessThemeGTK.h
new file mode 100644
index 0000000000..11ca0a1f92
--- /dev/null
+++ b/widget/headless/HeadlessThemeGTK.h
@@ -0,0 +1,61 @@
+/* -*- 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_HeadlessThemeGTK_h
+#define mozilla_widget_HeadlessThemeGTK_h
+
+#include "nsITheme.h"
+#include "nsNativeTheme.h"
+
+namespace mozilla {
+namespace widget {
+
+class HeadlessThemeGTK final : private nsNativeTheme, public nsITheme {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ HeadlessThemeGTK() = default;
+ NS_IMETHOD DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect,
+ const nsRect& aDirtyRect) override;
+
+ [[nodiscard]] LayoutDeviceIntMargin GetWidgetBorder(
+ nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+
+ bool GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) override;
+ NS_IMETHOD GetMinimumWidgetSize(nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ mozilla::LayoutDeviceIntSize* aResult,
+ bool* aIsOverridable) override;
+
+ NS_IMETHOD WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance,
+ nsAtom* aAttribute, bool* aShouldRepaint,
+ const nsAttrValue* aOldValue) override;
+
+ NS_IMETHOD ThemeChanged() override;
+
+ NS_IMETHOD_(bool)
+ ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) override;
+
+ NS_IMETHOD_(bool) WidgetIsContainer(StyleAppearance aAppearance) override;
+
+ NS_IMETHOD_(bool)
+ ThemeDrawsFocusForWidget(StyleAppearance aAppearance) override;
+
+ virtual bool ThemeNeedsComboboxDropmarker() override;
+
+ protected:
+ virtual ~HeadlessThemeGTK() = default;
+};
+
+} // namespace widget
+} // namespace mozilla
+
+#endif // mozilla_widget_HeadlessThemeGTK_h
diff --git a/widget/headless/HeadlessWidget.cpp b/widget/headless/HeadlessWidget.cpp
new file mode 100644
index 0000000000..f4f3acda7e
--- /dev/null
+++ b/widget/headless/HeadlessWidget.cpp
@@ -0,0 +1,503 @@
+/* -*- 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 "HeadlessCompositorWidget.h"
+#include "Layers.h"
+#include "BasicLayers.h"
+#include "BasicEvents.h"
+#include "MouseEvents.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/TextEvents.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),
+ 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);
+ }
+
+ // 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(x, y) && mWindowType != eWindowType_popup) {
+ return;
+ }
+
+ mBounds.MoveTo(x, y);
+ NotifyRollupGeometryChange();
+}
+
+LayoutDeviceIntPoint HeadlessWidget::WidgetToScreenOffset() {
+ return mTopLevel->GetBounds().TopLeft();
+}
+
+LayerManager* HeadlessWidget::GetLayerManager(
+ PLayerTransactionChild* aShadowManager, LayersBackend aBackendHint,
+ LayerManagerPersistence aPersistence) {
+ return nsBaseWidget::GetLayerManager(aShadowManager, aBackendHint,
+ aPersistence);
+}
+
+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);
+ ConstrainSize(&width, &height);
+ mBounds.SizeTo(LayoutDeviceIntSize(width, height));
+
+ 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) {
+ if (!mBounds.IsEqualXY(aX, aY)) {
+ NotifyWindowMoved(aX, aY);
+ }
+ return Resize(aWidth, aHeight, aRepaint);
+}
+
+void HeadlessWidget::SetSizeMode(nsSizeMode aMode) {
+ LOG(("HeadlessWidget::SetSizeMode [%p] %d\n", (void*)this, aMode));
+
+ if (aMode == mSizeMode) {
+ return;
+ }
+
+ nsBaseWidget::SetSizeMode(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: {
+ Resize(mRestoreBounds.X(), mRestoreBounds.Y(), 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))) {
+ Resize(0, 0, 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,
+ nsIScreen* aTargetScreen) {
+ // 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);
+ nsCOMPtr<nsIScreen> targetScreen(aTargetScreen);
+ NS_DispatchToCurrentThread(NS_NewRunnableFunction(
+ "HeadlessWidget::MakeFullScreen",
+ [self, targetScreen, aFullScreen]() -> void {
+ self->InfallibleMakeFullScreen(aFullScreen, targetScreen);
+ }));
+
+ 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;
+ }
+
+ HeadlessKeyBindings& bindings = HeadlessKeyBindings::GetInstance();
+ bindings.GetEditCommands(aType, aEvent, 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,
+ uint32_t aNativeMessage,
+ uint32_t aModifierFlags,
+ nsIObserver* aObserver) {
+ AutoObserverNotifier notifier(aObserver, "mouseevent");
+ EventMessage msg;
+ switch (aNativeMessage) {
+ case MOZ_HEADLESS_MOUSE_MOVE:
+ msg = eMouseMove;
+ break;
+ case MOZ_HEADLESS_MOUSE_DOWN:
+ msg = eMouseDown;
+ break;
+ case MOZ_HEADLESS_MOUSE_UP:
+ msg = eMouseUp;
+ break;
+ default:
+ 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 = MouseButton::ePrimary;
+ }
+ 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(), PR_IntervalNow(), TimeStamp::Now(),
+ aPointerId, aPointerState, pointInWindow, aPointerPressure,
+ aPointerOrientation);
+ DispatchTouchInput(inputToDispatch);
+ 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..25cb1623ba
--- /dev/null
+++ b/widget/headless/HeadlessWidget.h
@@ -0,0 +1,187 @@
+/* -*- 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_MOUSE_MOVE 3 // GDK_MOTION_NOTIFY
+# define MOZ_HEADLESS_MOUSE_DOWN 4 // GDK_BUTTON_PRESS
+# define MOZ_HEADLESS_MOUSE_UP 7 // GDK_BUTTON_RELEASE
+# 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_MOUSE_MOVE 1 // MOUSEEVENTF_MOVE
+# define MOZ_HEADLESS_MOUSE_DOWN 2 // MOUSEEVENTF_LEFTDOWN
+# define MOZ_HEADLESS_MOUSE_UP 4 // MOUSEEVENTF_LEFTUP
+# 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_MOUSE_MOVE 5 // NSEventTypeMouseMoved
+# define MOZ_HEADLESS_MOUSE_DOWN 1 // NSEventTypeLeftMouseDown
+# define MOZ_HEADLESS_MOUSE_UP 2 // NSEventTypeLeftMouseUp
+# 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_MOUSE_MOVE 7 // ACTION_HOVER_MOVE
+# define MOZ_HEADLESS_MOUSE_DOWN 5 // ACTION_POINTER_DOWN
+# define MOZ_HEADLESS_MOUSE_UP 6 // ACTION_POINTER_UP
+# define MOZ_HEADLESS_SCROLL_MULTIPLIER 1
+# define MOZ_HEADLESS_SCROLL_DELTA_MODE \
+ mozilla::dom::WheelEvent_Binding::DOM_DELTA_LINE
+#else
+# define MOZ_HEADLESS_MOUSE_MOVE -1
+# define MOZ_HEADLESS_MOUSE_DOWN -1
+# define MOZ_HEADLESS_MOUSE_UP -1
+# define MOZ_HEADLESS_SCROLL_MULTIPLIER -1
+# define MOZ_HEADLESS_SCROLL_DELTA_MODE -1
+#endif
+
+namespace mozilla {
+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 void SetSizeMode(nsSizeMode aMode) override;
+ virtual nsresult MakeFullScreen(bool aFullScreen,
+ nsIScreen* aTargetScreen = nullptr) override;
+ virtual void Enable(bool aState) override;
+ virtual bool IsEnabled() const override;
+ virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override;
+ virtual nsresult ConfigureChildren(
+ const nsTArray<Configuration>& aConfigurations) override {
+ MOZ_ASSERT_UNREACHABLE(
+ "Headless widgets do not support configuring children.");
+ return NS_ERROR_FAILURE;
+ }
+ 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 LayerManager* GetLayerManager(
+ PLayerTransactionChild* aShadowManager = nullptr,
+ LayersBackend aBackendHint = mozilla::layers::LayersBackend::LAYERS_NONE,
+ LayerManagerPersistence aPersistence = LAYER_MANAGER_CURRENT) override;
+
+ void SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) override;
+
+ [[nodiscard]] virtual nsresult AttachNativeKeyEvent(
+ WidgetKeyboardEvent& aEvent) override;
+ 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,
+ uint32_t aNativeMessage,
+ uint32_t aModifierFlags,
+ nsIObserver* aObserver) override;
+ virtual nsresult SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint,
+ nsIObserver* aObserver) override {
+ return SynthesizeNativeMouseEvent(aPoint, MOZ_HEADLESS_MOUSE_MOVE, 0,
+ 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;
+
+ private:
+ ~HeadlessWidget();
+ bool mEnabled;
+ bool mVisible;
+ bool mDestroyed;
+ nsIWidget* mTopLevel;
+ HeadlessCompositorWidget* mCompositorWidget;
+ // The size mode before entering fullscreen mode.
+ nsSizeMode mLastSizeMode;
+ // The last size mode set while the window was visible.
+ nsSizeMode mEffectiveSizeMode;
+ 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();
+ // 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..3dba0a2ddd
--- /dev/null
+++ b/widget/headless/HeadlessWidgetTypes.ipdlh
@@ -0,0 +1,18 @@
+/* -*- 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/. */
+
+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..ea9a69c11b
--- /dev/null
+++ b/widget/headless/moz.build
@@ -0,0 +1,49 @@
+# -*- 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",
+ "HeadlessThemeGTK.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"
diff --git a/widget/headless/tests/.eslintrc.js b/widget/headless/tests/.eslintrc.js
new file mode 100644
index 0000000000..69e89d0054
--- /dev/null
+++ b/widget/headless/tests/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/xpcshell-test"],
+};
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..79486a9d8c
--- /dev/null
+++ b/widget/headless/tests/test_headless.js
@@ -0,0 +1,215 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const gProfDir = 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..6c0ffcd5a1
--- /dev/null
+++ b/widget/headless/tests/test_headless_clipboard.js
@@ -0,0 +1,46 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+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