diff options
Diffstat (limited to '')
25 files changed, 2105 insertions, 0 deletions
diff --git a/widget/headless/HeadlessClipboard.cpp b/widget/headless/HeadlessClipboard.cpp new file mode 100644 index 0000000000..be419af523 --- /dev/null +++ b/widget/headless/HeadlessClipboard.cpp @@ -0,0 +1,154 @@ +/* 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::widget { + +NS_IMPL_ISUPPORTS_INHERITED0(HeadlessClipboard, nsBaseClipboard) + +HeadlessClipboard::HeadlessClipboard() + : nsBaseClipboard(mozilla::dom::ClipboardCapabilities( + true /* supportsSelectionClipboard */, + true /* supportsFindClipboard */, + true /* supportsSelectionCache */)) { + for (auto& clipboard : mClipboards) { + clipboard = MakeUnique<HeadlessClipboardData>(); + } +} + +NS_IMETHODIMP +HeadlessClipboard::SetNativeClipboardData(nsITransferable* aTransferable, + int32_t aWhichClipboard) { + MOZ_DIAGNOSTIC_ASSERT(aTransferable); + MOZ_DIAGNOSTIC_ASSERT( + nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)); + + // Clear out the clipboard in order to set the new data. + EmptyNativeClipboardData(aWhichClipboard); + + nsTArray<nsCString> flavors; + nsresult rv = aTransferable->FlavorsTransferableCanExport(flavors); + if (NS_FAILED(rv)) { + return rv; + } + + auto& clipboard = mClipboards[aWhichClipboard]; + MOZ_ASSERT(clipboard); + + for (const auto& flavor : flavors) { + if (!flavor.EqualsLiteral(kTextMime) && !flavor.EqualsLiteral(kHTMLMime)) { + continue; + } + + nsCOMPtr<nsISupports> data; + rv = aTransferable->GetTransferData(flavor.get(), getter_AddRefs(data)); + if (NS_FAILED(rv)) { + continue; + } + + nsCOMPtr<nsISupportsString> wideString = do_QueryInterface(data); + if (!wideString) { + continue; + } + + nsAutoString utf16string; + wideString->GetData(utf16string); + flavor.EqualsLiteral(kTextMime) ? clipboard->SetText(utf16string) + : clipboard->SetHTML(utf16string); + } + + return NS_OK; +} + +NS_IMETHODIMP +HeadlessClipboard::GetNativeClipboardData(nsITransferable* aTransferable, + int32_t aWhichClipboard) { + MOZ_DIAGNOSTIC_ASSERT(aTransferable); + MOZ_DIAGNOSTIC_ASSERT( + nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)); + + nsTArray<nsCString> flavors; + nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + auto& clipboard = mClipboards[aWhichClipboard]; + MOZ_ASSERT(clipboard); + + for (const auto& flavor : flavors) { + if (!flavor.EqualsLiteral(kTextMime) && !flavor.EqualsLiteral(kHTMLMime)) { + continue; + } + + bool isText = flavor.EqualsLiteral(kTextMime); + if (!(isText ? clipboard->HasText() : clipboard->HasHTML())) { + continue; + } + + nsCOMPtr<nsISupportsString> dataWrapper = + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID, &rv); + rv = dataWrapper->SetData(isText ? clipboard->GetText() + : clipboard->GetHTML()); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsCOMPtr<nsISupports> genericDataWrapper = do_QueryInterface(dataWrapper); + rv = aTransferable->SetTransferData(flavor.get(), genericDataWrapper); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + // XXX Other platforms only fill the first available type, too. + break; + } + + return NS_OK; +} + +nsresult HeadlessClipboard::EmptyNativeClipboardData(int32_t aWhichClipboard) { + MOZ_DIAGNOSTIC_ASSERT( + nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)); + auto& clipboard = mClipboards[aWhichClipboard]; + MOZ_ASSERT(clipboard); + clipboard->Clear(); + return NS_OK; +} + +mozilla::Result<int32_t, nsresult> +HeadlessClipboard::GetNativeClipboardSequenceNumber(int32_t aWhichClipboard) { + MOZ_DIAGNOSTIC_ASSERT( + nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)); + auto& clipboard = mClipboards[aWhichClipboard]; + MOZ_ASSERT(clipboard); + return clipboard->GetChangeCount(); + ; +} + +mozilla::Result<bool, nsresult> +HeadlessClipboard::HasNativeClipboardDataMatchingFlavors( + const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) { + MOZ_DIAGNOSTIC_ASSERT( + nsIClipboard::IsClipboardTypeSupported(aWhichClipboard)); + + auto& clipboard = mClipboards[aWhichClipboard]; + MOZ_ASSERT(clipboard); + + // Retrieve the union of all aHasType in aFlavorList + for (auto& flavor : aFlavorList) { + if ((flavor.EqualsLiteral(kTextMime) && clipboard->HasText()) || + (flavor.EqualsLiteral(kHTMLMime) && clipboard->HasHTML())) { + return true; + } + } + return false; +} + +} // namespace mozilla::widget diff --git a/widget/headless/HeadlessClipboard.h b/widget/headless/HeadlessClipboard.h new file mode 100644 index 0000000000..697fc78f8b --- /dev/null +++ b/widget/headless/HeadlessClipboard.h @@ -0,0 +1,45 @@ +/* -*- 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 "nsBaseClipboard.h" +#include "nsIClipboard.h" +#include "mozilla/UniquePtr.h" +#include "HeadlessClipboardData.h" + +namespace mozilla { +namespace widget { + +class HeadlessClipboard final : public nsBaseClipboard { + public: + HeadlessClipboard(); + + NS_DECL_ISUPPORTS_INHERITED + + protected: + ~HeadlessClipboard() = default; + + // Implement the native clipboard behavior. + NS_IMETHOD SetNativeClipboardData(nsITransferable* aTransferable, + int32_t aWhichClipboard) override; + NS_IMETHOD GetNativeClipboardData(nsITransferable* aTransferable, + int32_t aWhichClipboard) override; + nsresult EmptyNativeClipboardData(int32_t aWhichClipboard) override; + mozilla::Result<int32_t, nsresult> GetNativeClipboardSequenceNumber( + int32_t aWhichClipboard) override; + mozilla::Result<bool, nsresult> HasNativeClipboardDataMatchingFlavors( + const nsTArray<nsCString>& aFlavorList, int32_t aWhichClipboard) override; + + private: + UniquePtr<HeadlessClipboardData> + mClipboards[nsIClipboard::kClipboardTypeCount]; +}; + +} // namespace widget +} // namespace mozilla + +#endif diff --git a/widget/headless/HeadlessClipboardData.cpp b/widget/headless/HeadlessClipboardData.cpp new file mode 100644 index 0000000000..8a10488403 --- /dev/null +++ b/widget/headless/HeadlessClipboardData.cpp @@ -0,0 +1,35 @@ +/* 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::widget { + +void HeadlessClipboardData::SetText(const nsAString& aText) { + mPlain = aText; + mChangeCount++; +} + +bool HeadlessClipboardData::HasText() const { return !mPlain.IsVoid(); } + +const nsAString& HeadlessClipboardData::GetText() const { return mPlain; } + +void HeadlessClipboardData::SetHTML(const nsAString& aHTML) { + mHTML = aHTML; + mChangeCount++; +} + +bool HeadlessClipboardData::HasHTML() const { return !mHTML.IsVoid(); } + +const nsAString& HeadlessClipboardData::GetHTML() const { return mHTML; } + +int32_t HeadlessClipboardData::GetChangeCount() const { return mChangeCount; } + +void HeadlessClipboardData::Clear() { + mPlain.SetIsVoid(true); + mHTML.SetIsVoid(true); + mChangeCount++; +} + +} // namespace mozilla::widget diff --git a/widget/headless/HeadlessClipboardData.h b/widget/headless/HeadlessClipboardData.h new file mode 100644 index 0000000000..6e9a0109e7 --- /dev/null +++ b/widget/headless/HeadlessClipboardData.h @@ -0,0 +1,44 @@ +/* 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: + HeadlessClipboardData() : mPlain(VoidString()), mHTML(VoidString()) {} + ~HeadlessClipboardData() = default; + + // For text/plain + void SetText(const nsAString& aText); + bool HasText() const; + const nsAString& GetText() const; + + // For text/html + void SetHTML(const nsAString& aHTML); + bool HasHTML() const; + const nsAString& GetHTML() const; + + int32_t GetChangeCount() const; + + // For other APIs + void Clear(); + + private: + nsString mPlain; + nsString mHTML; + + int32_t mChangeCount = 0; +}; + +} // 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..27ed07c461 --- /dev/null +++ b/widget/headless/HeadlessKeyBindingsCocoa.mm @@ -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/. */ + +#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..f8f6270cd7 --- /dev/null +++ b/widget/headless/HeadlessLookAndFeelGTK.cpp @@ -0,0 +1,223 @@ +/* -*- 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: + 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 = 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: + case IntID::PrefersReducedTransparency: + aResult = 0; + break; + case IntID::InvertedColors: + 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..083d026d3c --- /dev/null +++ b/widget/headless/HeadlessWidget.cpp @@ -0,0 +1,625 @@ +/* -*- 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 "mozilla/widget/Screen.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), + mAlwaysOnTop(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, + widget::InitData* aInitData) { + MOZ_ASSERT(!aNativeParent, "No native parents for headless widgets."); + + BaseCreate(nullptr, aInitData); + + mBounds = aRect; + mRestoreBounds = aRect; + + mAlwaysOnTop = aInitData && aInitData->mAlwaysOnTop; + + if (aParent) { + mTopLevel = aParent->GetTopLevelWidget(); + } else { + mTopLevel = this; + } + + return NS_OK; +} + +already_AddRefed<nsIWidget> HeadlessWidget::CreateChild( + const LayoutDeviceIntRect& aRect, widget::InitData* 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(mWindowType == WindowType::TopLevel || + mWindowType == WindowType::Dialog || + mWindowType == WindowType::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. + // NB: alwaysontop windows are generally used for peripheral indicators, + // so we don't focus them by default. + if (aState && !mAlwaysOnTop && + (mWindowType == WindowType::TopLevel || + mWindowType == WindowType::Dialog || mWindowType == WindowType::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 == WindowType::TopLevel || + mWindowType == WindowType::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 != WindowType::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); + } + + // 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..9856991ef3 --- /dev/null +++ b/widget/headless/HeadlessWidget.h @@ -0,0 +1,182 @@ +/* -*- 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, + widget::InitData* aInitData = nullptr) override; + using nsBaseWidget::Create; // for Create signature not overridden here + virtual already_AddRefed<nsIWidget> CreateChild( + const LayoutDeviceIntRect& aRect, widget::InitData* 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( + const 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; + bool mAlwaysOnTop; + 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..a46c3a10e3 --- /dev/null +++ b/widget/headless/moz.build @@ -0,0 +1,48 @@ +# -*- 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" 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..9656ad382b --- /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.toml"] diff --git a/widget/headless/tests/test_headless.js b/widget/headless/tests/test_headless.js new file mode 100644 index 0000000000..f9183245d2 --- /dev/null +++ b/widget/headless/tests/test_headless.js @@ -0,0 +1,224 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +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 = Services.io.newURI(`${BASE}/headless.html`); +const HEADLESS_BUTTON_URL = Services.io.newURI(`${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_setup(function () { + Services.prefs.setBoolPref("security.allow_unsafe_parent_loads", true); +}); + +registerCleanupFunction(function () { + Services.prefs.clearUserPref("security.allow_unsafe_parent_loads"); +}); + +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..862e343001 --- /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/plain"); + + clipboard.getData(trans, Ci.nsIClipboard.kGlobalClipboard); + + try { + var data = {}; + trans.getTransferData("text/plain", 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.toml b/widget/headless/tests/xpcshell.toml new file mode 100644 index 0000000000..29c5fd100e --- /dev/null +++ b/widget/headless/tests/xpcshell.toml @@ -0,0 +1,14 @@ +[DEFAULT] +run-if = [ + "os == 'linux'", + "os == 'win'", +] +headless = true + +["test_headless.js"] +support-files = [ + "headless.html", + "headless_button.html", +] + +["test_headless_clipboard.js"] |