diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /widget/headless | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.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')
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 |