From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- layout/forms/HTMLSelectEventListener.cpp | 856 +++++++++++++ layout/forms/HTMLSelectEventListener.h | 103 ++ layout/forms/ListMutationObserver.cpp | 92 ++ layout/forms/ListMutationObserver.h | 67 + layout/forms/crashtests/1102791.html | 33 + layout/forms/crashtests/1140216.html | 20 + layout/forms/crashtests/1182414.html | 17 + layout/forms/crashtests/1212688.html | 27 + layout/forms/crashtests/1228670.xhtml | 7 + layout/forms/crashtests/1279354.html | 21 + layout/forms/crashtests/1388230-1.html | 3 + layout/forms/crashtests/1388230-2.html | 1 + layout/forms/crashtests/1405830.html | 19 + layout/forms/crashtests/1418477.html | 4 + layout/forms/crashtests/1432853.html | 8 + layout/forms/crashtests/1460787-1.html | 7 + layout/forms/crashtests/1464165-1.html | 14 + layout/forms/crashtests/1471157.html | 11 + layout/forms/crashtests/1488219.html | 26 + layout/forms/crashtests/1600207.html | 9 + layout/forms/crashtests/1600367.html | 8 + layout/forms/crashtests/1617753.html | 21 + layout/forms/crashtests/166750-1.html | 14 + layout/forms/crashtests/1679471.html | 8 + layout/forms/crashtests/1690166-1.html | 19 + layout/forms/crashtests/1690166-2.html | 19 + layout/forms/crashtests/1873802-1.html | 114 ++ layout/forms/crashtests/200347-1.html | 8 + layout/forms/crashtests/203041-1.html | 24 + layout/forms/crashtests/213390-1.html | 23 + layout/forms/crashtests/258101-1.html | 18 + layout/forms/crashtests/266225-1.html | 7 + layout/forms/crashtests/310426-1.xhtml | 9 + layout/forms/crashtests/310520-1.xhtml | 19 + layout/forms/crashtests/315752-1.xhtml | 21 + layout/forms/crashtests/317502-1.xhtml | 13 + layout/forms/crashtests/321894.html | 17 + layout/forms/crashtests/343510-1.html | 4 + layout/forms/crashtests/363696-1.xhtml | 10 + layout/forms/crashtests/363696-2.html | 2 + layout/forms/crashtests/363696-3.html | 5 + layout/forms/crashtests/366205-1.html | 11 + layout/forms/crashtests/366537-1.xhtml | 32 + layout/forms/crashtests/367587-1.html | 37 + layout/forms/crashtests/370703-1.html | 30 + layout/forms/crashtests/370940-1.html | 28 + layout/forms/crashtests/370967.html | 13 + layout/forms/crashtests/378369.html | 19 + layout/forms/crashtests/380116-1.xhtml | 11 + layout/forms/crashtests/382610-1.html | 11 + layout/forms/crashtests/383887-1.html | 20 + layout/forms/crashtests/386554-1.html | 14 + layout/forms/crashtests/388374-1.xhtml | 22 + layout/forms/crashtests/388374-2.html | 25 + layout/forms/crashtests/393656-1.xhtml | 13 + layout/forms/crashtests/393656-2.xhtml | 22 + layout/forms/crashtests/399262.html | 50 + layout/forms/crashtests/402852-1.html | 2 + layout/forms/crashtests/403148-1.html | 22 + layout/forms/crashtests/404118-1.html | 5 + layout/forms/crashtests/404123-1.html | 12 + layout/forms/crashtests/407066.html | 1 + layout/forms/crashtests/451316.html | 7 + layout/forms/crashtests/455451-1.html | 17 + layout/forms/crashtests/457537-1.html | 17 + layout/forms/crashtests/457537-2.html | 17 + layout/forms/crashtests/498698-1.html | 6 + layout/forms/crashtests/513113-1.html | 6 + layout/forms/crashtests/538062-1.xhtml | 20 + layout/forms/crashtests/570624-1.html | 15 + layout/forms/crashtests/578604-1.html | 17 + layout/forms/crashtests/590302-1.xhtml | 4 + layout/forms/crashtests/626014.xhtml | 20 + layout/forms/crashtests/639733.xhtml | 26 + layout/forms/crashtests/669767.html | 14 + layout/forms/crashtests/682684-binding.xml | 4 + layout/forms/crashtests/682684.xhtml | 3 + layout/forms/crashtests/865602.html | 9 + layout/forms/crashtests/893331.html | 9 + layout/forms/crashtests/893332-1.html | 10 + layout/forms/crashtests/944198.html | 9 + layout/forms/crashtests/949891.xhtml | 5 + layout/forms/crashtests/959311.html | 17 + layout/forms/crashtests/960277-2.html | 14 + layout/forms/crashtests/997709-1.html | 5 + layout/forms/crashtests/crashtests.list | 80 ++ layout/forms/moz.build | 54 + layout/forms/nsButtonFrameRenderer.cpp | 471 +++++++ layout/forms/nsButtonFrameRenderer.h | 83 ++ layout/forms/nsCheckboxRadioFrame.cpp | 167 +++ layout/forms/nsCheckboxRadioFrame.h | 88 ++ layout/forms/nsColorControlFrame.cpp | 128 ++ layout/forms/nsColorControlFrame.h | 60 + layout/forms/nsComboboxControlFrame.cpp | 965 ++++++++++++++ layout/forms/nsComboboxControlFrame.h | 244 ++++ layout/forms/nsDateTimeControlFrame.cpp | 198 +++ layout/forms/nsDateTimeControlFrame.h | 66 + layout/forms/nsFieldSetFrame.cpp | 938 ++++++++++++++ layout/forms/nsFieldSetFrame.h | 114 ++ layout/forms/nsFileControlFrame.cpp | 422 +++++++ layout/forms/nsFileControlFrame.h | 137 ++ layout/forms/nsGfxButtonControlFrame.cpp | 178 +++ layout/forms/nsGfxButtonControlFrame.h | 62 + layout/forms/nsHTMLButtonControlFrame.cpp | 394 ++++++ layout/forms/nsHTMLButtonControlFrame.h | 110 ++ layout/forms/nsIFormControlFrame.h | 41 + layout/forms/nsISelectControlFrame.h | 50 + layout/forms/nsITextControlFrame.h | 44 + layout/forms/nsImageControlFrame.cpp | 151 +++ layout/forms/nsListControlFrame.cpp | 1211 ++++++++++++++++++ layout/forms/nsListControlFrame.h | 350 ++++++ layout/forms/nsMeterFrame.cpp | 224 ++++ layout/forms/nsMeterFrame.h | 77 ++ layout/forms/nsNumberControlFrame.cpp | 175 +++ layout/forms/nsNumberControlFrame.h | 105 ++ layout/forms/nsProgressFrame.cpp | 250 ++++ layout/forms/nsProgressFrame.h | 84 ++ layout/forms/nsRangeFrame.cpp | 780 ++++++++++++ layout/forms/nsRangeFrame.h | 213 ++++ layout/forms/nsSearchControlFrame.cpp | 82 ++ layout/forms/nsSearchControlFrame.h | 68 + layout/forms/nsSelectsAreaFrame.cpp | 189 +++ layout/forms/nsSelectsAreaFrame.h | 58 + layout/forms/nsTextControlFrame.cpp | 1325 ++++++++++++++++++++ layout/forms/nsTextControlFrame.h | 358 ++++++ layout/forms/test/bug287446_subframe.html | 38 + layout/forms/test/bug477700_subframe.html | 39 + layout/forms/test/bug536567_iframe.html | 9 + layout/forms/test/bug536567_subframe.html | 14 + layout/forms/test/bug564115_window.html | 10 + layout/forms/test/chrome.toml | 8 + layout/forms/test/mochitest.toml | 121 ++ layout/forms/test/test_bug1111995.html | 60 + layout/forms/test/test_bug1301290.html | 49 + layout/forms/test/test_bug1305282.html | 57 + layout/forms/test/test_bug1327129.html | 385 ++++++ layout/forms/test/test_bug1529036.html | 73 ++ layout/forms/test/test_bug231389.html | 55 + layout/forms/test/test_bug287446.html | 74 ++ layout/forms/test/test_bug345267.html | 97 ++ layout/forms/test/test_bug346043.html | 65 + layout/forms/test/test_bug348236.html | 123 ++ layout/forms/test/test_bug353539.html | 52 + layout/forms/test/test_bug365410.html | 132 ++ layout/forms/test/test_bug378670.html | 55 + layout/forms/test/test_bug402198.html | 77 ++ layout/forms/test/test_bug411236.html | 71 ++ layout/forms/test/test_bug446663.html | 80 ++ layout/forms/test/test_bug476308.html | 31 + layout/forms/test/test_bug477531.html | 65 + layout/forms/test/test_bug477700.html | 59 + layout/forms/test/test_bug534785.html | 88 ++ layout/forms/test/test_bug536567_perwindowpb.html | 215 ++++ layout/forms/test/test_bug542914.html | 115 ++ layout/forms/test/test_bug549170.html | 77 ++ layout/forms/test/test_bug562447.html | 62 + layout/forms/test/test_bug563642.html | 82 ++ layout/forms/test/test_bug564115.html | 57 + layout/forms/test/test_bug571352.html | 86 ++ layout/forms/test/test_bug572406.html | 48 + layout/forms/test/test_bug572649.html | 63 + layout/forms/test/test_bug595310.html | 64 + layout/forms/test/test_bug620936.html | 35 + layout/forms/test/test_bug644542.html | 63 + layout/forms/test/test_bug672810.html | 120 ++ layout/forms/test/test_bug704049.html | 50 + layout/forms/test/test_bug717878_input_scroll.html | 107 ++ layout/forms/test/test_bug869314.html | 55 + layout/forms/test/test_bug903715.html | 81 ++ layout/forms/test/test_bug935876.html | 502 ++++++++ layout/forms/test/test_bug957562.html | 43 + layout/forms/test/test_bug960277.html | 29 + layout/forms/test/test_listcontrol_search.html | 46 + layout/forms/test/test_readonly.html | 58 + .../test/test_select_collapsed_page_keys.html | 43 + .../test_select_key_navigation_bug1498769.html | 123 ++ .../test/test_select_key_navigation_bug961363.html | 131 ++ layout/forms/test/test_select_prevent_default.html | 116 ++ layout/forms/test/test_select_reframe.html | 52 + layout/forms/test/test_select_vertical.html | 75 ++ layout/forms/test/test_textarea_resize.html | 102 ++ .../forms/test/test_unstyled_control_height.html | 72 ++ 182 files changed, 17952 insertions(+) create mode 100644 layout/forms/HTMLSelectEventListener.cpp create mode 100644 layout/forms/HTMLSelectEventListener.h create mode 100644 layout/forms/ListMutationObserver.cpp create mode 100644 layout/forms/ListMutationObserver.h create mode 100644 layout/forms/crashtests/1102791.html create mode 100644 layout/forms/crashtests/1140216.html create mode 100644 layout/forms/crashtests/1182414.html create mode 100644 layout/forms/crashtests/1212688.html create mode 100644 layout/forms/crashtests/1228670.xhtml create mode 100644 layout/forms/crashtests/1279354.html create mode 100644 layout/forms/crashtests/1388230-1.html create mode 100644 layout/forms/crashtests/1388230-2.html create mode 100644 layout/forms/crashtests/1405830.html create mode 100644 layout/forms/crashtests/1418477.html create mode 100644 layout/forms/crashtests/1432853.html create mode 100644 layout/forms/crashtests/1460787-1.html create mode 100644 layout/forms/crashtests/1464165-1.html create mode 100644 layout/forms/crashtests/1471157.html create mode 100644 layout/forms/crashtests/1488219.html create mode 100644 layout/forms/crashtests/1600207.html create mode 100644 layout/forms/crashtests/1600367.html create mode 100644 layout/forms/crashtests/1617753.html create mode 100644 layout/forms/crashtests/166750-1.html create mode 100644 layout/forms/crashtests/1679471.html create mode 100644 layout/forms/crashtests/1690166-1.html create mode 100644 layout/forms/crashtests/1690166-2.html create mode 100644 layout/forms/crashtests/1873802-1.html create mode 100644 layout/forms/crashtests/200347-1.html create mode 100644 layout/forms/crashtests/203041-1.html create mode 100644 layout/forms/crashtests/213390-1.html create mode 100644 layout/forms/crashtests/258101-1.html create mode 100644 layout/forms/crashtests/266225-1.html create mode 100644 layout/forms/crashtests/310426-1.xhtml create mode 100644 layout/forms/crashtests/310520-1.xhtml create mode 100644 layout/forms/crashtests/315752-1.xhtml create mode 100644 layout/forms/crashtests/317502-1.xhtml create mode 100644 layout/forms/crashtests/321894.html create mode 100644 layout/forms/crashtests/343510-1.html create mode 100644 layout/forms/crashtests/363696-1.xhtml create mode 100644 layout/forms/crashtests/363696-2.html create mode 100644 layout/forms/crashtests/363696-3.html create mode 100644 layout/forms/crashtests/366205-1.html create mode 100644 layout/forms/crashtests/366537-1.xhtml create mode 100644 layout/forms/crashtests/367587-1.html create mode 100644 layout/forms/crashtests/370703-1.html create mode 100644 layout/forms/crashtests/370940-1.html create mode 100644 layout/forms/crashtests/370967.html create mode 100644 layout/forms/crashtests/378369.html create mode 100644 layout/forms/crashtests/380116-1.xhtml create mode 100644 layout/forms/crashtests/382610-1.html create mode 100644 layout/forms/crashtests/383887-1.html create mode 100644 layout/forms/crashtests/386554-1.html create mode 100644 layout/forms/crashtests/388374-1.xhtml create mode 100644 layout/forms/crashtests/388374-2.html create mode 100644 layout/forms/crashtests/393656-1.xhtml create mode 100644 layout/forms/crashtests/393656-2.xhtml create mode 100644 layout/forms/crashtests/399262.html create mode 100644 layout/forms/crashtests/402852-1.html create mode 100644 layout/forms/crashtests/403148-1.html create mode 100644 layout/forms/crashtests/404118-1.html create mode 100644 layout/forms/crashtests/404123-1.html create mode 100644 layout/forms/crashtests/407066.html create mode 100644 layout/forms/crashtests/451316.html create mode 100644 layout/forms/crashtests/455451-1.html create mode 100644 layout/forms/crashtests/457537-1.html create mode 100644 layout/forms/crashtests/457537-2.html create mode 100644 layout/forms/crashtests/498698-1.html create mode 100644 layout/forms/crashtests/513113-1.html create mode 100644 layout/forms/crashtests/538062-1.xhtml create mode 100644 layout/forms/crashtests/570624-1.html create mode 100644 layout/forms/crashtests/578604-1.html create mode 100644 layout/forms/crashtests/590302-1.xhtml create mode 100644 layout/forms/crashtests/626014.xhtml create mode 100644 layout/forms/crashtests/639733.xhtml create mode 100644 layout/forms/crashtests/669767.html create mode 100644 layout/forms/crashtests/682684-binding.xml create mode 100644 layout/forms/crashtests/682684.xhtml create mode 100644 layout/forms/crashtests/865602.html create mode 100644 layout/forms/crashtests/893331.html create mode 100644 layout/forms/crashtests/893332-1.html create mode 100644 layout/forms/crashtests/944198.html create mode 100644 layout/forms/crashtests/949891.xhtml create mode 100644 layout/forms/crashtests/959311.html create mode 100644 layout/forms/crashtests/960277-2.html create mode 100644 layout/forms/crashtests/997709-1.html create mode 100644 layout/forms/crashtests/crashtests.list create mode 100644 layout/forms/moz.build create mode 100644 layout/forms/nsButtonFrameRenderer.cpp create mode 100644 layout/forms/nsButtonFrameRenderer.h create mode 100644 layout/forms/nsCheckboxRadioFrame.cpp create mode 100644 layout/forms/nsCheckboxRadioFrame.h create mode 100644 layout/forms/nsColorControlFrame.cpp create mode 100644 layout/forms/nsColorControlFrame.h create mode 100644 layout/forms/nsComboboxControlFrame.cpp create mode 100644 layout/forms/nsComboboxControlFrame.h create mode 100644 layout/forms/nsDateTimeControlFrame.cpp create mode 100644 layout/forms/nsDateTimeControlFrame.h create mode 100644 layout/forms/nsFieldSetFrame.cpp create mode 100644 layout/forms/nsFieldSetFrame.h create mode 100644 layout/forms/nsFileControlFrame.cpp create mode 100644 layout/forms/nsFileControlFrame.h create mode 100644 layout/forms/nsGfxButtonControlFrame.cpp create mode 100644 layout/forms/nsGfxButtonControlFrame.h create mode 100644 layout/forms/nsHTMLButtonControlFrame.cpp create mode 100644 layout/forms/nsHTMLButtonControlFrame.h create mode 100644 layout/forms/nsIFormControlFrame.h create mode 100644 layout/forms/nsISelectControlFrame.h create mode 100644 layout/forms/nsITextControlFrame.h create mode 100644 layout/forms/nsImageControlFrame.cpp create mode 100644 layout/forms/nsListControlFrame.cpp create mode 100644 layout/forms/nsListControlFrame.h create mode 100644 layout/forms/nsMeterFrame.cpp create mode 100644 layout/forms/nsMeterFrame.h create mode 100644 layout/forms/nsNumberControlFrame.cpp create mode 100644 layout/forms/nsNumberControlFrame.h create mode 100644 layout/forms/nsProgressFrame.cpp create mode 100644 layout/forms/nsProgressFrame.h create mode 100644 layout/forms/nsRangeFrame.cpp create mode 100644 layout/forms/nsRangeFrame.h create mode 100644 layout/forms/nsSearchControlFrame.cpp create mode 100644 layout/forms/nsSearchControlFrame.h create mode 100644 layout/forms/nsSelectsAreaFrame.cpp create mode 100644 layout/forms/nsSelectsAreaFrame.h create mode 100644 layout/forms/nsTextControlFrame.cpp create mode 100644 layout/forms/nsTextControlFrame.h create mode 100644 layout/forms/test/bug287446_subframe.html create mode 100644 layout/forms/test/bug477700_subframe.html create mode 100644 layout/forms/test/bug536567_iframe.html create mode 100644 layout/forms/test/bug536567_subframe.html create mode 100644 layout/forms/test/bug564115_window.html create mode 100644 layout/forms/test/chrome.toml create mode 100644 layout/forms/test/mochitest.toml create mode 100644 layout/forms/test/test_bug1111995.html create mode 100644 layout/forms/test/test_bug1301290.html create mode 100644 layout/forms/test/test_bug1305282.html create mode 100644 layout/forms/test/test_bug1327129.html create mode 100644 layout/forms/test/test_bug1529036.html create mode 100644 layout/forms/test/test_bug231389.html create mode 100644 layout/forms/test/test_bug287446.html create mode 100644 layout/forms/test/test_bug345267.html create mode 100644 layout/forms/test/test_bug346043.html create mode 100644 layout/forms/test/test_bug348236.html create mode 100644 layout/forms/test/test_bug353539.html create mode 100644 layout/forms/test/test_bug365410.html create mode 100644 layout/forms/test/test_bug378670.html create mode 100644 layout/forms/test/test_bug402198.html create mode 100644 layout/forms/test/test_bug411236.html create mode 100644 layout/forms/test/test_bug446663.html create mode 100644 layout/forms/test/test_bug476308.html create mode 100644 layout/forms/test/test_bug477531.html create mode 100644 layout/forms/test/test_bug477700.html create mode 100644 layout/forms/test/test_bug534785.html create mode 100644 layout/forms/test/test_bug536567_perwindowpb.html create mode 100644 layout/forms/test/test_bug542914.html create mode 100644 layout/forms/test/test_bug549170.html create mode 100644 layout/forms/test/test_bug562447.html create mode 100644 layout/forms/test/test_bug563642.html create mode 100644 layout/forms/test/test_bug564115.html create mode 100644 layout/forms/test/test_bug571352.html create mode 100644 layout/forms/test/test_bug572406.html create mode 100644 layout/forms/test/test_bug572649.html create mode 100644 layout/forms/test/test_bug595310.html create mode 100644 layout/forms/test/test_bug620936.html create mode 100644 layout/forms/test/test_bug644542.html create mode 100644 layout/forms/test/test_bug672810.html create mode 100644 layout/forms/test/test_bug704049.html create mode 100644 layout/forms/test/test_bug717878_input_scroll.html create mode 100644 layout/forms/test/test_bug869314.html create mode 100644 layout/forms/test/test_bug903715.html create mode 100644 layout/forms/test/test_bug935876.html create mode 100644 layout/forms/test/test_bug957562.html create mode 100644 layout/forms/test/test_bug960277.html create mode 100644 layout/forms/test/test_listcontrol_search.html create mode 100644 layout/forms/test/test_readonly.html create mode 100644 layout/forms/test/test_select_collapsed_page_keys.html create mode 100644 layout/forms/test/test_select_key_navigation_bug1498769.html create mode 100644 layout/forms/test/test_select_key_navigation_bug961363.html create mode 100644 layout/forms/test/test_select_prevent_default.html create mode 100644 layout/forms/test/test_select_reframe.html create mode 100644 layout/forms/test/test_select_vertical.html create mode 100644 layout/forms/test/test_textarea_resize.html create mode 100644 layout/forms/test/test_unstyled_control_height.html (limited to 'layout/forms') diff --git a/layout/forms/HTMLSelectEventListener.cpp b/layout/forms/HTMLSelectEventListener.cpp new file mode 100644 index 0000000000..4f6b1e3561 --- /dev/null +++ b/layout/forms/HTMLSelectEventListener.cpp @@ -0,0 +1,856 @@ +/* -*- 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 "HTMLSelectEventListener.h" + +#include "nsListControlFrame.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/Casting.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/TextEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "mozilla/dom/HTMLOptionElement.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/ClearOnShutdown.h" + +using namespace mozilla; +using namespace mozilla::dom; + +static bool IsOptionInteractivelySelectable(HTMLSelectElement& aSelect, + HTMLOptionElement& aOption, + bool aIsCombobox) { + if (aSelect.IsOptionDisabled(&aOption)) { + return false; + } + if (!aIsCombobox) { + return aOption.GetPrimaryFrame(); + } + // In dropdown mode no options have frames, but we can check whether they + // are rendered / not in a display: none subtree. + if (!aOption.HasServoData() || Servo_Element_IsDisplayNone(&aOption)) { + return false; + } + // TODO(emilio): This is a bit silly and doesn't match the options that we + // show / don't show in the dropdown, but matches the frame construction we + // do for multiple selects. For backwards compat also don't allow selecting + // options in a display: contents subtree interactively. + // test_select_key_navigation_bug1498769.html tests for this and should + // probably be changed (and this loop removed) or alternatively + // SelectChild.jsm should be changed to match it. + for (Element* el = &aOption; el && el != &aSelect; + el = el->GetParentElement()) { + if (Servo_Element_IsDisplayContents(el)) { + return false; + } + } + return true; +} + +namespace mozilla { + +static StaticAutoPtr sIncrementalString; +static TimeStamp gLastKeyTime; +static uintptr_t sLastKeyListener = 0; +static constexpr int32_t kNothingSelected = -1; + +static nsString& GetIncrementalString() { + MOZ_ASSERT(sLastKeyListener != 0); + if (!sIncrementalString) { + sIncrementalString = new nsString(); + ClearOnShutdown(&sIncrementalString); + } + return *sIncrementalString; +} + +class MOZ_RAII AutoIncrementalSearchHandler { + public: + explicit AutoIncrementalSearchHandler(HTMLSelectEventListener& aListener) { + if (sLastKeyListener != uintptr_t(&aListener)) { + sLastKeyListener = uintptr_t(&aListener); + GetIncrementalString().Truncate(); + // To make it easier to handle time comparisons in the other methods, + // initialize gLastKeyTime to a value in the past. + gLastKeyTime = TimeStamp::Now() - + TimeDuration::FromMilliseconds( + StaticPrefs::ui_menu_incremental_search_timeout() * 2); + } + } + ~AutoIncrementalSearchHandler() { + if (!mResettingCancelled) { + GetIncrementalString().Truncate(); + } + } + void CancelResetting() { mResettingCancelled = true; } + + private: + bool mResettingCancelled = false; +}; + +NS_IMPL_ISUPPORTS(HTMLSelectEventListener, nsIMutationObserver, + nsIDOMEventListener) + +HTMLSelectEventListener::~HTMLSelectEventListener() { + if (sLastKeyListener == uintptr_t(this)) { + sLastKeyListener = 0; + } +} + +nsListControlFrame* HTMLSelectEventListener::GetListControlFrame() const { + if (mIsCombobox) { + MOZ_ASSERT(!mElement->GetPrimaryFrame() || + !mElement->GetPrimaryFrame()->IsListControlFrame()); + return nullptr; + } + return do_QueryFrame(mElement->GetPrimaryFrame()); +} + +int32_t HTMLSelectEventListener::GetEndSelectionIndex() const { + if (auto* lf = GetListControlFrame()) { + return lf->GetEndSelectionIndex(); + } + // Combobox selects only have one selected index, so the end and start is the + // same. + return mElement->SelectedIndex(); +} + +bool HTMLSelectEventListener::IsOptionInteractivelySelectable( + uint32_t aIndex) const { + HTMLOptionElement* option = mElement->Item(aIndex); + return option && + ::IsOptionInteractivelySelectable(*mElement, *option, mIsCombobox); +} + +//--------------------------------------------------------------------- +// Ok, the entire idea of this routine is to move to the next item that +// is suppose to be selected. If the item is disabled then we search in +// the same direction looking for the next item to select. If we run off +// the end of the list then we start at the end of the list and search +// backwards until we get back to the original item or an enabled option +// +// aStartIndex - the index to start searching from +// aNewIndex - will get set to the new index if it finds one +// aNumOptions - the total number of options in the list +// aDoAdjustInc - the initial index increment / decrement +// aDoAdjustIncNext - the subsequent index increment/decrement used to search +// for the next enabled option +// +// the aDoAdjustInc could be a "1" for a single item or +// any number greater representing a page of items +// +void HTMLSelectEventListener::AdjustIndexForDisabledOpt( + int32_t aStartIndex, int32_t& aNewIndex, int32_t aNumOptions, + int32_t aDoAdjustInc, int32_t aDoAdjustIncNext) { + // Cannot select anything if there is nothing to select + if (aNumOptions == 0) { + aNewIndex = kNothingSelected; + return; + } + + // means we reached the end of the list and now we are searching backwards + bool doingReverse = false; + // lowest index in the search range + int32_t bottom = 0; + // highest index in the search range + int32_t top = aNumOptions; + + // Start off keyboard options at selectedIndex if nothing else is defaulted to + // + // XXX Perhaps this should happen for mouse too, to start off shift click + // automatically in multiple ... to do this, we'd need to override + // OnOptionSelected and set mStartSelectedIndex if nothing is selected. Not + // sure of the effects, though, so I'm not doing it just yet. + int32_t startIndex = aStartIndex; + if (startIndex < bottom) { + startIndex = mElement->SelectedIndex(); + } + int32_t newIndex = startIndex + aDoAdjustInc; + + // make sure we start off in the range + if (newIndex < bottom) { + newIndex = 0; + } else if (newIndex >= top) { + newIndex = aNumOptions - 1; + } + + while (true) { + // if the newIndex is selectable, we are golden, bail out + if (IsOptionInteractivelySelectable(newIndex)) { + break; + } + + // it WAS disabled, so sart looking ahead for the next enabled option + newIndex += aDoAdjustIncNext; + + // well, if we reach end reverse the search + if (newIndex < bottom) { + if (doingReverse) { + return; // if we are in reverse mode and reach the end bail out + } + // reset the newIndex to the end of the list we hit + // reverse the incrementer + // set the other end of the list to our original starting index + newIndex = bottom; + aDoAdjustIncNext = 1; + doingReverse = true; + top = startIndex; + } else if (newIndex >= top) { + if (doingReverse) { + return; // if we are in reverse mode and reach the end bail out + } + // reset the newIndex to the end of the list we hit + // reverse the incrementer + // set the other end of the list to our original starting index + newIndex = top - 1; + aDoAdjustIncNext = -1; + doingReverse = true; + bottom = startIndex; + } + } + + // Looks like we found one + aNewIndex = newIndex; +} + +NS_IMETHODIMP +HTMLSelectEventListener::HandleEvent(dom::Event* aEvent) { + nsAutoString eventType; + aEvent->GetType(eventType); + if (eventType.EqualsLiteral("keydown")) { + return KeyDown(aEvent); + } + if (eventType.EqualsLiteral("keypress")) { + return KeyPress(aEvent); + } + if (eventType.EqualsLiteral("mousedown")) { + if (aEvent->DefaultPrevented()) { + return NS_OK; + } + return MouseDown(aEvent); + } + if (eventType.EqualsLiteral("mouseup")) { + // Don't try to honor defaultPrevented here - it's not web compatible. + // (bug 1194733) + return MouseUp(aEvent); + } + if (eventType.EqualsLiteral("mousemove")) { + // I don't think we want to honor defaultPrevented on mousemove + // in general, and it would only prevent highlighting here. + return MouseMove(aEvent); + } + + MOZ_ASSERT_UNREACHABLE("Unexpected eventType"); + return NS_OK; +} + +void HTMLSelectEventListener::Attach() { + mElement->AddSystemEventListener(u"keydown"_ns, this, false, false); + mElement->AddSystemEventListener(u"keypress"_ns, this, false, false); + mElement->AddSystemEventListener(u"mousedown"_ns, this, false, false); + mElement->AddSystemEventListener(u"mouseup"_ns, this, false, false); + mElement->AddSystemEventListener(u"mousemove"_ns, this, false, false); + + if (mIsCombobox) { + mElement->AddMutationObserver(this); + } +} + +void HTMLSelectEventListener::Detach() { + mElement->RemoveSystemEventListener(u"keydown"_ns, this, false); + mElement->RemoveSystemEventListener(u"keypress"_ns, this, false); + mElement->RemoveSystemEventListener(u"mousedown"_ns, this, false); + mElement->RemoveSystemEventListener(u"mouseup"_ns, this, false); + mElement->RemoveSystemEventListener(u"mousemove"_ns, this, false); + + if (mIsCombobox) { + mElement->RemoveMutationObserver(this); + if (mElement->OpenInParentProcess()) { + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "HTMLSelectEventListener::Detach", [element = mElement] { + // Don't hide the dropdown if the element has another frame already, + // this prevents closing dropdowns on reframe, see bug 1440506. + // + // FIXME(emilio): The flush is needed to deal with reframes started + // from DOM node removal. But perhaps we can be a bit smarter here. + if (!element->IsCombobox() || + !element->GetPrimaryFrame(FlushType::Frames)) { + nsContentUtils::DispatchChromeEvent( + element->OwnerDoc(), element, u"mozhidedropdown"_ns, + CanBubble::eYes, Cancelable::eNo); + } + })); + } + } +} + +const uint32_t kMaxDropdownRows = 20; // matches the setting for 4.x browsers + +int32_t HTMLSelectEventListener::ItemsPerPage() const { + uint32_t size = [&] { + if (mIsCombobox) { + return kMaxDropdownRows; + } + if (auto* lf = GetListControlFrame()) { + return lf->GetNumDisplayRows(); + } + return mElement->Size(); + }(); + if (size <= 1) { + return 1; + } + if (MOZ_UNLIKELY(size > INT32_MAX)) { + return INT32_MAX - 1; + } + return AssertedCast(size - 1u); +} + +void HTMLSelectEventListener::OptionValueMightHaveChanged( + nsIContent* aMutatingNode) { +#ifdef ACCESSIBILITY + if (nsAccessibilityService* acc = GetAccService()) { + acc->ComboboxOptionMaybeChanged(mElement->OwnerDoc()->GetPresShell(), + aMutatingNode); + } +#endif +} + +void HTMLSelectEventListener::AttributeChanged(dom::Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue) { + if (aElement->IsHTMLElement(nsGkAtoms::option) && + aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::label) { + // A11y has its own mutation listener for this so no need to do + // OptionValueMightHaveChanged(). + ComboboxMightHaveChanged(); + } +} + +void HTMLSelectEventListener::CharacterDataChanged( + nsIContent* aContent, const CharacterDataChangeInfo&) { + if (nsContentUtils::IsInSameAnonymousTree(mElement, aContent)) { + OptionValueMightHaveChanged(aContent); + ComboboxMightHaveChanged(); + } +} + +void HTMLSelectEventListener::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + if (nsContentUtils::IsInSameAnonymousTree(mElement, aChild)) { + OptionValueMightHaveChanged(aChild); + ComboboxMightHaveChanged(); + } +} + +void HTMLSelectEventListener::ContentAppended(nsIContent* aFirstNewContent) { + if (nsContentUtils::IsInSameAnonymousTree(mElement, aFirstNewContent)) { + OptionValueMightHaveChanged(aFirstNewContent); + ComboboxMightHaveChanged(); + } +} + +void HTMLSelectEventListener::ContentInserted(nsIContent* aChild) { + if (nsContentUtils::IsInSameAnonymousTree(mElement, aChild)) { + OptionValueMightHaveChanged(aChild); + ComboboxMightHaveChanged(); + } +} + +void HTMLSelectEventListener::ComboboxMightHaveChanged() { + if (nsIFrame* f = mElement->GetPrimaryFrame()) { + PresShell* ps = f->PresShell(); + // nsComoboxControlFrame::Reflow updates the selected text. AddOption / + // RemoveOption / etc takes care of keeping the displayed index up to date. + ps->FrameNeedsReflow(f, IntrinsicDirty::FrameAncestorsAndDescendants, + NS_FRAME_IS_DIRTY); +#ifdef ACCESSIBILITY + if (nsAccessibilityService* acc = GetAccService()) { + acc->ScheduleAccessibilitySubtreeUpdate(ps, mElement); + } +#endif + } +} + +void HTMLSelectEventListener::FireOnInputAndOnChange() { + RefPtr element = mElement; + element->UserFinishedInteracting(/* aChanged = */ true); +} + +static void FireDropDownEvent(HTMLSelectElement* aElement, bool aShow, + bool aIsSourceTouchEvent) { + const auto eventName = [&] { + if (aShow) { + return aIsSourceTouchEvent ? u"mozshowdropdown-sourcetouch"_ns + : u"mozshowdropdown"_ns; + } + return u"mozhidedropdown"_ns; + }(); + nsContentUtils::DispatchChromeEvent(aElement->OwnerDoc(), aElement, eventName, + CanBubble::eYes, Cancelable::eNo); +} + +nsresult HTMLSelectEventListener::MouseDown(dom::Event* aMouseEvent) { + NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null."); + + MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent(); + NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE); + + if (mElement->State().HasState(ElementState::DISABLED)) { + return NS_OK; + } + + // only allow selection with the left button + // if a right button click is on the combobox itself + // or on the select when in listbox mode, then let the click through + const bool isLeftButton = mouseEvent->Button() == 0; + if (!isLeftButton) { + return NS_OK; + } + + if (mIsCombobox) { + uint16_t inputSource = mouseEvent->InputSource(); + if (mElement->OpenInParentProcess()) { + nsCOMPtr target = do_QueryInterface(aMouseEvent->GetTarget()); + if (target && target->IsHTMLElement(nsGkAtoms::option)) { + return NS_OK; + } + } + + const bool isSourceTouchEvent = + inputSource == MouseEvent_Binding::MOZ_SOURCE_TOUCH; + FireDropDownEvent(mElement, !mElement->OpenInParentProcess(), + isSourceTouchEvent); + return NS_OK; + } + + if (nsListControlFrame* list = GetListControlFrame()) { + mButtonDown = true; + return list->HandleLeftButtonMouseDown(aMouseEvent); + } + return NS_OK; +} + +nsresult HTMLSelectEventListener::MouseUp(dom::Event* aMouseEvent) { + NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null."); + + MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent(); + NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE); + + mButtonDown = false; + + if (mElement->State().HasState(ElementState::DISABLED)) { + return NS_OK; + } + + if (nsListControlFrame* lf = GetListControlFrame()) { + lf->CaptureMouseEvents(false); + } + + // only allow selection with the left button + // if a right button click is on the combobox itself + // or on the select when in listbox mode, then let the click through + const bool isLeftButton = mouseEvent->Button() == 0; + if (!isLeftButton) { + return NS_OK; + } + + if (nsListControlFrame* lf = GetListControlFrame()) { + return lf->HandleLeftButtonMouseUp(aMouseEvent); + } + + return NS_OK; +} + +nsresult HTMLSelectEventListener::MouseMove(dom::Event* aMouseEvent) { + NS_ASSERTION(aMouseEvent != nullptr, "aMouseEvent is null."); + + MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent(); + NS_ENSURE_TRUE(mouseEvent, NS_ERROR_FAILURE); + + if (!mButtonDown) { + return NS_OK; + } + + if (nsListControlFrame* lf = GetListControlFrame()) { + return lf->DragMove(aMouseEvent); + } + + return NS_OK; +} + +nsresult HTMLSelectEventListener::KeyPress(dom::Event* aKeyEvent) { + MOZ_ASSERT(aKeyEvent, "aKeyEvent is null."); + + if (mElement->State().HasState(ElementState::DISABLED)) { + return NS_OK; + } + + AutoIncrementalSearchHandler incrementalHandler(*this); + + const WidgetKeyboardEvent* keyEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + MOZ_ASSERT(keyEvent, + "DOM event must have WidgetKeyboardEvent for its internal event"); + + // Select option with this as the first character + // XXX Not I18N compliant + + // Don't do incremental search if the key event has already consumed. + if (keyEvent->DefaultPrevented()) { + return NS_OK; + } + + if (keyEvent->IsAlt()) { + return NS_OK; + } + + // With some keyboard layout, space key causes non-ASCII space. + // So, the check in keydown event handler isn't enough, we need to check it + // again with keypress event. + if (keyEvent->mCharCode != ' ') { + mControlSelectMode = false; + } + + const bool isControlOrMeta = + keyEvent->IsControl() +#if !defined(XP_WIN) && !defined(MOZ_WIDGET_GTK) + // Ignore Windows Logo key press in Win/Linux because it's not a usual + // modifier for applications. Here wants to check "Accel" like modifier. + || keyEvent->IsMeta() +#endif + ; + if (isControlOrMeta && keyEvent->mCharCode != ' ') { + return NS_OK; + } + + // NOTE: If mKeyCode of keypress event is not 0, mCharCode is always 0. + // Therefore, all non-printable keys are not handled after this block. + if (!keyEvent->mCharCode) { + // Backspace key will delete the last char in the string. Otherwise, + // non-printable keypress should reset incremental search. + if (keyEvent->mKeyCode == NS_VK_BACK) { + incrementalHandler.CancelResetting(); + if (!GetIncrementalString().IsEmpty()) { + GetIncrementalString().Truncate(GetIncrementalString().Length() - 1); + } + aKeyEvent->PreventDefault(); + } else { + // XXX When a select element has focus, even if the key causes nothing, + // it might be better to call preventDefault() here because nobody + // should expect one of other elements including chrome handles the + // key event. + } + return NS_OK; + } + + incrementalHandler.CancelResetting(); + + // We ate the key if we got this far. + aKeyEvent->PreventDefault(); + + // XXX Why don't we check/modify timestamp first? + + // Incremental Search: if time elapsed is below + // ui.menu.incremental_search.timeout, append this keystroke to the search + // string we will use to find options and start searching at the current + // keystroke. Otherwise, Truncate the string if it's been a long time + // since our last keypress. + if ((keyEvent->mTimeStamp - gLastKeyTime).ToMilliseconds() > + StaticPrefs::ui_menu_incremental_search_timeout()) { + // If this is ' ' and we are at the beginning of the string, treat it as + // "select this option" (bug 191543) + if (keyEvent->mCharCode == ' ') { + // Actually process the new index and let the selection code + // do the scrolling for us + PostHandleKeyEvent(GetEndSelectionIndex(), keyEvent->mCharCode, + keyEvent->IsShift(), isControlOrMeta); + + return NS_OK; + } + + GetIncrementalString().Truncate(); + } + + gLastKeyTime = keyEvent->mTimeStamp; + + // Append this keystroke to the search string. + char16_t uniChar = ToLowerCase(static_cast(keyEvent->mCharCode)); + GetIncrementalString().Append(uniChar); + + // See bug 188199, if all letters in incremental string are same, just try to + // match the first one + nsAutoString incrementalString(GetIncrementalString()); + uint32_t charIndex = 1, stringLength = incrementalString.Length(); + while (charIndex < stringLength && + incrementalString[charIndex] == incrementalString[charIndex - 1]) { + charIndex++; + } + if (charIndex == stringLength) { + incrementalString.Truncate(1); + stringLength = 1; + } + + // Determine where we're going to start reading the string + // If we have multiple characters to look for, we start looking *at* the + // current option. If we have only one character to look for, we start + // looking *after* the current option. + // Exception: if there is no option selected to start at, we always start + // *at* 0. + int32_t startIndex = mElement->SelectedIndex(); + if (startIndex == kNothingSelected) { + startIndex = 0; + } else if (stringLength == 1) { + startIndex++; + } + + // now make sure there are options or we are wasting our time + RefPtr options = mElement->Options(); + uint32_t numOptions = options->Length(); + + for (uint32_t i = 0; i < numOptions; ++i) { + uint32_t index = (i + startIndex) % numOptions; + RefPtr optionElement = options->ItemAsOption(index); + if (!optionElement || !::IsOptionInteractivelySelectable( + *mElement, *optionElement, mIsCombobox)) { + continue; + } + + nsAutoString text; + optionElement->GetRenderedLabel(text); + if (!StringBeginsWith( + nsContentUtils::TrimWhitespace< + nsContentUtils::IsHTMLWhitespaceOrNBSP>(text, false), + incrementalString, nsCaseInsensitiveStringComparator)) { + continue; + } + + if (mIsCombobox) { + if (optionElement->Selected()) { + return NS_OK; + } + optionElement->SetSelected(true); + FireOnInputAndOnChange(); + return NS_OK; + } + + if (nsListControlFrame* lf = GetListControlFrame()) { + bool wasChanged = + lf->PerformSelection(index, keyEvent->IsShift(), isControlOrMeta); + if (!wasChanged) { + return NS_OK; + } + FireOnInputAndOnChange(); + } + break; + } + + return NS_OK; +} + +nsresult HTMLSelectEventListener::KeyDown(dom::Event* aKeyEvent) { + MOZ_ASSERT(aKeyEvent, "aKeyEvent is null."); + + if (mElement->State().HasState(ElementState::DISABLED)) { + return NS_OK; + } + + AutoIncrementalSearchHandler incrementalHandler(*this); + + if (aKeyEvent->DefaultPrevented()) { + return NS_OK; + } + + const WidgetKeyboardEvent* keyEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + MOZ_ASSERT(keyEvent, + "DOM event must have WidgetKeyboardEvent for its internal event"); + + bool dropDownMenuOnUpDown; + bool dropDownMenuOnSpace; +#ifdef XP_MACOSX + dropDownMenuOnUpDown = mIsCombobox && !mElement->OpenInParentProcess(); + dropDownMenuOnSpace = mIsCombobox && !keyEvent->IsAlt() && + !keyEvent->IsControl() && !keyEvent->IsMeta(); +#else + dropDownMenuOnUpDown = mIsCombobox && keyEvent->IsAlt(); + dropDownMenuOnSpace = mIsCombobox && !mElement->OpenInParentProcess(); +#endif + bool withinIncrementalSearchTime = + (keyEvent->mTimeStamp - gLastKeyTime).ToMilliseconds() <= + StaticPrefs::ui_menu_incremental_search_timeout(); + if ((dropDownMenuOnUpDown && + (keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_DOWN)) || + (dropDownMenuOnSpace && keyEvent->mKeyCode == NS_VK_SPACE && + !withinIncrementalSearchTime)) { + FireDropDownEvent(mElement, !mElement->OpenInParentProcess(), false); + aKeyEvent->PreventDefault(); + return NS_OK; + } + if (keyEvent->IsAlt()) { + return NS_OK; + } + + // We should not change the selection if the popup is "opened in the parent + // process" (even when we're in single-process mode). + const bool shouldSelect = !mIsCombobox || !mElement->OpenInParentProcess(); + + // now make sure there are options or we are wasting our time + RefPtr options = mElement->Options(); + uint32_t numOptions = options->Length(); + + // this is the new index to set + int32_t newIndex = kNothingSelected; + + bool isControlOrMeta = + keyEvent->IsControl() +#if !defined(XP_WIN) && !defined(MOZ_WIDGET_GTK) + // Ignore Windows Logo key press in Win/Linux because it's not a usual + // modifier for applications. Here wants to check "Accel" like modifier. + || keyEvent->IsMeta() +#endif + ; + // Don't try to handle multiple-select pgUp/pgDown in single-select lists. + if (isControlOrMeta && !mElement->Multiple() && + (keyEvent->mKeyCode == NS_VK_PAGE_UP || + keyEvent->mKeyCode == NS_VK_PAGE_DOWN)) { + return NS_OK; + } + if (isControlOrMeta && + (keyEvent->mKeyCode == NS_VK_UP || keyEvent->mKeyCode == NS_VK_LEFT || + keyEvent->mKeyCode == NS_VK_DOWN || keyEvent->mKeyCode == NS_VK_RIGHT || + keyEvent->mKeyCode == NS_VK_HOME || keyEvent->mKeyCode == NS_VK_END)) { + // Don't go into multiple-select mode unless this list can handle it. + isControlOrMeta = mControlSelectMode = mElement->Multiple(); + } else if (keyEvent->mKeyCode != NS_VK_SPACE) { + mControlSelectMode = false; + } + + switch (keyEvent->mKeyCode) { + case NS_VK_UP: + case NS_VK_LEFT: + if (shouldSelect) { + AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex, + int32_t(numOptions), -1, -1); + } + break; + case NS_VK_DOWN: + case NS_VK_RIGHT: + if (shouldSelect) { + AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex, + int32_t(numOptions), 1, 1); + } + break; + case NS_VK_RETURN: + // If this is single select listbox, Enter key doesn't cause anything. + if (!mElement->Multiple()) { + return NS_OK; + } + + newIndex = GetEndSelectionIndex(); + break; + case NS_VK_PAGE_UP: { + if (shouldSelect) { + AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex, + int32_t(numOptions), -ItemsPerPage(), -1); + } + break; + } + case NS_VK_PAGE_DOWN: { + if (shouldSelect) { + AdjustIndexForDisabledOpt(GetEndSelectionIndex(), newIndex, + int32_t(numOptions), ItemsPerPage(), 1); + } + break; + } + case NS_VK_HOME: + if (shouldSelect) { + AdjustIndexForDisabledOpt(0, newIndex, int32_t(numOptions), 0, 1); + } + break; + case NS_VK_END: + if (shouldSelect) { + AdjustIndexForDisabledOpt(int32_t(numOptions) - 1, newIndex, + int32_t(numOptions), 0, -1); + } + break; + default: // printable key will be handled by keypress event. + incrementalHandler.CancelResetting(); + return NS_OK; + } + + aKeyEvent->PreventDefault(); + + // Actually process the new index and let the selection code + // do the scrolling for us + PostHandleKeyEvent(newIndex, 0, keyEvent->IsShift(), isControlOrMeta); + return NS_OK; +} + +HTMLOptionElement* HTMLSelectEventListener::GetCurrentOption() const { + // The mEndSelectionIndex is what is currently being selected. Use + // the selected index if this is kNothingSelected. + int32_t endIndex = GetEndSelectionIndex(); + int32_t focusedIndex = + endIndex == kNothingSelected ? mElement->SelectedIndex() : endIndex; + if (focusedIndex != kNothingSelected) { + return mElement->Item(AssertedCast(focusedIndex)); + } + + // There is no selected option. Return the first non-disabled option, if any. + return GetNonDisabledOptionFrom(0); +} + +HTMLOptionElement* HTMLSelectEventListener::GetNonDisabledOptionFrom( + int32_t aFromIndex, int32_t* aFoundIndex) const { + const uint32_t length = mElement->Length(); + for (uint32_t i = std::max(aFromIndex, 0); i < length; ++i) { + if (IsOptionInteractivelySelectable(i)) { + if (aFoundIndex) { + *aFoundIndex = i; + } + return mElement->Item(i); + } + } + return nullptr; +} + +void HTMLSelectEventListener::PostHandleKeyEvent(int32_t aNewIndex, + uint32_t aCharCode, + bool aIsShift, + bool aIsControlOrMeta) { + if (aNewIndex == kNothingSelected) { + int32_t endIndex = GetEndSelectionIndex(); + int32_t focusedIndex = + endIndex == kNothingSelected ? mElement->SelectedIndex() : endIndex; + if (focusedIndex != kNothingSelected) { + return; + } + // No options are selected. In this case the focus ring is on the first + // non-disabled option (if any), so we should behave as if that's the option + // the user acted on. + if (!GetNonDisabledOptionFrom(0, &aNewIndex)) { + return; + } + } + + if (mIsCombobox) { + RefPtr newOption = mElement->Item(aNewIndex); + MOZ_ASSERT(newOption); + if (newOption->Selected()) { + return; + } + newOption->SetSelected(true); + FireOnInputAndOnChange(); + return; + } + if (nsListControlFrame* lf = GetListControlFrame()) { + lf->UpdateSelectionAfterKeyEvent(aNewIndex, aCharCode, aIsShift, + aIsControlOrMeta, mControlSelectMode); + } +} + +} // namespace mozilla diff --git a/layout/forms/HTMLSelectEventListener.h b/layout/forms/HTMLSelectEventListener.h new file mode 100644 index 0000000000..452610a217 --- /dev/null +++ b/layout/forms/HTMLSelectEventListener.h @@ -0,0 +1,103 @@ +/* -*- 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_HTMLSelectEventListener_h +#define mozilla_HTMLSelectEventListener_h + +#include "nsIDOMEventListener.h" +#include "nsStubMutationObserver.h" + +class nsIFrame; +class nsListControlFrame; + +namespace mozilla { + +namespace dom { +class HTMLSelectElement; +class HTMLOptionElement; +class Event; +} // namespace dom + +/** + * HTMLSelectEventListener + * This class is responsible for propagating events to the select element while + * it has a frame. + * Frames are not refcounted so they can't be used as event listeners. + */ + +class HTMLSelectEventListener final : public nsStubMutationObserver, + public nsIDOMEventListener { + public: + enum class SelectType : uint8_t { Listbox, Combobox }; + HTMLSelectEventListener(dom::HTMLSelectElement& aElement, + SelectType aSelectType) + : mElement(&aElement), mIsCombobox(aSelectType == SelectType::Combobox) { + Attach(); + } + + NS_DECL_ISUPPORTS + + // For comboboxes, we need to keep the list up to date when options change. + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + + // nsIDOMEventListener + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD HandleEvent(dom::Event*) override; + + void Attach(); + void Detach(); + + dom::HTMLOptionElement* GetCurrentOption() const; + + MOZ_CAN_RUN_SCRIPT void FireOnInputAndOnChange(); + + private: + // This is always guaranteed to be > 0, but callers want signed integers so we + // do the cast for them. + int32_t ItemsPerPage() const; + + nsListControlFrame* GetListControlFrame() const; + + MOZ_CAN_RUN_SCRIPT nsresult KeyDown(dom::Event*); + MOZ_CAN_RUN_SCRIPT nsresult KeyPress(dom::Event*); + MOZ_CAN_RUN_SCRIPT nsresult MouseDown(dom::Event*); + MOZ_CAN_RUN_SCRIPT nsresult MouseUp(dom::Event*); + MOZ_CAN_RUN_SCRIPT nsresult MouseMove(dom::Event*); + + void AdjustIndexForDisabledOpt(int32_t aStartIndex, int32_t& aNewIndex, + int32_t aNumOptions, int32_t aDoAdjustInc, + int32_t aDoAdjustIncNext); + bool IsOptionInteractivelySelectable(uint32_t aIndex) const; + int32_t GetEndSelectionIndex() const; + + MOZ_CAN_RUN_SCRIPT + void PostHandleKeyEvent(int32_t aNewIndex, uint32_t aCharCode, bool aIsShift, + bool aIsControlOrMeta); + + /** + * Return the first non-disabled option starting at aFromIndex (inclusive). + * @param aFoundIndex if non-null, set to the index of the returned option + */ + dom::HTMLOptionElement* GetNonDisabledOptionFrom( + int32_t aFromIndex, int32_t* aFoundIndex = nullptr) const; + + void ComboboxMightHaveChanged(); + void OptionValueMightHaveChanged(nsIContent* aMutatingNode); + + ~HTMLSelectEventListener(); + + RefPtr mElement; + const bool mIsCombobox; + bool mButtonDown = false; + bool mControlSelectMode = false; +}; + +} // namespace mozilla + +#endif // mozilla_HTMLSelectEventListener_h diff --git a/layout/forms/ListMutationObserver.cpp b/layout/forms/ListMutationObserver.cpp new file mode 100644 index 0000000000..13f9e367a0 --- /dev/null +++ b/layout/forms/ListMutationObserver.cpp @@ -0,0 +1,92 @@ +/* -*- 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 "ListMutationObserver.h" + +#include "mozilla/dom/HTMLInputElement.h" +#include "nsIFrame.h" + +namespace mozilla { +NS_IMPL_ISUPPORTS(ListMutationObserver, nsIMutationObserver) + +ListMutationObserver::~ListMutationObserver() = default; + +void ListMutationObserver::Attach(bool aRepaint) { + nsAutoString id; + if (InputElement().GetAttr(nsGkAtoms::list_, id)) { + Unlink(); + RefPtr idAtom = NS_AtomizeMainThread(id); + ResetWithID(InputElement(), idAtom); + AddObserverIfNeeded(); + } + if (aRepaint) { + mOwningElementFrame->InvalidateFrame(); + } +} + +void ListMutationObserver::AddObserverIfNeeded() { + if (auto* list = get()) { + if (list->IsHTMLElement(nsGkAtoms::datalist)) { + list->AddMutationObserver(this); + } + } +} + +void ListMutationObserver::RemoveObserverIfNeeded(dom::Element* aList) { + if (aList && aList->IsHTMLElement(nsGkAtoms::datalist)) { + aList->RemoveMutationObserver(this); + } +} + +void ListMutationObserver::Detach() { + RemoveObserverIfNeeded(); + Unlink(); +} + +dom::HTMLInputElement& ListMutationObserver::InputElement() const { + MOZ_ASSERT(mOwningElementFrame->GetContent()->IsHTMLElement(nsGkAtoms::input), + "bad cast"); + return *static_cast( + mOwningElementFrame->GetContent()); +} + +void ListMutationObserver::AttributeChanged(dom::Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue) { + if (aAttribute == nsGkAtoms::value && aNameSpaceID == kNameSpaceID_None && + aElement->IsHTMLElement(nsGkAtoms::option)) { + mOwningElementFrame->InvalidateFrame(); + } +} + +void ListMutationObserver::CharacterDataChanged( + nsIContent* aContent, const CharacterDataChangeInfo& aInfo) { + mOwningElementFrame->InvalidateFrame(); +} + +void ListMutationObserver::ContentAppended(nsIContent* aFirstNewContent) { + mOwningElementFrame->InvalidateFrame(); +} + +void ListMutationObserver::ContentInserted(nsIContent* aChild) { + mOwningElementFrame->InvalidateFrame(); +} + +void ListMutationObserver::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + mOwningElementFrame->InvalidateFrame(); +} + +void ListMutationObserver::ElementChanged(dom::Element* aFrom, + dom::Element* aTo) { + IDTracker::ElementChanged(aFrom, aTo); + RemoveObserverIfNeeded(aFrom); + AddObserverIfNeeded(); + mOwningElementFrame->InvalidateFrame(); +} + +} // namespace mozilla diff --git a/layout/forms/ListMutationObserver.h b/layout/forms/ListMutationObserver.h new file mode 100644 index 0000000000..8c81077fb6 --- /dev/null +++ b/layout/forms/ListMutationObserver.h @@ -0,0 +1,67 @@ +/* -*- 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_ListMutationObserver_h +#define mozilla_ListMutationObserver_h + +#include "IDTracker.h" +#include "nsStubMutationObserver.h" + +class nsIFrame; + +namespace mozilla { + +namespace dom { +class HTMLInputElement; +} // namespace dom + +/** + * ListMutationObserver + * This class invalidates paint for the input element's frame when the content + * of its @list changes, or when the @list ID identifies a different element. It + * does *not* invalidate paint when the @list attribute itself changes. + */ + +class ListMutationObserver final : public nsStubMutationObserver, + public dom::IDTracker { + public: + explicit ListMutationObserver(nsIFrame& aOwningElementFrame, + bool aRepaint = false) + : mOwningElementFrame(&aOwningElementFrame) { + // We can skip invalidating paint if the frame is still being initialized. + Attach(aRepaint); + } + + NS_DECL_ISUPPORTS + + // We need to invalidate paint when the list or its options change. + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + /** + * Triggered when the same @list ID identifies a different element than + * before. + */ + void ElementChanged(dom::Element* aFrom, dom::Element* aTo) override; + + void Attach(bool aRepaint = true); + void Detach(); + void AddObserverIfNeeded(); + void RemoveObserverIfNeeded(dom::Element* aList); + void RemoveObserverIfNeeded() { RemoveObserverIfNeeded(get()); } + dom::HTMLInputElement& InputElement() const; + + private: + ~ListMutationObserver(); + + nsIFrame* mOwningElementFrame; +}; +} // namespace mozilla + +#endif // mozilla_ListMutationObserver_h diff --git a/layout/forms/crashtests/1102791.html b/layout/forms/crashtests/1102791.html new file mode 100644 index 0000000000..0fd64d7553 --- /dev/null +++ b/layout/forms/crashtests/1102791.html @@ -0,0 +1,33 @@ + + + + Testcase for bug 1102791 + + + + + + + + diff --git a/layout/forms/crashtests/1140216.html b/layout/forms/crashtests/1140216.html new file mode 100644 index 0000000000..72612b8b3c --- /dev/null +++ b/layout/forms/crashtests/1140216.html @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/layout/forms/crashtests/1182414.html b/layout/forms/crashtests/1182414.html new file mode 100644 index 0000000000..3aaa0bbfcc --- /dev/null +++ b/layout/forms/crashtests/1182414.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/layout/forms/crashtests/1212688.html b/layout/forms/crashtests/1212688.html new file mode 100644 index 0000000000..68262d907f --- /dev/null +++ b/layout/forms/crashtests/1212688.html @@ -0,0 +1,27 @@ + + diff --git a/layout/forms/crashtests/1228670.xhtml b/layout/forms/crashtests/1228670.xhtml new file mode 100644 index 0000000000..cc8d309c0e --- /dev/null +++ b/layout/forms/crashtests/1228670.xhtml @@ -0,0 +1,7 @@ + + + + + diff --git a/layout/forms/crashtests/1279354.html b/layout/forms/crashtests/1279354.html new file mode 100644 index 0000000000..5013392526 --- /dev/null +++ b/layout/forms/crashtests/1279354.html @@ -0,0 +1,21 @@ + + + + + Testcase for bug 1279354 + + + +
+1 +
+2 +
+3 +
+ + + diff --git a/layout/forms/crashtests/1388230-1.html b/layout/forms/crashtests/1388230-1.html new file mode 100644 index 0000000000..6662a6ef01 --- /dev/null +++ b/layout/forms/crashtests/1388230-1.html @@ -0,0 +1,3 @@ + + + diff --git a/layout/forms/crashtests/1388230-2.html b/layout/forms/crashtests/1388230-2.html new file mode 100644 index 0000000000..630031d750 --- /dev/null +++ b/layout/forms/crashtests/1388230-2.html @@ -0,0 +1 @@ + diff --git a/layout/forms/crashtests/1405830.html b/layout/forms/crashtests/1405830.html new file mode 100644 index 0000000000..c16eeca86a --- /dev/null +++ b/layout/forms/crashtests/1405830.html @@ -0,0 +1,19 @@ + + + + + + +0 +Q +- +w +g +k diff --git a/layout/forms/crashtests/1418477.html b/layout/forms/crashtests/1418477.html new file mode 100644 index 0000000000..0be4732d58 --- /dev/null +++ b/layout/forms/crashtests/1418477.html @@ -0,0 +1,4 @@ + + + + diff --git a/layout/forms/crashtests/1432853.html b/layout/forms/crashtests/1432853.html new file mode 100644 index 0000000000..bc0735c47f --- /dev/null +++ b/layout/forms/crashtests/1432853.html @@ -0,0 +1,8 @@ + +
+

+ + + diff --git a/layout/forms/crashtests/1488219.html b/layout/forms/crashtests/1488219.html new file mode 100644 index 0000000000..d10d3ef014 --- /dev/null +++ b/layout/forms/crashtests/1488219.html @@ -0,0 +1,26 @@ + + + + + + +A +
+
+
+
+
+ + diff --git a/layout/forms/crashtests/1600207.html b/layout/forms/crashtests/1600207.html new file mode 100644 index 0000000000..a54a0d0c7d --- /dev/null +++ b/layout/forms/crashtests/1600207.html @@ -0,0 +1,9 @@ + +
+ +

diff --git a/layout/forms/crashtests/1600367.html b/layout/forms/crashtests/1600367.html new file mode 100644 index 0000000000..7a481f814e --- /dev/null +++ b/layout/forms/crashtests/1600367.html @@ -0,0 +1,8 @@ + + +
diff --git a/layout/forms/crashtests/1617753.html b/layout/forms/crashtests/1617753.html new file mode 100644 index 0000000000..213c483451 --- /dev/null +++ b/layout/forms/crashtests/1617753.html @@ -0,0 +1,21 @@ + +
+
+ + diff --git a/layout/forms/crashtests/166750-1.html b/layout/forms/crashtests/166750-1.html new file mode 100644 index 0000000000..d873be1b29 --- /dev/null +++ b/layout/forms/crashtests/166750-1.html @@ -0,0 +1,14 @@ + + + +Killer + + + + + + \ No newline at end of file diff --git a/layout/forms/crashtests/1679471.html b/layout/forms/crashtests/1679471.html new file mode 100644 index 0000000000..8d2bbc8eb0 --- /dev/null +++ b/layout/forms/crashtests/1679471.html @@ -0,0 +1,8 @@ + + +
+ + + +h45@|V+J)
+

+N=y)sO6 +

+ + +

+ + + + + + + +
    +
  1. +
+
+

+#]76[#<I%!"Rrs.wL + +}} + + +k9 +
6,$.MxUA<F
+ + + + + + diff --git a/layout/forms/crashtests/200347-1.html b/layout/forms/crashtests/200347-1.html new file mode 100644 index 0000000000..b41ec8365a --- /dev/null +++ b/layout/forms/crashtests/200347-1.html @@ -0,0 +1,8 @@ + + +
+ Crash test +
Hello, my name is Inigo Montoya.
hello world content is the best content around, I love hello world content to death, especially when it wraps; that just gives me the chills. Anything less than hello world content is uncivilized. +
+ + diff --git a/layout/forms/crashtests/203041-1.html b/layout/forms/crashtests/203041-1.html new file mode 100644 index 0000000000..32d95b40e6 --- /dev/null +++ b/layout/forms/crashtests/203041-1.html @@ -0,0 +1,24 @@ + + + + + + + +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/layout/forms/crashtests/213390-1.html b/layout/forms/crashtests/213390-1.html new file mode 100644 index 0000000000..0ff94ec542 --- /dev/null +++ b/layout/forms/crashtests/213390-1.html @@ -0,0 +1,23 @@ + + + + + + + +
W9A1."\84gtu6%d
+ + + +
+ 1 +
+ +
+
+ + + diff --git a/layout/forms/crashtests/266225-1.html b/layout/forms/crashtests/266225-1.html new file mode 100644 index 0000000000..c714125dd5 --- /dev/null +++ b/layout/forms/crashtests/266225-1.html @@ -0,0 +1,7 @@ + + + + +
Test
+ + diff --git a/layout/forms/crashtests/310426-1.xhtml b/layout/forms/crashtests/310426-1.xhtml new file mode 100644 index 0000000000..683dba679f --- /dev/null +++ b/layout/forms/crashtests/310426-1.xhtml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/layout/forms/crashtests/310520-1.xhtml b/layout/forms/crashtests/310520-1.xhtml new file mode 100644 index 0000000000..6e111832a4 --- /dev/null +++ b/layout/forms/crashtests/310520-1.xhtml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/layout/forms/crashtests/315752-1.xhtml b/layout/forms/crashtests/315752-1.xhtml new file mode 100644 index 0000000000..66d3d793d7 --- /dev/null +++ b/layout/forms/crashtests/315752-1.xhtml @@ -0,0 +1,21 @@ + + + + +Settings - Unclassified NewsBoard + + + + + + + + + \ No newline at end of file diff --git a/layout/forms/crashtests/317502-1.xhtml b/layout/forms/crashtests/317502-1.xhtml new file mode 100644 index 0000000000..7ef5f0b4c1 --- /dev/null +++ b/layout/forms/crashtests/317502-1.xhtml @@ -0,0 +1,13 @@ + + + + + Testcase, bug 317502 + + + + + + + diff --git a/layout/forms/crashtests/321894.html b/layout/forms/crashtests/321894.html new file mode 100644 index 0000000000..0a82645ba4 --- /dev/null +++ b/layout/forms/crashtests/321894.html @@ -0,0 +1,17 @@ + + +Testcase bug 321894 - ASSERTION: RemovedAsPrimaryFrame called after PreDestroy: 'PR_FALSE' + + + +This shouldn't assert in a debug build + + +
+ + + +
+
+ + diff --git a/layout/forms/crashtests/343510-1.html b/layout/forms/crashtests/343510-1.html new file mode 100644 index 0000000000..00cb94f211 --- /dev/null +++ b/layout/forms/crashtests/343510-1.html @@ -0,0 +1,4 @@ + + + + + \ No newline at end of file diff --git a/layout/forms/crashtests/363696-2.html b/layout/forms/crashtests/363696-2.html new file mode 100644 index 0000000000..9d93edd185 --- /dev/null +++ b/layout/forms/crashtests/363696-2.html @@ -0,0 +1,2 @@ +
+ diff --git a/layout/forms/crashtests/363696-3.html b/layout/forms/crashtests/363696-3.html new file mode 100644 index 0000000000..e1d38076ed --- /dev/null +++ b/layout/forms/crashtests/363696-3.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/layout/forms/crashtests/366205-1.html b/layout/forms/crashtests/366205-1.html new file mode 100644 index 0000000000..2a3786afe0 --- /dev/null +++ b/layout/forms/crashtests/366205-1.html @@ -0,0 +1,11 @@ + + + + + +
Clicking the button below should not trigger an assertion
+ + + + + \ No newline at end of file diff --git a/layout/forms/crashtests/366537-1.xhtml b/layout/forms/crashtests/366537-1.xhtml new file mode 100644 index 0000000000..b799cd2ca2 --- /dev/null +++ b/layout/forms/crashtests/366537-1.xhtml @@ -0,0 +1,32 @@ + + + + + + + + + +legend + +

H3

+ + + + + + diff --git a/layout/forms/crashtests/367587-1.html b/layout/forms/crashtests/367587-1.html new file mode 100644 index 0000000000..9bffe569ed --- /dev/null +++ b/layout/forms/crashtests/367587-1.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/layout/forms/crashtests/370703-1.html b/layout/forms/crashtests/370703-1.html new file mode 100644 index 0000000000..b447b180b5 --- /dev/null +++ b/layout/forms/crashtests/370703-1.html @@ -0,0 +1,30 @@ + + + + + + +
+ + + +
+ + + +
+ + + diff --git a/layout/forms/crashtests/370940-1.html b/layout/forms/crashtests/370940-1.html new file mode 100644 index 0000000000..ccaeb9643b --- /dev/null +++ b/layout/forms/crashtests/370940-1.html @@ -0,0 +1,28 @@ + + + + + +

العربي

+ + + diff --git a/layout/forms/crashtests/370967.html b/layout/forms/crashtests/370967.html new file mode 100644 index 0000000000..be7854b41c --- /dev/null +++ b/layout/forms/crashtests/370967.html @@ -0,0 +1,13 @@ + + + + +Untitled + + +Focus the input of the isindex, then do a reload, Mozilla should not crash + + + + + diff --git a/layout/forms/crashtests/378369.html b/layout/forms/crashtests/378369.html new file mode 100644 index 0000000000..90327735ef --- /dev/null +++ b/layout/forms/crashtests/378369.html @@ -0,0 +1,19 @@ + + +Testcase bug - Crash [@ nsEventListenerManager::FixContextMenuEvent] when firing contextmenu event in display: none iframe + + +This page should not crash Mozilla + + + + + diff --git a/layout/forms/crashtests/380116-1.xhtml b/layout/forms/crashtests/380116-1.xhtml new file mode 100644 index 0000000000..cb37ff079a --- /dev/null +++ b/layout/forms/crashtests/380116-1.xhtml @@ -0,0 +1,11 @@ + + + + + + + +
+ + + diff --git a/layout/forms/crashtests/382610-1.html b/layout/forms/crashtests/382610-1.html new file mode 100644 index 0000000000..9fe9c5b5c1 --- /dev/null +++ b/layout/forms/crashtests/382610-1.html @@ -0,0 +1,11 @@ + + + + +
+ +
+ + + + diff --git a/layout/forms/crashtests/383887-1.html b/layout/forms/crashtests/383887-1.html new file mode 100644 index 0000000000..65d4751f7d --- /dev/null +++ b/layout/forms/crashtests/383887-1.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ +
+
+
+ + + diff --git a/layout/forms/crashtests/386554-1.html b/layout/forms/crashtests/386554-1.html new file mode 100644 index 0000000000..585f1e5b54 --- /dev/null +++ b/layout/forms/crashtests/386554-1.html @@ -0,0 +1,14 @@ + + + + + +
+ + + + + diff --git a/layout/forms/crashtests/388374-1.xhtml b/layout/forms/crashtests/388374-1.xhtml new file mode 100644 index 0000000000..2a871f68b4 --- /dev/null +++ b/layout/forms/crashtests/388374-1.xhtml @@ -0,0 +1,22 @@ + + + + + + + + + + + + +
1
+ + + diff --git a/layout/forms/crashtests/388374-2.html b/layout/forms/crashtests/388374-2.html new file mode 100644 index 0000000000..81918c37cd --- /dev/null +++ b/layout/forms/crashtests/388374-2.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + +
+ + + diff --git a/layout/forms/crashtests/393656-1.xhtml b/layout/forms/crashtests/393656-1.xhtml new file mode 100644 index 0000000000..22a9326c5f --- /dev/null +++ b/layout/forms/crashtests/393656-1.xhtml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/layout/forms/crashtests/393656-2.xhtml b/layout/forms/crashtests/393656-2.xhtml new file mode 100644 index 0000000000..7d9adaf20d --- /dev/null +++ b/layout/forms/crashtests/393656-2.xhtml @@ -0,0 +1,22 @@ + + + + + +
+
XXX
+ +
+
+
+
+
+
+
+ + + diff --git a/layout/forms/crashtests/399262.html b/layout/forms/crashtests/399262.html new file mode 100644 index 0000000000..77bc0a16db --- /dev/null +++ b/layout/forms/crashtests/399262.html @@ -0,0 +1,50 @@ + + + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ + + + + + + +
+ + + + + +
+ +
+ + + + + +
+ diff --git a/layout/forms/crashtests/402852-1.html b/layout/forms/crashtests/402852-1.html new file mode 100644 index 0000000000..e5ba9adb13 --- /dev/null +++ b/layout/forms/crashtests/402852-1.html @@ -0,0 +1,2 @@ + +
diff --git a/layout/forms/crashtests/403148-1.html b/layout/forms/crashtests/403148-1.html new file mode 100644 index 0000000000..cfb38fdb13 --- /dev/null +++ b/layout/forms/crashtests/403148-1.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/layout/forms/crashtests/404118-1.html b/layout/forms/crashtests/404118-1.html new file mode 100644 index 0000000000..40a2ac72b0 --- /dev/null +++ b/layout/forms/crashtests/404118-1.html @@ -0,0 +1,5 @@ + + + + + diff --git a/layout/forms/crashtests/404123-1.html b/layout/forms/crashtests/404123-1.html new file mode 100644 index 0000000000..73fbaa363c --- /dev/null +++ b/layout/forms/crashtests/404123-1.html @@ -0,0 +1,12 @@ + + + + + + +
+ +
+ + + diff --git a/layout/forms/crashtests/407066.html b/layout/forms/crashtests/407066.html new file mode 100644 index 0000000000..98c4d0f4ae --- /dev/null +++ b/layout/forms/crashtests/407066.html @@ -0,0 +1 @@ +
text diff --git a/layout/forms/crashtests/451316.html b/layout/forms/crashtests/451316.html new file mode 100644 index 0000000000..51fda22bc0 --- /dev/null +++ b/layout/forms/crashtests/451316.html @@ -0,0 +1,7 @@ + + + +
+
+ + diff --git a/layout/forms/crashtests/455451-1.html b/layout/forms/crashtests/455451-1.html new file mode 100644 index 0000000000..fd7d7b6b37 --- /dev/null +++ b/layout/forms/crashtests/455451-1.html @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/layout/forms/crashtests/457537-1.html b/layout/forms/crashtests/457537-1.html new file mode 100644 index 0000000000..167d3b7076 --- /dev/null +++ b/layout/forms/crashtests/457537-1.html @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/layout/forms/crashtests/457537-2.html b/layout/forms/crashtests/457537-2.html new file mode 100644 index 0000000000..626b035b48 --- /dev/null +++ b/layout/forms/crashtests/457537-2.html @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/layout/forms/crashtests/498698-1.html b/layout/forms/crashtests/498698-1.html new file mode 100644 index 0000000000..fa8e2e17a8 --- /dev/null +++ b/layout/forms/crashtests/498698-1.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/layout/forms/crashtests/513113-1.html b/layout/forms/crashtests/513113-1.html new file mode 100644 index 0000000000..dc04ef6b6e --- /dev/null +++ b/layout/forms/crashtests/513113-1.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/layout/forms/crashtests/538062-1.xhtml b/layout/forms/crashtests/538062-1.xhtml new file mode 100644 index 0000000000..431f7ab847 --- /dev/null +++ b/layout/forms/crashtests/538062-1.xhtml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/layout/forms/crashtests/570624-1.html b/layout/forms/crashtests/570624-1.html new file mode 100644 index 0000000000..47a2777282 --- /dev/null +++ b/layout/forms/crashtests/570624-1.html @@ -0,0 +1,15 @@ + + + + + + + diff --git a/layout/forms/crashtests/578604-1.html b/layout/forms/crashtests/578604-1.html new file mode 100644 index 0000000000..a7e3a870dd --- /dev/null +++ b/layout/forms/crashtests/578604-1.html @@ -0,0 +1,17 @@ + + + + + + + +

Crash Test Case

+
+ +
+ + + diff --git a/layout/forms/crashtests/590302-1.xhtml b/layout/forms/crashtests/590302-1.xhtml new file mode 100644 index 0000000000..85d49cb76c --- /dev/null +++ b/layout/forms/crashtests/590302-1.xhtml @@ -0,0 +1,4 @@ + +
+ + diff --git a/layout/forms/crashtests/626014.xhtml b/layout/forms/crashtests/626014.xhtml new file mode 100644 index 0000000000..05a2361d3d --- /dev/null +++ b/layout/forms/crashtests/626014.xhtml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/layout/forms/crashtests/639733.xhtml b/layout/forms/crashtests/639733.xhtml new file mode 100644 index 0000000000..a6d5b01a8d --- /dev/null +++ b/layout/forms/crashtests/639733.xhtml @@ -0,0 +1,26 @@ + + + + + +
+ + diff --git a/layout/forms/crashtests/669767.html b/layout/forms/crashtests/669767.html new file mode 100644 index 0000000000..f41a9125c2 --- /dev/null +++ b/layout/forms/crashtests/669767.html @@ -0,0 +1,14 @@ + + +Untitled + + + + + + + + diff --git a/layout/forms/crashtests/682684-binding.xml b/layout/forms/crashtests/682684-binding.xml new file mode 100644 index 0000000000..910eec33c8 --- /dev/null +++ b/layout/forms/crashtests/682684-binding.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/layout/forms/crashtests/682684.xhtml b/layout/forms/crashtests/682684.xhtml new file mode 100644 index 0000000000..2b7b2c83fa --- /dev/null +++ b/layout/forms/crashtests/682684.xhtml @@ -0,0 +1,3 @@ + + + diff --git a/layout/forms/crashtests/865602.html b/layout/forms/crashtests/865602.html new file mode 100644 index 0000000000..f560e7a385 --- /dev/null +++ b/layout/forms/crashtests/865602.html @@ -0,0 +1,9 @@ + + + +
+ + +
+ + diff --git a/layout/forms/crashtests/893331.html b/layout/forms/crashtests/893331.html new file mode 100644 index 0000000000..86a01f363e --- /dev/null +++ b/layout/forms/crashtests/893331.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/layout/forms/crashtests/893332-1.html b/layout/forms/crashtests/893332-1.html new file mode 100644 index 0000000000..1cd17c0b1c --- /dev/null +++ b/layout/forms/crashtests/893332-1.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/layout/forms/crashtests/944198.html b/layout/forms/crashtests/944198.html new file mode 100644 index 0000000000..0da7bc54dd --- /dev/null +++ b/layout/forms/crashtests/944198.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/layout/forms/crashtests/949891.xhtml b/layout/forms/crashtests/949891.xhtml new file mode 100644 index 0000000000..8fbfe1e422 --- /dev/null +++ b/layout/forms/crashtests/949891.xhtml @@ -0,0 +1,5 @@ + + + + + diff --git a/layout/forms/crashtests/959311.html b/layout/forms/crashtests/959311.html new file mode 100644 index 0000000000..1b4817221b --- /dev/null +++ b/layout/forms/crashtests/959311.html @@ -0,0 +1,17 @@ + + + + + + + +
+ + + diff --git a/layout/forms/crashtests/960277-2.html b/layout/forms/crashtests/960277-2.html new file mode 100644 index 0000000000..ea771ae798 --- /dev/null +++ b/layout/forms/crashtests/960277-2.html @@ -0,0 +1,14 @@ + + +
+
+
+ diff --git a/layout/forms/crashtests/997709-1.html b/layout/forms/crashtests/997709-1.html new file mode 100644 index 0000000000..baf5d0b14b --- /dev/null +++ b/layout/forms/crashtests/997709-1.html @@ -0,0 +1,5 @@ + +
+
+ +
diff --git a/layout/forms/crashtests/crashtests.list b/layout/forms/crashtests/crashtests.list new file mode 100644 index 0000000000..da43270ba6 --- /dev/null +++ b/layout/forms/crashtests/crashtests.list @@ -0,0 +1,80 @@ +load 166750-1.html +load 200347-1.html +load 203041-1.html +load 213390-1.html +load 258101-1.html +load 266225-1.html +load 310426-1.xhtml +load 310520-1.xhtml +load 315752-1.xhtml +load 317502-1.xhtml +load 321894.html +load 343510-1.html +load chrome://reftest/content/crashtests/layout/forms/crashtests/363696-1.xhtml +load 363696-2.html +load 363696-3.html +load 366205-1.html +load 366537-1.xhtml +load 367587-1.html +load 370703-1.html +load 370940-1.html +load 370967.html +load 378369.html +load 380116-1.xhtml +load 382610-1.html +load 383887-1.html +load 386554-1.html +load 388374-1.xhtml +load 388374-2.html +load 393656-1.xhtml +load 393656-2.xhtml +load 399262.html +load 402852-1.html +load 403148-1.html +load 404118-1.html +load 404123-1.html +load 407066.html +load 451316.html +load 455451-1.html +load 457537-1.html +load 457537-2.html +load 498698-1.html +load 513113-1.html +load 538062-1.xhtml +load 570624-1.html +asserts(1) load 578604-1.html # bug 584564 +asserts(4-7) load 590302-1.xhtml # bug 584564 +load 626014.xhtml +load 639733.xhtml +load 669767.html +load 682684.xhtml +load 865602.html +load 893331.html +load 893332-1.html +load 944198.html +load 949891.xhtml +load 959311.html +load 960277-2.html +load 997709-1.html +load 1102791.html +load 1140216.html +load 1182414.html +load 1212688.html +load 1228670.xhtml +load 1279354.html +load 1388230-1.html +load 1388230-2.html +load 1405830.html +load 1418477.html +load 1432853.html +asserts(1-4) load 1460787-1.html +load 1464165-1.html +load 1471157.html +load 1488219.html +load 1600207.html +load 1600367.html +load 1617753.html +load 1679471.html +load 1690166-1.html +load 1690166-2.html +load 1873802-1.html diff --git a/layout/forms/moz.build b/layout/forms/moz.build new file mode 100644 index 0000000000..838a17bf92 --- /dev/null +++ b/layout/forms/moz.build @@ -0,0 +1,54 @@ +# -*- 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 = ("Core", "Layout: Form Controls") + +MOCHITEST_MANIFESTS += ["test/mochitest.toml"] +MOCHITEST_CHROME_MANIFESTS += ["test/chrome.toml"] + +EXPORTS += [ + "nsIFormControlFrame.h", + "nsISelectControlFrame.h", + "nsITextControlFrame.h", +] + +UNIFIED_SOURCES += [ + "HTMLSelectEventListener.cpp", + "ListMutationObserver.cpp", + "nsButtonFrameRenderer.cpp", + "nsCheckboxRadioFrame.cpp", + "nsColorControlFrame.cpp", + "nsComboboxControlFrame.cpp", + "nsDateTimeControlFrame.cpp", + "nsFieldSetFrame.cpp", + "nsFileControlFrame.cpp", + "nsGfxButtonControlFrame.cpp", + "nsHTMLButtonControlFrame.cpp", + "nsImageControlFrame.cpp", + "nsListControlFrame.cpp", + "nsMeterFrame.cpp", + "nsNumberControlFrame.cpp", + "nsProgressFrame.cpp", + "nsRangeFrame.cpp", + "nsSearchControlFrame.cpp", + "nsSelectsAreaFrame.cpp", + "nsTextControlFrame.cpp", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "../base", + "../generic", + "../painting", + "../style", + "../xul", + "/dom/base", + "/dom/html", +] diff --git a/layout/forms/nsButtonFrameRenderer.cpp b/layout/forms/nsButtonFrameRenderer.cpp new file mode 100644 index 0000000000..598610cf72 --- /dev/null +++ b/layout/forms/nsButtonFrameRenderer.cpp @@ -0,0 +1,471 @@ +/* -*- 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 "nsButtonFrameRenderer.h" +#include "nsCSSRendering.h" +#include "nsPresContext.h" +#include "nsPresContextInlines.h" +#include "nsGkAtoms.h" +#include "nsCSSPseudoElements.h" +#include "nsNameSpaceManager.h" +#include "mozilla/ServoStyleSet.h" +#include "mozilla/Unused.h" +#include "nsDisplayList.h" +#include "nsITheme.h" +#include "nsIFrame.h" +#include "mozilla/dom/Element.h" + +#include "gfxUtils.h" +#include "mozilla/layers/RenderRootStateManager.h" + +using namespace mozilla; +using namespace mozilla::image; +using namespace mozilla::layers; + +namespace mozilla { +class nsDisplayButtonBoxShadowOuter; +class nsDisplayButtonBorder; +class nsDisplayButtonForeground; +} // namespace mozilla + +nsButtonFrameRenderer::nsButtonFrameRenderer() : mFrame(nullptr) { + MOZ_COUNT_CTOR(nsButtonFrameRenderer); +} + +nsButtonFrameRenderer::~nsButtonFrameRenderer() { + MOZ_COUNT_DTOR(nsButtonFrameRenderer); +} + +void nsButtonFrameRenderer::SetFrame(nsIFrame* aFrame, + nsPresContext* aPresContext) { + mFrame = aFrame; + ReResolveStyles(aPresContext); +} + +nsIFrame* nsButtonFrameRenderer::GetFrame() { return mFrame; } + +void nsButtonFrameRenderer::SetDisabled(bool aDisabled, bool aNotify) { + dom::Element* element = mFrame->GetContent()->AsElement(); + if (aDisabled) + element->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, u""_ns, aNotify); + else + element->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, aNotify); +} + +bool nsButtonFrameRenderer::isDisabled() { + return mFrame->GetContent()->AsElement()->IsDisabled(); +} + +nsresult nsButtonFrameRenderer::DisplayButton(nsDisplayListBuilder* aBuilder, + nsDisplayList* aBackground, + nsDisplayList* aForeground) { + if (!mFrame->StyleEffects()->mBoxShadow.IsEmpty()) { + aBackground->AppendNewToTop(aBuilder, + GetFrame()); + } + + nsRect buttonRect = + mFrame->GetRectRelativeToSelf() + aBuilder->ToReferenceFrame(mFrame); + + const AppendedBackgroundType result = + nsDisplayBackgroundImage::AppendBackgroundItemsToTop( + aBuilder, mFrame, buttonRect, aBackground); + if (result == AppendedBackgroundType::None) { + aBuilder->BuildCompositorHitTestInfoIfNeeded(GetFrame(), aBackground); + } + + aBackground->AppendNewToTop(aBuilder, GetFrame(), + this); + + // Only display focus rings if we actually have them. Since at most one + // button would normally display a focus ring, most buttons won't have them. + if (mInnerFocusStyle && mInnerFocusStyle->StyleBorder()->HasBorder() && + mFrame->IsThemed() && + mFrame->PresContext()->Theme()->ThemeWantsButtonInnerFocusRing()) { + aForeground->AppendNewToTop(aBuilder, GetFrame(), + this); + } + return NS_OK; +} + +void nsButtonFrameRenderer::GetButtonInnerFocusRect(const nsRect& aRect, + nsRect& aResult) { + aResult = aRect; + aResult.Deflate(mFrame->GetUsedBorderAndPadding()); + + if (mInnerFocusStyle) { + nsMargin innerFocusPadding(0, 0, 0, 0); + mInnerFocusStyle->StylePadding()->GetPadding(innerFocusPadding); + + nsMargin framePadding = mFrame->GetUsedPadding(); + + innerFocusPadding.top = std::min(innerFocusPadding.top, framePadding.top); + innerFocusPadding.right = + std::min(innerFocusPadding.right, framePadding.right); + innerFocusPadding.bottom = + std::min(innerFocusPadding.bottom, framePadding.bottom); + innerFocusPadding.left = + std::min(innerFocusPadding.left, framePadding.left); + + aResult.Inflate(innerFocusPadding); + } +} + +ImgDrawResult nsButtonFrameRenderer::PaintInnerFocusBorder( + nsDisplayListBuilder* aBuilder, nsPresContext* aPresContext, + gfxContext& aRenderingContext, const nsRect& aDirtyRect, + const nsRect& aRect) { + // we draw the -moz-focus-inner border just inside the button's + // normal border and padding, to match Windows themes. + + nsRect rect; + + PaintBorderFlags flags = aBuilder->ShouldSyncDecodeImages() + ? PaintBorderFlags::SyncDecodeImages + : PaintBorderFlags(); + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + if (mInnerFocusStyle) { + GetButtonInnerFocusRect(aRect, rect); + + result &= + nsCSSRendering::PaintBorder(aPresContext, aRenderingContext, mFrame, + aDirtyRect, rect, mInnerFocusStyle, flags); + } + + return result; +} + +Maybe +nsButtonFrameRenderer::CreateInnerFocusBorderRenderer( + nsDisplayListBuilder* aBuilder, nsPresContext* aPresContext, + gfxContext* aRenderingContext, const nsRect& aDirtyRect, + const nsRect& aRect, bool* aBorderIsEmpty) { + if (mInnerFocusStyle) { + nsRect rect; + GetButtonInnerFocusRect(aRect, rect); + + gfx::DrawTarget* dt = + aRenderingContext ? aRenderingContext->GetDrawTarget() : nullptr; + return nsCSSRendering::CreateBorderRenderer( + aPresContext, dt, mFrame, aDirtyRect, rect, mInnerFocusStyle, + aBorderIsEmpty); + } + + return Nothing(); +} + +ImgDrawResult nsButtonFrameRenderer::PaintBorder(nsDisplayListBuilder* aBuilder, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, + const nsRect& aRect) { + // get the button rect this is inside the focus and outline rects + nsRect buttonRect = aRect; + ComputedStyle* context = mFrame->Style(); + + PaintBorderFlags borderFlags = aBuilder->ShouldSyncDecodeImages() + ? PaintBorderFlags::SyncDecodeImages + : PaintBorderFlags(); + + nsCSSRendering::PaintBoxShadowInner(aPresContext, aRenderingContext, mFrame, + buttonRect); + + ImgDrawResult result = + nsCSSRendering::PaintBorder(aPresContext, aRenderingContext, mFrame, + aDirtyRect, buttonRect, context, borderFlags); + + return result; +} + +/** + * Call this when styles change + */ +void nsButtonFrameRenderer::ReResolveStyles(nsPresContext* aPresContext) { + // get all the styles + ServoStyleSet* styleSet = aPresContext->StyleSet(); + + // get styles assigned to -moz-focus-inner (ie dotted border on Windows) + mInnerFocusStyle = styleSet->ProbePseudoElementStyle( + *mFrame->GetContent()->AsElement(), PseudoStyleType::mozFocusInner, + nullptr, mFrame->Style()); +} + +ComputedStyle* nsButtonFrameRenderer::GetComputedStyle(int32_t aIndex) const { + switch (aIndex) { + case NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX: + return mInnerFocusStyle; + default: + return nullptr; + } +} + +void nsButtonFrameRenderer::SetComputedStyle(int32_t aIndex, + ComputedStyle* aComputedStyle) { + switch (aIndex) { + case NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX: + mInnerFocusStyle = aComputedStyle; + break; + } +} + +namespace mozilla { + +class nsDisplayButtonBoxShadowOuter : public nsPaintedDisplayItem { + public: + nsDisplayButtonBoxShadowOuter(nsDisplayListBuilder* aBuilder, + nsIFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayButtonBoxShadowOuter); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayButtonBoxShadowOuter) + + virtual bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override; + + bool CanBuildWebRenderDisplayItems(); + + virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const override; + NS_DISPLAY_DECL_NAME("ButtonBoxShadowOuter", TYPE_BUTTON_BOX_SHADOW_OUTER) +}; + +nsRect nsDisplayButtonBoxShadowOuter::GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const { + *aSnap = false; + return mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame(); +} + +void nsDisplayButtonBoxShadowOuter::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + nsRect frameRect = nsRect(ToReferenceFrame(), mFrame->GetSize()); + + nsCSSRendering::PaintBoxShadowOuter(mFrame->PresContext(), *aCtx, mFrame, + frameRect, GetPaintRect(aBuilder, aCtx)); +} + +bool nsDisplayButtonBoxShadowOuter::CanBuildWebRenderDisplayItems() { + // FIXME(emilio): Is this right? That doesn't make much sense. + if (mFrame->StyleEffects()->mBoxShadow.IsEmpty()) { + return false; + } + + bool hasBorderRadius; + bool nativeTheme = + nsCSSRendering::HasBoxShadowNativeTheme(mFrame, hasBorderRadius); + + // We don't support native themed things yet like box shadows around + // input buttons. + return !nativeTheme; +} + +bool nsDisplayButtonBoxShadowOuter::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + if (!CanBuildWebRenderDisplayItems()) { + return false; + } + int32_t appUnitsPerDevPixel = mFrame->PresContext()->AppUnitsPerDevPixel(); + nsRect shadowRect = nsRect(ToReferenceFrame(), mFrame->GetSize()); + LayoutDeviceRect deviceBox = + LayoutDeviceRect::FromAppUnits(shadowRect, appUnitsPerDevPixel); + wr::LayoutRect deviceBoxRect = wr::ToLayoutRect(deviceBox); + + bool dummy; + LayoutDeviceRect clipRect = LayoutDeviceRect::FromAppUnits( + GetBounds(aDisplayListBuilder, &dummy), appUnitsPerDevPixel); + wr::LayoutRect deviceClipRect = wr::ToLayoutRect(clipRect); + + bool hasBorderRadius; + Unused << nsCSSRendering::HasBoxShadowNativeTheme(mFrame, hasBorderRadius); + + LayoutDeviceSize zeroSize; + wr::BorderRadius borderRadius = + wr::ToBorderRadius(zeroSize, zeroSize, zeroSize, zeroSize); + if (hasBorderRadius) { + gfx::RectCornerRadii borderRadii; + hasBorderRadius = nsCSSRendering::GetBorderRadii(shadowRect, shadowRect, + mFrame, borderRadii); + if (hasBorderRadius) { + borderRadius = wr::ToBorderRadius(borderRadii); + } + } + + const Span shadows = + mFrame->StyleEffects()->mBoxShadow.AsSpan(); + MOZ_ASSERT(!shadows.IsEmpty()); + + for (const StyleBoxShadow& shadow : Reversed(shadows)) { + if (shadow.inset) { + continue; + } + float blurRadius = + float(shadow.base.blur.ToAppUnits()) / float(appUnitsPerDevPixel); + gfx::DeviceColor shadowColor = + ToDeviceColor(nsCSSRendering::GetShadowColor(shadow.base, mFrame, 1.0)); + + LayoutDevicePoint shadowOffset = LayoutDevicePoint::FromAppUnits( + nsPoint(shadow.base.horizontal.ToAppUnits(), + shadow.base.vertical.ToAppUnits()), + appUnitsPerDevPixel); + + float spreadRadius = + float(shadow.spread.ToAppUnits()) / float(appUnitsPerDevPixel); + + aBuilder.PushBoxShadow(deviceBoxRect, deviceClipRect, !BackfaceIsHidden(), + deviceBoxRect, wr::ToLayoutVector2D(shadowOffset), + wr::ToColorF(shadowColor), blurRadius, spreadRadius, + borderRadius, wr::BoxShadowClipMode::Outset); + } + return true; +} + +class nsDisplayButtonBorder final : public nsPaintedDisplayItem { + public: + nsDisplayButtonBorder(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, + nsButtonFrameRenderer* aRenderer) + : nsPaintedDisplayItem(aBuilder, aFrame), mBFR(aRenderer) { + MOZ_COUNT_CTOR(nsDisplayButtonBorder); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayButtonBorder) + + virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect, + HitTestState* aState, + nsTArray* aOutFrames) override { + aOutFrames->AppendElement(mFrame); + } + virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const override; + virtual bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override; + NS_DISPLAY_DECL_NAME("ButtonBorderBackground", TYPE_BUTTON_BORDER_BACKGROUND) + private: + nsButtonFrameRenderer* mBFR; +}; + +bool nsDisplayButtonBorder::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + // This is really a combination of paint box shadow inner + + // paint border. + aBuilder.StartGroup(this); + const nsRect buttonRect = nsRect(ToReferenceFrame(), mFrame->GetSize()); + bool snap; + nsRect visible = GetBounds(aDisplayListBuilder, &snap); + nsDisplayBoxShadowInner::CreateInsetBoxShadowWebRenderCommands( + aBuilder, aSc, visible, mFrame, buttonRect); + + bool borderIsEmpty = false; + Maybe br = nsCSSRendering::CreateBorderRenderer( + mFrame->PresContext(), nullptr, mFrame, nsRect(), + nsRect(ToReferenceFrame(), mFrame->GetSize()), mFrame->Style(), + &borderIsEmpty, mFrame->GetSkipSides()); + if (!br) { + if (borderIsEmpty) { + aBuilder.FinishGroup(); + } else { + aBuilder.CancelGroup(true); + } + + return borderIsEmpty; + } + + br->CreateWebRenderCommands(this, aBuilder, aResources, aSc); + aBuilder.FinishGroup(); + return true; +} + +void nsDisplayButtonBorder::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + NS_ASSERTION(mFrame, "No frame?"); + nsPresContext* pc = mFrame->PresContext(); + nsRect r = nsRect(ToReferenceFrame(), mFrame->GetSize()); + + // draw the border and background inside the focus and outline borders + Unused << mBFR->PaintBorder(aBuilder, pc, *aCtx, GetPaintRect(aBuilder, aCtx), + r); +} + +nsRect nsDisplayButtonBorder::GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const { + *aSnap = false; + return aBuilder->IsForEventDelivery() + ? nsRect(ToReferenceFrame(), mFrame->GetSize()) + : mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame(); +} + +class nsDisplayButtonForeground final : public nsPaintedDisplayItem { + public: + nsDisplayButtonForeground(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, + nsButtonFrameRenderer* aRenderer) + : nsPaintedDisplayItem(aBuilder, aFrame), mBFR(aRenderer) { + MOZ_COUNT_CTOR(nsDisplayButtonForeground); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayButtonForeground) + + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override; + NS_DISPLAY_DECL_NAME("ButtonForeground", TYPE_BUTTON_FOREGROUND) + private: + nsButtonFrameRenderer* mBFR; +}; + +void nsDisplayButtonForeground::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + nsRect r = nsRect(ToReferenceFrame(), mFrame->GetSize()); + + // Draw the -moz-focus-inner border + Unused << mBFR->PaintInnerFocusBorder(aBuilder, mFrame->PresContext(), *aCtx, + GetPaintRect(aBuilder, aCtx), r); +} + +bool nsDisplayButtonForeground::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + Maybe br; + bool borderIsEmpty = false; + bool dummy; + nsRect r = nsRect(ToReferenceFrame(), mFrame->GetSize()); + br = mBFR->CreateInnerFocusBorderRenderer( + aDisplayListBuilder, mFrame->PresContext(), nullptr, + GetBounds(aDisplayListBuilder, &dummy), r, &borderIsEmpty); + + if (!br) { + return borderIsEmpty; + } + + aBuilder.StartGroup(this); + br->CreateWebRenderCommands(this, aBuilder, aResources, aSc); + aBuilder.FinishGroup(); + + return true; +} + +} // namespace mozilla diff --git a/layout/forms/nsButtonFrameRenderer.h b/layout/forms/nsButtonFrameRenderer.h new file mode 100644 index 0000000000..ea5a9cdea4 --- /dev/null +++ b/layout/forms/nsButtonFrameRenderer.h @@ -0,0 +1,83 @@ +/* -*- 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 nsButtonFrameRenderer_h___ +#define nsButtonFrameRenderer_h___ + +#include "nsMargin.h" +#include "nsCSSRenderingBorders.h" + +class gfxContext; +class nsIFrame; +class nsPresContext; +struct nsRect; + +namespace mozilla { +class nsDisplayList; +class nsDisplayListBuilder; +} // namespace mozilla + +#define NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX 0 +#define NS_BUTTON_RENDERER_LAST_CONTEXT_INDEX \ + NS_BUTTON_RENDERER_FOCUS_INNER_CONTEXT_INDEX + +class nsButtonFrameRenderer { + using nsDisplayList = mozilla::nsDisplayList; + using nsDisplayListBuilder = mozilla::nsDisplayListBuilder; + + typedef mozilla::image::ImgDrawResult ImgDrawResult; + typedef mozilla::ComputedStyle ComputedStyle; + + public: + nsButtonFrameRenderer(); + ~nsButtonFrameRenderer(); + + /** + * Create display list items for the button + */ + nsresult DisplayButton(nsDisplayListBuilder* aBuilder, + nsDisplayList* aBackground, + nsDisplayList* aForeground); + + ImgDrawResult PaintInnerFocusBorder(nsDisplayListBuilder* aBuilder, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, + const nsRect& aRect); + + mozilla::Maybe CreateInnerFocusBorderRenderer( + nsDisplayListBuilder* aBuilder, nsPresContext* aPresContext, + gfxContext* aRenderingContext, const nsRect& aDirtyRect, + const nsRect& aRect, bool* aBorderIsEmpty); + + ImgDrawResult PaintBorder(nsDisplayListBuilder* aBuilder, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, const nsRect& aRect); + + void SetFrame(nsIFrame* aFrame, nsPresContext* aPresContext); + + void SetDisabled(bool aDisabled, bool notify); + + bool isActive(); + bool isDisabled(); + + void GetButtonInnerFocusRect(const nsRect& aRect, nsRect& aResult); + + ComputedStyle* GetComputedStyle(int32_t aIndex) const; + void SetComputedStyle(int32_t aIndex, ComputedStyle* aComputedStyle); + void ReResolveStyles(nsPresContext* aPresContext); + + nsIFrame* GetFrame(); + + private: + // cached style for optional inner focus outline (used on Windows). + RefPtr mInnerFocusStyle; + + nsIFrame* mFrame; +}; + +#endif diff --git a/layout/forms/nsCheckboxRadioFrame.cpp b/layout/forms/nsCheckboxRadioFrame.cpp new file mode 100644 index 0000000000..e2b8541613 --- /dev/null +++ b/layout/forms/nsCheckboxRadioFrame.cpp @@ -0,0 +1,167 @@ +/* -*- 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 "nsCheckboxRadioFrame.h" + +#include "nsGkAtoms.h" +#include "nsLayoutUtils.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/PresShell.h" +#include "nsIContent.h" +#include "nsStyleConsts.h" + +using namespace mozilla; +using mozilla::dom::HTMLInputElement; + +// #define FCF_NOISY + +nsCheckboxRadioFrame* NS_NewCheckboxRadioFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsCheckboxRadioFrame(aStyle, aPresShell->GetPresContext()); +} + +nsCheckboxRadioFrame::nsCheckboxRadioFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsAtomicContainerFrame(aStyle, aPresContext, kClassID) {} + +nsCheckboxRadioFrame::~nsCheckboxRadioFrame() = default; + +NS_IMPL_FRAMEARENA_HELPERS(nsCheckboxRadioFrame) + +NS_QUERYFRAME_HEAD(nsCheckboxRadioFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsAtomicContainerFrame) + +nscoord nsCheckboxRadioFrame::DefaultSize() { + if (StyleDisplay()->HasAppearance()) { + return PresContext()->Theme()->GetCheckboxRadioPrefSize(); + } + return CSSPixel::ToAppUnits(9); +} + +/* virtual */ +void nsCheckboxRadioFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + DO_GLOBAL_REFLOW_COUNT_DSP("nsCheckboxRadioFrame"); + DisplayBorderBackgroundOutline(aBuilder, aLists); +} + +/* virtual */ +nscoord nsCheckboxRadioFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + result = StyleDisplay()->HasAppearance() ? DefaultSize() : 0; + return result; +} + +/* virtual */ +nscoord nsCheckboxRadioFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + result = StyleDisplay()->HasAppearance() ? DefaultSize() : 0; + return result; +} + +/* virtual */ +LogicalSize nsCheckboxRadioFrame::ComputeAutoSize( + gfxContext* aRC, WritingMode aWM, const LogicalSize& aCBSize, + nscoord aAvailableISize, const LogicalSize& aMargin, + const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides, + ComputeSizeFlags aFlags) { + LogicalSize size(aWM, 0, 0); + if (!StyleDisplay()->HasAppearance()) { + return size; + } + + // Note: this call always set the BSize to NS_UNCONSTRAINEDSIZE. + size = nsAtomicContainerFrame::ComputeAutoSize( + aRC, aWM, aCBSize, aAvailableISize, aMargin, aBorderPadding, + aSizeOverrides, aFlags); + size.BSize(aWM) = DefaultSize(); + return size; +} + +Maybe nsCheckboxRadioFrame::GetNaturalBaselineBOffset( + WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const { + NS_ASSERTION(!IsSubtreeDirty(), "frame must not be dirty"); + + if (aBaselineGroup == BaselineSharingGroup::Last) { + return Nothing{}; + } + + if (StyleDisplay()->IsBlockOutsideStyle()) { + return Nothing{}; + } + + // For appearance:none we use a standard CSS baseline, i.e. synthesized from + // our margin-box. + if (!StyleDisplay()->HasAppearance()) { + return Nothing{}; + } + + if (aWM.IsCentralBaseline()) { + return Some(GetLogicalUsedBorderAndPadding(aWM).BStart(aWM) + + ContentBSize(aWM) / 2); + } + // This is for compatibility with Chrome, Safari and Edge (Dec 2016). + // Treat radio buttons and checkboxes as having an intrinsic baseline + // at the block-end of the control (use the block-end content edge rather + // than the margin edge). + // For "inverted" lines (typically in writing-mode:vertical-lr), use the + // block-start end instead. + return Some(aWM.IsLineInverted() + ? GetLogicalUsedBorderAndPadding(aWM).BStart(aWM) + : BSize(aWM) - GetLogicalUsedBorderAndPadding(aWM).BEnd(aWM)); +} + +void nsCheckboxRadioFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsCheckboxRadioFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + NS_FRAME_TRACE( + NS_FRAME_TRACE_CALLS, + ("enter nsCheckboxRadioFrame::Reflow: aMaxSize=%d,%d", + aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight())); + + const auto wm = aReflowInput.GetWritingMode(); + aDesiredSize.SetSize(wm, aReflowInput.ComputedSizeWithBorderPadding(wm)); + + if (nsLayoutUtils::FontSizeInflationEnabled(aPresContext)) { + float inflation = nsLayoutUtils::FontSizeInflationFor(this); + aDesiredSize.Width() *= inflation; + aDesiredSize.Height() *= inflation; + } + + NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS, + ("exit nsCheckboxRadioFrame::Reflow: size=%d,%d", + aDesiredSize.Width(), aDesiredSize.Height())); + + aDesiredSize.SetOverflowAreasToDesiredBounds(); + FinishAndStoreOverflow(&aDesiredSize); +} + +void nsCheckboxRadioFrame::SetFocus(bool aOn, bool aRepaint) {} + +nsresult nsCheckboxRadioFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + // Check for disabled content so that selection works properly (?). + if (IsContentDisabled()) { + return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + return NS_OK; +} + +nsresult nsCheckboxRadioFrame::SetFormProperty(nsAtom* aName, + const nsAString& aValue) { + return NS_OK; +} diff --git a/layout/forms/nsCheckboxRadioFrame.h b/layout/forms/nsCheckboxRadioFrame.h new file mode 100644 index 0000000000..6b1a902925 --- /dev/null +++ b/layout/forms/nsCheckboxRadioFrame.h @@ -0,0 +1,88 @@ +/* -*- 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 nsCheckboxRadioFrame_h___ +#define nsCheckboxRadioFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsIFormControlFrame.h" +#include "nsAtomicContainerFrame.h" +#include "nsDisplayList.h" + +/** + * nsCheckboxRadioFrame is used for radio buttons and checkboxes. + */ +class nsCheckboxRadioFrame final : public nsAtomicContainerFrame, + public nsIFormControlFrame { + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsCheckboxRadioFrame) + + explicit nsCheckboxRadioFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + // nsIFrame replacements + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + /** + * Both GetMinISize and GetPrefISize will return whatever GetIntrinsicISize + * returns. + */ + nscoord GetMinISize(gfxContext* aRenderingContext) override; + nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + /** + * Our auto size is just intrinsic width and intrinsic height. + */ + mozilla::LogicalSize ComputeAutoSize( + gfxContext* aRenderingContext, mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorderPadding, + const mozilla::StyleSizeOverrides& aSizeOverrides, + mozilla::ComputeSizeFlags aFlags) override; + + /** + * Respond to a gui event + * @see nsIFrame::HandleEvent + */ + nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + Maybe GetNaturalBaselineBOffset( + mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const override; + + /** + * Respond to the request to resize and/or reflow + * @see nsIFrame::Reflow + */ + void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + // new behavior + + void SetFocus(bool aOn = true, bool aRepaint = false) override; + + // nsIFormControlFrame + nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"CheckboxRadio"_ns, aResult); + } +#endif + + protected: + virtual ~nsCheckboxRadioFrame(); + + nscoord DefaultSize(); +}; + +#endif diff --git a/layout/forms/nsColorControlFrame.cpp b/layout/forms/nsColorControlFrame.cpp new file mode 100644 index 0000000000..f921016d06 --- /dev/null +++ b/layout/forms/nsColorControlFrame.cpp @@ -0,0 +1,128 @@ +/* -*- 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 "nsColorControlFrame.h" + +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "nsCSSPseudoElements.h" +#include "nsGkAtoms.h" +#include "nsIFormControl.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/Document.h" + +using namespace mozilla; +using mozilla::dom::CallerType; +using mozilla::dom::Document; +using mozilla::dom::Element; +using mozilla::dom::HTMLInputElement; + +nsColorControlFrame::nsColorControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsHTMLButtonControlFrame(aStyle, aPresContext, kClassID) {} + +nsIFrame* NS_NewColorControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsColorControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsColorControlFrame) + +NS_QUERYFRAME_HEAD(nsColorControlFrame) + NS_QUERYFRAME_ENTRY(nsColorControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsHTMLButtonControlFrame) + +void nsColorControlFrame::Destroy(DestroyContext& aContext) { + aContext.AddAnonymousContent(mColorContent.forget()); + nsHTMLButtonControlFrame::Destroy(aContext); +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsColorControlFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"ColorControl"_ns, aResult); +} +#endif + +// Create the color area for the button. +// The frame will be generated by the frame constructor. +nsresult nsColorControlFrame::CreateAnonymousContent( + nsTArray& aElements) { + RefPtr doc = mContent->GetComposedDoc(); + mColorContent = doc->CreateHTMLElement(nsGkAtoms::div); + mColorContent->SetPseudoElementType(PseudoStyleType::mozColorSwatch); + + // Mark the element to be native anonymous before setting any attributes. + mColorContent->SetIsNativeAnonymousRoot(); + + nsresult rv = UpdateColor(); + NS_ENSURE_SUCCESS(rv, rv); + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + aElements.AppendElement(mColorContent); + + return NS_OK; +} + +void nsColorControlFrame::AppendAnonymousContentTo( + nsTArray& aElements, uint32_t aFilter) { + if (mColorContent) { + aElements.AppendElement(mColorContent); + } +} + +nsresult nsColorControlFrame::UpdateColor() { + // Get the color from the "value" property of our content; it will return the + // default color (through the sanitization algorithm) if the value is empty. + nsAutoString color; + HTMLInputElement* elt = HTMLInputElement::FromNode(mContent); + elt->GetValue(color, CallerType::System); + + if (color.IsEmpty()) { + // OK, there is one case the color string might be empty -- if our content + // is still being created, i.e. if it has mDoneCreating==false. In that + // case, we simply do nothing, because we'll be called again with a complete + // content node before we ever reflow or paint. Specifically: we can expect + // that HTMLInputElement::DoneCreatingElement() will set mDoneCreating to + // true (which enables sanitization) and then it'll call SetValueInternal(), + // which produces a nonempty color (via sanitization), and then it'll call + // this function here, and we'll get the nonempty default color. + MOZ_ASSERT(HasAnyStateBits(NS_FRAME_FIRST_REFLOW), + "Content node's GetValue() should return a valid color string " + "by the time we've been reflowed (the default color, in case " + "no valid color is set)"); + return NS_OK; + } + + // Set the background-color CSS property of the swatch element to this color. + return mColorContent->SetAttr(kNameSpaceID_None, nsGkAtoms::style, + u"background-color:"_ns + color, + /* aNotify */ true); +} + +nsresult nsColorControlFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + NS_ASSERTION(mColorContent, "The color div must exist"); + + // If the value attribute is set, update the color box, but only if we're + // still a color control, which might not be the case if the type attribute + // was removed/changed. + nsCOMPtr fctrl = do_QueryInterface(GetContent()); + if (fctrl->ControlType() == FormControlType::InputColor && + aNameSpaceID == kNameSpaceID_None && nsGkAtoms::value == aAttribute) { + UpdateColor(); + } + return nsHTMLButtonControlFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); +} + +nsContainerFrame* nsColorControlFrame::GetContentInsertionFrame() { + return this; +} diff --git a/layout/forms/nsColorControlFrame.h b/layout/forms/nsColorControlFrame.h new file mode 100644 index 0000000000..a85816324f --- /dev/null +++ b/layout/forms/nsColorControlFrame.h @@ -0,0 +1,60 @@ +/* -*- 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 nsColorControlFrame_h___ +#define nsColorControlFrame_h___ + +#include "nsCOMPtr.h" +#include "nsHTMLButtonControlFrame.h" +#include "nsIAnonymousContentCreator.h" + +namespace mozilla { +enum class PseudoStyleType : uint8_t; +class PresShell; +} // namespace mozilla + +// Class which implements the input type=color + +class nsColorControlFrame final : public nsHTMLButtonControlFrame, + public nsIAnonymousContentCreator { + typedef mozilla::PseudoStyleType PseudoStyleType; + typedef mozilla::dom::Element Element; + + public: + friend nsIFrame* NS_NewColorControlFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + void Destroy(DestroyContext&) override; + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsColorControlFrame) + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent( + nsTArray& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray& aElements, + uint32_t aFilter) override; + + // nsIFrame + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + virtual nsContainerFrame* GetContentInsertionFrame() override; + + // Refresh the color swatch, using associated input's value + nsresult UpdateColor(); + + private: + explicit nsColorControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + nsCOMPtr mColorContent; +}; + +#endif // nsColorControlFrame_h___ diff --git a/layout/forms/nsComboboxControlFrame.cpp b/layout/forms/nsComboboxControlFrame.cpp new file mode 100644 index 0000000000..a935e3c761 --- /dev/null +++ b/layout/forms/nsComboboxControlFrame.cpp @@ -0,0 +1,965 @@ +/* -*- 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 "nsComboboxControlFrame.h" + +#include "gfxContext.h" +#include "gfxUtils.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/PathHelpers.h" +#include "nsCOMPtr.h" +#include "nsDeviceContext.h" +#include "nsFocusManager.h" +#include "nsCheckboxRadioFrame.h" +#include "nsGkAtoms.h" +#include "nsCSSAnonBoxes.h" +#include "nsHTMLParts.h" +#include "nsIFormControl.h" +#include "nsILayoutHistoryState.h" +#include "nsNameSpaceManager.h" +#include "nsListControlFrame.h" +#include "nsPIDOMWindow.h" +#include "mozilla/PresState.h" +#include "nsView.h" +#include "nsViewManager.h" +#include "nsIContentInlines.h" +#include "nsIDOMEventListener.h" +#include "nsISelectControlFrame.h" +#include "nsContentUtils.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "mozilla/dom/Document.h" +#include "nsIScrollableFrame.h" +#include "mozilla/ServoStyleSet.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsLayoutUtils.h" +#include "nsDisplayList.h" +#include "nsITheme.h" +#include "nsStyleConsts.h" +#include "nsTextFrameUtils.h" +#include "nsTextRunTransformations.h" +#include "HTMLSelectEventListener.h" +#include "mozilla/Likely.h" +#include +#include "nsTextNode.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/PresShellInlines.h" +#include "mozilla/Unused.h" +#include "gfx2DGlue.h" +#include "mozilla/widget/nsAutoRollup.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +NS_IMETHODIMP +nsComboboxControlFrame::RedisplayTextEvent::Run() { + if (mControlFrame) mControlFrame->HandleRedisplayTextEvent(); + return NS_OK; +} + +// Drop down list event management. +// The combo box uses the following strategy for managing the drop-down list. +// If the combo box or its arrow button is clicked on the drop-down list is +// displayed If mouse exits the combo box with the drop-down list displayed the +// drop-down list is asked to capture events The drop-down list will capture all +// events including mouse down and up and will always return with +// ListWasSelected method call regardless of whether an item in the list was +// actually selected. +// The ListWasSelected code will turn off mouse-capture for the drop-down list. +// The drop-down list does not explicitly set capture when it is in the +// drop-down mode. + +nsComboboxControlFrame* NS_NewComboboxControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + nsComboboxControlFrame* it = new (aPresShell) + nsComboboxControlFrame(aStyle, aPresShell->GetPresContext()); + return it; +} + +NS_IMPL_FRAMEARENA_HELPERS(nsComboboxControlFrame) + +//----------------------------------------------------------- +// Reflow Debugging Macros +// These let us "see" how many reflow counts are happening +//----------------------------------------------------------- +#ifdef DO_REFLOW_COUNTER + +# define MAX_REFLOW_CNT 1024 +static int32_t gTotalReqs = 0; +; +static int32_t gTotalReflows = 0; +; +static int32_t gReflowControlCntRQ[MAX_REFLOW_CNT]; +static int32_t gReflowControlCnt[MAX_REFLOW_CNT]; +static int32_t gReflowInx = -1; + +# define REFLOW_COUNTER() \ + if (mReflowId > -1) gReflowControlCnt[mReflowId]++; + +# define REFLOW_COUNTER_REQUEST() \ + if (mReflowId > -1) gReflowControlCntRQ[mReflowId]++; + +# define REFLOW_COUNTER_DUMP(__desc) \ + if (mReflowId > -1) { \ + gTotalReqs += gReflowControlCntRQ[mReflowId]; \ + gTotalReflows += gReflowControlCnt[mReflowId]; \ + printf("** Id:%5d %s RF: %d RQ: %d %d/%d %5.2f\n", mReflowId, \ + (__desc), gReflowControlCnt[mReflowId], \ + gReflowControlCntRQ[mReflowId], gTotalReflows, gTotalReqs, \ + float(gTotalReflows) / float(gTotalReqs) * 100.0f); \ + } + +# define REFLOW_COUNTER_INIT() \ + if (gReflowInx < MAX_REFLOW_CNT) { \ + gReflowInx++; \ + mReflowId = gReflowInx; \ + gReflowControlCnt[mReflowId] = 0; \ + gReflowControlCntRQ[mReflowId] = 0; \ + } else { \ + mReflowId = -1; \ + } + +// reflow messages +# define REFLOW_DEBUG_MSG(_msg1) printf((_msg1)) +# define REFLOW_DEBUG_MSG2(_msg1, _msg2) printf((_msg1), (_msg2)) +# define REFLOW_DEBUG_MSG3(_msg1, _msg2, _msg3) \ + printf((_msg1), (_msg2), (_msg3)) +# define REFLOW_DEBUG_MSG4(_msg1, _msg2, _msg3, _msg4) \ + printf((_msg1), (_msg2), (_msg3), (_msg4)) + +#else //------------- + +# define REFLOW_COUNTER_REQUEST() +# define REFLOW_COUNTER() +# define REFLOW_COUNTER_DUMP(__desc) +# define REFLOW_COUNTER_INIT() + +# define REFLOW_DEBUG_MSG(_msg) +# define REFLOW_DEBUG_MSG2(_msg1, _msg2) +# define REFLOW_DEBUG_MSG3(_msg1, _msg2, _msg3) +# define REFLOW_DEBUG_MSG4(_msg1, _msg2, _msg3, _msg4) + +#endif + +//------------------------------------------ +// This is for being VERY noisy +//------------------------------------------ +#ifdef DO_VERY_NOISY +# define REFLOW_NOISY_MSG(_msg1) printf((_msg1)) +# define REFLOW_NOISY_MSG2(_msg1, _msg2) printf((_msg1), (_msg2)) +# define REFLOW_NOISY_MSG3(_msg1, _msg2, _msg3) \ + printf((_msg1), (_msg2), (_msg3)) +# define REFLOW_NOISY_MSG4(_msg1, _msg2, _msg3, _msg4) \ + printf((_msg1), (_msg2), (_msg3), (_msg4)) +#else +# define REFLOW_NOISY_MSG(_msg) +# define REFLOW_NOISY_MSG2(_msg1, _msg2) +# define REFLOW_NOISY_MSG3(_msg1, _msg2, _msg3) +# define REFLOW_NOISY_MSG4(_msg1, _msg2, _msg3, _msg4) +#endif + +//------------------------------------------ +// Displays value in pixels or twips +//------------------------------------------ +#ifdef DO_PIXELS +# define PX(__v) __v / 15 +#else +# define PX(__v) __v +#endif + +//------------------------------------------------------ +//-- Done with macros +//------------------------------------------------------ + +nsComboboxControlFrame::nsComboboxControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsBlockFrame(aStyle, aPresContext, kClassID), + mDisplayFrame(nullptr), + mButtonFrame(nullptr), + mDisplayISize(0), + mMaxDisplayISize(0), + mRecentSelectedIndex(NS_SKIP_NOTIFY_INDEX), + mDisplayedIndex(-1), + mInRedisplayText(false), + mIsOpenInParentProcess(false){REFLOW_COUNTER_INIT()} + + //-------------------------------------------------------------- + nsComboboxControlFrame::~nsComboboxControlFrame() { + REFLOW_COUNTER_DUMP("nsCCF"); +} + +//-------------------------------------------------------------- + +NS_QUERYFRAME_HEAD(nsComboboxControlFrame) + NS_QUERYFRAME_ENTRY(nsComboboxControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsISelectControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) + +#ifdef ACCESSIBILITY +a11y::AccType nsComboboxControlFrame::AccessibleType() { + return a11y::eHTMLComboboxType; +} +#endif + +void nsComboboxControlFrame::SetFocus(bool aOn, bool aRepaint) { + // This is needed on a temporary basis. It causes the focus + // rect to be drawn. This is much faster than ReResolvingStyle + // Bug 32920 + InvalidateFrame(); +} + +nsPoint nsComboboxControlFrame::GetCSSTransformTranslation() { + nsIFrame* frame = this; + bool is3DTransform = false; + Matrix transform; + while (frame) { + nsIFrame* parent; + Matrix4x4Flagged ctm = frame->GetTransformMatrix( + ViewportType::Layout, RelativeTo{nullptr}, &parent); + Matrix matrix; + if (ctm.Is2D(&matrix)) { + transform = transform * matrix; + } else { + is3DTransform = true; + break; + } + frame = parent; + } + nsPoint translation; + if (!is3DTransform && !transform.HasNonTranslation()) { + nsPresContext* pc = PresContext(); + // To get the translation introduced only by transforms we subtract the + // regular non-transform translation. + nsRootPresContext* rootPC = pc->GetRootPresContext(); + if (rootPC) { + int32_t apd = pc->AppUnitsPerDevPixel(); + translation.x = NSFloatPixelsToAppUnits(transform._31, apd); + translation.y = NSFloatPixelsToAppUnits(transform._32, apd); + translation -= GetOffsetToCrossDoc(rootPC->PresShell()->GetRootFrame()); + } + } + return translation; +} + +//---------------------------------------------------------- +// +//---------------------------------------------------------- +#ifdef DO_REFLOW_DEBUG +static int myCounter = 0; + +static void printSize(char* aDesc, nscoord aSize) { + printf(" %s: ", aDesc); + if (aSize == NS_UNCONSTRAINEDSIZE) { + printf("UC"); + } else { + printf("%d", PX(aSize)); + } +} +#endif + +//------------------------------------------------------------------- +//-- Main Reflow for the Combobox +//------------------------------------------------------------------- + +bool nsComboboxControlFrame::HasDropDownButton() const { + const nsStyleDisplay* disp = StyleDisplay(); + // FIXME(emilio): Blink also shows this for menulist-button and such... Seems + // more similar to our mac / linux implementation. + return disp->EffectiveAppearance() == StyleAppearance::Menulist && + (!IsThemed(disp) || + PresContext()->Theme()->ThemeNeedsComboboxDropmarker()); +} + +nscoord nsComboboxControlFrame::DropDownButtonISize() { + if (!HasDropDownButton()) { + return 0; + } + + nsPresContext* pc = PresContext(); + LayoutDeviceIntSize dropdownButtonSize = pc->Theme()->GetMinimumWidgetSize( + pc, this, StyleAppearance::MozMenulistArrowButton); + return pc->DevPixelsToAppUnits(dropdownButtonSize.width); +} + +int32_t nsComboboxControlFrame::CharCountOfLargestOptionForInflation() const { + uint32_t maxLength = 0; + nsAutoString label; + for (auto i : IntegerRange(Select().Options()->Length())) { + GetOptionText(i, label); + maxLength = std::max( + maxLength, + nsTextFrameUtils::ComputeApproximateLengthWithWhitespaceCompression( + label, StyleText())); + } + if (MOZ_UNLIKELY(maxLength > uint32_t(INT32_MAX))) { + return INT32_MAX; + } + return int32_t(maxLength); +} + +nscoord nsComboboxControlFrame::GetLongestOptionISize( + gfxContext* aRenderingContext) const { + // Compute the width of each option's (potentially text-transformed) text, + // and use the widest one as part of our intrinsic size. + nscoord maxOptionSize = 0; + nsAutoString label; + nsAutoString transformedLabel; + RefPtr fm = + nsLayoutUtils::GetInflatedFontMetricsForFrame(this); + const nsStyleText* textStyle = StyleText(); + auto textTransform = textStyle->mTextTransform.IsNone() + ? Nothing() + : Some(textStyle->mTextTransform); + nsAtom* language = StyleFont()->mLanguage; + AutoTArray charsToMergeArray; + AutoTArray deletedCharsArray; + for (auto i : IntegerRange(Select().Options()->Length())) { + GetOptionText(i, label); + const nsAutoString* stringToUse = &label; + if (textTransform || + textStyle->mWebkitTextSecurity != StyleTextSecurity::None) { + transformedLabel.Truncate(); + charsToMergeArray.SetLengthAndRetainStorage(0); + deletedCharsArray.SetLengthAndRetainStorage(0); + nsCaseTransformTextRunFactory::TransformString( + label, transformedLabel, textTransform, + textStyle->TextSecurityMaskChar(), + /* aCaseTransformsOnly = */ false, language, charsToMergeArray, + deletedCharsArray); + stringToUse = &transformedLabel; + } + maxOptionSize = std::max(maxOptionSize, + nsLayoutUtils::AppUnitWidthOfStringBidi( + *stringToUse, this, *fm, *aRenderingContext)); + } + if (maxOptionSize) { + // HACK: Add one app unit to workaround silly Netgear router styling, see + // bug 1769580. In practice since this comes from font metrics is unlikely + // to be perceivable. + maxOptionSize += 1; + } + return maxOptionSize; +} + +nscoord nsComboboxControlFrame::GetIntrinsicISize(gfxContext* aRenderingContext, + IntrinsicISizeType aType) { + Maybe containISize = ContainIntrinsicISize(NS_UNCONSTRAINEDSIZE); + if (containISize && *containISize != NS_UNCONSTRAINEDSIZE) { + return *containISize; + } + + nscoord displayISize = mDisplayFrame->IntrinsicISizeOffsets().padding; + if (!containISize && !StyleContent()->mContent.IsNone()) { + displayISize += GetLongestOptionISize(aRenderingContext); + } + + // Add room for the dropmarker button (if there is one). + displayISize += DropDownButtonISize(); + return displayISize; +} + +nscoord nsComboboxControlFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord minISize; + DISPLAY_MIN_INLINE_SIZE(this, minISize); + minISize = GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::MinISize); + return minISize; +} + +nscoord nsComboboxControlFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord prefISize; + DISPLAY_PREF_INLINE_SIZE(this, prefISize); + prefISize = + GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::PrefISize); + return prefISize; +} + +dom::HTMLSelectElement& nsComboboxControlFrame::Select() const { + return *static_cast(GetContent()); +} + +void nsComboboxControlFrame::GetOptionText(uint32_t aIndex, + nsAString& aText) const { + aText.Truncate(); + if (Element* el = Select().Options()->GetElementAt(aIndex)) { + static_cast(el)->GetRenderedLabel(aText); + } +} + +void nsComboboxControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + // Constraints we try to satisfy: + + // 1) Default inline size of button is the vertical scrollbar size + // 2) If the inline size of button is bigger than our inline size, set + // inline size of button to 0. + // 3) Default block size of button is block size of display area + // 4) Inline size of display area is whatever is left over from our + // inline size after allocating inline size for the button. + + if (!mDisplayFrame) { + NS_ERROR("Why did the frame constructor allow this to happen? Fix it!!"); + return; + } + + // Make sure the displayed text is the same as the selected option, + // bug 297389. + mDisplayedIndex = Select().SelectedIndex(); + + // In dropped down mode the "selected index" is the hovered menu item, + // we want the last selected item which is |mDisplayedIndex| in this case. + RedisplayText(); + + WritingMode wm = aReflowInput.GetWritingMode(); + + // Check if the theme specifies a minimum size for the dropdown button + // first. + const nscoord buttonISize = DropDownButtonISize(); + const auto borderPadding = aReflowInput.ComputedLogicalBorderPadding(wm); + const auto padding = aReflowInput.ComputedLogicalPadding(wm); + const auto border = borderPadding - padding; + + mDisplayISize = aReflowInput.ComputedISize() - buttonISize; + mMaxDisplayISize = mDisplayISize + padding.IEnd(wm); + + nsBlockFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus); + + // The button should occupy the same space as a scrollbar, and its position + // starts from the border edge. + if (mButtonFrame) { + LogicalRect buttonRect(wm); + buttonRect.IStart(wm) = borderPadding.IStart(wm) + mMaxDisplayISize; + buttonRect.BStart(wm) = border.BStart(wm); + + buttonRect.ISize(wm) = buttonISize; + buttonRect.BSize(wm) = mDisplayFrame->BSize(wm) + padding.BStartEnd(wm); + + const nsSize containerSize = aDesiredSize.PhysicalSize(); + mButtonFrame->SetRect(buttonRect, containerSize); + } + + if (!aStatus.IsInlineBreakBefore() && !aStatus.IsFullyComplete()) { + // This frame didn't fit inside a fragmentation container. Splitting + // a nsComboboxControlFrame makes no sense, so we override the status here. + aStatus.Reset(); + } +} + +void nsComboboxControlFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsBlockFrame::Init(aContent, aParent, aPrevInFlow); + + mEventListener = new HTMLSelectEventListener( + Select(), HTMLSelectEventListener::SelectType::Combobox); +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsComboboxControlFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"ComboboxControl"_ns, aResult); +} +#endif + +/////////////////////////////////////////////////////////////// + +nsresult nsComboboxControlFrame::RedisplaySelectedText() { + nsAutoScriptBlocker scriptBlocker; + mDisplayedIndex = Select().SelectedIndex(); + return RedisplayText(); +} + +nsresult nsComboboxControlFrame::RedisplayText() { + nsString previewValue; + nsString previousText(mDisplayedOptionTextOrPreview); + + Select().GetPreviewValue(previewValue); + // Get the text to display + if (!previewValue.IsEmpty()) { + mDisplayedOptionTextOrPreview = previewValue; + } else if (mDisplayedIndex != -1 && !StyleContent()->mContent.IsNone()) { + GetOptionText(mDisplayedIndex, mDisplayedOptionTextOrPreview); + } else { + mDisplayedOptionTextOrPreview.Truncate(); + } + + REFLOW_DEBUG_MSG2( + "RedisplayText \"%s\"\n", + NS_LossyConvertUTF16toASCII(mDisplayedOptionTextOrPreview).get()); + + // Send reflow command because the new text maybe larger + nsresult rv = NS_OK; + if (mDisplayContent && !previousText.Equals(mDisplayedOptionTextOrPreview)) { + // Don't call ActuallyDisplayText(true) directly here since that + // could cause recursive frame construction. See bug 283117 and the comment + // in HandleRedisplayTextEvent() below. + + // Revoke outstanding events to avoid out-of-order events which could mean + // displaying the wrong text. + mRedisplayTextEvent.Revoke(); + + NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(), + "If we happen to run our redisplay event now, we might kill " + "ourselves!"); + + mRedisplayTextEvent = new RedisplayTextEvent(this); + nsContentUtils::AddScriptRunner(mRedisplayTextEvent.get()); + } + return rv; +} + +void nsComboboxControlFrame::HandleRedisplayTextEvent() { + // First, make sure that the content model is up to date and we've + // constructed the frames for all our content in the right places. + // Otherwise they'll end up under the wrong insertion frame when we + // ActuallyDisplayText, since that flushes out the content sink by + // calling SetText on a DOM node with aNotify set to true. See bug + // 289730. + AutoWeakFrame weakThis(this); + PresContext()->Document()->FlushPendingNotifications( + FlushType::ContentAndNotify); + if (!weakThis.IsAlive()) return; + + // Redirect frame insertions during this method (see + // GetContentInsertionFrame()) so that any reframing that the frame + // constructor forces upon us is inserted into the correct parent + // (mDisplayFrame). See bug 282607. + MOZ_ASSERT(!mInRedisplayText, "Nested RedisplayText"); + mInRedisplayText = true; + mRedisplayTextEvent.Forget(); + + ActuallyDisplayText(true); + if (!weakThis.IsAlive()) { + return; + } + + // XXXbz This should perhaps be IntrinsicDirty::None. Check. + PresShell()->FrameNeedsReflow(mDisplayFrame, + IntrinsicDirty::FrameAncestorsAndDescendants, + NS_FRAME_IS_DIRTY); + + mInRedisplayText = false; +} + +void nsComboboxControlFrame::ActuallyDisplayText(bool aNotify) { + RefPtr displayContent = mDisplayContent; + if (mDisplayedOptionTextOrPreview.IsEmpty()) { + // Have to use a space character of some sort for line-block-size + // calculations to be right. Also, the space character must be zero-width + // in order for the the inline-size calculations to be consistent between + // size-contained comboboxes vs. empty comboboxes. + // + // XXXdholbert Does this space need to be "non-breaking"? I'm not sure + // if it matters, but we previously had a comment here (added in 2002) + // saying "Have to use a non-breaking space for line-height calculations + // to be right". So I'll stick with a non-breaking space for now... + static const char16_t space = 0xFEFF; + displayContent->SetText(&space, 1, aNotify); + } else { + displayContent->SetText(mDisplayedOptionTextOrPreview, aNotify); + } +} + +int32_t nsComboboxControlFrame::GetIndexOfDisplayArea() { + return mDisplayedIndex; +} + +bool nsComboboxControlFrame::IsDroppedDown() const { + return Select().OpenInParentProcess(); +} + +//---------------------------------------------------------------------- +// nsISelectControlFrame +//---------------------------------------------------------------------- +NS_IMETHODIMP +nsComboboxControlFrame::DoneAddingChildren(bool aIsDone) { return NS_OK; } + +NS_IMETHODIMP +nsComboboxControlFrame::AddOption(int32_t aIndex) { + if (aIndex <= mDisplayedIndex) { + ++mDisplayedIndex; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsComboboxControlFrame::RemoveOption(int32_t aIndex) { + if (Select().Options()->Length()) { + if (aIndex < mDisplayedIndex) { + --mDisplayedIndex; + } else if (aIndex == mDisplayedIndex) { + mDisplayedIndex = 0; // IE6 compat + RedisplayText(); + } + } else { + // If we removed the last option, we need to blank things out + mDisplayedIndex = -1; + RedisplayText(); + } + return NS_OK; +} + +NS_IMETHODIMP_(void) +nsComboboxControlFrame::OnSetSelectedIndex(int32_t aOldIndex, + int32_t aNewIndex) { + nsAutoScriptBlocker scriptBlocker; + mDisplayedIndex = aNewIndex; + RedisplayText(); +} + +// End nsISelectControlFrame +//---------------------------------------------------------------------- + +nsresult nsComboboxControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + NS_ENSURE_ARG_POINTER(aEventStatus); + + if (nsEventStatus_eConsumeNoDefault == *aEventStatus) { + return NS_OK; + } + + if (mContent->AsElement()->State().HasState(dom::ElementState::DISABLED)) { + return NS_OK; + } + + // If we have style that affects how we are selected, feed event down to + // nsIFrame::HandleEvent so that selection takes place when appropriate. + if (IsContentDisabled()) { + return nsBlockFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + return NS_OK; +} + +nsContainerFrame* nsComboboxControlFrame::GetContentInsertionFrame() { + return mInRedisplayText ? mDisplayFrame : nullptr; +} + +void nsComboboxControlFrame::AppendDirectlyOwnedAnonBoxes( + nsTArray& aResult) { + aResult.AppendElement(OwnedAnonBox(mDisplayFrame)); +} + +nsresult nsComboboxControlFrame::CreateAnonymousContent( + nsTArray& aElements) { + // The frames used to display the combo box and the button used to popup the + // dropdown list are created through anonymous content. The dropdown list is + // not created through anonymous content because its frame is initialized + // specifically for the drop-down case and it is placed a special list + // referenced through NS_COMBO_FRAME_POPUP_LIST_INDEX to keep separate from + // the layout of the display and button. + // + // Note: The value attribute of the display content is set when an item is + // selected in the dropdown list. If the content specified below does not + // honor the value attribute than nothing will be displayed. + + // For now the content that is created corresponds to two input buttons. It + // would be better to create the tag as something other than input, but then + // there isn't any way to create a button frame since it isn't possible to set + // the display type in CSS2 to create a button frame. + + // create content used for display + // nsAtom* tag = NS_Atomize("mozcombodisplay"); + + // Add a child text content node for the label + + nsNodeInfoManager* nimgr = mContent->NodeInfo()->NodeInfoManager(); + + mDisplayContent = new (nimgr) nsTextNode(nimgr); + + // set the value of the text node + mDisplayedIndex = Select().SelectedIndex(); + if (mDisplayedIndex != -1) { + GetOptionText(mDisplayedIndex, mDisplayedOptionTextOrPreview); + } + ActuallyDisplayText(false); + + aElements.AppendElement(mDisplayContent); + if (HasDropDownButton()) { + mButtonContent = mContent->OwnerDoc()->CreateHTMLElement(nsGkAtoms::button); + if (!mButtonContent) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // make someone to listen to the button. + mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::type, u"button"_ns, + false); + // Set tabindex="-1" so that the button is not tabbable + mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns, + false); + aElements.AppendElement(mButtonContent); + } + + return NS_OK; +} + +void nsComboboxControlFrame::AppendAnonymousContentTo( + nsTArray& aElements, uint32_t aFilter) { + if (mDisplayContent) { + aElements.AppendElement(mDisplayContent); + } + + if (mButtonContent) { + aElements.AppendElement(mButtonContent); + } +} + +nsIContent* nsComboboxControlFrame::GetDisplayNode() const { + return mDisplayContent; +} + +// XXXbz this is a for-now hack. Now that display:inline-block works, +// need to revisit this. +class nsComboboxDisplayFrame final : public nsBlockFrame { + public: + NS_DECL_FRAMEARENA_HELPERS(nsComboboxDisplayFrame) + + nsComboboxDisplayFrame(ComputedStyle* aStyle, + nsComboboxControlFrame* aComboBox) + : nsBlockFrame(aStyle, aComboBox->PresContext(), kClassID), + mComboBox(aComboBox) {} + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const final { + return MakeFrameName(u"ComboboxDisplay"_ns, aResult); + } +#endif + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, nsReflowStatus& aStatus) final; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) final; + + protected: + nsComboboxControlFrame* mComboBox; +}; + +NS_IMPL_FRAMEARENA_HELPERS(nsComboboxDisplayFrame) + +void nsComboboxDisplayFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + MOZ_ASSERT(aReflowInput.mParentReflowInput && + aReflowInput.mParentReflowInput->mFrame == mComboBox, + "Combobox's frame tree is wrong!"); + + ReflowInput state(aReflowInput); + if (state.ComputedBSize() == NS_UNCONSTRAINEDSIZE) { + state.SetLineHeight(state.mParentReflowInput->GetLineHeight()); + } + const WritingMode wm = aReflowInput.GetWritingMode(); + const LogicalMargin bp = state.ComputedLogicalBorderPadding(wm); + MOZ_ASSERT(bp.BStartEnd(wm) == 0, + "We shouldn't have border and padding in the block axis in UA!"); + nscoord inlineBp = bp.IStartEnd(wm); + nscoord computedISize = mComboBox->mDisplayISize - inlineBp; + + // Other UAs ignore padding in some (but not all) platforms for (themed only) + // comboboxes. Instead of doing that, we prevent that padding if present from + // clipping the display text, by enforcing the display text minimum size in + // that situation. + const bool shouldHonorMinISize = + mComboBox->StyleDisplay()->EffectiveAppearance() == + StyleAppearance::Menulist; + if (shouldHonorMinISize) { + computedISize = std::max(state.ComputedMinISize(), computedISize); + // Don't let this size go over mMaxDisplayISize, since that'd be + // observable via clientWidth / scrollWidth. + computedISize = + std::min(computedISize, mComboBox->mMaxDisplayISize - inlineBp); + } + + state.SetComputedISize(std::max(0, computedISize)); + nsBlockFrame::Reflow(aPresContext, aDesiredSize, state, aStatus); + aStatus.Reset(); // this type of frame can't be split +} + +void nsComboboxDisplayFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + nsDisplayListCollection set(aBuilder); + nsBlockFrame::BuildDisplayList(aBuilder, set); + + // remove background items if parent frame is themed + if (mComboBox->IsThemed()) { + set.BorderBackground()->DeleteAll(aBuilder); + } + + set.MoveTo(aLists); +} + +nsIFrame* nsComboboxControlFrame::CreateFrameForDisplayNode() { + MOZ_ASSERT(mDisplayContent); + + // Get PresShell + mozilla::PresShell* ps = PresShell(); + ServoStyleSet* styleSet = ps->StyleSet(); + + // create the ComputedStyle for the anonymous block frame and text frame + RefPtr computedStyle = + styleSet->ResolveInheritingAnonymousBoxStyle( + PseudoStyleType::mozDisplayComboboxControlFrame, mComputedStyle); + + RefPtr textComputedStyle = + styleSet->ResolveStyleForText(mDisplayContent, mComputedStyle); + + // Start by creating our anonymous block frame + mDisplayFrame = new (ps) nsComboboxDisplayFrame(computedStyle, this); + mDisplayFrame->Init(mContent, this, nullptr); + + // Create a text frame and put it inside the block frame + nsIFrame* textFrame = NS_NewTextFrame(ps, textComputedStyle); + + // initialize the text frame + textFrame->Init(mDisplayContent, mDisplayFrame, nullptr); + mDisplayContent->SetPrimaryFrame(textFrame); + + mDisplayFrame->SetInitialChildList(FrameChildListID::Principal, + nsFrameList(textFrame, textFrame)); + return mDisplayFrame; +} + +void nsComboboxControlFrame::Destroy(DestroyContext& aContext) { + // Revoke any pending RedisplayTextEvent + mRedisplayTextEvent.Revoke(); + + mEventListener->Detach(); + + // Cleanup frames in popup child list + aContext.AddAnonymousContent(mDisplayContent.forget()); + aContext.AddAnonymousContent(mButtonContent.forget()); + nsBlockFrame::Destroy(aContext); +} + +const nsFrameList& nsComboboxControlFrame::GetChildList( + ChildListID aListID) const { + return nsBlockFrame::GetChildList(aListID); +} + +void nsComboboxControlFrame::GetChildLists(nsTArray* aLists) const { + nsBlockFrame::GetChildLists(aLists); +} + +void nsComboboxControlFrame::SetInitialChildList(ChildListID aListID, + nsFrameList&& aChildList) { + for (nsIFrame* f : aChildList) { + MOZ_ASSERT(f->GetParent() == this, "Unexpected parent"); + nsCOMPtr formControl = do_QueryInterface(f->GetContent()); + if (formControl && + formControl->ControlType() == FormControlType::ButtonButton) { + mButtonFrame = f; + break; + } + } + nsBlockFrame::SetInitialChildList(aListID, std::move(aChildList)); +} + +namespace mozilla { + +class nsDisplayComboboxFocus : public nsPaintedDisplayItem { + public: + nsDisplayComboboxFocus(nsDisplayListBuilder* aBuilder, + nsComboboxControlFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayComboboxFocus); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayComboboxFocus) + + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + NS_DISPLAY_DECL_NAME("ComboboxFocus", TYPE_COMBOBOX_FOCUS) +}; + +void nsDisplayComboboxFocus::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + static_cast(mFrame)->PaintFocus( + *aCtx->GetDrawTarget(), ToReferenceFrame()); +} + +} // namespace mozilla + +void nsComboboxControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + if (aBuilder->IsForEventDelivery()) { + // Don't allow children to receive events. + // REVIEW: following old GetFrameForPoint + DisplayBorderBackgroundOutline(aBuilder, aLists); + } else { + // REVIEW: Our in-flow child frames are inline-level so they will paint in + // our content list, so we don't need to mess with layers. + nsBlockFrame::BuildDisplayList(aBuilder, aLists); + } + + // draw a focus indicator only when focus rings should be drawn + if (Select().State().HasState(dom::ElementState::FOCUSRING) && IsThemed() && + PresContext()->Theme()->ThemeWantsButtonInnerFocusRing()) { + aLists.Content()->AppendNewToTop(aBuilder, this); + } + + DisplaySelectionOverlay(aBuilder, aLists.Content()); +} + +void nsComboboxControlFrame::PaintFocus(DrawTarget& aDrawTarget, nsPoint aPt) { + /* Do we need to do anything? */ + dom::ElementState state = mContent->AsElement()->State(); + if (state.HasState(dom::ElementState::DISABLED) || + !state.HasState(dom::ElementState::FOCUS)) { + return; + } + + int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel(); + + nsRect clipRect = mDisplayFrame->GetRect() + aPt; + aDrawTarget.PushClipRect( + NSRectToSnappedRect(clipRect, appUnitsPerDevPixel, aDrawTarget)); + + StrokeOptions strokeOptions; + nsLayoutUtils::InitDashPattern(strokeOptions, StyleBorderStyle::Dotted); + ColorPattern color(ToDeviceColor(StyleText()->mColor)); + nscoord onePixel = nsPresContext::CSSPixelsToAppUnits(1); + clipRect.width -= onePixel; + clipRect.height -= onePixel; + Rect r = ToRect(nsLayoutUtils::RectToGfxRect(clipRect, appUnitsPerDevPixel)); + StrokeSnappedEdgesOfRect(r, aDrawTarget, color, strokeOptions); + + aDrawTarget.PopClip(); +} + +//--------------------------------------------------------- +// gets the content (an option) by index and then set it as +// being selected or not selected +//--------------------------------------------------------- +NS_IMETHODIMP +nsComboboxControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) { + if (aSelected) { + nsAutoScriptBlocker blocker; + mDisplayedIndex = aIndex; + RedisplayText(); + } else { + AutoWeakFrame weakFrame(this); + RedisplaySelectedText(); + if (weakFrame.IsAlive()) { + FireValueChangeEvent(); // Fire after old option is unselected + } + } + return NS_OK; +} + +void nsComboboxControlFrame::FireValueChangeEvent() { + // Fire ValueChange event to indicate data value of combo box has changed + nsContentUtils::AddScriptRunner(new AsyncEventDispatcher( + mContent, u"ValueChange"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo)); +} diff --git a/layout/forms/nsComboboxControlFrame.h b/layout/forms/nsComboboxControlFrame.h new file mode 100644 index 0000000000..e878c0ebe3 --- /dev/null +++ b/layout/forms/nsComboboxControlFrame.h @@ -0,0 +1,244 @@ +/* -*- 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 nsComboboxControlFrame_h___ +#define nsComboboxControlFrame_h___ + +#ifdef DEBUG_evaughan +// #define DEBUG_rods +#endif + +#ifdef DEBUG_rods +// #define DO_REFLOW_DEBUG +// #define DO_REFLOW_COUNTER +// #define DO_UNCONSTRAINED_CHECK +// #define DO_PIXELS +// #define DO_NEW_REFLOW +#endif + +// Mark used to indicate when onchange has been fired for current combobox item +#define NS_SKIP_NOTIFY_INDEX -2 + +#include "mozilla/Attributes.h" +#include "nsBlockFrame.h" +#include "nsIFormControlFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsISelectControlFrame.h" +#include "nsIRollupListener.h" +#include "nsThreadUtils.h" + +class nsComboboxDisplayFrame; +class nsTextNode; + +namespace mozilla { +class PresShell; +class HTMLSelectEventListener; +namespace dom { +class HTMLSelectElement; +} + +namespace gfx { +class DrawTarget; +} // namespace gfx +} // namespace mozilla + +class nsComboboxControlFrame final : public nsBlockFrame, + public nsIFormControlFrame, + public nsIAnonymousContentCreator, + public nsISelectControlFrame { + using DrawTarget = mozilla::gfx::DrawTarget; + using Element = mozilla::dom::Element; + + public: + friend nsComboboxControlFrame* NS_NewComboboxControlFrame( + mozilla::PresShell* aPresShell, ComputedStyle* aStyle); + friend class nsComboboxDisplayFrame; + + explicit nsComboboxControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + ~nsComboboxControlFrame(); + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsComboboxControlFrame) + + // nsIAnonymousContentCreator + nsresult CreateAnonymousContent(nsTArray& aElements) final; + void AppendAnonymousContentTo(nsTArray& aElements, + uint32_t aFilter) final; + + nsIContent* GetDisplayNode() const; + nsIFrame* CreateFrameForDisplayNode(); + +#ifdef ACCESSIBILITY + mozilla::a11y::AccType AccessibleType() final; +#endif + + nscoord GetMinISize(gfxContext* aRenderingContext) final; + + nscoord GetPrefISize(gfxContext* aRenderingContext) final; + + void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, nsReflowStatus& aStatus) final; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) final; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) final; + + void PaintFocus(DrawTarget& aDrawTarget, nsPoint aPt); + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) final; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const final; +#endif + void Destroy(DestroyContext&) final; + + void SetInitialChildList(ChildListID aListID, nsFrameList&& aChildList) final; + const nsFrameList& GetChildList(ChildListID aListID) const final; + void GetChildLists(nsTArray* aLists) const final; + + nsContainerFrame* GetContentInsertionFrame() final; + + // Return the dropdown and display frame. + void AppendDirectlyOwnedAnonBoxes(nsTArray& aResult) final; + + // nsIFormControlFrame + nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) final { + return NS_OK; + } + + /** + * Inform the control that it got (or lost) focus. + * If it lost focus, the dropdown menu will be rolled up if needed, + * and FireOnChange() will be called. + * @param aOn true if got focus, false if lost focus. + * @param aRepaint if true then force repaint (NOTE: we always force repaint + * currently) + * @note This method might destroy |this|. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void SetFocus(bool aOn, bool aRepaint) final; + + /** + * Return the available space before and after this frame for + * placing the drop-down list, and the current 2D translation. + * Note that either or both can be less than or equal to zero, + * if both are then the drop-down should be closed. + */ + void GetAvailableDropdownSpace(mozilla::WritingMode aWM, nscoord* aBefore, + nscoord* aAfter, + mozilla::LogicalPoint* aTranslation); + int32_t GetIndexOfDisplayArea(); + /** + * @note This method might destroy |this|. + */ + nsresult RedisplaySelectedText(); + int32_t UpdateRecentIndex(int32_t aIndex); + + bool IsDroppedDown() const; + + // nsISelectControlFrame + NS_IMETHOD AddOption(int32_t index) final; + NS_IMETHOD RemoveOption(int32_t index) final; + NS_IMETHOD DoneAddingChildren(bool aIsDone) final; + NS_IMETHOD OnOptionSelected(int32_t aIndex, bool aSelected) final; + NS_IMETHOD_(void) + OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) final; + + int32_t CharCountOfLargestOptionForInflation() const; + + protected: + friend class RedisplayTextEvent; + friend class nsAsyncResize; + friend class nsResizeDropdownAtFinalPosition; + + // Return true if we should render a dropdown button. + bool HasDropDownButton() const; + nscoord DropDownButtonISize(); + + enum DropDownPositionState { + // can't show the dropdown at its current position + eDropDownPositionSuppressed, + // a resize reflow is pending, don't show it yet + eDropDownPositionPendingResize, + // the dropdown has its final size and position and can be displayed here + eDropDownPositionFinal + }; + DropDownPositionState AbsolutelyPositionDropDown(); + + nscoord GetLongestOptionISize(gfxContext*) const; + + // Helper for GetMinISize/GetPrefISize + nscoord GetIntrinsicISize(gfxContext* aRenderingContext, + mozilla::IntrinsicISizeType aType); + + class RedisplayTextEvent : public mozilla::Runnable { + public: + NS_DECL_NSIRUNNABLE + explicit RedisplayTextEvent(nsComboboxControlFrame* c) + : mozilla::Runnable("nsComboboxControlFrame::RedisplayTextEvent"), + mControlFrame(c) {} + void Revoke() { mControlFrame = nullptr; } + + private: + nsComboboxControlFrame* mControlFrame; + }; + + void CheckFireOnChange(); + void FireValueChangeEvent(); + nsresult RedisplayText(); + void HandleRedisplayTextEvent(); + void ActuallyDisplayText(bool aNotify); + + // If our total transform to the root frame of the root document is only a 2d + // translation then return that translation, otherwise returns (0,0). + nsPoint GetCSSTransformTranslation(); + + mozilla::dom::HTMLSelectElement& Select() const; + void GetOptionText(uint32_t aIndex, nsAString& aText) const; + + RefPtr mDisplayContent; // Anonymous content used to display the + // current selection + RefPtr mButtonContent; // Anonymous content for the button + nsContainerFrame* mDisplayFrame; // frame to display selection + nsIFrame* mButtonFrame; // button frame + + // The inline size of our display area. Used by that frame's reflow + // to size to the full inline size except the drop-marker. + nscoord mDisplayISize; + // The maximum inline size of our display area, which is the + // nsComoboxControlFrame's border-box. + // + // Going over this would be observable via DOM APIs like client / scrollWidth. + nscoord mMaxDisplayISize; + + nsRevocableEventPtr mRedisplayTextEvent; + + int32_t mRecentSelectedIndex; + int32_t mDisplayedIndex; + nsString mDisplayedOptionTextOrPreview; + + RefPtr mEventListener; + + // See comment in HandleRedisplayTextEvent(). + bool mInRedisplayText; + bool mIsOpenInParentProcess; + + // static class data member for Bug 32920 + // only one control can be focused at a time + static nsComboboxControlFrame* sFocused; + +#ifdef DO_REFLOW_COUNTER + int32_t mReflowId; +#endif +}; + +#endif diff --git a/layout/forms/nsDateTimeControlFrame.cpp b/layout/forms/nsDateTimeControlFrame.cpp new file mode 100644 index 0000000000..b19f787dbc --- /dev/null +++ b/layout/forms/nsDateTimeControlFrame.cpp @@ -0,0 +1,198 @@ +/* -*- 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/. */ + +/** + * This frame type is used for input type=date, time, month, week, and + * datetime-local. + */ + +#include "nsDateTimeControlFrame.h" + +#include "mozilla/PresShell.h" +#include "nsLayoutUtils.h" +#include "nsTextControlFrame.h" + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewDateTimeControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsDateTimeControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsDateTimeControlFrame) + +NS_QUERYFRAME_HEAD(nsDateTimeControlFrame) + NS_QUERYFRAME_ENTRY(nsDateTimeControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +nsDateTimeControlFrame::nsDateTimeControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsContainerFrame(aStyle, aPresContext, kClassID) {} + +nscoord nsDateTimeControlFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + + nsIFrame* kid = mFrames.FirstChild(); + if (kid) { // display:none? + result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, kid, + IntrinsicISizeType::MinISize); + } else { + result = 0; + } + + return result; +} + +nscoord nsDateTimeControlFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + + nsIFrame* kid = mFrames.FirstChild(); + if (kid) { // display:none? + result = nsLayoutUtils::IntrinsicForContainer( + aRenderingContext, kid, IntrinsicISizeType::PrefISize); + } else { + result = 0; + } + + return result; +} + +Maybe nsDateTimeControlFrame::GetNaturalBaselineBOffset( + WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const { + return nsTextControlFrame::GetSingleLineTextControlBaseline( + this, mFirstBaseline, aWM, aBaselineGroup); +} + +void nsDateTimeControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + + DO_GLOBAL_REFLOW_COUNT("nsDateTimeControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + NS_FRAME_TRACE( + NS_FRAME_TRACE_CALLS, + ("enter nsDateTimeControlFrame::Reflow: availSize=%d,%d", + aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight())); + + NS_ASSERTION(mFrames.GetLength() <= 1, + "There should be no more than 1 frames"); + + const WritingMode myWM = aReflowInput.GetWritingMode(); + + { + auto baseline = nsTextControlFrame::ComputeBaseline( + this, aReflowInput, /* aForSingleLineControl = */ true); + mFirstBaseline = baseline.valueOr(NS_INTRINSIC_ISIZE_UNKNOWN); + if (baseline) { + aDesiredSize.SetBlockStartAscent(*baseline); + } + } + + // The ISize of our content box, which is the available ISize + // for our anonymous content: + const nscoord contentBoxISize = aReflowInput.ComputedISize(); + nscoord contentBoxBSize = aReflowInput.ComputedBSize(); + + // Figure out our border-box sizes as well (by adding borderPadding to + // content-box sizes): + const auto borderPadding = aReflowInput.ComputedLogicalBorderPadding(myWM); + const nscoord borderBoxISize = + contentBoxISize + borderPadding.IStartEnd(myWM); + + nscoord borderBoxBSize; + if (contentBoxBSize != NS_UNCONSTRAINEDSIZE) { + borderBoxBSize = contentBoxBSize + borderPadding.BStartEnd(myWM); + } // else, we'll figure out borderBoxBSize after we resolve contentBoxBSize. + + nsIFrame* inputAreaFrame = mFrames.FirstChild(); + if (!inputAreaFrame) { // display:none? + if (contentBoxBSize == NS_UNCONSTRAINEDSIZE) { + contentBoxBSize = 0; + borderBoxBSize = borderPadding.BStartEnd(myWM); + } + } else { + ReflowOutput childDesiredSize(aReflowInput); + + WritingMode wm = inputAreaFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + + ReflowInput childReflowInput(aPresContext, aReflowInput, inputAreaFrame, + availSize); + + // Convert input area margin into my own writing-mode (in case it differs): + LogicalMargin childMargin = childReflowInput.ComputedLogicalMargin(myWM); + + // offsets of input area frame within this frame: + LogicalPoint childOffset = + borderPadding.StartOffset(myWM) + childMargin.StartOffset(myWM); + + nsReflowStatus childStatus; + // We initially reflow the child with a dummy containerSize; positioning + // will be fixed later. + const nsSize dummyContainerSize; + ReflowChild(inputAreaFrame, aPresContext, childDesiredSize, + childReflowInput, myWM, childOffset, dummyContainerSize, + ReflowChildFlags::Default, childStatus); + MOZ_ASSERT(childStatus.IsFullyComplete(), + "We gave our child unconstrained available block-size, " + "so it should be complete"); + + nscoord childMarginBoxBSize = + childDesiredSize.BSize(myWM) + childMargin.BStartEnd(myWM); + + if (contentBoxBSize == NS_UNCONSTRAINEDSIZE) { + // We are intrinsically sized -- we should shrinkwrap the input area's + // block-size, or our line-height: + contentBoxBSize = + std::max(aReflowInput.GetLineHeight(), childMarginBoxBSize); + + // Make sure we obey min/max-bsize in the case when we're doing intrinsic + // sizing (we get it for free when we have a non-intrinsic + // aReflowInput.ComputedBSize()). Note that we do this before + // adjusting for borderpadding, since ComputedMaxBSize and + // ComputedMinBSize are content heights. + contentBoxBSize = aReflowInput.ApplyMinMaxBSize(contentBoxBSize); + + borderBoxBSize = contentBoxBSize + borderPadding.BStartEnd(myWM); + } + + // Center child in block axis + nscoord extraSpace = contentBoxBSize - childMarginBoxBSize; + childOffset.B(myWM) += std::max(0, extraSpace / 2); + + // Needed in FinishReflowChild, for logical-to-physical conversion: + nsSize borderBoxSize = + LogicalSize(myWM, borderBoxISize, borderBoxBSize).GetPhysicalSize(myWM); + + // Place the child + FinishReflowChild(inputAreaFrame, aPresContext, childDesiredSize, + &childReflowInput, myWM, childOffset, borderBoxSize, + ReflowChildFlags::Default); + } + + LogicalSize logicalDesiredSize(myWM, borderBoxISize, borderBoxBSize); + aDesiredSize.SetSize(myWM, logicalDesiredSize); + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + if (inputAreaFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, inputAreaFrame); + } + + FinishAndStoreOverflow(&aDesiredSize); + + NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS, + ("exit nsDateTimeControlFrame::Reflow: size=%d,%d", + aDesiredSize.Width(), aDesiredSize.Height())); +} diff --git a/layout/forms/nsDateTimeControlFrame.h b/layout/forms/nsDateTimeControlFrame.h new file mode 100644 index 0000000000..0f2af85a34 --- /dev/null +++ b/layout/forms/nsDateTimeControlFrame.h @@ -0,0 +1,66 @@ +/* -*- 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/. */ + +/** + * This frame type is used for input type=date, time, month, week, and + * datetime-local. + * + * NOTE: some of the above-mentioned input types are still to-be-implemented. + * See nsCSSFrameConstructor::FindInputData, as well as bug 1286182 (date), + * bug 1306215 (month), bug 1306216 (week) and bug 1306217 (datetime-local). + */ + +#ifndef nsDateTimeControlFrame_h__ +#define nsDateTimeControlFrame_h__ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" + +namespace mozilla { +class PresShell; +namespace dom { +struct DateTimeValue; +} // namespace dom +} // namespace mozilla + +class nsDateTimeControlFrame final : public nsContainerFrame { + typedef mozilla::dom::DateTimeValue DateTimeValue; + + explicit nsDateTimeControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + public: + friend nsIFrame* NS_NewDateTimeControlFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsDateTimeControlFrame) + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"DateTimeControl"_ns, aResult); + } +#endif + + // Reflow + nscoord GetMinISize(gfxContext* aRenderingContext) override; + + nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + Maybe GetNaturalBaselineBOffset( + mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const override; + + nscoord mFirstBaseline = NS_INTRINSIC_ISIZE_UNKNOWN; +}; + +#endif // nsDateTimeControlFrame_h__ diff --git a/layout/forms/nsFieldSetFrame.cpp b/layout/forms/nsFieldSetFrame.cpp new file mode 100644 index 0000000000..03781da8bd --- /dev/null +++ b/layout/forms/nsFieldSetFrame.cpp @@ -0,0 +1,938 @@ +/* -*- 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 "nsFieldSetFrame.h" +#include "mozilla/dom/HTMLLegendElement.h" + +#include +#include "gfxContext.h" +#include "mozilla/Baseline.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/Likely.h" +#include "mozilla/PresShell.h" +#include "mozilla/Maybe.h" +#include "mozilla/webrender/WebRenderAPI.h" +#include "nsBlockFrame.h" +#include "nsCSSAnonBoxes.h" +#include "nsCSSFrameConstructor.h" +#include "nsCSSRendering.h" +#include "nsDisplayList.h" +#include "nsGkAtoms.h" +#include "nsIFrameInlines.h" +#include "nsIScrollableFrame.h" +#include "nsLayoutUtils.h" +#include "nsStyleConsts.h" + +using namespace mozilla; +using namespace mozilla::gfx; +using namespace mozilla::layout; +using image::ImgDrawResult; + +nsContainerFrame* NS_NewFieldSetFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) nsFieldSetFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsFieldSetFrame) +NS_QUERYFRAME_HEAD(nsFieldSetFrame) + NS_QUERYFRAME_ENTRY(nsFieldSetFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +nsFieldSetFrame::nsFieldSetFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsContainerFrame(aStyle, aPresContext, kClassID), + mLegendRect(GetWritingMode()) { + mLegendSpace = 0; +} + +nsRect nsFieldSetFrame::VisualBorderRectRelativeToSelf() const { + WritingMode wm = GetWritingMode(); + LogicalRect r(wm, LogicalPoint(wm, 0, 0), GetLogicalSize(wm)); + nsSize containerSize = r.Size(wm).GetPhysicalSize(wm); + nsIFrame* legend = GetLegend(); + if (legend && !GetPrevInFlow()) { + nscoord legendSize = legend->GetLogicalSize(wm).BSize(wm); + auto legendMargin = legend->GetLogicalUsedMargin(wm); + nscoord legendStartMargin = legendMargin.BStart(wm); + nscoord legendEndMargin = legendMargin.BEnd(wm); + nscoord border = GetUsedBorder().Side(wm.PhysicalSide(eLogicalSideBStart)); + // Calculate the offset from the border area block-axis start edge needed to + // center-align our border with the legend's border-box (in the block-axis). + nscoord off = (legendStartMargin + legendSize / 2) - border / 2; + // We don't want to display our border above our border area. + if (off > nscoord(0)) { + nscoord marginBoxSize = legendStartMargin + legendSize + legendEndMargin; + if (marginBoxSize > border) { + // We don't want to display our border below the legend's margin-box, + // so we align it to the block-axis end if that happens. + nscoord overflow = off + border - marginBoxSize; + if (overflow > nscoord(0)) { + off -= overflow; + } + r.BStart(wm) += off; + r.BSize(wm) -= off; + } + } + } + return r.GetPhysicalRect(wm, containerSize); +} + +nsContainerFrame* nsFieldSetFrame::GetInner() const { + for (nsIFrame* child : mFrames) { + if (child->Style()->GetPseudoType() == PseudoStyleType::fieldsetContent) { + return static_cast(child); + } + } + return nullptr; +} + +nsIFrame* nsFieldSetFrame::GetLegend() const { + for (nsIFrame* child : mFrames) { + if (child->Style()->GetPseudoType() != PseudoStyleType::fieldsetContent) { + return child; + } + } + return nullptr; +} + +namespace mozilla { + +class nsDisplayFieldSetBorder final : public nsPaintedDisplayItem { + public: + nsDisplayFieldSetBorder(nsDisplayListBuilder* aBuilder, + nsFieldSetFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayFieldSetBorder); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayFieldSetBorder) + + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; + bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override; + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const override; + NS_DISPLAY_DECL_NAME("FieldSetBorder", TYPE_FIELDSET_BORDER_BACKGROUND) +}; + +void nsDisplayFieldSetBorder::Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) { + Unused << static_cast(mFrame)->PaintBorder( + aBuilder, *aCtx, ToReferenceFrame(), GetPaintRect(aBuilder, aCtx)); +} + +nsRect nsDisplayFieldSetBorder::GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const { + // Just go ahead and claim our frame's overflow rect as the bounds, because we + // may have border-image-outset or other features that cause borders to extend + // outside the border rect. We could try to duplicate all the complexity + // nsDisplayBorder has here, but keeping things in sync would be a pain, and + // this code is not typically performance-sensitive. + *aSnap = false; + return Frame()->InkOverflowRectRelativeToSelf() + ToReferenceFrame(); +} + +bool nsDisplayFieldSetBorder::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + auto frame = static_cast(mFrame); + auto offset = ToReferenceFrame(); + Maybe clipOut; + + nsRect rect = frame->VisualBorderRectRelativeToSelf() + offset; + nsDisplayBoxShadowInner::CreateInsetBoxShadowWebRenderCommands( + aBuilder, aSc, rect, mFrame, rect); + + if (nsIFrame* legend = frame->GetLegend()) { + nsRect legendRect = legend->GetNormalRect() + offset; + + // Make sure we clip all of the border in case the legend is smaller. + nscoord borderTopWidth = frame->GetUsedBorder().top; + if (legendRect.height < borderTopWidth) { + legendRect.height = borderTopWidth; + legendRect.y = offset.y; + } + + if (!legendRect.IsEmpty()) { + // We need to clip out the part of the border where the legend would go + auto appUnitsPerDevPixel = frame->PresContext()->AppUnitsPerDevPixel(); + auto layoutRect = wr::ToLayoutRect(LayoutDeviceRect::FromAppUnits( + frame->InkOverflowRectRelativeToSelf() + offset, + appUnitsPerDevPixel)); + + wr::ComplexClipRegion region; + region.rect = wr::ToLayoutRect( + LayoutDeviceRect::FromAppUnits(legendRect, appUnitsPerDevPixel)); + region.mode = wr::ClipMode::ClipOut; + region.radii = wr::EmptyBorderRadius(); + + auto rect_clip = aBuilder.DefineRectClip(Nothing(), layoutRect); + auto complex_clip = aBuilder.DefineRoundedRectClip(Nothing(), region); + auto clipChain = + aBuilder.DefineClipChain({rect_clip, complex_clip}, true); + clipOut.emplace(aBuilder, clipChain); + } + } else { + rect = nsRect(offset, frame->GetRect().Size()); + } + + ImgDrawResult drawResult = nsCSSRendering::CreateWebRenderCommandsForBorder( + this, mFrame, rect, aBuilder, aResources, aSc, aManager, + aDisplayListBuilder); + if (drawResult == ImgDrawResult::NOT_SUPPORTED) { + return false; + } + return true; +}; + +} // namespace mozilla + +void nsFieldSetFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + // Paint our background and border in a special way. + // REVIEW: We don't really need to check frame emptiness here; if it's empty, + // the background/border display item won't do anything, and if it isn't + // empty, we need to paint the outline + if (!HasAnyStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER) && + IsVisibleForPainting()) { + DisplayOutsetBoxShadowUnconditional(aBuilder, aLists.BorderBackground()); + + const nsRect rect = + VisualBorderRectRelativeToSelf() + aBuilder->ToReferenceFrame(this); + + nsDisplayBackgroundImage::AppendBackgroundItemsToTop( + aBuilder, this, rect, aLists.BorderBackground(), + /* aAllowWillPaintBorderOptimization = */ false); + + aLists.BorderBackground()->AppendNewToTop(aBuilder, + this); + + DisplayOutlineUnconditional(aBuilder, aLists); + + DO_GLOBAL_REFLOW_COUNT_DSP("nsFieldSetFrame"); + } + + if (GetPrevInFlow()) { + DisplayOverflowContainers(aBuilder, aLists); + } + + nsDisplayListCollection contentDisplayItems(aBuilder); + if (nsIFrame* inner = GetInner()) { + // Collect the inner frame's display items into their own collection. + // We need to be calling BuildDisplayList on it before the legend in + // case it contains out-of-flow frames whose placeholders are in the + // legend. However, we want the inner frame's display items to be + // after the legend's display items in z-order, so we need to save them + // and append them later. + BuildDisplayListForChild(aBuilder, inner, contentDisplayItems); + } + if (nsIFrame* legend = GetLegend()) { + // The legend's background goes on our BlockBorderBackgrounds list because + // it's a block child. + nsDisplayListSet set(aLists, aLists.BlockBorderBackgrounds()); + BuildDisplayListForChild(aBuilder, legend, set); + } + // Put the inner frame's display items on the master list. Note that this + // moves its border/background display items to our BorderBackground() list, + // which isn't really correct, but it's OK because the inner frame is + // anonymous and can't have its own border and background. + contentDisplayItems.MoveTo(aLists); +} + +ImgDrawResult nsFieldSetFrame::PaintBorder(nsDisplayListBuilder* aBuilder, + gfxContext& aRenderingContext, + nsPoint aPt, + const nsRect& aDirtyRect) { + // If the border is smaller than the legend, move the border down + // to be centered on the legend. We call VisualBorderRectRelativeToSelf() to + // compute the border positioning. + // FIXME: This means border-radius clamping is incorrect; we should + // override nsIFrame::GetBorderRadii. + nsRect rect = VisualBorderRectRelativeToSelf() + aPt; + nsPresContext* presContext = PresContext(); + + const auto skipSides = GetSkipSides(); + PaintBorderFlags borderFlags = aBuilder->ShouldSyncDecodeImages() + ? PaintBorderFlags::SyncDecodeImages + : PaintBorderFlags(); + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + nsCSSRendering::PaintBoxShadowInner(presContext, aRenderingContext, this, + rect); + + if (nsIFrame* legend = GetLegend()) { + // We want to avoid drawing our border under the legend, so clip out the + // legend while drawing our border. We don't want to use mLegendRect here, + // because we do want to draw our border under the legend's inline-start and + // -end margins. And we use GetNormalRect(), not GetRect(), because we do + // not want relative positioning applied to the legend to change how our + // border looks. + nsRect legendRect = legend->GetNormalRect() + aPt; + + // Make sure we clip all of the border in case the legend is smaller. + nscoord borderTopWidth = GetUsedBorder().top; + if (legendRect.height < borderTopWidth) { + legendRect.height = borderTopWidth; + legendRect.y = aPt.y; + } + + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + // We set up a clip path which has our rect clockwise and the legend rect + // counterclockwise, with FILL_WINDING as the fill rule. That will allow us + // to paint within our rect but outside the legend rect. For "our rect" we + // use our ink overflow rect (relative to ourselves, so it's not affected + // by transforms), because we can have borders sticking outside our border + // box (e.g. due to border-image-outset). + RefPtr pathBuilder = + drawTarget->CreatePathBuilder(FillRule::FILL_WINDING); + int32_t appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel(); + AppendRectToPath(pathBuilder, + NSRectToSnappedRect(InkOverflowRectRelativeToSelf() + aPt, + appUnitsPerDevPixel, *drawTarget), + true); + AppendRectToPath( + pathBuilder, + NSRectToSnappedRect(legendRect, appUnitsPerDevPixel, *drawTarget), + false); + RefPtr clipPath = pathBuilder->Finish(); + + aRenderingContext.Save(); + aRenderingContext.Clip(clipPath); + result &= nsCSSRendering::PaintBorder(presContext, aRenderingContext, this, + aDirtyRect, rect, mComputedStyle, + borderFlags, skipSides); + aRenderingContext.Restore(); + } else { + result &= nsCSSRendering::PaintBorder( + presContext, aRenderingContext, this, aDirtyRect, + nsRect(aPt, mRect.Size()), mComputedStyle, borderFlags, skipSides); + } + + return result; +} + +nscoord nsFieldSetFrame::GetIntrinsicISize(gfxContext* aRenderingContext, + IntrinsicISizeType aType) { + // Both inner and legend are children, and if the fieldset is + // size-contained they should not contribute to the intrinsic size. + if (Maybe containISize = ContainIntrinsicISize()) { + return *containISize; + } + + nscoord legendWidth = 0; + if (nsIFrame* legend = GetLegend()) { + legendWidth = + nsLayoutUtils::IntrinsicForContainer(aRenderingContext, legend, aType); + } + + nscoord contentWidth = 0; + if (nsIFrame* inner = GetInner()) { + // Ignore padding on the inner, since the padding will be applied to the + // outer instead, and the padding computed for the inner is wrong + // for percentage padding. + contentWidth = nsLayoutUtils::IntrinsicForContainer( + aRenderingContext, inner, aType, nsLayoutUtils::IGNORE_PADDING); + } + + return std::max(legendWidth, contentWidth); +} + +nscoord nsFieldSetFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result = 0; + DISPLAY_MIN_INLINE_SIZE(this, result); + + result = GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::MinISize); + return result; +} + +nscoord nsFieldSetFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result = 0; + DISPLAY_PREF_INLINE_SIZE(this, result); + + result = GetIntrinsicISize(aRenderingContext, IntrinsicISizeType::PrefISize); + return result; +} + +/* virtual */ +void nsFieldSetFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + using LegendAlignValue = mozilla::dom::HTMLLegendElement::LegendAlignValue; + + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsFieldSetFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + NS_WARNING_ASSERTION(aReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE, + "Should have a precomputed inline-size!"); + + OverflowAreas ocBounds; + nsReflowStatus ocStatus; + auto* prevInFlow = static_cast(GetPrevInFlow()); + if (prevInFlow) { + ReflowOverflowContainerChildren(aPresContext, aReflowInput, ocBounds, + ReflowChildFlags::Default, ocStatus); + + AutoFrameListPtr prevOverflowFrames(PresContext(), + prevInFlow->StealOverflowFrames()); + if (prevOverflowFrames) { + nsContainerFrame::ReparentFrameViewList(*prevOverflowFrames, prevInFlow, + this); + mFrames.InsertFrames(this, nullptr, std::move(*prevOverflowFrames)); + } + } + + bool reflowInner; + bool reflowLegend; + nsIFrame* legend = GetLegend(); + nsContainerFrame* inner = GetInner(); + if (!legend || !inner) { + if (DrainSelfOverflowList()) { + legend = GetLegend(); + inner = GetInner(); + } + } + if (aReflowInput.ShouldReflowAllKids() || GetNextInFlow() || + aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE) { + reflowInner = inner != nullptr; + reflowLegend = legend != nullptr; + } else { + reflowInner = inner && inner->IsSubtreeDirty(); + reflowLegend = legend && legend->IsSubtreeDirty(); + } + + // @note |this| frame applies borders but not any padding. Our anonymous + // inner frame applies the padding (but not borders). + const auto wm = GetWritingMode(); + auto skipSides = PreReflowBlockLevelLogicalSkipSides(); + LogicalMargin border = + aReflowInput.ComputedLogicalBorder(wm).ApplySkipSides(skipSides); + LogicalSize availSize(wm, aReflowInput.ComputedSize().ISize(wm), + aReflowInput.AvailableBSize()); + + // Figure out how big the legend is if there is one. + LogicalMargin legendMargin(wm); + Maybe legendReflowInput; + if (legend) { + const auto legendWM = legend->GetWritingMode(); + LogicalSize legendAvailSize = availSize.ConvertTo(legendWM, wm); + ComputeSizeFlags sizeFlags; + if (legend->StylePosition()->ISize(wm).IsAuto()) { + sizeFlags = ComputeSizeFlag::ShrinkWrap; + } + ReflowInput::InitFlags initFlags; // intentionally empty + StyleSizeOverrides sizeOverrides; // intentionally empty + legendReflowInput.emplace(aPresContext, aReflowInput, legend, + legendAvailSize, Nothing(), initFlags, + sizeOverrides, sizeFlags); + } + const bool avoidBreakInside = ShouldAvoidBreakInside(aReflowInput); + if (reflowLegend) { + ReflowOutput legendDesiredSize(aReflowInput); + + // We'll move the legend to its proper place later, so the position + // and containerSize passed here are unimportant. + const nsSize dummyContainerSize; + ReflowChild(legend, aPresContext, legendDesiredSize, *legendReflowInput, wm, + LogicalPoint(wm), dummyContainerSize, + ReflowChildFlags::NoMoveFrame, aStatus); + + if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE && + !(HasAnyStateBits(NS_FRAME_OUT_OF_FLOW) && + aReflowInput.mStyleDisplay->IsAbsolutelyPositionedStyle()) && + !prevInFlow && !aReflowInput.mFlags.mIsTopOfPage) { + // Propagate break-before from the legend to the fieldset. + if (legend->StyleDisplay()->BreakBefore() || + aStatus.IsInlineBreakBefore()) { + aStatus.SetInlineLineBreakBeforeAndReset(); + return; + } + // Honor break-inside:avoid by breaking before instead. + if (MOZ_UNLIKELY(avoidBreakInside) && !aStatus.IsFullyComplete()) { + aStatus.SetInlineLineBreakBeforeAndReset(); + return; + } + } + + // Calculate the legend's margin-box rectangle. + legendMargin = legend->GetLogicalUsedMargin(wm); + mLegendRect = LogicalRect( + wm, 0, 0, legendDesiredSize.ISize(wm) + legendMargin.IStartEnd(wm), + legendDesiredSize.BSize(wm) + legendMargin.BStartEnd(wm)); + // We subtract mLegendSpace from inner's content-box block-size below. + nscoord oldSpace = mLegendSpace; + mLegendSpace = 0; + nscoord borderBStart = border.BStart(wm); + if (!prevInFlow) { + if (mLegendRect.BSize(wm) > borderBStart) { + mLegendSpace = mLegendRect.BSize(wm) - borderBStart; + } else { + // Calculate the border-box position that would center the legend's + // border-box within the fieldset border: + nscoord off = (borderBStart - legendDesiredSize.BSize(wm)) / 2; + off -= legendMargin.BStart(wm); // convert to a margin-box position + if (off > nscoord(0)) { + // Align the legend to the end if center-aligning it would overflow. + nscoord overflow = off + mLegendRect.BSize(wm) - borderBStart; + if (overflow > nscoord(0)) { + off -= overflow; + } + mLegendRect.BStart(wm) += off; + } + } + } else { + mLegendSpace = mLegendRect.BSize(wm); + } + + // If mLegendSpace changes then we need to reflow |inner| as well. + if (mLegendSpace != oldSpace && inner) { + reflowInner = true; + } + + FinishReflowChild(legend, aPresContext, legendDesiredSize, + legendReflowInput.ptr(), wm, LogicalPoint(wm), + dummyContainerSize, ReflowChildFlags::NoMoveFrame); + EnsureChildContinuation(legend, aStatus); + if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE && + !legend->GetWritingMode().IsOrthogonalTo(wm) && + legend->StyleDisplay()->BreakAfter() && + (!legendReflowInput->mFlags.mIsTopOfPage || + mLegendRect.BSize(wm) > 0) && + aStatus.IsComplete()) { + // Pretend that we ran out of space to push children of |inner|. + // XXX(mats) perhaps pushing the inner frame would be more correct, + // but we don't support that yet. + availSize.BSize(wm) = nscoord(0); + aStatus.Reset(); + aStatus.SetIncomplete(); + } + } else if (!legend) { + mLegendRect.SetEmpty(); + mLegendSpace = 0; + } else { + // mLegendSpace and mLegendRect haven't changed, but we need + // the used margin when placing the legend. + legendMargin = legend->GetLogicalUsedMargin(wm); + } + + // This containerSize is incomplete as yet: it does not include the size + // of the |inner| frame itself. + nsSize containerSize = + (LogicalSize(wm, 0, mLegendSpace) + border.Size(wm)).GetPhysicalSize(wm); + if (reflowInner) { + LogicalSize innerAvailSize = availSize; + innerAvailSize.ISize(wm) = + aReflowInput.ComputedSizeWithPadding(wm).ISize(wm); + nscoord remainingComputedBSize = aReflowInput.ComputedBSize(); + if (prevInFlow && remainingComputedBSize != NS_UNCONSTRAINEDSIZE) { + // Subtract the consumed BSize associated with the legend. + for (nsIFrame* prev = prevInFlow; prev; prev = prev->GetPrevInFlow()) { + auto* prevFieldSet = static_cast(prev); + remainingComputedBSize -= prevFieldSet->mLegendSpace; + } + remainingComputedBSize = std::max(0, remainingComputedBSize); + } + if (innerAvailSize.BSize(wm) != NS_UNCONSTRAINEDSIZE) { + innerAvailSize.BSize(wm) -= + std::max(mLegendRect.BSize(wm), border.BStart(wm)); + if (StyleBorder()->mBoxDecorationBreak == + StyleBoxDecorationBreak::Clone && + (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE || + remainingComputedBSize + + aReflowInput.ComputedLogicalBorderPadding(wm).BStartEnd( + wm) >= + availSize.BSize(wm))) { + innerAvailSize.BSize(wm) -= border.BEnd(wm); + } + innerAvailSize.BSize(wm) = std::max(0, innerAvailSize.BSize(wm)); + } + ReflowInput kidReflowInput(aPresContext, aReflowInput, inner, + innerAvailSize, Nothing(), + ReflowInput::InitFlag::CallerWillInit); + // Override computed padding, in case it's percentage padding + kidReflowInput.Init( + aPresContext, Nothing(), Nothing(), + Some(aReflowInput.ComputedLogicalPadding(inner->GetWritingMode()))); + + // Propagate the aspect-ratio flag to |inner| (i.e. the container frame + // wrapped by nsFieldSetFrame), so we can let |inner|'s reflow code handle + // automatic content-based minimum. + // Note: Init() resets this flag, so we have to copy it again here. + if (aReflowInput.mFlags.mIsBSizeSetByAspectRatio) { + kidReflowInput.mFlags.mIsBSizeSetByAspectRatio = true; + } + + if (kidReflowInput.mFlags.mIsTopOfPage) { + // Prevent break-before from |inner| if we have a legend. + kidReflowInput.mFlags.mIsTopOfPage = !legend; + } + // Our child is "height:100%" but we actually want its height to be reduced + // by the amount of content-height the legend is eating up, unless our + // height is unconstrained (in which case the child's will be too). + if (aReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE) { + kidReflowInput.SetComputedBSize( + std::max(0, remainingComputedBSize - mLegendSpace)); + } + + if (aReflowInput.ComputedMinBSize() > 0) { + kidReflowInput.SetComputedMinBSize( + std::max(0, aReflowInput.ComputedMinBSize() - mLegendSpace)); + } + + if (aReflowInput.ComputedMaxBSize() != NS_UNCONSTRAINEDSIZE) { + kidReflowInput.SetComputedMaxBSize( + std::max(0, aReflowInput.ComputedMaxBSize() - mLegendSpace)); + } + + ReflowOutput kidDesiredSize(kidReflowInput); + NS_ASSERTION( + kidReflowInput.ComputedPhysicalMargin() == nsMargin(0, 0, 0, 0), + "Margins on anonymous fieldset child not supported!"); + LogicalPoint pt(wm, border.IStart(wm), border.BStart(wm) + mLegendSpace); + + // We don't know the correct containerSize until we have reflowed |inner|, + // so we use a dummy value for now; FinishReflowChild will fix the position + // if necessary. + const nsSize dummyContainerSize; + nsReflowStatus status; + // If our legend needs a continuation then *this* frame will have + // a continuation as well so we should keep our inner frame continuations + // too (even if 'inner' ends up being COMPLETE here). This ensures that + // our continuation will have a reasonable inline-size. + ReflowChildFlags flags = aStatus.IsFullyComplete() + ? ReflowChildFlags::Default + : ReflowChildFlags::NoDeleteNextInFlowChild; + ReflowChild(inner, aPresContext, kidDesiredSize, kidReflowInput, wm, pt, + dummyContainerSize, flags, status); + + // Honor break-inside:avoid when possible by returning a BreakBefore status. + if (MOZ_UNLIKELY(avoidBreakInside) && !prevInFlow && + !aReflowInput.mFlags.mIsTopOfPage && + availSize.BSize(wm) != NS_UNCONSTRAINEDSIZE) { + if (status.IsInlineBreakBefore() || !status.IsFullyComplete()) { + aStatus.SetInlineLineBreakBeforeAndReset(); + return; + } + } + + // Update containerSize to account for size of the inner frame, so that + // FinishReflowChild can position it correctly. + containerSize += kidDesiredSize.PhysicalSize(); + FinishReflowChild(inner, aPresContext, kidDesiredSize, &kidReflowInput, wm, + pt, containerSize, ReflowChildFlags::Default); + EnsureChildContinuation(inner, status); + aStatus.MergeCompletionStatusFrom(status); + NS_FRAME_TRACE_REFLOW_OUT("FieldSet::Reflow", aStatus); + } else if (inner) { + // |inner| didn't need to be reflowed but we do need to include its size + // in containerSize. + containerSize += inner->GetSize(); + } else { + // No |inner| means it was already complete in an earlier continuation. + MOZ_ASSERT(prevInFlow, "first-in-flow should always have an inner frame"); + for (nsIFrame* prev = prevInFlow; prev; prev = prev->GetPrevInFlow()) { + auto* prevFieldSet = static_cast(prev); + if (auto* prevInner = prevFieldSet->GetInner()) { + containerSize += prevInner->GetSize(); + break; + } + } + } + + LogicalRect contentRect(wm); + if (inner) { + // We don't support margins on inner, so our content rect is just the + // inner's border-box. (We don't really care about container size at this + // point, as we'll figure out the actual positioning later.) + contentRect = inner->GetLogicalRect(wm, containerSize); + } else if (prevInFlow) { + auto size = prevInFlow->GetPaddingRectRelativeToSelf().Size(); + contentRect.ISize(wm) = wm.IsVertical() ? size.height : size.width; + } + + if (legend) { + // The legend is positioned inline-wards within the inner's content rect + // (so that padding on the fieldset affects the legend position). + LogicalRect innerContentRect = contentRect; + innerContentRect.Deflate(wm, aReflowInput.ComputedLogicalPadding(wm)); + // If the inner content rect is larger than the legend, we can align the + // legend. + if (innerContentRect.ISize(wm) > mLegendRect.ISize(wm)) { + // NOTE legend @align values are: left/right/center + // GetLogicalAlign converts left/right to start/end for the given WM. + // @see HTMLLegendElement::ParseAttribute/LogicalAlign + auto* legendElement = + dom::HTMLLegendElement::FromNode(legend->GetContent()); + switch (legendElement->LogicalAlign(wm)) { + case LegendAlignValue::InlineEnd: + mLegendRect.IStart(wm) = + innerContentRect.IEnd(wm) - mLegendRect.ISize(wm); + break; + case LegendAlignValue::Center: + // Note: rounding removed; there doesn't seem to be any need + mLegendRect.IStart(wm) = + innerContentRect.IStart(wm) + + (innerContentRect.ISize(wm) - mLegendRect.ISize(wm)) / 2; + break; + case LegendAlignValue::InlineStart: + mLegendRect.IStart(wm) = innerContentRect.IStart(wm); + break; + default: + MOZ_ASSERT_UNREACHABLE("unexpected GetLogicalAlign value"); + } + } else { + // otherwise just start-align it. + mLegendRect.IStart(wm) = innerContentRect.IStart(wm); + } + + // place the legend + LogicalRect actualLegendRect = mLegendRect; + actualLegendRect.Deflate(wm, legendMargin); + LogicalPoint actualLegendPos(actualLegendRect.Origin(wm)); + + // Note that legend's writing mode may be different from the fieldset's, + // so we need to convert offsets before applying them to it (bug 1134534). + LogicalMargin offsets = legendReflowInput->ComputedLogicalOffsets(wm); + ReflowInput::ApplyRelativePositioning(legend, wm, offsets, &actualLegendPos, + containerSize); + + legend->SetPosition(wm, actualLegendPos, containerSize); + nsContainerFrame::PositionFrameView(legend); + nsContainerFrame::PositionChildViews(legend); + } + + // Skip our block-end border if we're INCOMPLETE. + if (!aStatus.IsComplete() && + StyleBorder()->mBoxDecorationBreak != StyleBoxDecorationBreak::Clone) { + border.BEnd(wm) = nscoord(0); + } + + // Return our size and our result. + LogicalSize finalSize( + wm, contentRect.ISize(wm) + border.IStartEnd(wm), + mLegendSpace + border.BStartEnd(wm) + (inner ? inner->BSize(wm) : 0)); + if (Maybe containBSize = + aReflowInput.mFrame->ContainIntrinsicBSize()) { + // If we're size-contained in block axis, then we must set finalSize + // according to contain-intrinsic-block-size, disregarding legend and inner. + // Note: normally the fieldset's own padding (which we still must honor) + // would be accounted for as part of inner's size (see kidReflowInput.Init() + // call above). So: since we're disregarding sizing information from + // 'inner', we need to account for that padding ourselves here. + nscoord contentBoxBSize = + aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE + ? aReflowInput.ApplyMinMaxBSize(*containBSize) + : aReflowInput.ComputedBSize(); + finalSize.BSize(wm) = + contentBoxBSize + + aReflowInput.ComputedLogicalBorderPadding(wm).BStartEnd(wm); + } + + if (aStatus.IsComplete() && + aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE && + finalSize.BSize(wm) > aReflowInput.AvailableBSize() && + border.BEnd(wm) > 0 && aReflowInput.AvailableBSize() > border.BEnd(wm)) { + // Our end border doesn't fit but it should fit in the next column/page. + if (MOZ_UNLIKELY(avoidBreakInside)) { + aStatus.SetInlineLineBreakBeforeAndReset(); + return; + } else { + if (StyleBorder()->mBoxDecorationBreak == + StyleBoxDecorationBreak::Slice) { + finalSize.BSize(wm) -= border.BEnd(wm); + } + aStatus.SetIncomplete(); + } + } + + if (!aStatus.IsComplete()) { + MOZ_ASSERT(aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE, + "must be Complete in an unconstrained available block-size"); + // Stretch our BSize to fill the fragmentainer. + finalSize.BSize(wm) = + std::max(finalSize.BSize(wm), aReflowInput.AvailableBSize()); + } + aDesiredSize.SetSize(wm, finalSize); + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + if (legend) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, legend); + } + if (inner) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, inner); + } + + // Merge overflow container bounds and status. + aDesiredSize.mOverflowAreas.UnionWith(ocBounds); + aStatus.MergeCompletionStatusFrom(ocStatus); + + FinishReflowWithAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput, + aStatus); + InvalidateFrame(); +} + +void nsFieldSetFrame::SetInitialChildList(ChildListID aListID, + nsFrameList&& aChildList) { + nsContainerFrame::SetInitialChildList(aListID, std::move(aChildList)); + if (nsBlockFrame* legend = do_QueryFrame(GetLegend())) { + // A rendered legend always establish a new formatting context. + // https://html.spec.whatwg.org/multipage/rendering.html#rendered-legend + legend->AddStateBits(NS_BLOCK_STATIC_BFC); + } + MOZ_ASSERT( + aListID != FrameChildListID::Principal || GetInner() || GetLegend(), + "Setting principal child list should populate our inner frame " + "or our rendered legend"); +} + +void nsFieldSetFrame::AppendFrames(ChildListID aListID, + nsFrameList&& aFrameList) { + MOZ_ASSERT(aListID == FrameChildListID::NoReflowPrincipal && + HasAnyStateBits(NS_FRAME_FIRST_REFLOW), + "AppendFrames should only be used from " + "nsCSSFrameConstructor::ConstructFieldSetFrame"); + nsContainerFrame::AppendFrames(aListID, std::move(aFrameList)); + MOZ_ASSERT(GetInner(), "at this point we should have an inner frame"); +} + +void nsFieldSetFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, + nsFrameList&& aFrameList) { + MOZ_ASSERT( + aListID == FrameChildListID::Principal && !aPrevFrame && !GetLegend(), + "InsertFrames should only be used to prepend a rendered legend " + "from nsCSSFrameConstructor::ConstructFramesFromItemList"); + nsContainerFrame::InsertFrames(aListID, aPrevFrame, aPrevFrameLine, + std::move(aFrameList)); + MOZ_ASSERT(GetLegend()); + if (nsBlockFrame* legend = do_QueryFrame(GetLegend())) { + // A rendered legend always establish a new formatting context. + // https://html.spec.whatwg.org/multipage/rendering.html#rendered-legend + legend->AddStateBits(NS_BLOCK_STATIC_BFC); + } +} + +#ifdef DEBUG +void nsFieldSetFrame::RemoveFrame(DestroyContext&, ChildListID, nsIFrame*) { + MOZ_CRASH("nsFieldSetFrame::RemoveFrame not supported"); +} +#endif + +#ifdef ACCESSIBILITY +a11y::AccType nsFieldSetFrame::AccessibleType() { + return a11y::eHTMLGroupboxType; +} +#endif + +BaselineSharingGroup nsFieldSetFrame::GetDefaultBaselineSharingGroup() const { + switch (StyleDisplay()->DisplayInside()) { + case mozilla::StyleDisplayInside::Grid: + case mozilla::StyleDisplayInside::Flex: + return BaselineSharingGroup::First; + default: + return BaselineSharingGroup::Last; + } +} + +nscoord nsFieldSetFrame::SynthesizeFallbackBaseline( + WritingMode aWM, BaselineSharingGroup aBaselineGroup) const { + return Baseline::SynthesizeBOffsetFromMarginBox(this, aWM, aBaselineGroup); +} + +Maybe nsFieldSetFrame::GetNaturalBaselineBOffset( + WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext aExportContext) const { + if (StyleDisplay()->IsContainLayout()) { + // If we are layout-contained, our child 'inner' should not + // affect how we calculate our baseline. + return Nothing{}; + } + nsIFrame* inner = GetInner(); + if (MOZ_UNLIKELY(!inner)) { + return Nothing{}; + } + MOZ_ASSERT(!inner->GetWritingMode().IsOrthogonalTo(aWM)); + const auto result = + inner->GetNaturalBaselineBOffset(aWM, aBaselineGroup, aExportContext); + if (!result) { + return Nothing{}; + } + nscoord innerBStart = inner->BStart(aWM, GetSize()); + if (aBaselineGroup == BaselineSharingGroup::First) { + return Some(*result + innerBStart); + } + return Some(*result + BSize(aWM) - (innerBStart + inner->BSize(aWM))); +} + +nsIScrollableFrame* nsFieldSetFrame::GetScrollTargetFrame() const { + return do_QueryFrame(GetInner()); +} + +void nsFieldSetFrame::AppendDirectlyOwnedAnonBoxes( + nsTArray& aResult) { + if (nsIFrame* kid = GetInner()) { + aResult.AppendElement(OwnedAnonBox(kid)); + } +} + +void nsFieldSetFrame::EnsureChildContinuation(nsIFrame* aChild, + const nsReflowStatus& aStatus) { + MOZ_ASSERT(aChild == GetLegend() || aChild == GetInner(), + "unexpected child frame"); + nsIFrame* nif = aChild->GetNextInFlow(); + if (aStatus.IsFullyComplete()) { + if (nif) { + // NOTE: we want to avoid our DEBUG version of RemoveFrame above. + DestroyContext context(PresShell()); + nsContainerFrame::RemoveFrame(context, + FrameChildListID::NoReflowPrincipal, nif); + MOZ_ASSERT(!aChild->GetNextInFlow()); + } + } else { + nsFrameList nifs; + if (!nif) { + auto* fc = PresShell()->FrameConstructor(); + nif = fc->CreateContinuingFrame(aChild, this); + if (aStatus.IsOverflowIncomplete()) { + nif->AddStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER); + } + nifs = nsFrameList(nif, nif); + } else { + // Steal all nifs and push them again in case they are currently on + // the wrong list. + for (nsIFrame* n = nif; n; n = n->GetNextInFlow()) { + n->GetParent()->StealFrame(n); + nifs.AppendFrame(this, n); + if (aStatus.IsOverflowIncomplete()) { + n->AddStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER); + } else { + n->RemoveStateBits(NS_FRAME_IS_OVERFLOW_CONTAINER); + } + } + } + if (aStatus.IsOverflowIncomplete()) { + if (nsFrameList* eoc = GetExcessOverflowContainers()) { + eoc->AppendFrames(nullptr, std::move(nifs)); + } else { + SetExcessOverflowContainers(std::move(nifs)); + } + } else { + if (nsFrameList* oc = GetOverflowFrames()) { + oc->AppendFrames(nullptr, std::move(nifs)); + } else { + SetOverflowFrames(std::move(nifs)); + } + } + } +} diff --git a/layout/forms/nsFieldSetFrame.h b/layout/forms/nsFieldSetFrame.h new file mode 100644 index 0000000000..741b3bef52 --- /dev/null +++ b/layout/forms/nsFieldSetFrame.h @@ -0,0 +1,114 @@ +/* -*- 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 nsFieldSetFrame_h___ +#define nsFieldSetFrame_h___ + +#include "mozilla/Attributes.h" +#include "ImgDrawResult.h" +#include "nsContainerFrame.h" +#include "nsIScrollableFrame.h" + +class nsFieldSetFrame final : public nsContainerFrame { + typedef mozilla::image::ImgDrawResult ImgDrawResult; + + public: + NS_DECL_FRAMEARENA_HELPERS(nsFieldSetFrame) + NS_DECL_QUERYFRAME + + explicit nsFieldSetFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + + nscoord GetIntrinsicISize(gfxContext* aRenderingContext, + mozilla::IntrinsicISizeType); + nscoord GetMinISize(gfxContext* aRenderingContext) override; + nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + /** + * The area to paint box-shadows around. It's the border rect except + * when there's a we offset the y-position to the center of it. + */ + nsRect VisualBorderRectRelativeToSelf() const override; + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + nscoord SynthesizeFallbackBaseline( + mozilla::WritingMode aWM, + BaselineSharingGroup aBaselineGroup) const override; + BaselineSharingGroup GetDefaultBaselineSharingGroup() const override; + Maybe GetNaturalBaselineBOffset( + mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext aExportContext) const override; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + ImgDrawResult PaintBorder(nsDisplayListBuilder* aBuilder, + gfxContext& aRenderingContext, nsPoint aPt, + const nsRect& aDirtyRect); + + void SetInitialChildList(ChildListID aListID, + nsFrameList&& aChildList) override; + void AppendFrames(ChildListID aListID, nsFrameList&& aFrameList) override; + void InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, + nsFrameList&& aFrameList) override; +#ifdef DEBUG + void RemoveFrame(DestroyContext&, ChildListID aListID, + nsIFrame* aOldFrame) override; +#endif + + nsIScrollableFrame* GetScrollTargetFrame() const override; + + // Return the block wrapper around our kids. + void AppendDirectlyOwnedAnonBoxes(nsTArray& aResult) override; + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"FieldSet"_ns, aResult); + } +#endif + + /** + * Return the anonymous frame that contains all descendants except the legend + * frame. This can be a block/grid/flex/scroll frame. It always has + * the pseudo type nsCSSAnonBoxes::fieldsetContent. If it's a scroll frame, + * the scrolled frame can be a block/grid/flex frame. + * This may return null, for example for a fieldset that is a true overflow + * container. + */ + nsContainerFrame* GetInner() const; + + /** + * Return the frame that represents the rendered legend if any. + * https://html.spec.whatwg.org/multipage/rendering.html#rendered-legend + */ + nsIFrame* GetLegend() const; + + /** @see mLegendSpace below */ + nscoord LegendSpace() const { return mLegendSpace; } + + protected: + /** + * Convenience method to create a continuation for aChild after we've + * reflowed it and got the reflow status aStatus. + */ + void EnsureChildContinuation(nsIFrame* aChild, const nsReflowStatus& aStatus); + + mozilla::LogicalRect mLegendRect; + + // This is how much to subtract from our inner frame's content-box block-size + // to account for a protruding legend. It's zero if there's no legend or + // the legend fits entirely inside our start border. + nscoord mLegendSpace; +}; + +#endif // nsFieldSetFrame_h___ diff --git a/layout/forms/nsFileControlFrame.cpp b/layout/forms/nsFileControlFrame.cpp new file mode 100644 index 0000000000..d24158d9dc --- /dev/null +++ b/layout/forms/nsFileControlFrame.cpp @@ -0,0 +1,422 @@ +/* -*- 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 "nsFileControlFrame.h" + +#include "nsGkAtoms.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/NodeInfo.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/DOMStringList.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/DragEvent.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/FileList.h" +#include "mozilla/dom/HTMLButtonElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/TextEditor.h" +#include "MiddleCroppingBlockFrame.h" +#include "nsIFrame.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "nsIFile.h" +#include "nsLayoutUtils.h" +#include "nsTextNode.h" +#include "nsTextFrame.h" +#include "gfxContext.h" + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewFileControlFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) + nsFileControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsFileControlFrame) + +nsFileControlFrame::nsFileControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsBlockFrame(aStyle, aPresContext, kClassID) {} + +void nsFileControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsBlockFrame::Init(aContent, aParent, aPrevInFlow); + + mMouseListener = new DnDListener(this); +} + +void nsFileControlFrame::Destroy(DestroyContext& aContext) { + NS_ENSURE_TRUE_VOID(mContent); + + // Remove the events. + if (mContent) { + mContent->RemoveSystemEventListener(u"drop"_ns, mMouseListener, false); + mContent->RemoveSystemEventListener(u"dragover"_ns, mMouseListener, false); + } + + aContext.AddAnonymousContent(mTextContent.forget()); + aContext.AddAnonymousContent(mBrowseFilesOrDirs.forget()); + + mMouseListener->ForgetFrame(); + nsBlockFrame::Destroy(aContext); +} + +static already_AddRefed MakeAnonButton( + Document* aDoc, const char* labelKey, HTMLInputElement* aInputElement) { + RefPtr button = aDoc->CreateHTMLElement(nsGkAtoms::button); + // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any + // attribute. + button->SetIsNativeAnonymousRoot(); + button->SetPseudoElementType(PseudoStyleType::fileSelectorButton); + + // Set the file picking button text depending on the current locale. + nsAutoString buttonTxt; + nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + labelKey, aDoc, buttonTxt); + + auto* nim = aDoc->NodeInfoManager(); + // Set the browse button text. It's a bit of a pain to do because we want to + // make sure we are not notifying. + RefPtr textContent = new (nim) nsTextNode(nim); + textContent->SetText(buttonTxt, false); + + IgnoredErrorResult error; + button->AppendChildTo(textContent, false, error); + if (error.Failed()) { + return nullptr; + } + + auto* buttonElement = HTMLButtonElement::FromNode(button); + // We allow tabbing over the input itself, not the button. + buttonElement->SetTabIndex(-1, IgnoreErrors()); + return button.forget(); +} + +nsresult nsFileControlFrame::CreateAnonymousContent( + nsTArray& aElements) { + nsCOMPtr doc = mContent->GetComposedDoc(); + RefPtr fileContent = HTMLInputElement::FromNode(mContent); + + mBrowseFilesOrDirs = MakeAnonButton(doc, "Browse", fileContent); + if (!mBrowseFilesOrDirs) { + return NS_ERROR_OUT_OF_MEMORY; + } + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + aElements.AppendElement(mBrowseFilesOrDirs); + + // Create and setup the text showing the selected files. + mTextContent = doc->CreateHTMLElement(nsGkAtoms::label); + // NOTE: SetIsNativeAnonymousRoot() has to be called before setting any + // attribute. + mTextContent->SetIsNativeAnonymousRoot(); + RefPtr text = + new (doc->NodeInfoManager()) nsTextNode(doc->NodeInfoManager()); + mTextContent->AppendChildTo(text, false, IgnoreErrors()); + + aElements.AppendElement(mTextContent); + + // We should be able to interact with the element by doing drag and drop. + mContent->AddSystemEventListener(u"drop"_ns, mMouseListener, false); + mContent->AddSystemEventListener(u"dragover"_ns, mMouseListener, false); + + SyncDisabledState(); + + return NS_OK; +} + +void nsFileControlFrame::AppendAnonymousContentTo( + nsTArray& aElements, uint32_t aFilter) { + if (mBrowseFilesOrDirs) { + aElements.AppendElement(mBrowseFilesOrDirs); + } + + if (mTextContent) { + aElements.AppendElement(mTextContent); + } +} + +NS_QUERYFRAME_HEAD(nsFileControlFrame) + NS_QUERYFRAME_ENTRY(nsFileControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) + +void nsFileControlFrame::SetFocus(bool aOn, bool aRepaint) {} + +static void AppendBlobImplAsDirectory(nsTArray& aArray, + BlobImpl* aBlobImpl, + nsIContent* aContent) { + MOZ_ASSERT(aBlobImpl); + MOZ_ASSERT(aBlobImpl->IsDirectory()); + + nsAutoString fullpath; + ErrorResult err; + aBlobImpl->GetMozFullPath(fullpath, SystemCallerGuarantee(), err); + if (err.Failed()) { + err.SuppressException(); + return; + } + + nsCOMPtr file; + nsresult rv = NS_NewLocalFile(fullpath, true, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsPIDOMWindowInner* inner = aContent->OwnerDoc()->GetInnerWindow(); + if (!inner || !inner->IsCurrentInnerWindow()) { + return; + } + + RefPtr directory = Directory::Create(inner->AsGlobal(), file); + MOZ_ASSERT(directory); + + OwningFileOrDirectory* element = aArray.AppendElement(); + element->SetAsDirectory() = directory; +} + +/** + * This is called when we receive a drop or a dragover. + */ +NS_IMETHODIMP +nsFileControlFrame::DnDListener::HandleEvent(Event* aEvent) { + NS_ASSERTION(mFrame, "We should have been unregistered"); + + if (aEvent->DefaultPrevented()) { + return NS_OK; + } + + DragEvent* dragEvent = aEvent->AsDragEvent(); + if (!dragEvent) { + return NS_OK; + } + + RefPtr dataTransfer = dragEvent->GetDataTransfer(); + if (!IsValidDropData(dataTransfer)) { + return NS_OK; + } + + RefPtr inputElement = + HTMLInputElement::FromNode(mFrame->GetContent()); + bool supportsMultiple = inputElement->HasAttr(nsGkAtoms::multiple); + if (!CanDropTheseFiles(dataTransfer, supportsMultiple)) { + dataTransfer->SetDropEffect(u"none"_ns); + aEvent->StopPropagation(); + return NS_OK; + } + + nsAutoString eventType; + aEvent->GetType(eventType); + if (eventType.EqualsLiteral("dragover")) { + // Prevent default if we can accept this drag data + aEvent->PreventDefault(); + return NS_OK; + } + + if (eventType.EqualsLiteral("drop")) { + aEvent->StopPropagation(); + aEvent->PreventDefault(); + + RefPtr fileList = + dataTransfer->GetFiles(*nsContentUtils::GetSystemPrincipal()); + + RefPtr webkitDir; + nsresult rv = + GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir)); + NS_ENSURE_SUCCESS(rv, NS_OK); + + nsTArray array; + if (webkitDir) { + AppendBlobImplAsDirectory(array, webkitDir, inputElement); + inputElement->MozSetDndFilesAndDirectories(array); + } else { + bool blinkFileSystemEnabled = + StaticPrefs::dom_webkitBlink_filesystem_enabled(); + if (blinkFileSystemEnabled) { + FileList* files = static_cast(fileList.get()); + if (files) { + for (uint32_t i = 0; i < files->Length(); ++i) { + File* file = files->Item(i); + if (file) { + if (file->Impl() && file->Impl()->IsDirectory()) { + AppendBlobImplAsDirectory(array, file->Impl(), inputElement); + } else { + OwningFileOrDirectory* element = array.AppendElement(); + element->SetAsFile() = file; + } + } + } + } + } + + // Entries API. + if (blinkFileSystemEnabled) { + // This is rather ugly. Pass the directories as Files using SetFiles, + // but then if blink filesystem API is enabled, it wants + // FileOrDirectory array. + inputElement->SetFiles(fileList, true); + inputElement->UpdateEntries(array); + } + // Normal DnD + else { + inputElement->SetFiles(fileList, true); + } + + RefPtr textEditor; + DebugOnly rvIgnored = + nsContentUtils::DispatchInputEvent(inputElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to dispatch input event"); + nsContentUtils::DispatchTrustedEvent(inputElement->OwnerDoc(), + inputElement, u"change"_ns, + CanBubble::eYes, Cancelable::eNo); + } + } + + return NS_OK; +} + +nsresult nsFileControlFrame::DnDListener::GetBlobImplForWebkitDirectory( + FileList* aFileList, BlobImpl** aBlobImpl) { + *aBlobImpl = nullptr; + + HTMLInputElement* inputElement = + HTMLInputElement::FromNode(mFrame->GetContent()); + bool webkitDirPicker = StaticPrefs::dom_webkitBlink_dirPicker_enabled() && + inputElement->HasAttr(nsGkAtoms::webkitdirectory); + if (!webkitDirPicker) { + return NS_OK; + } + + if (!aFileList) { + return NS_ERROR_FAILURE; + } + + // webkitdirectory doesn't care about the length of the file list but + // only about the first item on it. + uint32_t len = aFileList->Length(); + if (len) { + File* file = aFileList->Item(0); + if (file) { + BlobImpl* impl = file->Impl(); + if (impl && impl->IsDirectory()) { + RefPtr retVal = impl; + retVal.swap(*aBlobImpl); + return NS_OK; + } + } + } + + return NS_ERROR_FAILURE; +} + +bool nsFileControlFrame::DnDListener::IsValidDropData( + DataTransfer* aDataTransfer) { + if (!aDataTransfer) { + return false; + } + + // We only support dropping files onto a file upload control + return aDataTransfer->HasFile(); +} + +bool nsFileControlFrame::DnDListener::CanDropTheseFiles( + DataTransfer* aDataTransfer, bool aSupportsMultiple) { + RefPtr fileList = + aDataTransfer->GetFiles(*nsContentUtils::GetSystemPrincipal()); + + RefPtr webkitDir; + nsresult rv = + GetBlobImplForWebkitDirectory(fileList, getter_AddRefs(webkitDir)); + // Just check if either there isn't webkitdirectory attribute, or + // fileList has a directory which can be dropped to the element. + // No need to use webkitDir for anything here. + NS_ENSURE_SUCCESS(rv, false); + + uint32_t listLength = 0; + if (fileList) { + listLength = fileList->Length(); + } + return listLength <= 1 || aSupportsMultiple; +} + +void nsFileControlFrame::SyncDisabledState() { + if (mContent->AsElement()->State().HasState(ElementState::DISABLED)) { + mBrowseFilesOrDirs->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled, u""_ns, + true); + } else { + mBrowseFilesOrDirs->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled, true); + } +} + +void nsFileControlFrame::ElementStateChanged(ElementState aStates) { + if (aStates.HasState(ElementState::DISABLED)) { + nsContentUtils::AddScriptRunner(new SyncDisabledStateEvent(this)); + } +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsFileControlFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"FileControl"_ns, aResult); +} +#endif + +nsresult nsFileControlFrame::SetFormProperty(nsAtom* aName, + const nsAString& aValue) { + if (nsGkAtoms::value == aName) { + if (MiddleCroppingBlockFrame* f = + do_QueryFrame(mTextContent->GetPrimaryFrame())) { + f->UpdateDisplayedValueToUncroppedValue(true); + } + } + return NS_OK; +} + +#ifdef ACCESSIBILITY +a11y::AccType nsFileControlFrame::AccessibleType() { + return a11y::eHTMLFileInputType; +} +#endif + +NS_IMPL_ISUPPORTS(nsFileControlFrame::MouseListener, nsIDOMEventListener) + +class FileControlLabelFrame final : public MiddleCroppingBlockFrame { + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(FileControlLabelFrame) + + FileControlLabelFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : MiddleCroppingBlockFrame(aStyle, aPresContext, kClassID) {} + + HTMLInputElement& FileInput() const { + return *HTMLInputElement::FromNode(mContent->GetParent()); + } + + void GetUncroppedValue(nsAString& aValue) override { + return FileInput().GetDisplayFileName(aValue); + } +}; + +NS_QUERYFRAME_HEAD(FileControlLabelFrame) + NS_QUERYFRAME_ENTRY(FileControlLabelFrame) +NS_QUERYFRAME_TAIL_INHERITING(MiddleCroppingBlockFrame) +NS_IMPL_FRAMEARENA_HELPERS(FileControlLabelFrame) + +nsIFrame* NS_NewFileControlLabelFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + FileControlLabelFrame(aStyle, aPresShell->GetPresContext()); +} diff --git a/layout/forms/nsFileControlFrame.h b/layout/forms/nsFileControlFrame.h new file mode 100644 index 0000000000..c58dccc763 --- /dev/null +++ b/layout/forms/nsFileControlFrame.h @@ -0,0 +1,137 @@ +/* -*- 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 nsFileControlFrame_h___ +#define nsFileControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsBlockFrame.h" +#include "nsIFormControlFrame.h" +#include "nsIDOMEventListener.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" + +namespace mozilla::dom { +class FileList; +class BlobImpl; +class DataTransfer; +} // namespace mozilla::dom + +class nsFileControlFrame final : public nsBlockFrame, + public nsIFormControlFrame, + public nsIAnonymousContentCreator { + using Element = mozilla::dom::Element; + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsFileControlFrame) + + explicit nsFileControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + // nsIFormControlFrame + nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) override; + void SetFocus(bool aOn, bool aRepaint) override; + + void Destroy(DestroyContext&) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override; +#endif + + void ElementStateChanged(mozilla::dom::ElementState aStates) override; + + // nsIAnonymousContentCreator + nsresult CreateAnonymousContent(nsTArray&) override; + void AppendAnonymousContentTo(nsTArray&, + uint32_t aFilter) override; + +#ifdef ACCESSIBILITY + mozilla::a11y::AccType AccessibleType() override; +#endif + + protected: + class MouseListener; + friend class MouseListener; + class MouseListener : public nsIDOMEventListener { + public: + NS_DECL_ISUPPORTS + + explicit MouseListener(nsFileControlFrame* aFrame) : mFrame(aFrame) {} + + void ForgetFrame() { mFrame = nullptr; } + + protected: + virtual ~MouseListener() = default; + + nsFileControlFrame* mFrame; + }; + + class SyncDisabledStateEvent; + friend class SyncDisabledStateEvent; + class SyncDisabledStateEvent : public mozilla::Runnable { + public: + explicit SyncDisabledStateEvent(nsFileControlFrame* aFrame) + : mozilla::Runnable("nsFileControlFrame::SyncDisabledStateEvent"), + mFrame(aFrame) {} + + NS_IMETHOD Run() override { + nsFileControlFrame* frame = + static_cast(mFrame.GetFrame()); + NS_ENSURE_STATE(frame); + + frame->SyncDisabledState(); + return NS_OK; + } + + private: + WeakFrame mFrame; + }; + + class DnDListener : public MouseListener { + public: + explicit DnDListener(nsFileControlFrame* aFrame) : MouseListener(aFrame) {} + + // nsIDOMEventListener + MOZ_CAN_RUN_SCRIPT_BOUNDARY + NS_IMETHOD HandleEvent(mozilla::dom::Event* aEvent) override; + + nsresult GetBlobImplForWebkitDirectory(mozilla::dom::FileList* aFileList, + mozilla::dom::BlobImpl** aBlobImpl); + + bool IsValidDropData(mozilla::dom::DataTransfer* aDataTransfer); + bool CanDropTheseFiles(mozilla::dom::DataTransfer* aDataTransfer, + bool aSupportsMultiple); + }; + + /** + * The text box input. + * @see nsFileControlFrame::CreateAnonymousContent + */ + RefPtr mTextContent; + /** + * The button to open a file or directory picker. + * @see nsFileControlFrame::CreateAnonymousContent + */ + RefPtr mBrowseFilesOrDirs; + + /** + * Drag and drop mouse listener. + * This makes sure we don't get used after destruction. + */ + RefPtr mMouseListener; + + protected: + /** + * Sync the disabled state of the content with anonymous children. + */ + void SyncDisabledState(); +}; + +#endif // nsFileControlFrame_h___ diff --git a/layout/forms/nsGfxButtonControlFrame.cpp b/layout/forms/nsGfxButtonControlFrame.cpp new file mode 100644 index 0000000000..37aa996c27 --- /dev/null +++ b/layout/forms/nsGfxButtonControlFrame.cpp @@ -0,0 +1,178 @@ +/* -*- 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 "nsGfxButtonControlFrame.h" +#include "nsIFormControl.h" +#include "nsGkAtoms.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "nsContentUtils.h" +#include "nsTextNode.h" + +using namespace mozilla; + +nsGfxButtonControlFrame::nsGfxButtonControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsHTMLButtonControlFrame(aStyle, aPresContext, kClassID) {} + +nsContainerFrame* NS_NewGfxButtonControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsGfxButtonControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsGfxButtonControlFrame) + +void nsGfxButtonControlFrame::Destroy(DestroyContext& aContext) { + aContext.AddAnonymousContent(mTextContent.forget()); + nsHTMLButtonControlFrame::Destroy(aContext); +} + +#ifdef DEBUG_FRAME_DUMP +nsresult nsGfxButtonControlFrame::GetFrameName(nsAString& aResult) const { + return MakeFrameName(u"ButtonControl"_ns, aResult); +} +#endif + +// Create the text content used as label for the button. +// The frame will be generated by the frame constructor. +nsresult nsGfxButtonControlFrame::CreateAnonymousContent( + nsTArray& aElements) { + nsAutoString label; + nsresult rv = GetLabel(label); + NS_ENSURE_SUCCESS(rv, rv); + + // Add a child text content node for the label + mTextContent = new (mContent->NodeInfo()->NodeInfoManager()) + nsTextNode(mContent->NodeInfo()->NodeInfoManager()); + + // set the value of the text node and add it to the child list + mTextContent->SetText(label, false); + aElements.AppendElement(mTextContent); + + return NS_OK; +} + +void nsGfxButtonControlFrame::AppendAnonymousContentTo( + nsTArray& aElements, uint32_t aFilter) { + if (mTextContent) { + aElements.AppendElement(mTextContent); + } +} + +NS_QUERYFRAME_HEAD(nsGfxButtonControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsHTMLButtonControlFrame) + +// Initially we hardcoded the default strings here. +// Next, we used html.css to store the default label for various types +// of buttons. (nsGfxButtonControlFrame::DoNavQuirksReflow rev 1.20) +// However, since html.css is not internationalized, we now grab the default +// label from a string bundle as is done for all other UI strings. +// See bug 16999 for further details. +nsresult nsGfxButtonControlFrame::GetDefaultLabel(nsAString& aString) const { + nsCOMPtr form = do_QueryInterface(mContent); + NS_ENSURE_TRUE(form, NS_ERROR_UNEXPECTED); + + auto type = form->ControlType(); + const char* prop; + if (type == FormControlType::InputReset) { + prop = "Reset"; + } else if (type == FormControlType::InputSubmit) { + prop = "Submit"; + } else { + aString.Truncate(); + return NS_OK; + } + + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eFORMS_PROPERTIES, prop, mContent->OwnerDoc(), aString); +} + +nsresult nsGfxButtonControlFrame::GetLabel(nsString& aLabel) { + // Get the text from the "value" property on our content if there is + // one; otherwise set it to a default value (localized). + auto* elt = dom::HTMLInputElement::FromNode(mContent); + if (elt && elt->HasAttr(nsGkAtoms::value)) { + elt->GetValue(aLabel, dom::CallerType::System); + } else { + // Generate localized label. + // We can't make any assumption as to what the default would be + // because the value is localized for non-english platforms, thus + // it might not be the string "Reset", "Submit Query", or "Browse..." + nsresult rv; + rv = GetDefaultLabel(aLabel); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Compress whitespace out of label if needed. + if (!StyleText()->WhiteSpaceIsSignificant()) { + aLabel.CompressWhitespace(); + } else if (aLabel.Length() > 2 && aLabel.First() == ' ' && + aLabel.CharAt(aLabel.Length() - 1) == ' ') { + // This is a bit of a hack. The reason this is here is as follows: we now + // have default padding on our buttons to make them non-ugly. + // Unfortunately, IE-windows does not have such padding, so people will + // stick values like " ok " (with the spaces) in the buttons in an attempt + // to make them look decent. Unfortunately, if they do this the button + // looks way too big in Mozilla. Worse yet, if they do this _and_ set a + // fixed width for the button we run into trouble because our focus-rect + // border/padding and outer border take up 10px of the horizontal button + // space or so; the result is that the text is misaligned, even with the + // recentering we do in nsHTMLButtonControlFrame::Reflow. So to solve + // this, even if the whitespace is significant, single leading and trailing + // _spaces_ (and not other whitespace) are removed. The proper solution, + // of course, is to not have the focus rect painting taking up 6px of + // horizontal space. We should do that instead (changing the renderer) and + // remove this. + aLabel.Cut(0, 1); + aLabel.Truncate(aLabel.Length() - 1); + } + + return NS_OK; +} + +nsresult nsGfxButtonControlFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + nsresult rv = NS_OK; + + // If the value attribute is set, update the text of the label + if (nsGkAtoms::value == aAttribute) { + if (mTextContent && mContent) { + nsAutoString label; + rv = GetLabel(label); + NS_ENSURE_SUCCESS(rv, rv); + + mTextContent->SetText(label, true); + } else { + rv = NS_ERROR_UNEXPECTED; + } + + // defer to HTMLButtonControlFrame + } else { + rv = nsHTMLButtonControlFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); + } + return rv; +} + +nsContainerFrame* nsGfxButtonControlFrame::GetContentInsertionFrame() { + return this; +} + +nsresult nsGfxButtonControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + // Override the HandleEvent to prevent the nsIFrame::HandleEvent + // from being called. The nsIFrame::HandleEvent causes the button label + // to be selected (Drawn with an XOR rectangle over the label) + + if (IsContentDisabled()) { + return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + return NS_OK; +} diff --git a/layout/forms/nsGfxButtonControlFrame.h b/layout/forms/nsGfxButtonControlFrame.h new file mode 100644 index 0000000000..32d4689559 --- /dev/null +++ b/layout/forms/nsGfxButtonControlFrame.h @@ -0,0 +1,62 @@ +/* -*- 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 nsGfxButtonControlFrame_h___ +#define nsGfxButtonControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsHTMLButtonControlFrame.h" +#include "nsCOMPtr.h" +#include "nsIAnonymousContentCreator.h" + +class nsTextNode; + +// Class which implements the input[type=button, reset, submit] and +// browse button for input[type=file]. +// The label for button is specified through generated content +// in the ua.css file. + +class nsGfxButtonControlFrame final : public nsHTMLButtonControlFrame, + public nsIAnonymousContentCreator { + public: + NS_DECL_FRAMEARENA_HELPERS(nsGfxButtonControlFrame) + + explicit nsGfxButtonControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + void Destroy(DestroyContext&) override; + + virtual nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override; +#endif + + NS_DECL_QUERYFRAME + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent( + nsTArray& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray& aElements, + uint32_t aFilter) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + virtual nsContainerFrame* GetContentInsertionFrame() override; + + protected: + nsresult GetDefaultLabel(nsAString& aLabel) const; + + nsresult GetLabel(nsString& aLabel); + + private: + RefPtr mTextContent; +}; + +#endif diff --git a/layout/forms/nsHTMLButtonControlFrame.cpp b/layout/forms/nsHTMLButtonControlFrame.cpp new file mode 100644 index 0000000000..7a599f093b --- /dev/null +++ b/layout/forms/nsHTMLButtonControlFrame.cpp @@ -0,0 +1,394 @@ +/* -*- 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 "nsHTMLButtonControlFrame.h" + +#include "mozilla/Baseline.h" +#include "mozilla/PresShell.h" +#include "nsIFrameInlines.h" +#include "nsContainerFrame.h" +#include "nsIFormControlFrame.h" +#include "nsPresContext.h" +#include "nsLayoutUtils.h" +#include "nsGkAtoms.h" +#include "nsButtonFrameRenderer.h" +#include "nsDisplayList.h" +#include + +using namespace mozilla; + +nsContainerFrame* NS_NewHTMLButtonControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsHTMLButtonControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsHTMLButtonControlFrame) + +nsHTMLButtonControlFrame::nsHTMLButtonControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext, + nsIFrame::ClassID aID) + : nsContainerFrame(aStyle, aPresContext, aID) {} + +nsHTMLButtonControlFrame::~nsHTMLButtonControlFrame() = default; + +void nsHTMLButtonControlFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsContainerFrame::Init(aContent, aParent, aPrevInFlow); + mRenderer.SetFrame(this, PresContext()); +} + +NS_QUERYFRAME_HEAD(nsHTMLButtonControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +#ifdef ACCESSIBILITY +a11y::AccType nsHTMLButtonControlFrame::AccessibleType() { + return a11y::eHTMLButtonType; +} +#endif + +void nsHTMLButtonControlFrame::SetFocus(bool aOn, bool aRepaint) {} + +nsresult nsHTMLButtonControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + // if disabled do nothing + if (mRenderer.isDisabled()) { + return NS_OK; + } + + // mouse clicks are handled by content + // we don't want our children to get any events. So just pass it to frame. + return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus); +} + +bool nsHTMLButtonControlFrame::ShouldClipPaintingToBorderBox() { + // FIXME(emilio): probably should account for per-axis clipping... + return StyleDisplay()->mOverflowX != StyleOverflow::Visible; +} + +void nsHTMLButtonControlFrame::BuildDisplayList( + nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { + nsDisplayList onTop(aBuilder); + if (IsVisibleForPainting()) { + // Clip the button itself to its border area for event hit testing. + Maybe eventClipState; + if (aBuilder->IsForEventDelivery()) { + eventClipState.emplace(aBuilder); + nsRect rect(aBuilder->ToReferenceFrame(this), GetSize()); + nscoord radii[8]; + bool hasRadii = GetBorderRadii(radii); + eventClipState->ClipContainingBlockDescendants( + rect, hasRadii ? radii : nullptr); + } + + mRenderer.DisplayButton(aBuilder, aLists.BorderBackground(), &onTop); + } + + nsDisplayListCollection set(aBuilder); + + { + DisplayListClipState::AutoSaveRestore clipState(aBuilder); + + if (ShouldClipPaintingToBorderBox()) { + nsMargin border = StyleBorder()->GetComputedBorder(); + nsRect rect(aBuilder->ToReferenceFrame(this), GetSize()); + rect.Deflate(border); + nscoord radii[8]; + bool hasRadii = GetPaddingBoxBorderRadii(radii); + clipState.ClipContainingBlockDescendants(rect, + hasRadii ? radii : nullptr); + } + + BuildDisplayListForChild(aBuilder, mFrames.FirstChild(), set, + DisplayChildFlag::ForcePseudoStackingContext); + } + + // Put the foreground outline and focus rects on top of the children + set.Content()->AppendToTop(&onTop); + set.MoveTo(aLists); + + DisplayOutline(aBuilder, aLists); + + // to draw border when selected in editor + DisplaySelectionOverlay(aBuilder, aLists.Content()); +} + +nscoord nsHTMLButtonControlFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + if (Maybe containISize = ContainIntrinsicISize()) { + result = *containISize; + } else { + nsIFrame* kid = mFrames.FirstChild(); + result = nsLayoutUtils::IntrinsicForContainer(aRenderingContext, kid, + IntrinsicISizeType::MinISize); + } + return result; +} + +nscoord nsHTMLButtonControlFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + if (Maybe containISize = ContainIntrinsicISize()) { + result = *containISize; + } else { + nsIFrame* kid = mFrames.FirstChild(); + result = nsLayoutUtils::IntrinsicForContainer( + aRenderingContext, kid, IntrinsicISizeType::PrefISize); + } + return result; +} + +void nsHTMLButtonControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsHTMLButtonControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + // Reflow the child + nsIFrame* firstKid = mFrames.FirstChild(); + + MOZ_ASSERT(firstKid, "Button should have a child frame for its contents"); + MOZ_ASSERT(!firstKid->GetNextSibling(), + "Button should have exactly one child frame"); + MOZ_ASSERT( + firstKid->Style()->GetPseudoType() == PseudoStyleType::buttonContent, + "Button's child frame has unexpected pseudo type!"); + + // XXXbz Eventually we may want to check-and-bail if + // !aReflowInput.ShouldReflowAllKids() && + // !firstKid->IsSubtreeDirty(). + // We'd need to cache our ascent for that, of course. + + // Reflow the contents of the button. + // (This populates our aDesiredSize, too.) + ReflowButtonContents(aPresContext, aDesiredSize, aReflowInput, firstKid); + + if (!ShouldClipPaintingToBorderBox()) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, firstKid); + } + // else, we ignore child overflow -- anything that overflows beyond our + // own border-box will get clipped when painting. + + FinishReflowWithAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput, + aStatus); + + // We're always complete and we don't support overflow containers + // so we shouldn't have a next-in-flow ever. + aStatus.Reset(); + MOZ_ASSERT(!GetNextInFlow()); +} + +void nsHTMLButtonControlFrame::ReflowButtonContents( + nsPresContext* aPresContext, ReflowOutput& aButtonDesiredSize, + const ReflowInput& aButtonReflowInput, nsIFrame* aFirstKid) { + WritingMode wm = GetWritingMode(); + LogicalSize availSize = aButtonReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + + // shorthand for a value we need to use in a bunch of places + const LogicalMargin& clbp = + aButtonReflowInput.ComputedLogicalBorderPadding(wm); + + LogicalPoint childPos(wm); + childPos.I(wm) = clbp.IStart(wm); + availSize.ISize(wm) = std::max(availSize.ISize(wm), 0); + + ReflowInput contentsReflowInput(aPresContext, aButtonReflowInput, aFirstKid, + availSize); + + nsReflowStatus contentsReflowStatus; + ReflowOutput contentsDesiredSize(aButtonReflowInput); + childPos.B(wm) = 0; // This will be set properly later, after reflowing the + // child to determine its size. + + if (aFirstKid->IsFlexOrGridContainer()) { + // XXX: Should we use ResetResizeFlags::Yes? + contentsReflowInput.SetComputedBSize(aButtonReflowInput.ComputedBSize(), + ReflowInput::ResetResizeFlags::No); + contentsReflowInput.SetComputedMinBSize( + aButtonReflowInput.ComputedMinBSize()); + contentsReflowInput.SetComputedMaxBSize( + aButtonReflowInput.ComputedMaxBSize()); + } + + // We just pass a dummy containerSize here, as the child will be + // repositioned later by FinishReflowChild. + nsSize dummyContainerSize; + ReflowChild(aFirstKid, aPresContext, contentsDesiredSize, contentsReflowInput, + wm, childPos, dummyContainerSize, ReflowChildFlags::Default, + contentsReflowStatus); + MOZ_ASSERT(contentsReflowStatus.IsComplete(), + "We gave button-contents frame unconstrained available height, " + "so it should be complete"); + + // Compute the button's content-box size: + LogicalSize buttonContentBox(wm); + if (aButtonReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE) { + // Button has a fixed block-size -- that's its content-box bSize. + buttonContentBox.BSize(wm) = aButtonReflowInput.ComputedBSize(); + } else { + // Button is intrinsically sized -- it should shrinkwrap the + // button-contents' bSize. But if it has size containment in block axis, + // ignore the contents and use contain-intrinsic-block-size. + nscoord bSize = aButtonReflowInput.mFrame->ContainIntrinsicBSize().valueOr( + contentsDesiredSize.BSize(wm)); + + // Make sure we obey min/max-bSize in the case when we're doing intrinsic + // sizing (we get it for free when we have a non-intrinsic + // aButtonReflowInput.ComputedBSize()). Note that we do this before + // adjusting for borderpadding, since mComputedMaxBSize and + // mComputedMinBSize are content bSizes. + buttonContentBox.BSize(wm) = aButtonReflowInput.ApplyMinMaxBSize(bSize); + } + if (aButtonReflowInput.ComputedISize() != NS_UNCONSTRAINEDSIZE) { + buttonContentBox.ISize(wm) = aButtonReflowInput.ComputedISize(); + } else { + nscoord iSize = aButtonReflowInput.mFrame->ContainIntrinsicISize().valueOr( + contentsDesiredSize.ISize(wm)); + buttonContentBox.ISize(wm) = aButtonReflowInput.ApplyMinMaxISize(iSize); + } + + // Center child in the block-direction in the button + // (technically, inside of the button's focus-padding area) + nscoord extraSpace = + buttonContentBox.BSize(wm) - contentsDesiredSize.BSize(wm); + + childPos.B(wm) = std::max(0, extraSpace / 2); + + // Adjust childPos.B() to be in terms of the button's frame-rect: + childPos.B(wm) += clbp.BStart(wm); + + nsSize containerSize = (buttonContentBox + clbp.Size(wm)).GetPhysicalSize(wm); + + // Place the child + FinishReflowChild(aFirstKid, aPresContext, contentsDesiredSize, + &contentsReflowInput, wm, childPos, containerSize, + ReflowChildFlags::Default); + + // Make sure we have a useful 'ascent' value for the child + if (contentsDesiredSize.BlockStartAscent() == + ReflowOutput::ASK_FOR_BASELINE) { + WritingMode wm = aButtonReflowInput.GetWritingMode(); + contentsDesiredSize.SetBlockStartAscent(aFirstKid->GetLogicalBaseline(wm)); + } + + // OK, we're done with the child frame. + // Use what we learned to populate the button frame's reflow metrics. + // * Button's height & width are content-box size + border-box contribution: + aButtonDesiredSize.SetSize( + wm, + LogicalSize(wm, aButtonReflowInput.ComputedISize() + clbp.IStartEnd(wm), + buttonContentBox.BSize(wm) + clbp.BStartEnd(wm))); + + // * Button's ascent is its child's ascent, plus the child's block-offset + // within our frame... unless it's orthogonal, in which case we'll use the + // contents inline-size as an approximation for now. + // XXX is there a better strategy? should we include border-padding? + if (!aButtonReflowInput.mStyleDisplay->IsContainLayout()) { + if (aButtonDesiredSize.GetWritingMode().IsOrthogonalTo(wm)) { + aButtonDesiredSize.SetBlockStartAscent( + wm.IsAlphabeticalBaseline() ? contentsDesiredSize.ISize(wm) + : contentsDesiredSize.ISize(wm) / 2); + } else { + aButtonDesiredSize.SetBlockStartAscent( + contentsDesiredSize.BlockStartAscent() + childPos.B(wm)); + } + } // else: we're layout-contained, and so we have no baseline. + + aButtonDesiredSize.SetOverflowAreasToDesiredBounds(); +} + +Maybe nsHTMLButtonControlFrame::GetNaturalBaselineBOffset( + WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext aExportContext) const { + if (StyleDisplay()->IsContainLayout()) { + return Nothing{}; + } + + nsIFrame* inner = mFrames.FirstChild(); + if (MOZ_UNLIKELY(inner->GetWritingMode().IsOrthogonalTo(aWM))) { + return Nothing{}; + } + auto result = + inner->GetNaturalBaselineBOffset(aWM, aBaselineGroup, aExportContext) + .valueOrFrom([inner, aWM, aBaselineGroup]() { + return Baseline::SynthesizeBOffsetFromBorderBox(inner, aWM, + aBaselineGroup); + }); + + nscoord innerBStart = inner->BStart(aWM, GetSize()); + if (aBaselineGroup == BaselineSharingGroup::First) { + return Some(result + innerBStart); + } + return Some(result + BSize(aWM) - (innerBStart + inner->BSize(aWM))); +} + +BaselineSharingGroup nsHTMLButtonControlFrame::GetDefaultBaselineSharingGroup() + const { + nsIFrame* firstKid = mFrames.FirstChild(); + + MOZ_ASSERT(firstKid, "Button should have a child frame for its contents"); + MOZ_ASSERT(!firstKid->GetNextSibling(), + "Button should have exactly one child frame"); + return firstKid->GetDefaultBaselineSharingGroup(); +} + +nscoord nsHTMLButtonControlFrame::SynthesizeFallbackBaseline( + mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup) const { + return Baseline::SynthesizeBOffsetFromMarginBox(this, aWM, aBaselineGroup); +} + +nsresult nsHTMLButtonControlFrame::SetFormProperty(nsAtom* aName, + const nsAString& aValue) { + if (nsGkAtoms::value == aName) { + return mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::value, + aValue, true); + } + return NS_OK; +} + +ComputedStyle* nsHTMLButtonControlFrame::GetAdditionalComputedStyle( + int32_t aIndex) const { + return mRenderer.GetComputedStyle(aIndex); +} + +void nsHTMLButtonControlFrame::SetAdditionalComputedStyle( + int32_t aIndex, ComputedStyle* aComputedStyle) { + mRenderer.SetComputedStyle(aIndex, aComputedStyle); +} + +void nsHTMLButtonControlFrame::AppendDirectlyOwnedAnonBoxes( + nsTArray& aResult) { + MOZ_ASSERT(mFrames.FirstChild(), "Must have our button-content anon box"); + MOZ_ASSERT(!mFrames.FirstChild()->GetNextSibling(), + "Must only have our button-content anon box"); + aResult.AppendElement(OwnedAnonBox(mFrames.FirstChild())); +} + +#ifdef DEBUG +void nsHTMLButtonControlFrame::AppendFrames(ChildListID aListID, + nsFrameList&& aFrameList) { + MOZ_CRASH("unsupported operation"); +} + +void nsHTMLButtonControlFrame::InsertFrames( + ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, nsFrameList&& aFrameList) { + MOZ_CRASH("unsupported operation"); +} + +void nsHTMLButtonControlFrame::RemoveFrame(DestroyContext&, ChildListID, + nsIFrame*) { + MOZ_CRASH("unsupported operation"); +} +#endif diff --git a/layout/forms/nsHTMLButtonControlFrame.h b/layout/forms/nsHTMLButtonControlFrame.h new file mode 100644 index 0000000000..b4409d66a7 --- /dev/null +++ b/layout/forms/nsHTMLButtonControlFrame.h @@ -0,0 +1,110 @@ +/* -*- 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 nsHTMLButtonControlFrame_h___ +#define nsHTMLButtonControlFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIFormControlFrame.h" +#include "nsButtonFrameRenderer.h" + +class gfxContext; +class nsPresContext; + +class nsHTMLButtonControlFrame : public nsContainerFrame, + public nsIFormControlFrame { + public: + explicit nsHTMLButtonControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsHTMLButtonControlFrame(aStyle, aPresContext, kClassID) {} + + ~nsHTMLButtonControlFrame(); + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsHTMLButtonControlFrame) + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + nscoord GetMinISize(gfxContext* aRenderingContext) override; + + nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + Maybe GetNaturalBaselineBOffset( + mozilla::WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext aExportContext) const override; + + nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + ComputedStyle* GetAdditionalComputedStyle(int32_t aIndex) const override; + void SetAdditionalComputedStyle(int32_t aIndex, + ComputedStyle* aComputedStyle) override; + +#ifdef DEBUG + void AppendFrames(ChildListID aListID, nsFrameList&& aFrameList) override; + void InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, + nsFrameList&& aFrameList) override; + void RemoveFrame(DestroyContext&, ChildListID, nsIFrame*) override; +#endif + +#ifdef ACCESSIBILITY + mozilla::a11y::AccType AccessibleType() override; +#endif + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"HTMLButtonControl"_ns, aResult); + } +#endif + + // nsIFormControlFrame + void SetFocus(bool aOn, bool aRepaint) override; + nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) override; + + // Inserted child content gets its frames parented by our child block + nsContainerFrame* GetContentInsertionFrame() override { + return PrincipalChildList().FirstChild()->GetContentInsertionFrame(); + } + + // Return the ::-moz-button-content anonymous box. + void AppendDirectlyOwnedAnonBoxes(nsTArray& aResult) override; + + protected: + nsHTMLButtonControlFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, + nsIFrame::ClassID aID); + + // Indicates whether we should clip our children's painting to our + // border-box (either because of "overflow" or because of legacy reasons + // about how -flavored buttons work). + bool ShouldClipPaintingToBorderBox(); + + // Reflows the button's sole child frame, and computes the desired size + // of the button itself from the results. + void ReflowButtonContents(nsPresContext* aPresContext, + ReflowOutput& aButtonDesiredSize, + const ReflowInput& aButtonReflowInput, + nsIFrame* aFirstKid); + + BaselineSharingGroup GetDefaultBaselineSharingGroup() const override; + nscoord SynthesizeFallbackBaseline( + mozilla::WritingMode aWM, + BaselineSharingGroup aBaselineGroup) const override; + + nsButtonFrameRenderer mRenderer; +}; + +#endif diff --git a/layout/forms/nsIFormControlFrame.h b/layout/forms/nsIFormControlFrame.h new file mode 100644 index 0000000000..45b7c63aa4 --- /dev/null +++ b/layout/forms/nsIFormControlFrame.h @@ -0,0 +1,41 @@ +/* -*- 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 nsIFormControlFrame_h___ +#define nsIFormControlFrame_h___ + +#include "nsQueryFrame.h" +#include "nsStringFwd.h" + +class nsAtom; + +/** + * nsIFormControlFrame is the common interface for frames of form controls. It + * provides a uniform way of creating widgets, resizing, and painting. + * @see nsLeafFrame and its base classes for more info + */ +class nsIFormControlFrame : public nsQueryFrame { + public: + NS_DECL_QUERYFRAME_TARGET(nsIFormControlFrame) + + /** + * + * @param aOn + * @param aRepaint + */ + virtual void SetFocus(bool aOn = true, bool aRepaint = false) = 0; + + /** + * Set a property on the form control frame. + * + * @param aName name of the property to set + * @param aValue value of the property + * @returns NS_OK if the property name is valid, otherwise an error code + */ + virtual nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) = 0; +}; + +#endif diff --git a/layout/forms/nsISelectControlFrame.h b/layout/forms/nsISelectControlFrame.h new file mode 100644 index 0000000000..b849f04d52 --- /dev/null +++ b/layout/forms/nsISelectControlFrame.h @@ -0,0 +1,50 @@ +/* -*- 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 nsISelectControlFrame_h___ +#define nsISelectControlFrame_h___ + +#include "nsQueryFrame.h" + +/** + * nsISelectControlFrame is the interface for combo boxes and listboxes + */ +class nsISelectControlFrame : public nsQueryFrame { + public: + NS_DECL_QUERYFRAME_TARGET(nsISelectControlFrame) + + /** + * Adds an option to the list at index + */ + + NS_IMETHOD AddOption(int32_t index) = 0; + + /** + * Removes the option at index. The caller must have a live script + * blocker while calling this method. + */ + NS_IMETHOD RemoveOption(int32_t index) = 0; + + /** + * Sets whether the parser is done adding children + * @param aIsDone whether the parser is done adding children + */ + NS_IMETHOD DoneAddingChildren(bool aIsDone) = 0; + + /** + * Notify the frame when an option is selected + */ + NS_IMETHOD OnOptionSelected(int32_t aIndex, bool aSelected) = 0; + + /** + * Notify the frame when selectedIndex was changed. This might + * destroy the frame. + */ + NS_IMETHOD_(void) + OnSetSelectedIndex(int32_t aOldIndex, int32_t aNewIndex) = 0; +}; + +#endif diff --git a/layout/forms/nsITextControlFrame.h b/layout/forms/nsITextControlFrame.h new file mode 100644 index 0000000000..a1c1df1e2e --- /dev/null +++ b/layout/forms/nsITextControlFrame.h @@ -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/. */ + +#ifndef nsITextControlFrame_h___ +#define nsITextControlFrame_h___ + +#include "nsIFormControlFrame.h" +#include "mozilla/AlreadyAddRefed.h" + +class nsISelectionController; +class nsFrameSelection; + +namespace mozilla { +class TextEditor; +} // namespace mozilla + +// FIXME(emilio): This has only one implementation, seems it could be removed... +class nsITextControlFrame : public nsIFormControlFrame { + public: + NS_DECL_QUERYFRAME_TARGET(nsITextControlFrame) + + enum class SelectionDirection : uint8_t { None, Forward, Backward }; + + virtual already_AddRefed GetTextEditor() = 0; + + MOZ_CAN_RUN_SCRIPT NS_IMETHOD + SetSelectionRange(uint32_t aSelectionStart, uint32_t aSelectionEnd, + SelectionDirection = SelectionDirection::None) = 0; + + NS_IMETHOD GetOwnedSelectionController(nsISelectionController** aSelCon) = 0; + virtual nsFrameSelection* GetOwnedFrameSelection() = 0; + + /** + * Ensure editor is initialized with the proper flags and the default value. + * @throws NS_ERROR_NOT_INITIALIZED if mEditor has not been created + * @throws various and sundry other things + */ + virtual nsresult EnsureEditorInitialized() = 0; +}; + +#endif diff --git a/layout/forms/nsImageControlFrame.cpp b/layout/forms/nsImageControlFrame.cpp new file mode 100644 index 0000000000..17e3893a2f --- /dev/null +++ b/layout/forms/nsImageControlFrame.cpp @@ -0,0 +1,151 @@ +/* -*- 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 "nsImageFrame.h" + +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "nsIFormControlFrame.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsLayoutUtils.h" +#include "nsIContent.h" + +using namespace mozilla; + +class nsImageControlFrame final : public nsImageFrame, + public nsIFormControlFrame { + public: + explicit nsImageControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + ~nsImageControlFrame() final; + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) final; + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsImageControlFrame) + + void Reflow(nsPresContext*, ReflowOutput&, const ReflowInput&, + nsReflowStatus&) final; + + nsresult HandleEvent(nsPresContext*, WidgetGUIEvent*, nsEventStatus*) final; + +#ifdef ACCESSIBILITY + mozilla::a11y::AccType AccessibleType() final; +#endif + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const final { + return MakeFrameName(u"ImageControl"_ns, aResult); + } +#endif + + Cursor GetCursor(const nsPoint&) final; + + // nsIFormContromFrame + void SetFocus(bool aOn, bool aRepaint) final; + nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) final; +}; + +nsImageControlFrame::nsImageControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsImageFrame(aStyle, aPresContext, kClassID) {} + +nsImageControlFrame::~nsImageControlFrame() = default; + +nsIFrame* NS_NewImageControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsImageControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsImageControlFrame) + +void nsImageControlFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsImageFrame::Init(aContent, aParent, aPrevInFlow); + + if (aPrevInFlow) { + return; + } + + mContent->SetProperty(nsGkAtoms::imageClickedPoint, new CSSIntPoint(0, 0), + nsINode::DeleteProperty); +} + +NS_QUERYFRAME_HEAD(nsImageControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsImageFrame) + +#ifdef ACCESSIBILITY +a11y::AccType nsImageControlFrame::AccessibleType() { + if (mContent->IsAnyOfHTMLElements(nsGkAtoms::button, nsGkAtoms::input)) { + return a11y::eHTMLButtonType; + } + + return a11y::eNoType; +} +#endif + +void nsImageControlFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + DO_GLOBAL_REFLOW_COUNT("nsImageControlFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + return nsImageFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, + aStatus); +} + +nsresult nsImageControlFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + NS_ENSURE_ARG_POINTER(aEventStatus); + + // Don't do anything if the event has already been handled by someone + if (nsEventStatus_eConsumeNoDefault == *aEventStatus) { + return NS_OK; + } + + if (IsContentDisabled()) { + return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus); + } + + *aEventStatus = nsEventStatus_eIgnore; + + if (aEvent->mMessage == eMouseUp && + aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary) { + // Store click point for HTMLInputElement::SubmitNamesValues + // Do this on MouseUp because the specs don't say and that's what IE does + auto* lastClickedPoint = static_cast( + mContent->GetProperty(nsGkAtoms::imageClickedPoint)); + if (lastClickedPoint) { + // normally lastClickedPoint is not null, as it's allocated in Init() + nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo( + aEvent, RelativeTo{this}); + *lastClickedPoint = TranslateEventCoords(pt); + } + } + return nsImageFrame::HandleEvent(aPresContext, aEvent, aEventStatus); +} + +void nsImageControlFrame::SetFocus(bool aOn, bool aRepaint) {} + +nsIFrame::Cursor nsImageControlFrame::GetCursor(const nsPoint&) { + StyleCursorKind kind = StyleUI()->Cursor().keyword; + if (kind == StyleCursorKind::Auto) { + kind = StyleCursorKind::Pointer; + } + return Cursor{kind, AllowCustomCursorImage::Yes}; +} + +nsresult nsImageControlFrame::SetFormProperty(nsAtom* aName, + const nsAString& aValue) { + return NS_OK; +} diff --git a/layout/forms/nsListControlFrame.cpp b/layout/forms/nsListControlFrame.cpp new file mode 100644 index 0000000000..44ce9fde13 --- /dev/null +++ b/layout/forms/nsListControlFrame.cpp @@ -0,0 +1,1211 @@ +/* -*- 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 "nscore.h" +#include "nsCOMPtr.h" +#include "nsUnicharUtils.h" +#include "nsListControlFrame.h" +#include "HTMLSelectEventListener.h" +#include "nsGkAtoms.h" +#include "nsComboboxControlFrame.h" +#include "nsFontMetrics.h" +#include "nsIScrollableFrame.h" +#include "nsCSSRendering.h" +#include "nsLayoutUtils.h" +#include "nsDisplayList.h" +#include "nsContentUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/HTMLOptGroupElement.h" +#include "mozilla/dom/HTMLOptionsCollection.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/TextEvents.h" +#include + +using namespace mozilla; +using namespace mozilla::dom; + +// Static members +nsListControlFrame* nsListControlFrame::mFocused = nullptr; + +//--------------------------------------------------------- +nsListControlFrame* NS_NewListControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + nsListControlFrame* it = + new (aPresShell) nsListControlFrame(aStyle, aPresShell->GetPresContext()); + + it->AddStateBits(NS_FRAME_INDEPENDENT_SELECTION); + + return it; +} + +NS_IMPL_FRAMEARENA_HELPERS(nsListControlFrame) + +nsListControlFrame::nsListControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsHTMLScrollFrame(aStyle, aPresContext, kClassID, false), + mMightNeedSecondPass(false), + mHasPendingInterruptAtStartOfReflow(false), + mForceSelection(false) { + mChangesSinceDragStart = false; + + mIsAllContentHere = false; + mIsAllFramesHere = false; + mHasBeenInitialized = false; + mNeedToReset = true; + mPostChildrenLoadedReset = false; +} + +nsListControlFrame::~nsListControlFrame() = default; + +Maybe nsListControlFrame::GetNaturalBaselineBOffset( + WritingMode aWM, BaselineSharingGroup aBaselineGroup, + BaselineExportContext) const { + // Unlike scroll frames which we inherit from, we don't export a baseline. + return Nothing{}; +} +// for Bug 47302 (remove this comment later) +void nsListControlFrame::Destroy(DestroyContext& aContext) { + // get the receiver interface from the browser button's content node + NS_ENSURE_TRUE_VOID(mContent); + + // Clear the frame pointer on our event listener, just in case the + // event listener can outlive the frame. + + mEventListener->Detach(); + nsHTMLScrollFrame::Destroy(aContext); +} + +void nsListControlFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + // We allow visibility:hidden then we expect the + // frame of the NAC root parent to be that input's frame. We have to check for + // this via the content tree because we don't know whether extra frames will + // be wrapped around any of the elements between aFrame and the + // nsNumberControlFrame that we're looking for (e.g. flex wrappers). + nsIContent* content = aFrame->GetContent(); + auto* nacHost = content->GetClosestNativeAnonymousSubtreeRootParentOrHost(); + if (!nacHost) { + return nullptr; + } + auto* input = HTMLInputElement::FromNode(nacHost); + if (!input || input->ControlType() != FormControlType::InputNumber) { + return nullptr; + } + return do_QueryFrame(input->GetPrimaryFrame()); +} + +int32_t nsNumberControlFrame::GetSpinButtonForPointerEvent( + WidgetGUIEvent* aEvent) const { + MOZ_ASSERT(aEvent->mClass == eMouseEventClass, "Unexpected event type"); + + if (!mSpinBox) { + // we don't have a spinner + return eSpinButtonNone; + } + if (aEvent->mOriginalTarget == mSpinUp) { + return eSpinButtonUp; + } + if (aEvent->mOriginalTarget == mSpinDown) { + return eSpinButtonDown; + } + if (aEvent->mOriginalTarget == mSpinBox) { + // In the case that the up/down buttons are hidden (display:none) we use + // just the spin box element, spinning up if the pointer is over the top + // half of the element, or down if it's over the bottom half. This is + // important to handle since this is the state things are in for the + // default UA style sheet. See the comment in forms.css for why. + LayoutDeviceIntPoint absPoint = aEvent->mRefPoint; + nsPoint point = nsLayoutUtils::GetEventCoordinatesRelativeTo( + aEvent, absPoint, RelativeTo{mSpinBox->GetPrimaryFrame()}); + if (point != nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)) { + if (point.y < mSpinBox->GetPrimaryFrame()->GetSize().height / 2) { + return eSpinButtonUp; + } + return eSpinButtonDown; + } + } + return eSpinButtonNone; +} + +void nsNumberControlFrame::SpinnerStateChanged() const { + if (mSpinUp) { + nsIFrame* spinUpFrame = mSpinUp->GetPrimaryFrame(); + if (spinUpFrame && spinUpFrame->IsThemed()) { + spinUpFrame->InvalidateFrame(); + } + } + if (mSpinDown) { + nsIFrame* spinDownFrame = mSpinDown->GetPrimaryFrame(); + if (spinDownFrame && spinDownFrame->IsThemed()) { + spinDownFrame->InvalidateFrame(); + } + } +} + +bool nsNumberControlFrame::SpinnerUpButtonIsDepressed() const { + return HTMLInputElement::FromNode(mContent) + ->NumberSpinnerUpButtonIsDepressed(); +} + +bool nsNumberControlFrame::SpinnerDownButtonIsDepressed() const { + return HTMLInputElement::FromNode(mContent) + ->NumberSpinnerDownButtonIsDepressed(); +} + +void nsNumberControlFrame::AppendAnonymousContentTo( + nsTArray& aElements, uint32_t aFilter) { + nsTextControlFrame::AppendAnonymousContentTo(aElements, aFilter); + if (mSpinBox) { + aElements.AppendElement(mSpinBox); + } +} + +#ifdef ACCESSIBILITY +a11y::AccType nsNumberControlFrame::AccessibleType() { + return a11y::eHTMLSpinnerType; +} +#endif diff --git a/layout/forms/nsNumberControlFrame.h b/layout/forms/nsNumberControlFrame.h new file mode 100644 index 0000000000..d7201ea1e1 --- /dev/null +++ b/layout/forms/nsNumberControlFrame.h @@ -0,0 +1,105 @@ +/* -*- 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 nsNumberControlFrame_h__ +#define nsNumberControlFrame_h__ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsTextControlFrame.h" +#include "nsIFormControlFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" + +class nsITextControlFrame; +class nsPresContext; + +namespace mozilla { +enum class PseudoStyleType : uint8_t; +class PresShell; +class WidgetEvent; +class WidgetGUIEvent; +namespace dom { +class HTMLInputElement; +} // namespace dom +} // namespace mozilla + +/** + * This frame type is used for . + * + * TODO(emilio): Maybe merge with nsTextControlFrame? + */ +class nsNumberControlFrame final : public nsTextControlFrame { + friend nsIFrame* NS_NewNumberControlFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + typedef mozilla::PseudoStyleType PseudoStyleType; + typedef mozilla::dom::Element Element; + typedef mozilla::dom::HTMLInputElement HTMLInputElement; + typedef mozilla::WidgetEvent WidgetEvent; + typedef mozilla::WidgetGUIEvent WidgetGUIEvent; + + explicit nsNumberControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsNumberControlFrame) + + void Destroy(DestroyContext&) override; + +#ifdef ACCESSIBILITY + mozilla::a11y::AccType AccessibleType() override; +#endif + + // nsIAnonymousContentCreator + nsresult CreateAnonymousContent(nsTArray& aElements) override; + void AppendAnonymousContentTo(nsTArray& aElements, + uint32_t aFilter) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"NumberControl"_ns, aResult); + } +#endif + + /** + * If the frame is the frame for an nsNumberControlFrame's up or down spin + * button, returns the nsNumberControlFrame. Else returns nullptr. + */ + static nsNumberControlFrame* GetNumberControlFrameForSpinButton( + nsIFrame* aFrame); + + enum SpinButtonEnum { eSpinButtonNone, eSpinButtonUp, eSpinButtonDown }; + + /** + * Returns one of the SpinButtonEnum values to depending on whether the + * pointer event is over the spin-up button, the spin-down button, or + * neither. + */ + int32_t GetSpinButtonForPointerEvent(WidgetGUIEvent* aEvent) const; + + void SpinnerStateChanged() const; + + bool SpinnerUpButtonIsDepressed() const; + bool SpinnerDownButtonIsDepressed() const; + + /** + * Our element had HTMLInputElement::Select() called on it. + */ + void HandleSelectCall(); + + bool ShouldUseNativeStyleForSpinner() const; + + private: + // See nsNumberControlFrame::CreateAnonymousContent for a description of + // these. + nsCOMPtr mSpinBox; + nsCOMPtr mSpinUp; + nsCOMPtr mSpinDown; +}; + +#endif // nsNumberControlFrame_h__ diff --git a/layout/forms/nsProgressFrame.cpp b/layout/forms/nsProgressFrame.cpp new file mode 100644 index 0000000000..2f0d727473 --- /dev/null +++ b/layout/forms/nsProgressFrame.cpp @@ -0,0 +1,250 @@ +/* -*- 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 "nsProgressFrame.h" + +#include "mozilla/PresShell.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLProgressElement.h" +#include "nsIContent.h" +#include "nsLayoutUtils.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "nsFontMetrics.h" +#include + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewProgressFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsProgressFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsProgressFrame) + +nsProgressFrame::nsProgressFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsContainerFrame(aStyle, aPresContext, kClassID), mBarDiv(nullptr) {} + +nsProgressFrame::~nsProgressFrame() = default; + +void nsProgressFrame::Destroy(DestroyContext& aContext) { + NS_ASSERTION(!GetPrevContinuation(), + "nsProgressFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + aContext.AddAnonymousContent(mBarDiv.forget()); + nsContainerFrame::Destroy(aContext); +} + +nsresult nsProgressFrame::CreateAnonymousContent( + nsTArray& aElements) { + // Create the progress bar div. + nsCOMPtr doc = mContent->GetComposedDoc(); + mBarDiv = doc->CreateHTMLElement(nsGkAtoms::div); + + // Associate ::-moz-progress-bar pseudo-element to the anonymous child. + if (StaticPrefs::layout_css_modern_range_pseudos_enabled()) { + // TODO(emilio): Create also a slider-track pseudo-element. + mBarDiv->SetPseudoElementType(PseudoStyleType::sliderFill); + } else { + mBarDiv->SetPseudoElementType(PseudoStyleType::mozProgressBar); + } + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + aElements.AppendElement(mBarDiv); + + return NS_OK; +} + +void nsProgressFrame::AppendAnonymousContentTo(nsTArray& aElements, + uint32_t aFilter) { + if (mBarDiv) { + aElements.AppendElement(mBarDiv); + } +} + +NS_QUERYFRAME_HEAD(nsProgressFrame) + NS_QUERYFRAME_ENTRY(nsProgressFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +void nsProgressFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + BuildDisplayListForInline(aBuilder, aLists); +} + +void nsProgressFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsProgressFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + NS_ASSERTION(mBarDiv, "Progress bar div must exist!"); + NS_ASSERTION( + PrincipalChildList().GetLength() == 1 && + PrincipalChildList().FirstChild() == mBarDiv->GetPrimaryFrame(), + "unexpected child frames"); + NS_ASSERTION(!GetPrevContinuation(), + "nsProgressFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + + const auto wm = aReflowInput.GetWritingMode(); + aDesiredSize.SetSize(wm, aReflowInput.ComputedSizeWithBorderPadding(wm)); + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + for (auto childFrame : PrincipalChildList()) { + ReflowChildFrame(childFrame, aPresContext, aReflowInput, aStatus); + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, childFrame); + } + + FinishAndStoreOverflow(&aDesiredSize); + + aStatus.Reset(); // This type of frame can't be split. +} + +void nsProgressFrame::ReflowChildFrame(nsIFrame* aChild, + nsPresContext* aPresContext, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + bool vertical = ResolvedOrientationIsVertical(); + WritingMode wm = aChild->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput reflowInput(aPresContext, aReflowInput, aChild, availSize); + nscoord size = + vertical ? aReflowInput.ComputedHeight() : aReflowInput.ComputedWidth(); + nscoord xoffset = aReflowInput.ComputedPhysicalBorderPadding().left; + nscoord yoffset = aReflowInput.ComputedPhysicalBorderPadding().top; + + double position = static_cast(GetContent())->Position(); + + // Force the bar's size to match the current progress. + // When indeterminate, the progress' size will be 100%. + if (position >= 0.0) { + size *= position; + } + + if (!vertical && wm.IsPhysicalRTL()) { + xoffset += aReflowInput.ComputedWidth() - size; + } + + // The bar size is fixed in these cases: + // - the progress position is determined: the bar size is fixed according + // to it's value. + // - the progress position is indeterminate and the bar appearance should be + // shown as native: the bar size is forced to 100%. + // Otherwise (when the progress is indeterminate and the bar appearance isn't + // native), the bar size isn't fixed and can be set by the author. + if (position != -1 || ShouldUseNativeStyle()) { + if (vertical) { + // We want the bar to begin at the bottom. + yoffset += aReflowInput.ComputedHeight() - size; + + size -= reflowInput.ComputedPhysicalMargin().TopBottom() + + reflowInput.ComputedPhysicalBorderPadding().TopBottom(); + size = std::max(size, 0); + reflowInput.SetComputedHeight(size); + } else { + size -= reflowInput.ComputedPhysicalMargin().LeftRight() + + reflowInput.ComputedPhysicalBorderPadding().LeftRight(); + size = std::max(size, 0); + reflowInput.SetComputedWidth(size); + } + } else if (vertical) { + // For vertical progress bars, we need to position the bar specificly when + // the width isn't constrained (position == -1 and !ShouldUseNativeStyle()) + // because aReflowInput.ComputedHeight() - size == 0. + yoffset += aReflowInput.ComputedHeight() - reflowInput.ComputedHeight(); + } + + xoffset += reflowInput.ComputedPhysicalMargin().left; + yoffset += reflowInput.ComputedPhysicalMargin().top; + + ReflowOutput barDesiredSize(aReflowInput); + ReflowChild(aChild, aPresContext, barDesiredSize, reflowInput, xoffset, + yoffset, ReflowChildFlags::Default, aStatus); + FinishReflowChild(aChild, aPresContext, barDesiredSize, &reflowInput, xoffset, + yoffset, ReflowChildFlags::Default); +} + +nsresult nsProgressFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + NS_ASSERTION(mBarDiv, "Progress bar div must exist!"); + + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::max)) { + auto presShell = PresShell(); + for (auto childFrame : PrincipalChildList()) { + presShell->FrameNeedsReflow(childFrame, IntrinsicDirty::None, + NS_FRAME_IS_DIRTY); + } + InvalidateFrame(); + } + + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +LogicalSize nsProgressFrame::ComputeAutoSize( + gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize, + nscoord aAvailableISize, const LogicalSize& aMargin, + const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides, + ComputeSizeFlags aFlags) { + const WritingMode wm = GetWritingMode(); + LogicalSize autoSize(wm); + autoSize.BSize(wm) = autoSize.ISize(wm) = + StyleFont() + ->mFont.size.ScaledBy(nsLayoutUtils::FontSizeInflationFor(this)) + .ToAppUnits(); // 1em + + if (ResolvedOrientationIsVertical() == wm.IsVertical()) { + autoSize.ISize(wm) *= 10; // 10em + } else { + autoSize.BSize(wm) *= 10; // 10em + } + + return autoSize.ConvertTo(aWM, wm); +} + +nscoord nsProgressFrame::GetMinISize(gfxContext* aRenderingContext) { + RefPtr fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f); + + nscoord minISize = fontMet->Font().size.ToAppUnits(); // 1em + + if (ResolvedOrientationIsVertical() == GetWritingMode().IsVertical()) { + // The orientation is inline + minISize *= 10; // 10em + } + + return minISize; +} + +nscoord nsProgressFrame::GetPrefISize(gfxContext* aRenderingContext) { + return GetMinISize(aRenderingContext); +} + +bool nsProgressFrame::ShouldUseNativeStyle() const { + nsIFrame* barFrame = PrincipalChildList().FirstChild(); + + // Use the native style if these conditions are satisfied: + // - both frames use the native appearance; + // - neither frame has author specified rules setting the border or the + // background. + return StyleDisplay()->EffectiveAppearance() == + StyleAppearance::ProgressBar && + !Style()->HasAuthorSpecifiedBorderOrBackground() && barFrame && + barFrame->StyleDisplay()->EffectiveAppearance() == + StyleAppearance::Progresschunk && + !barFrame->Style()->HasAuthorSpecifiedBorderOrBackground(); +} diff --git a/layout/forms/nsProgressFrame.h b/layout/forms/nsProgressFrame.h new file mode 100644 index 0000000000..7d3618ee96 --- /dev/null +++ b/layout/forms/nsProgressFrame.h @@ -0,0 +1,84 @@ +/* -*- 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 nsProgressFrame_h___ +#define nsProgressFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsCOMPtr.h" + +namespace mozilla { +enum class PseudoStyleType : uint8_t; +} // namespace mozilla + +class nsProgressFrame final : public nsContainerFrame, + public nsIAnonymousContentCreator { + typedef mozilla::PseudoStyleType PseudoStyleType; + typedef mozilla::dom::Element Element; + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsProgressFrame) + + explicit nsProgressFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + virtual ~nsProgressFrame(); + + void Destroy(DestroyContext&) override; + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + virtual void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"Progress"_ns, aResult); + } +#endif + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent( + nsTArray& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray& aElements, + uint32_t aFilter) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + virtual mozilla::LogicalSize ComputeAutoSize( + gfxContext* aRenderingContext, mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorderPadding, + const mozilla::StyleSizeOverrides& aSizeOverrides, + mozilla::ComputeSizeFlags aFlags) override; + + virtual nscoord GetMinISize(gfxContext* aRenderingContext) override; + virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + /** + * Returns whether the frame and its child should use the native style. + */ + bool ShouldUseNativeStyle() const; + + protected: + // Helper function to reflow a child frame. + void ReflowChildFrame(nsIFrame* aChild, nsPresContext* aPresContext, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus); + + /** + * The div used to show the progress bar. + * @see nsProgressFrame::CreateAnonymousContent + */ + nsCOMPtr mBarDiv; +}; + +#endif diff --git a/layout/forms/nsRangeFrame.cpp b/layout/forms/nsRangeFrame.cpp new file mode 100644 index 0000000000..2cccff715a --- /dev/null +++ b/layout/forms/nsRangeFrame.cpp @@ -0,0 +1,780 @@ +/* -*- 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 "nsRangeFrame.h" + +#include "ListMutationObserver.h" +#include "mozilla/Assertions.h" +#include "mozilla/PresShell.h" +#include "mozilla/TouchEvents.h" + +#include "gfxContext.h" +#include "nsContentCreatorFunctions.h" +#include "nsCSSRendering.h" +#include "nsDisplayList.h" +#include "nsIContent.h" +#include "nsLayoutUtils.h" +#include "mozilla/dom/Document.h" +#include "nsGkAtoms.h" +#include "mozilla/dom/HTMLDataListElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLOptionElement.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "nsPresContext.h" +#include "nsNodeInfoManager.h" +#include "mozilla/dom/Element.h" +#include "mozilla/ServoStyleSet.h" +#include "nsTArray.h" + +#ifdef ACCESSIBILITY +# include "nsAccessibilityService.h" +#endif + +// Our intrinsic size is 12em in the main-axis and 1.3em in the cross-axis. +#define MAIN_AXIS_EM_SIZE 12 +#define CROSS_AXIS_EM_SIZE 1.3f + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::image; + +nsIFrame* NS_NewRangeFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsRangeFrame(aStyle, aPresShell->GetPresContext()); +} + +nsRangeFrame::nsRangeFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : nsContainerFrame(aStyle, aPresContext, kClassID) {} + +void nsRangeFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsContainerFrame::Init(aContent, aParent, aPrevInFlow); + if (InputElement().HasAttr(nsGkAtoms::list_)) { + mListMutationObserver = new ListMutationObserver(*this); + } +} + +nsRangeFrame::~nsRangeFrame() = default; + +NS_IMPL_FRAMEARENA_HELPERS(nsRangeFrame) + +NS_QUERYFRAME_HEAD(nsRangeFrame) + NS_QUERYFRAME_ENTRY(nsRangeFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +void nsRangeFrame::Destroy(DestroyContext& aContext) { + NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(), + "nsRangeFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + + if (mListMutationObserver) { + mListMutationObserver->Detach(); + } + aContext.AddAnonymousContent(mTrackDiv.forget()); + aContext.AddAnonymousContent(mProgressDiv.forget()); + aContext.AddAnonymousContent(mThumbDiv.forget()); + nsContainerFrame::Destroy(aContext); +} + +static already_AddRefed MakeAnonymousDiv( + Document& aDoc, PseudoStyleType aOldPseudoType, + PseudoStyleType aModernPseudoType, + nsTArray& aElements) { + RefPtr result = aDoc.CreateHTMLElement(nsGkAtoms::div); + + // Associate the pseudo-element with the anonymous child. + if (StaticPrefs::layout_css_modern_range_pseudos_enabled()) { + result->SetPseudoElementType(aModernPseudoType); + } else { + result->SetPseudoElementType(aOldPseudoType); + } + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier, or change the return type to void. + aElements.AppendElement(result); + + return result.forget(); +} + +nsresult nsRangeFrame::CreateAnonymousContent( + nsTArray& aElements) { + Document* doc = mContent->OwnerDoc(); + // Create the ::-moz-range-track pseudo-element (a div): + mTrackDiv = MakeAnonymousDiv(*doc, PseudoStyleType::mozRangeTrack, + PseudoStyleType::sliderTrack, aElements); + // Create the ::-moz-range-progress pseudo-element (a div): + mProgressDiv = MakeAnonymousDiv(*doc, PseudoStyleType::mozRangeProgress, + PseudoStyleType::sliderFill, aElements); + // Create the ::-moz-range-thumb pseudo-element (a div): + mThumbDiv = MakeAnonymousDiv(*doc, PseudoStyleType::mozRangeThumb, + PseudoStyleType::sliderThumb, aElements); + return NS_OK; +} + +void nsRangeFrame::AppendAnonymousContentTo(nsTArray& aElements, + uint32_t aFilter) { + if (mTrackDiv) { + aElements.AppendElement(mTrackDiv); + } + + if (mProgressDiv) { + aElements.AppendElement(mProgressDiv); + } + + if (mThumbDiv) { + aElements.AppendElement(mThumbDiv); + } +} + +void nsRangeFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + const nsStyleDisplay* disp = StyleDisplay(); + if (IsThemed(disp)) { + DisplayBorderBackgroundOutline(aBuilder, aLists); + // Only create items for the thumb. Specifically, we do not want the track + // to paint, since *our* background is used to paint the track, and we don't + // want the unthemed track painting over the top of the themed track. + // This logic is copied from + // nsContainerFrame::BuildDisplayListForNonBlockChildren as + // called by BuildDisplayListForInline. + if (nsIFrame* thumb = mThumbDiv->GetPrimaryFrame()) { + nsDisplayListSet set(aLists, aLists.Content()); + BuildDisplayListForChild(aBuilder, thumb, set, DisplayChildFlag::Inline); + } + } else { + BuildDisplayListForInline(aBuilder, aLists); + } +} + +void nsRangeFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsRangeFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + NS_ASSERTION(mTrackDiv, "::-moz-range-track div must exist!"); + NS_ASSERTION(mProgressDiv, "::-moz-range-progress div must exist!"); + NS_ASSERTION(mThumbDiv, "::-moz-range-thumb div must exist!"); + NS_ASSERTION(!GetPrevContinuation() && !GetNextContinuation(), + "nsRangeFrame should not have continuations; if it does we " + "need to call RegUnregAccessKey only for the first."); + + WritingMode wm = aReflowInput.GetWritingMode(); + nscoord computedBSize = aReflowInput.ComputedBSize(); + if (computedBSize == NS_UNCONSTRAINEDSIZE) { + computedBSize = 0; + } + const auto borderPadding = aReflowInput.ComputedLogicalBorderPadding(wm); + LogicalSize finalSize( + wm, aReflowInput.ComputedISize() + borderPadding.IStartEnd(wm), + computedBSize + borderPadding.BStartEnd(wm)); + aDesiredSize.SetSize(wm, finalSize); + + ReflowAnonymousContent(aPresContext, aDesiredSize, aReflowInput); + + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame(); + if (trackFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, trackFrame); + } + + nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame(); + if (rangeProgressFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, rangeProgressFrame); + } + + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + if (thumbFrame) { + ConsiderChildOverflow(aDesiredSize.mOverflowAreas, thumbFrame); + } + + FinishAndStoreOverflow(&aDesiredSize); + + MOZ_ASSERT(aStatus.IsEmpty(), "This type of frame can't be split."); +} + +void nsRangeFrame::ReflowAnonymousContent(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput) { + // The width/height of our content box, which is the available width/height + // for our anonymous content: + nscoord rangeFrameContentBoxWidth = aReflowInput.ComputedWidth(); + nscoord rangeFrameContentBoxHeight = aReflowInput.ComputedHeight(); + if (rangeFrameContentBoxHeight == NS_UNCONSTRAINEDSIZE) { + rangeFrameContentBoxHeight = 0; + } + + nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame(); + + if (trackFrame) { // display:none? + + // Position the track: + // The idea here is that we allow content authors to style the width, + // height, border and padding of the track, but we ignore margin and + // positioning properties and do the positioning ourself to keep the center + // of the track's border box on the center of the nsRangeFrame's content + // box. + + WritingMode wm = trackFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput trackReflowInput(aPresContext, aReflowInput, trackFrame, + availSize); + + // Find the x/y position of the track frame such that it will be positioned + // as described above. These coordinates are with respect to the + // nsRangeFrame's border-box. + nscoord trackX = rangeFrameContentBoxWidth / 2; + nscoord trackY = rangeFrameContentBoxHeight / 2; + + // Account for the track's border and padding (we ignore its margin): + trackX -= trackReflowInput.ComputedPhysicalBorderPadding().left + + trackReflowInput.ComputedWidth() / 2; + trackY -= trackReflowInput.ComputedPhysicalBorderPadding().top + + trackReflowInput.ComputedHeight() / 2; + + // Make relative to our border box instead of our content box: + trackX += aReflowInput.ComputedPhysicalBorderPadding().left; + trackY += aReflowInput.ComputedPhysicalBorderPadding().top; + + nsReflowStatus frameStatus; + ReflowOutput trackDesiredSize(aReflowInput); + ReflowChild(trackFrame, aPresContext, trackDesiredSize, trackReflowInput, + trackX, trackY, ReflowChildFlags::Default, frameStatus); + MOZ_ASSERT( + frameStatus.IsFullyComplete(), + "We gave our child unconstrained height, so it should be complete"); + FinishReflowChild(trackFrame, aPresContext, trackDesiredSize, + &trackReflowInput, trackX, trackY, + ReflowChildFlags::Default); + } + + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + + if (thumbFrame) { // display:none? + WritingMode wm = thumbFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput thumbReflowInput(aPresContext, aReflowInput, thumbFrame, + availSize); + + // Where we position the thumb depends on its size, so we first reflow + // the thumb at {0,0} to obtain its size, then position it afterwards. + + nsReflowStatus frameStatus; + ReflowOutput thumbDesiredSize(aReflowInput); + ReflowChild(thumbFrame, aPresContext, thumbDesiredSize, thumbReflowInput, 0, + 0, ReflowChildFlags::Default, frameStatus); + MOZ_ASSERT( + frameStatus.IsFullyComplete(), + "We gave our child unconstrained height, so it should be complete"); + FinishReflowChild(thumbFrame, aPresContext, thumbDesiredSize, + &thumbReflowInput, 0, 0, ReflowChildFlags::Default); + DoUpdateThumbPosition(thumbFrame, + nsSize(aDesiredSize.Width(), aDesiredSize.Height())); + } + + nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame(); + + if (rangeProgressFrame) { // display:none? + WritingMode wm = rangeProgressFrame->GetWritingMode(); + LogicalSize availSize = aReflowInput.ComputedSize(wm); + availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; + ReflowInput progressReflowInput(aPresContext, aReflowInput, + rangeProgressFrame, availSize); + + // We first reflow the range-progress frame at {0,0} to obtain its + // unadjusted dimensions, then we adjust it to so that the appropriate edge + // ends at the thumb. + + nsReflowStatus frameStatus; + ReflowOutput progressDesiredSize(aReflowInput); + ReflowChild(rangeProgressFrame, aPresContext, progressDesiredSize, + progressReflowInput, 0, 0, ReflowChildFlags::Default, + frameStatus); + MOZ_ASSERT( + frameStatus.IsFullyComplete(), + "We gave our child unconstrained height, so it should be complete"); + FinishReflowChild(rangeProgressFrame, aPresContext, progressDesiredSize, + &progressReflowInput, 0, 0, ReflowChildFlags::Default); + DoUpdateRangeProgressFrame( + rangeProgressFrame, + nsSize(aDesiredSize.Width(), aDesiredSize.Height())); + } +} + +#ifdef ACCESSIBILITY +a11y::AccType nsRangeFrame::AccessibleType() { return a11y::eHTMLRangeType; } +#endif + +double nsRangeFrame::GetValueAsFractionOfRange() { + const auto& input = InputElement(); + if (MOZ_UNLIKELY(!input.IsDoneCreating())) { + // Our element isn't done being created, so its values haven't yet been + // sanitized! (It's rare that we'd be reflowed when our element is in this + // state, but it can happen if the parser decides to yield while processing + // its tasks to build the element.) We can't trust that any of our numeric + // values will make sense until they've been sanitized; so for now, just + // use 0.0 as a fallback fraction-of-range value here (i.e. behave as if + // we're at our minimum, which is how the spec handles some edge cases). + return 0.0; + } + return GetDoubleAsFractionOfRange(input.GetValueAsDecimal()); +} + +double nsRangeFrame::GetDoubleAsFractionOfRange(const Decimal& aValue) { + auto& input = InputElement(); + + Decimal minimum = input.GetMinimum(); + Decimal maximum = input.GetMaximum(); + + MOZ_ASSERT(aValue.isFinite() && minimum.isFinite() && maximum.isFinite(), + "type=range should have a default maximum/minimum"); + + if (maximum <= minimum) { + // Avoid rounding triggering the assert by checking against an epsilon. + MOZ_ASSERT((aValue - minimum).abs().toDouble() < + std::numeric_limits::epsilon(), + "Unsanitized value"); + return 0.0; + } + + MOZ_ASSERT(aValue >= minimum && aValue <= maximum, "Unsanitized value"); + + return ((aValue - minimum) / (maximum - minimum)).toDouble(); +} + +Decimal nsRangeFrame::GetValueAtEventPoint(WidgetGUIEvent* aEvent) { + MOZ_ASSERT( + aEvent->mClass == eMouseEventClass || aEvent->mClass == eTouchEventClass, + "Unexpected event type - aEvent->mRefPoint may be meaningless"); + + MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); + dom::HTMLInputElement* input = + static_cast(GetContent()); + + MOZ_ASSERT(input->ControlType() == FormControlType::InputRange); + + Decimal minimum = input->GetMinimum(); + Decimal maximum = input->GetMaximum(); + MOZ_ASSERT(minimum.isFinite() && maximum.isFinite(), + "type=range should have a default maximum/minimum"); + if (maximum <= minimum) { + return minimum; + } + Decimal range = maximum - minimum; + + LayoutDeviceIntPoint absPoint; + if (aEvent->mClass == eTouchEventClass) { + MOZ_ASSERT(aEvent->AsTouchEvent()->mTouches.Length() == 1, + "Unexpected number of mTouches"); + absPoint = aEvent->AsTouchEvent()->mTouches[0]->mRefPoint; + } else { + absPoint = aEvent->mRefPoint; + } + nsPoint point = nsLayoutUtils::GetEventCoordinatesRelativeTo( + aEvent, absPoint, RelativeTo{this}); + + if (point == nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)) { + // We don't want to change the current value for this error state. + return static_cast(GetContent()) + ->GetValueAsDecimal(); + } + + nsRect rangeContentRect = GetContentRectRelativeToSelf(); + nsSize thumbSize; + + if (IsThemed()) { + // We need to get the size of the thumb from the theme. + nsPresContext* pc = PresContext(); + LayoutDeviceIntSize size = pc->Theme()->GetMinimumWidgetSize( + pc, this, StyleAppearance::RangeThumb); + thumbSize = + LayoutDeviceIntSize::ToAppUnits(size, pc->AppUnitsPerDevPixel()); + // For GTK, GetMinimumWidgetSize returns zero for the thumb dimension + // perpendicular to the orientation of the slider. That's okay since we + // only care about the dimension in the direction of the slider when using + // |thumbSize| below, but it means this assertion need to check + // IsHorizontal(). + MOZ_ASSERT((IsHorizontal() && thumbSize.width > 0) || + (!IsHorizontal() && thumbSize.height > 0), + "The thumb is expected to take up some slider space"); + } else { + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + if (thumbFrame) { // diplay:none? + thumbSize = thumbFrame->GetSize(); + } + } + + Decimal fraction; + if (IsHorizontal()) { + nscoord traversableDistance = rangeContentRect.width - thumbSize.width; + if (traversableDistance <= 0) { + return minimum; + } + nscoord posAtStart = rangeContentRect.x + thumbSize.width / 2; + nscoord posAtEnd = posAtStart + traversableDistance; + nscoord posOfPoint = mozilla::clamped(point.x, posAtStart, posAtEnd); + fraction = Decimal(posOfPoint - posAtStart) / Decimal(traversableDistance); + if (IsRightToLeft()) { + fraction = Decimal(1) - fraction; + } + } else { + nscoord traversableDistance = rangeContentRect.height - thumbSize.height; + if (traversableDistance <= 0) { + return minimum; + } + nscoord posAtStart = rangeContentRect.y + thumbSize.height / 2; + nscoord posAtEnd = posAtStart + traversableDistance; + nscoord posOfPoint = mozilla::clamped(point.y, posAtStart, posAtEnd); + // For a vertical range, the top (posAtStart) is the highest value, so we + // subtract the fraction from 1.0 to get that polarity correct. + fraction = Decimal(posOfPoint - posAtStart) / Decimal(traversableDistance); + if (IsUpwards()) { + fraction = Decimal(1) - fraction; + } + } + + MOZ_ASSERT(fraction >= Decimal(0) && fraction <= Decimal(1)); + return minimum + fraction * range; +} + +void nsRangeFrame::UpdateForValueChange() { + if (IsSubtreeDirty()) { + return; // we're going to be updated when we reflow + } + nsIFrame* rangeProgressFrame = mProgressDiv->GetPrimaryFrame(); + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + if (!rangeProgressFrame && !thumbFrame) { + return; // diplay:none? + } + if (rangeProgressFrame) { + DoUpdateRangeProgressFrame(rangeProgressFrame, GetSize()); + } + if (thumbFrame) { + DoUpdateThumbPosition(thumbFrame, GetSize()); + } + if (IsThemed()) { + // We don't know the exact dimensions or location of the thumb when native + // theming is applied, so we just repaint the entire range. + InvalidateFrame(); + } + +#ifdef ACCESSIBILITY + if (nsAccessibilityService* accService = GetAccService()) { + accService->RangeValueChanged(PresShell(), mContent); + } +#endif + + SchedulePaint(); +} + +nsTArray nsRangeFrame::TickMarks() { + nsTArray tickMarks; + auto& input = InputElement(); + auto* list = input.GetList(); + if (!list) { + return tickMarks; + } + auto min = input.GetMinimum(); + auto max = input.GetMaximum(); + auto* options = list->Options(); + nsAutoString label; + for (uint32_t i = 0; i < options->Length(); ++i) { + auto* item = options->Item(i); + auto* option = HTMLOptionElement::FromNode(item); + MOZ_ASSERT(option); + if (option->Disabled()) { + continue; + } + nsAutoString str; + option->GetValue(str); + auto tickMark = HTMLInputElement::StringToDecimal(str); + if (tickMark.isNaN() || tickMark < min || tickMark > max || + input.ValueIsStepMismatch(tickMark)) { + continue; + } + tickMarks.AppendElement(tickMark); + } + tickMarks.Sort(); + return tickMarks; +} + +Decimal nsRangeFrame::NearestTickMark(const Decimal& aValue) { + auto tickMarks = TickMarks(); + if (tickMarks.IsEmpty() || aValue.isNaN()) { + return Decimal::nan(); + } + size_t index; + if (BinarySearch(tickMarks, 0, tickMarks.Length(), aValue, &index)) { + return tickMarks[index]; + } + if (index == tickMarks.Length()) { + return tickMarks.LastElement(); + } + if (index == 0) { + return tickMarks[0]; + } + const auto& smallerTickMark = tickMarks[index - 1]; + const auto& largerTickMark = tickMarks[index]; + MOZ_ASSERT(smallerTickMark < aValue); + MOZ_ASSERT(largerTickMark > aValue); + return (aValue - smallerTickMark).abs() < (aValue - largerTickMark).abs() + ? smallerTickMark + : largerTickMark; +} + +mozilla::dom::HTMLInputElement& nsRangeFrame::InputElement() const { + MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); + auto& input = *static_cast(GetContent()); + MOZ_ASSERT(input.ControlType() == FormControlType::InputRange); + return input; +} + +void nsRangeFrame::DoUpdateThumbPosition(nsIFrame* aThumbFrame, + const nsSize& aRangeSize) { + MOZ_ASSERT(aThumbFrame); + + // The idea here is that we want to position the thumb so that the center + // of the thumb is on an imaginary line drawn from the middle of one edge + // of the range frame's content box to the middle of the opposite edge of + // its content box (the opposite edges being the left/right edge if the + // range is horizontal, or else the top/bottom edges if the range is + // vertical). How far along this line the center of the thumb is placed + // depends on the value of the range. + + nsMargin borderAndPadding = GetUsedBorderAndPadding(); + nsPoint newPosition(borderAndPadding.left, borderAndPadding.top); + + nsSize rangeContentBoxSize(aRangeSize); + rangeContentBoxSize.width -= borderAndPadding.LeftRight(); + rangeContentBoxSize.height -= borderAndPadding.TopBottom(); + + nsSize thumbSize = aThumbFrame->GetSize(); + double fraction = GetValueAsFractionOfRange(); + MOZ_ASSERT(fraction >= 0.0 && fraction <= 1.0); + + if (IsHorizontal()) { + if (thumbSize.width < rangeContentBoxSize.width) { + nscoord traversableDistance = rangeContentBoxSize.width - thumbSize.width; + if (IsRightToLeft()) { + newPosition.x += NSToCoordRound((1.0 - fraction) * traversableDistance); + } else { + newPosition.x += NSToCoordRound(fraction * traversableDistance); + } + newPosition.y += (rangeContentBoxSize.height - thumbSize.height) / 2; + } + } else { + if (thumbSize.height < rangeContentBoxSize.height) { + nscoord traversableDistance = + rangeContentBoxSize.height - thumbSize.height; + newPosition.x += (rangeContentBoxSize.width - thumbSize.width) / 2; + if (IsUpwards()) { + newPosition.y += NSToCoordRound((1.0 - fraction) * traversableDistance); + } else { + newPosition.y += NSToCoordRound(fraction * traversableDistance); + } + } + } + aThumbFrame->SetPosition(newPosition); +} + +void nsRangeFrame::DoUpdateRangeProgressFrame(nsIFrame* aRangeProgressFrame, + const nsSize& aRangeSize) { + MOZ_ASSERT(aRangeProgressFrame); + + // The idea here is that we want to position the ::-moz-range-progress + // pseudo-element so that the center line running along its length is on the + // corresponding center line of the nsRangeFrame's content box. In the other + // dimension, we align the "start" edge of the ::-moz-range-progress + // pseudo-element's border-box with the corresponding edge of the + // nsRangeFrame's content box, and we size the progress element's border-box + // to have a length of GetValueAsFractionOfRange() times the nsRangeFrame's + // content-box size. + + nsMargin borderAndPadding = GetUsedBorderAndPadding(); + nsSize progSize = aRangeProgressFrame->GetSize(); + nsRect progRect(borderAndPadding.left, borderAndPadding.top, progSize.width, + progSize.height); + + nsSize rangeContentBoxSize(aRangeSize); + rangeContentBoxSize.width -= borderAndPadding.LeftRight(); + rangeContentBoxSize.height -= borderAndPadding.TopBottom(); + + double fraction = GetValueAsFractionOfRange(); + MOZ_ASSERT(fraction >= 0.0 && fraction <= 1.0); + + if (IsHorizontal()) { + nscoord progLength = NSToCoordRound(fraction * rangeContentBoxSize.width); + if (IsRightToLeft()) { + progRect.x += rangeContentBoxSize.width - progLength; + } + progRect.y += (rangeContentBoxSize.height - progSize.height) / 2; + progRect.width = progLength; + } else { + nscoord progLength = NSToCoordRound(fraction * rangeContentBoxSize.height); + progRect.x += (rangeContentBoxSize.width - progSize.width) / 2; + if (IsUpwards()) { + progRect.y += rangeContentBoxSize.height - progLength; + } + progRect.height = progLength; + } + aRangeProgressFrame->SetRect(progRect); +} + +nsresult nsRangeFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType) { + NS_ASSERTION(mTrackDiv, "The track div must exist!"); + NS_ASSERTION(mThumbDiv, "The thumb div must exist!"); + + if (aNameSpaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::min || + aAttribute == nsGkAtoms::max || aAttribute == nsGkAtoms::step) { + // We want to update the position of the thumb, except in one special + // case: If the value attribute is being set, it is possible that we are + // in the middle of a type change away from type=range, under the + // SetAttr(..., nsGkAtoms::value, ...) call in HTMLInputElement:: + // HandleTypeChange. In that case the HTMLInputElement's type will + // already have changed, and if we call UpdateForValueChange() + // we'll fail the asserts under that call that check the type of our + // HTMLInputElement. Given that we're changing away from being a range + // and this frame will shortly be destroyed, there's no point in calling + // UpdateForValueChange() anyway. + MOZ_ASSERT(mContent->IsHTMLElement(nsGkAtoms::input), "bad cast"); + bool typeIsRange = + static_cast(GetContent())->ControlType() == + FormControlType::InputRange; + // If script changed the 's type before setting these attributes + // then we don't need to do anything since we are going to be reframed. + if (typeIsRange) { + UpdateForValueChange(); + } + } else if (aAttribute == nsGkAtoms::orient) { + PresShell()->FrameNeedsReflow(this, IntrinsicDirty::None, + NS_FRAME_IS_DIRTY); + } else if (aAttribute == nsGkAtoms::list_) { + const bool isRemoval = aModType == MutationEvent_Binding::REMOVAL; + if (mListMutationObserver) { + mListMutationObserver->Detach(); + if (isRemoval) { + mListMutationObserver = nullptr; + } else { + mListMutationObserver->Attach(); + } + } else if (!isRemoval) { + mListMutationObserver = new ListMutationObserver(*this, true); + } + } + } + + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +nscoord nsRangeFrame::AutoCrossSize(Length aEm) { + nscoord minCrossSize(0); + if (IsThemed()) { + nsPresContext* pc = PresContext(); + LayoutDeviceIntSize size = pc->Theme()->GetMinimumWidgetSize( + pc, this, StyleAppearance::RangeThumb); + minCrossSize = + pc->DevPixelsToAppUnits(IsHorizontal() ? size.height : size.width); + } + return std::max(minCrossSize, aEm.ScaledBy(CROSS_AXIS_EM_SIZE).ToAppUnits()); +} + +static mozilla::Length OneEm(nsRangeFrame* aFrame) { + return aFrame->StyleFont()->mFont.size.ScaledBy( + nsLayoutUtils::FontSizeInflationFor(aFrame)); +} + +LogicalSize nsRangeFrame::ComputeAutoSize( + gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize, + nscoord aAvailableISize, const LogicalSize& aMargin, + const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides, + ComputeSizeFlags aFlags) { + bool isInlineOriented = IsInlineOriented(); + auto em = OneEm(this); + + const WritingMode wm = GetWritingMode(); + LogicalSize autoSize(wm); + if (isInlineOriented) { + autoSize.ISize(wm) = em.ScaledBy(MAIN_AXIS_EM_SIZE).ToAppUnits(); + autoSize.BSize(wm) = AutoCrossSize(em); + } else { + autoSize.ISize(wm) = AutoCrossSize(em); + autoSize.BSize(wm) = em.ScaledBy(MAIN_AXIS_EM_SIZE).ToAppUnits(); + } + + return autoSize.ConvertTo(aWM, wm); +} + +nscoord nsRangeFrame::GetMinISize(gfxContext* aRenderingContext) { + const auto* pos = StylePosition(); + auto wm = GetWritingMode(); + if (pos->ISize(wm).HasPercent()) { + // https://drafts.csswg.org/css-sizing-3/#percentage-sizing + // https://drafts.csswg.org/css-sizing-3/#min-content-zero + return nsLayoutUtils::ResolveToLength( + pos->ISize(wm).AsLengthPercentage(), nscoord(0)); + } + return GetPrefISize(aRenderingContext); +} + +nscoord nsRangeFrame::GetPrefISize(gfxContext* aRenderingContext) { + auto em = OneEm(this); + if (IsInlineOriented()) { + return em.ScaledBy(MAIN_AXIS_EM_SIZE).ToAppUnits(); + } + return AutoCrossSize(em); +} + +bool nsRangeFrame::IsHorizontal() const { + dom::HTMLInputElement* element = + static_cast(GetContent()); + return element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient, + nsGkAtoms::horizontal, eCaseMatters) || + (!element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient, + nsGkAtoms::vertical, eCaseMatters) && + GetWritingMode().IsVertical() == + element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::orient, + nsGkAtoms::block, eCaseMatters)); +} + +double nsRangeFrame::GetMin() const { + return static_cast(GetContent()) + ->GetMinimum() + .toDouble(); +} + +double nsRangeFrame::GetMax() const { + return static_cast(GetContent()) + ->GetMaximum() + .toDouble(); +} + +double nsRangeFrame::GetValue() const { + return static_cast(GetContent()) + ->GetValueAsDecimal() + .toDouble(); +} + +bool nsRangeFrame::ShouldUseNativeStyle() const { + nsIFrame* trackFrame = mTrackDiv->GetPrimaryFrame(); + nsIFrame* progressFrame = mProgressDiv->GetPrimaryFrame(); + nsIFrame* thumbFrame = mThumbDiv->GetPrimaryFrame(); + + return StyleDisplay()->EffectiveAppearance() == StyleAppearance::Range && + trackFrame && + !trackFrame->Style()->HasAuthorSpecifiedBorderOrBackground() && + progressFrame && + !progressFrame->Style()->HasAuthorSpecifiedBorderOrBackground() && + thumbFrame && + !thumbFrame->Style()->HasAuthorSpecifiedBorderOrBackground(); +} diff --git a/layout/forms/nsRangeFrame.h b/layout/forms/nsRangeFrame.h new file mode 100644 index 0000000000..4d2073aa50 --- /dev/null +++ b/layout/forms/nsRangeFrame.h @@ -0,0 +1,213 @@ +/* -*- 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 nsRangeFrame_h___ +#define nsRangeFrame_h___ + +#include "mozilla/Attributes.h" +#include "mozilla/Decimal.h" +#include "mozilla/EventForwards.h" +#include "nsContainerFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "nsIDOMEventListener.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" + +class nsDisplayRangeFocusRing; + +namespace mozilla { +class ListMutationObserver; +class PresShell; +namespace dom { +class Event; +class HTMLInputElement; +} // namespace dom +} // namespace mozilla + +class nsRangeFrame final : public nsContainerFrame, + public nsIAnonymousContentCreator { + friend nsIFrame* NS_NewRangeFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + friend class nsDisplayRangeFocusRing; + + explicit nsRangeFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + virtual ~nsRangeFrame(); + + typedef mozilla::PseudoStyleType PseudoStyleType; + typedef mozilla::dom::Element Element; + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsRangeFrame) + + // nsIFrame overrides + void Destroy(DestroyContext&) override; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + virtual void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + +#ifdef DEBUG_FRAME_DUMP + virtual nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"Range"_ns, aResult); + } +#endif + +#ifdef ACCESSIBILITY + virtual mozilla::a11y::AccType AccessibleType() override; +#endif + + // nsIAnonymousContentCreator + virtual nsresult CreateAnonymousContent( + nsTArray& aElements) override; + virtual void AppendAnonymousContentTo(nsTArray& aElements, + uint32_t aFilter) override; + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + mozilla::LogicalSize ComputeAutoSize( + gfxContext* aRenderingContext, mozilla::WritingMode aWM, + const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize, + const mozilla::LogicalSize& aMargin, + const mozilla::LogicalSize& aBorderPadding, + const mozilla::StyleSizeOverrides& aSizeOverrides, + mozilla::ComputeSizeFlags aFlags) override; + + virtual nscoord GetMinISize(gfxContext* aRenderingContext) override; + virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + /** + * Returns true if the slider's thumb moves horizontally, or else false if it + * moves vertically. + */ + bool IsHorizontal() const; + + /** + * Returns true if the slider is oriented along the inline axis. + */ + bool IsInlineOriented() const { + return IsHorizontal() != GetWritingMode().IsVertical(); + } + + /** + * Returns true if the slider's thumb moves right-to-left for increasing + * values; only relevant when IsHorizontal() is true. + */ + bool IsRightToLeft() const { + MOZ_ASSERT(IsHorizontal()); + return GetWritingMode().IsPhysicalRTL(); + } + + /** + * Returns true if the range progresses upwards (for vertical ranges in + * horizontal writing mode, or for bidi-RTL in vertical mode). Only + * relevant when IsHorizontal() is false. + */ + bool IsUpwards() const { + MOZ_ASSERT(!IsHorizontal()); + mozilla::WritingMode wm = GetWritingMode(); + return wm.GetBlockDir() == mozilla::WritingMode::eBlockTB || + wm.GetInlineDir() == mozilla::WritingMode::eInlineBTT; + } + + double GetMin() const; + double GetMax() const; + double GetValue() const; + + /** + * Returns the input element's value as a fraction of the difference between + * the input's minimum and its maximum (i.e. returns 0.0 when the value is + * the same as the minimum, and returns 1.0 when the value is the same as the + * maximum). + */ + double GetValueAsFractionOfRange(); + + /** + * Returns the given value as a fraction of the difference between the input's + * minimum and its maximum (i.e. returns 0.0 when the value is the same as the + * input's minimum, and returns 1.0 when the value is the same as the input's + * maximum). + */ + double GetDoubleAsFractionOfRange(const mozilla::Decimal& value); + + /** + * Returns whether the frame and its child should use the native style. + */ + bool ShouldUseNativeStyle() const; + + mozilla::Decimal GetValueAtEventPoint(mozilla::WidgetGUIEvent* aEvent); + + /** + * Helper that's used when the value of the range changes to reposition the + * thumb, resize the range-progress element, and schedule a repaint. (This + * does not reflow, since the position and size of the thumb and + * range-progress element do not affect the position or size of any other + * frames.) + */ + void UpdateForValueChange(); + + nsTArray TickMarks(); + + /** + * Returns the given value's offset from the range's nearest list tick mark + * or NaN if there are no tick marks. + */ + mozilla::Decimal NearestTickMark(const mozilla::Decimal& aValue); + + protected: + mozilla::dom::HTMLInputElement& InputElement() const; + + private: + // Return our preferred size in the cross-axis (the axis perpendicular + // to the direction of movement of the thumb). + nscoord AutoCrossSize(mozilla::Length aEm); + + // Helper function which reflows the anonymous div frames. + void ReflowAnonymousContent(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput); + + void DoUpdateThumbPosition(nsIFrame* aThumbFrame, const nsSize& aRangeSize); + + void DoUpdateRangeProgressFrame(nsIFrame* aProgressFrame, + const nsSize& aRangeSize); + + /** + * The div used to show the ::-moz-range-track pseudo-element. + * @see nsRangeFrame::CreateAnonymousContent + */ + nsCOMPtr mTrackDiv; + + /** + * The div used to show the ::-moz-range-progress pseudo-element, which is + * used to (optionally) style the specific chunk of track leading up to the + * thumb's current position. + * @see nsRangeFrame::CreateAnonymousContent + */ + nsCOMPtr mProgressDiv; + + /** + * The div used to show the ::-moz-range-thumb pseudo-element. + * @see nsRangeFrame::CreateAnonymousContent + */ + nsCOMPtr mThumbDiv; + + /** + * This mutation observer is used to invalidate paint when the @list changes, + * when a @list exists. + */ + RefPtr mListMutationObserver; +}; + +#endif diff --git a/layout/forms/nsSearchControlFrame.cpp b/layout/forms/nsSearchControlFrame.cpp new file mode 100644 index 0000000000..e9249cca77 --- /dev/null +++ b/layout/forms/nsSearchControlFrame.cpp @@ -0,0 +1,82 @@ +/* -*- 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 "nsSearchControlFrame.h" + +#include "HTMLInputElement.h" +#include "mozilla/PresShell.h" +#include "nsGkAtoms.h" +#include "nsNameSpaceManager.h" +#include "nsStyleConsts.h" +#include "nsContentUtils.h" +#include "nsContentCreatorFunctions.h" +#include "nsCSSPseudoElements.h" +#include "nsICSSDeclaration.h" + +#ifdef ACCESSIBILITY +# include "mozilla/a11y/AccTypes.h" +#endif + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewSearchControlFrame(PresShell* aPresShell, + ComputedStyle* aStyle) { + return new (aPresShell) + nsSearchControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsSearchControlFrame) + +NS_QUERYFRAME_HEAD(nsSearchControlFrame) + NS_QUERYFRAME_ENTRY(nsSearchControlFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsTextControlFrame) + +nsSearchControlFrame::nsSearchControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsTextControlFrame(aStyle, aPresContext, kClassID) {} + +void nsSearchControlFrame::Destroy(DestroyContext& aContext) { + aContext.AddAnonymousContent(mClearButton.forget()); + nsTextControlFrame::Destroy(aContext); +} + +nsresult nsSearchControlFrame::CreateAnonymousContent( + nsTArray& aElements) { + // We create an anonymous tree for our input element that is structured as + // follows: + // + // input + // div - placeholder + // div - preview div + // div - editor root + // button - clear button + // + // If you change this, be careful to change the order of stuff in + // AppendAnonymousContentTo. + + nsTextControlFrame::CreateAnonymousContent(aElements); + + // FIXME: We could use nsTextControlFrame making the show password buttton + // code a bit more generic, or rename this frame and use it for password + // inputs. + // + // Create the ::-moz-search-clear-button pseudo-element: + mClearButton = MakeAnonElement(PseudoStyleType::mozSearchClearButton, nullptr, + nsGkAtoms::button); + + aElements.AppendElement(mClearButton); + + return NS_OK; +} + +void nsSearchControlFrame::AppendAnonymousContentTo( + nsTArray& aElements, uint32_t aFilter) { + nsTextControlFrame::AppendAnonymousContentTo(aElements, aFilter); + if (mClearButton) { + aElements.AppendElement(mClearButton); + } +} diff --git a/layout/forms/nsSearchControlFrame.h b/layout/forms/nsSearchControlFrame.h new file mode 100644 index 0000000000..0499fa84ea --- /dev/null +++ b/layout/forms/nsSearchControlFrame.h @@ -0,0 +1,68 @@ +/* -*- 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 nsSearchControlFrame_h__ +#define nsSearchControlFrame_h__ + +#include "mozilla/Attributes.h" +#include "nsTextControlFrame.h" +#include "nsIAnonymousContentCreator.h" +#include "mozilla/RefPtr.h" + +class nsITextControlFrame; +class nsPresContext; + +namespace mozilla { +enum class PseudoStyleType : uint8_t; +class PresShell; +namespace dom { +class Element; +} // namespace dom +} // namespace mozilla + +/** + * This frame type is used for . + */ +class nsSearchControlFrame final : public nsTextControlFrame { + friend nsIFrame* NS_NewSearchControlFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + using PseudoStyleType = mozilla::PseudoStyleType; + using Element = mozilla::dom::Element; + + explicit nsSearchControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsSearchControlFrame) + + void Destroy(DestroyContext&) override; + + // nsIAnonymousContentCreator + nsresult CreateAnonymousContent(nsTArray& aElements) override; + void AppendAnonymousContentTo(nsTArray& aElements, + uint32_t aFilter) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SearchControl"_ns, aResult); + } +#endif + + Element* GetAnonClearButton() const { return mClearButton; } + + /** + * Update visbility of the clear button depending on the value + */ + void UpdateClearButtonState(); + + private: + // See nsSearchControlFrame::CreateAnonymousContent of a description of these + RefPtr mClearButton; +}; + +#endif // nsSearchControlFrame_h__ diff --git a/layout/forms/nsSelectsAreaFrame.cpp b/layout/forms/nsSelectsAreaFrame.cpp new file mode 100644 index 0000000000..1218136023 --- /dev/null +++ b/layout/forms/nsSelectsAreaFrame.cpp @@ -0,0 +1,189 @@ +/* -*- 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 "nsSelectsAreaFrame.h" + +#include "mozilla/PresShell.h" +#include "nsIContent.h" +#include "nsListControlFrame.h" +#include "nsDisplayList.h" +#include "WritingModes.h" + +using namespace mozilla; + +nsContainerFrame* NS_NewSelectsAreaFrame(PresShell* aShell, + ComputedStyle* aStyle) { + nsSelectsAreaFrame* it = + new (aShell) nsSelectsAreaFrame(aStyle, aShell->GetPresContext()); + return it; +} + +NS_IMPL_FRAMEARENA_HELPERS(nsSelectsAreaFrame) + +NS_QUERYFRAME_HEAD(nsSelectsAreaFrame) + NS_QUERYFRAME_ENTRY(nsSelectsAreaFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) + +static nsListControlFrame* GetEnclosingListFrame(nsIFrame* aSelectsAreaFrame) { + nsIFrame* frame = aSelectsAreaFrame->GetParent(); + while (frame) { + if (frame->IsListControlFrame()) + return static_cast(frame); + frame = frame->GetParent(); + } + return nullptr; +} + +void nsSelectsAreaFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + nsListControlFrame* list = GetEnclosingListFrame(this); + NS_ASSERTION(list, + "Must have an nsListControlFrame! Frame constructor is " + "broken"); + + nsBlockFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus); + + // Check whether we need to suppress scrollbar updates. We want to do + // that if we're in a possible first pass and our block size of a row + // has changed. + if (list->MightNeedSecondPass()) { + nscoord newBSizeOfARow = list->CalcBSizeOfARow(); + // We'll need a second pass if our block size of a row changed. For + // comboboxes, we'll also need it if our block size changed. If + // we're going to do a second pass, suppress scrollbar updates for + // this pass. + if (newBSizeOfARow != mBSizeOfARow) { + mBSizeOfARow = newBSizeOfARow; + list->SetSuppressScrollbarUpdate(true); + } + } +} + +namespace mozilla { +/** + * This wrapper class lets us redirect mouse hits from the child frame of + * an option element to the element's own frame. + * REVIEW: This is what nsSelectsAreaFrame::GetFrameForPoint used to do + */ +class nsDisplayOptionEventGrabber : public nsDisplayWrapList { + public: + nsDisplayOptionEventGrabber(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, + nsDisplayItem* aItem) + : nsDisplayWrapList(aBuilder, aFrame, aItem) {} + nsDisplayOptionEventGrabber(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame, + nsDisplayList* aList) + : nsDisplayWrapList(aBuilder, aFrame, aList) {} + virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect, + HitTestState* aState, + nsTArray* aOutFrames) override; + virtual bool ShouldFlattenAway(nsDisplayListBuilder* aBuilder) override { + return false; + } + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override { + GetChildren()->Paint(aBuilder, aCtx, + mFrame->PresContext()->AppUnitsPerDevPixel()); + } + NS_DISPLAY_DECL_NAME("OptionEventGrabber", TYPE_OPTION_EVENT_GRABBER) +}; + +void nsDisplayOptionEventGrabber::HitTest(nsDisplayListBuilder* aBuilder, + const nsRect& aRect, + HitTestState* aState, + nsTArray* aOutFrames) { + nsTArray outFrames; + mList.HitTest(aBuilder, aRect, aState, &outFrames); + + for (uint32_t i = 0; i < outFrames.Length(); i++) { + nsIFrame* selectedFrame = outFrames.ElementAt(i); + while (selectedFrame && + !(selectedFrame->GetContent() && + selectedFrame->GetContent()->IsHTMLElement(nsGkAtoms::option))) { + selectedFrame = selectedFrame->GetParent(); + } + if (selectedFrame) { + aOutFrames->AppendElement(selectedFrame); + } else { + // keep the original result, which could be this frame + aOutFrames->AppendElement(outFrames.ElementAt(i)); + } + } +} + +class nsOptionEventGrabberWrapper : public nsDisplayItemWrapper { + public: + nsOptionEventGrabberWrapper() = default; + virtual nsDisplayItem* WrapList(nsDisplayListBuilder* aBuilder, + nsIFrame* aFrame, + nsDisplayList* aList) override { + return MakeDisplayItem(aBuilder, aFrame, + aList); + } + virtual nsDisplayItem* WrapItem(nsDisplayListBuilder* aBuilder, + nsDisplayItem* aItem) override { + return MakeDisplayItem(aBuilder, + aItem->Frame(), aItem); + } +}; + +class nsDisplayListFocus : public nsPaintedDisplayItem { + public: + nsDisplayListFocus(nsDisplayListBuilder* aBuilder, nsSelectsAreaFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayListFocus); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayListFocus) + + virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder, + bool* aSnap) const override { + *aSnap = false; + // override bounds because the list item focus ring may extend outside + // the nsSelectsAreaFrame + nsListControlFrame* listFrame = GetEnclosingListFrame(Frame()); + return listFrame->InkOverflowRectRelativeToSelf() + + listFrame->GetOffsetToCrossDoc(Frame()) + ToReferenceFrame(); + } + virtual void Paint(nsDisplayListBuilder* aBuilder, + gfxContext* aCtx) override { + nsListControlFrame* listFrame = GetEnclosingListFrame(Frame()); + // listFrame must be non-null or we wouldn't get called. + listFrame->PaintFocus( + aCtx->GetDrawTarget(), + listFrame->GetOffsetToCrossDoc(Frame()) + ToReferenceFrame()); + } + NS_DISPLAY_DECL_NAME("ListFocus", TYPE_LIST_FOCUS) +}; + +} // namespace mozilla + +void nsSelectsAreaFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + if (!aBuilder->IsForEventDelivery()) { + BuildDisplayListInternal(aBuilder, aLists); + return; + } + + nsDisplayListCollection set(aBuilder); + BuildDisplayListInternal(aBuilder, set); + + nsOptionEventGrabberWrapper wrapper; + wrapper.WrapLists(aBuilder, this, set, aLists); +} + +void nsSelectsAreaFrame::BuildDisplayListInternal( + nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { + nsBlockFrame::BuildDisplayList(aBuilder, aLists); + + nsListControlFrame* listFrame = GetEnclosingListFrame(this); + if (listFrame && listFrame->IsFocused()) { + // we can't just associate the display item with the list frame, + // because then the list's scrollframe won't clip it (the scrollframe + // only clips contained descendants). + aLists.Outlines()->AppendNewToTop(aBuilder, this); + } +} diff --git a/layout/forms/nsSelectsAreaFrame.h b/layout/forms/nsSelectsAreaFrame.h new file mode 100644 index 0000000000..d0cd5a3ba3 --- /dev/null +++ b/layout/forms/nsSelectsAreaFrame.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 nsSelectsAreaFrame_h___ +#define nsSelectsAreaFrame_h___ + +#include "mozilla/Attributes.h" +#include "nsBlockFrame.h" + +namespace mozilla { +class PresShell; +} // namespace mozilla + +class nsSelectsAreaFrame final : public nsBlockFrame { + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsSelectsAreaFrame) + + friend nsContainerFrame* NS_NewSelectsAreaFrame(mozilla::PresShell* aShell, + ComputedStyle* aStyle); + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + void BuildDisplayListInternal(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists); + + void Reflow(nsPresContext* aCX, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + nscoord BSizeOfARow() const { return mBSizeOfARow; } + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SelectsArea"_ns, aResult); + } +#endif + + protected: + explicit nsSelectsAreaFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsBlockFrame(aStyle, aPresContext, kClassID), + // initialize to wacky value so first call of + // nsSelectsAreaFrame::Reflow will always invalidate + mBSizeOfARow(nscoord_MIN) {} + + // We cache the block size of a single row so that changes to the + // "size" attribute, padding, etc. can all be handled with only one + // reflow. We'll have to reflow twice if someone changes our font + // size or something like that, so that the block size of our options + // will change. + nscoord mBSizeOfARow; +}; + +#endif /* nsSelectsAreaFrame_h___ */ diff --git a/layout/forms/nsTextControlFrame.cpp b/layout/forms/nsTextControlFrame.cpp new file mode 100644 index 0000000000..c51b94c56a --- /dev/null +++ b/layout/forms/nsTextControlFrame.cpp @@ -0,0 +1,1325 @@ +/* -*- 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 "mozilla/DebugOnly.h" + +#include "gfxContext.h" +#include "nsCOMPtr.h" +#include "nsFontMetrics.h" +#include "nsTextControlFrame.h" +#include "nsIEditor.h" +#include "nsCaret.h" +#include "nsCSSPseudoElements.h" +#include "nsDisplayList.h" +#include "nsGenericHTMLElement.h" +#include "nsTextFragment.h" +#include "nsNameSpaceManager.h" + +#include "nsIContent.h" +#include "nsIScrollableFrame.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsLayoutUtils.h" + +#include +#include "nsRange.h" //for selection setting helper func +#include "nsINode.h" +#include "nsPIDOMWindow.h" //needed for notify selection changed to update the menus ect. +#include "nsQueryObject.h" +#include "nsILayoutHistoryState.h" + +#include "nsFocusManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/PresShell.h" +#include "mozilla/PresState.h" +#include "mozilla/TextEditor.h" +#include "nsAttrValueInlines.h" +#include "mozilla/dom/Selection.h" +#include "nsContentUtils.h" +#include "nsTextNode.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLTextAreaElement.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/Text.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/Try.h" +#include "nsFrameSelection.h" + +#define DEFAULT_COLUMN_WIDTH 20 + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewTextControlFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) + nsTextControlFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsTextControlFrame) + +NS_QUERYFRAME_HEAD(nsTextControlFrame) + NS_QUERYFRAME_ENTRY(nsTextControlFrame) + NS_QUERYFRAME_ENTRY(nsIFormControlFrame) + NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator) + NS_QUERYFRAME_ENTRY(nsITextControlFrame) + NS_QUERYFRAME_ENTRY(nsIStatefulFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +#ifdef ACCESSIBILITY +a11y::AccType nsTextControlFrame::AccessibleType() { + return a11y::eHTMLTextFieldType; +} +#endif + +#ifdef DEBUG +class EditorInitializerEntryTracker { + public: + explicit EditorInitializerEntryTracker(nsTextControlFrame& frame) + : mFrame(frame), mFirstEntry(false) { + if (!mFrame.mInEditorInitialization) { + mFrame.mInEditorInitialization = true; + mFirstEntry = true; + } + } + ~EditorInitializerEntryTracker() { + if (mFirstEntry) { + mFrame.mInEditorInitialization = false; + } + } + bool EnteredMoreThanOnce() const { return !mFirstEntry; } + + private: + nsTextControlFrame& mFrame; + bool mFirstEntry; +}; +#endif + +class nsTextControlFrame::nsAnonDivObserver final + : public nsStubMutationObserver { + public: + explicit nsAnonDivObserver(nsTextControlFrame& aFrame) : mFrame(aFrame) {} + NS_DECL_ISUPPORTS + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + private: + ~nsAnonDivObserver() = default; + nsTextControlFrame& mFrame; +}; + +nsTextControlFrame::nsTextControlFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext, + nsIFrame::ClassID aClassID) + : nsContainerFrame(aStyle, aPresContext, aClassID) {} + +nsTextControlFrame::~nsTextControlFrame() = default; + +nsIScrollableFrame* nsTextControlFrame::GetScrollTargetFrame() const { + if (!mRootNode) { + return nullptr; + } + return do_QueryFrame(mRootNode->GetPrimaryFrame()); +} + +void nsTextControlFrame::Destroy(DestroyContext& aContext) { + RemoveProperty(TextControlInitializer()); + + // Unbind the text editor state object from the frame. The editor will live + // on, but things like controllers will be released. + RefPtr textControlElement = ControlElement(); + if (mMutationObserver) { + textControlElement->UnbindFromFrame(this); + mRootNode->RemoveMutationObserver(mMutationObserver); + mMutationObserver = nullptr; + } + + // If there is a drag session, user may be dragging selection in removing + // text node in the text control. If so, we should set source node to the + // text control because another text node may be recreated soon if the text + // control is just reframed. + if (nsCOMPtr dragSession = nsContentUtils::GetDragSession()) { + if (dragSession->IsDraggingTextInTextControl() && mRootNode && + mRootNode->GetFirstChild()) { + nsCOMPtr sourceNode; + if (NS_SUCCEEDED( + dragSession->GetSourceNode(getter_AddRefs(sourceNode))) && + mRootNode->Contains(sourceNode)) { + MOZ_ASSERT(sourceNode->IsText()); + dragSession->UpdateSource(textControlElement, nullptr); + } + } + } + // Otherwise, EventStateManager may track gesture to start drag with native + // anonymous nodes in the text control element. + else if (textControlElement->GetPresContext(Element::eForComposedDoc)) { + textControlElement->GetPresContext(Element::eForComposedDoc) + ->EventStateManager() + ->TextControlRootWillBeRemoved(*textControlElement); + } + + // If we're a subclass like nsNumberControlFrame, then it owns the root of the + // anonymous subtree where mRootNode is. + aContext.AddAnonymousContent(mRootNode.forget()); + aContext.AddAnonymousContent(mPlaceholderDiv.forget()); + aContext.AddAnonymousContent(mPreviewDiv.forget()); + aContext.AddAnonymousContent(mRevealButton.forget()); + + nsContainerFrame::Destroy(aContext); +} + +LogicalSize nsTextControlFrame::CalcIntrinsicSize( + gfxContext* aRenderingContext, WritingMode aWM, + float aFontSizeInflation) const { + LogicalSize intrinsicSize(aWM); + RefPtr fontMet = + nsLayoutUtils::GetFontMetricsForFrame(this, aFontSizeInflation); + const nscoord lineHeight = + ReflowInput::CalcLineHeight(*Style(), PresContext(), GetContent(), + NS_UNCONSTRAINEDSIZE, aFontSizeInflation); + // Use the larger of the font's "average" char width or the width of the + // zero glyph (if present) as the basis for resolving the size attribute. + const nscoord charWidth = + std::max(fontMet->ZeroOrAveCharWidth(), fontMet->AveCharWidth()); + const nscoord charMaxAdvance = fontMet->MaxAdvance(); + + // Initialize based on the width in characters. + const int32_t cols = GetCols(); + intrinsicSize.ISize(aWM) = cols * charWidth; + + // If we do not have what appears to be a fixed-width font, add a "slop" + // amount based on the max advance of the font (clamped to twice charWidth, + // because some fonts have a few extremely-wide outliers that would result + // in excessive width here; e.g. the triple-emdash ligature in SFNS Text), + // minus 4px. This helps avoid input fields becoming unusably narrow with + // small size values. + if (charMaxAdvance - charWidth > AppUnitsPerCSSPixel()) { + nscoord internalPadding = + std::max(0, std::min(charMaxAdvance, charWidth * 2) - + nsPresContext::CSSPixelsToAppUnits(4)); + internalPadding = RoundToMultiple(internalPadding, AppUnitsPerCSSPixel()); + intrinsicSize.ISize(aWM) += internalPadding; + } else { + // This is to account for the anonymous
having a 1 twip width + // in Full Standards mode, see BRFrame::Reflow and bug 228752. + if (PresContext()->CompatibilityMode() == eCompatibility_FullStandards) { + intrinsicSize.ISize(aWM) += 1; + } + } + + // Increment width with cols * letter-spacing. + { + const StyleLength& letterSpacing = StyleText()->mLetterSpacing; + if (!letterSpacing.IsZero()) { + intrinsicSize.ISize(aWM) += cols * letterSpacing.ToAppUnits(); + } + } + + // Set the height equal to total number of rows (times the height of each + // line, of course) + intrinsicSize.BSize(aWM) = lineHeight * GetRows(); + + // Add in the size of the scrollbars for textarea + if (IsTextArea()) { + nsIScrollableFrame* scrollableFrame = GetScrollTargetFrame(); + NS_ASSERTION(scrollableFrame, "Child must be scrollable"); + if (scrollableFrame) { + LogicalMargin scrollbarSizes(aWM, + scrollableFrame->GetDesiredScrollbarSizes()); + intrinsicSize.ISize(aWM) += scrollbarSizes.IStartEnd(aWM); + intrinsicSize.BSize(aWM) += scrollbarSizes.BStartEnd(aWM); + } + } + return intrinsicSize; +} + +nsresult nsTextControlFrame::EnsureEditorInitialized() { + // This method initializes our editor, if needed. + + // This code used to be called from CreateAnonymousContent(), but + // when the editor set the initial string, it would trigger a + // PresShell listener which called FlushPendingNotifications() + // during frame construction. This was causing other form controls + // to display wrong values. Additionally, calling this every time + // a text frame control is instantiated means that we're effectively + // instantiating the editor for all text fields, even if they + // never get used. So, now this method is being called lazily only + // when we actually need an editor. + + if (mEditorHasBeenInitialized) return NS_OK; + + Document* doc = mContent->GetComposedDoc(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + AutoWeakFrame weakFrame(this); + + // Flush out content on our document. Have to do this, because script + // blockers don't prevent the sink flushing out content and notifying in the + // process, which can destroy frames. + doc->FlushPendingNotifications(FlushType::ContentAndNotify); + NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_ERROR_FAILURE); + + // Make sure that editor init doesn't do things that would kill us off + // (especially off the script blockers it'll create for its DOM mutations). + { + RefPtr textControlElement = ControlElement(); + + // Hide selection changes during the initialization, as webpages should not + // be aware of these initializations + AutoHideSelectionChanges hideSelectionChanges( + textControlElement->GetConstFrameSelection()); + + nsAutoScriptBlocker scriptBlocker; + + // Time to mess with our security context... See comments in GetValue() + // for why this is needed. + mozilla::dom::AutoNoJSAPI nojsapi; + + // Make sure that we try to focus the content even if the method fails + class EnsureSetFocus { + public: + explicit EnsureSetFocus(nsTextControlFrame* aFrame) : mFrame(aFrame) {} + ~EnsureSetFocus() { + if (nsContentUtils::IsFocusedContent(mFrame->GetContent())) + mFrame->SetFocus(true, false); + } + + private: + nsTextControlFrame* mFrame; + }; + EnsureSetFocus makeSureSetFocusHappens(this); + +#ifdef DEBUG + // Make sure we are not being called again until we're finished. + // If reentrancy happens, just pretend that we don't have an editor. + const EditorInitializerEntryTracker tracker(*this); + NS_ASSERTION(!tracker.EnteredMoreThanOnce(), + "EnsureEditorInitialized has been called while a previous " + "call was in progress"); +#endif + + // Create an editor for the frame, if one doesn't already exist + nsresult rv = textControlElement->CreateEditor(); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_STATE(weakFrame.IsAlive()); + + // Set mEditorHasBeenInitialized so that subsequent calls will use the + // editor. + mEditorHasBeenInitialized = true; + + if (weakFrame.IsAlive()) { + uint32_t position = 0; + + // Set the selection to the end of the text field (bug 1287655), + // but only if the contents has changed (bug 1337392). + if (textControlElement->ValueChanged()) { + nsAutoString val; + textControlElement->GetTextEditorValue(val); + position = val.Length(); + } + + SetSelectionEndPoints(position, position); + } + } + NS_ENSURE_STATE(weakFrame.IsAlive()); + return NS_OK; +} + +already_AddRefed nsTextControlFrame::MakeAnonElement( + PseudoStyleType aPseudoType, Element* aParent, nsAtom* aTag) const { + MOZ_ASSERT(aPseudoType != PseudoStyleType::NotPseudo); + Document* doc = PresContext()->Document(); + RefPtr element = doc->CreateHTMLElement(aTag); + element->SetPseudoElementType(aPseudoType); + if (aPseudoType == PseudoStyleType::mozTextControlEditingRoot) { + // Make our root node editable + element->SetFlags(NODE_IS_EDITABLE); + } + + if (aPseudoType == PseudoStyleType::mozNumberSpinDown || + aPseudoType == PseudoStyleType::mozNumberSpinUp) { + element->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden, u"true"_ns, + false); + } + + if (aParent) { + aParent->AppendChildTo(element, false, IgnoreErrors()); + } + + return element.forget(); +} + +already_AddRefed nsTextControlFrame::MakeAnonDivWithTextNode( + PseudoStyleType aPseudoType) const { + RefPtr div = MakeAnonElement(aPseudoType); + + // Create the text node for the anonymous
element. + nsNodeInfoManager* nim = div->OwnerDoc()->NodeInfoManager(); + RefPtr textNode = new (nim) nsTextNode(nim); + // If the anonymous div element is not for the placeholder, we should + // mark the text node as "maybe modified frequently" for avoiding ASCII + // range checks at every input. + if (aPseudoType != PseudoStyleType::placeholder) { + textNode->MarkAsMaybeModifiedFrequently(); + // Additionally, this is a password field, the text node needs to be + // marked as "maybe masked" unless it's in placeholder. + if (IsPasswordTextControl()) { + textNode->MarkAsMaybeMasked(); + } + } + div->AppendChildTo(textNode, false, IgnoreErrors()); + return div.forget(); +} + +nsresult nsTextControlFrame::CreateAnonymousContent( + nsTArray& aElements) { + MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript()); + MOZ_ASSERT(mContent, "We should have a content!"); + + AddStateBits(NS_FRAME_INDEPENDENT_SELECTION); + + RefPtr textControlElement = ControlElement(); + mRootNode = MakeAnonElement(PseudoStyleType::mozTextControlEditingRoot); + if (NS_WARN_IF(!mRootNode)) { + return NS_ERROR_FAILURE; + } + + mMutationObserver = new nsAnonDivObserver(*this); + mRootNode->AddMutationObserver(mMutationObserver); + + // Bind the frame to its text control. + // + // This can realistically fail in paginated mode, where we may replicate + // fixed-positioned elements and the replicated frame will not get the chance + // to get an editor. + nsresult rv = textControlElement->BindToFrame(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + mRootNode->RemoveMutationObserver(mMutationObserver); + mMutationObserver = nullptr; + mRootNode = nullptr; + return rv; + } + + CreatePlaceholderIfNeeded(); + if (mPlaceholderDiv) { + aElements.AppendElement(mPlaceholderDiv); + } + CreatePreviewIfNeeded(); + if (mPreviewDiv) { + aElements.AppendElement(mPreviewDiv); + } + + // NOTE(emilio): We want the root node always after the placeholder so that + // background on the placeholder doesn't obscure the caret. + aElements.AppendElement(mRootNode); + + if (StaticPrefs::layout_forms_reveal_password_button_enabled() && + IsPasswordTextControl()) { + mRevealButton = + MakeAnonElement(PseudoStyleType::mozReveal, nullptr, nsGkAtoms::button); + mRevealButton->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden, + u"true"_ns, false); + mRevealButton->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns, + false); + aElements.AppendElement(mRevealButton); + } + + rv = UpdateValueDisplay(false); + NS_ENSURE_SUCCESS(rv, rv); + + InitializeEagerlyIfNeeded(); + return NS_OK; +} + +bool nsTextControlFrame::ShouldInitializeEagerly() const { + // textareas are eagerly initialized. + if (!IsSingleLineTextControl()) { + return true; + } + + // Also, input elements which have a cached selection should get eager + // editor initialization. + TextControlElement* textControlElement = ControlElement(); + if (textControlElement->HasCachedSelection()) { + return true; + } + + // So do input text controls with spellcheck=true + if (auto* htmlElement = nsGenericHTMLElement::FromNode(mContent)) { + if (htmlElement->Spellcheck()) { + return true; + } + } + + // If text in the editor is being dragged, we need the editor to create + // new source node for the drag session (TextEditor creates the text node + // in the anonymous
element. + if (nsCOMPtr dragSession = nsContentUtils::GetDragSession()) { + if (dragSession->IsDraggingTextInTextControl()) { + nsCOMPtr sourceNode; + if (NS_SUCCEEDED( + dragSession->GetSourceNode(getter_AddRefs(sourceNode))) && + sourceNode == textControlElement) { + return true; + } + } + } + + return false; +} + +void nsTextControlFrame::InitializeEagerlyIfNeeded() { + MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript(), + "Someone forgot a script blocker?"); + if (!ShouldInitializeEagerly()) { + return; + } + + EditorInitializer* initializer = new EditorInitializer(this); + SetProperty(TextControlInitializer(), initializer); + nsContentUtils::AddScriptRunner(initializer); +} + +void nsTextControlFrame::CreatePlaceholderIfNeeded() { + MOZ_ASSERT(!mPlaceholderDiv); + + // Do we need a placeholder node? + nsAutoString placeholder; + if (!mContent->AsElement()->GetAttr(nsGkAtoms::placeholder, placeholder)) { + return; + } + + mPlaceholderDiv = MakeAnonDivWithTextNode(PseudoStyleType::placeholder); + UpdatePlaceholderText(placeholder, false); +} + +void nsTextControlFrame::PlaceholderChanged(const nsAttrValue* aOld, + const nsAttrValue* aNew) { + if (!aOld || !aNew) { + return; // This should be handled by GetAttributeChangeHint. + } + + // If we've changed the attribute but we still haven't reframed, there's + // nothing to do either. + if (!mPlaceholderDiv) { + return; + } + + nsAutoString placeholder; + aNew->ToString(placeholder); + UpdatePlaceholderText(placeholder, true); +} + +void nsTextControlFrame::UpdatePlaceholderText(nsString& aPlaceholder, + bool aNotify) { + MOZ_DIAGNOSTIC_ASSERT(mPlaceholderDiv); + MOZ_DIAGNOSTIC_ASSERT(mPlaceholderDiv->GetFirstChild()); + + if (IsTextArea()) { // + + + diff --git a/layout/forms/test/test_bug1305282.html b/layout/forms/test/test_bug1305282.html new file mode 100644 index 0000000000..bd631f3444 --- /dev/null +++ b/layout/forms/test/test_bug1305282.html @@ -0,0 +1,57 @@ + + + + + Test for Bug 1305282 + + + + + +Mozilla Bug 1305282 +

+
+ +
+
+
+
+ + diff --git a/layout/forms/test/test_bug1327129.html b/layout/forms/test/test_bug1327129.html new file mode 100644 index 0000000000..0e52f140b3 --- /dev/null +++ b/layout/forms/test/test_bug1327129.html @@ -0,0 +1,385 @@ + + + + + + Test for Bug 1327129 + + + + + +Mozilla Bug 1327129 +

+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + diff --git a/layout/forms/test/test_bug1529036.html b/layout/forms/test/test_bug1529036.html new file mode 100644 index 0000000000..0d1c4fa207 --- /dev/null +++ b/layout/forms/test/test_bug1529036.html @@ -0,0 +1,73 @@ + + + + + + Test for Bug 1529036 + + + + + + + +Mozilla Bug 1529036 +

+ +
+
+ +
+ + + + + + +
+ + + diff --git a/layout/forms/test/test_bug231389.html b/layout/forms/test/test_bug231389.html new file mode 100644 index 0000000000..9239839247 --- /dev/null +++ b/layout/forms/test/test_bug231389.html @@ -0,0 +1,55 @@ + + + + + Test for Bug 231389 + + + + +Mozilla Bug 231389 +

+ +

+ +
+
+
+ + diff --git a/layout/forms/test/test_bug287446.html b/layout/forms/test/test_bug287446.html new file mode 100644 index 0000000000..bb9e3bec39 --- /dev/null +++ b/layout/forms/test/test_bug287446.html @@ -0,0 +1,74 @@ + + + + + Test for Bug 287446 + + + + +Mozilla Bug 287446 +

+ +

+ +
+
+
+ + diff --git a/layout/forms/test/test_bug345267.html b/layout/forms/test/test_bug345267.html new file mode 100644 index 0000000000..ba9a3bd555 --- /dev/null +++ b/layout/forms/test/test_bug345267.html @@ -0,0 +1,97 @@ + + + + + Test for Bug 345267 + + + + + +Mozilla Bug 345267 +

+ + + + + +

+ +
+
+
+ + + diff --git a/layout/forms/test/test_bug346043.html b/layout/forms/test/test_bug346043.html new file mode 100644 index 0000000000..e5db9bb8cf --- /dev/null +++ b/layout/forms/test/test_bug346043.html @@ -0,0 +1,65 @@ + + + + + + Test for Bug 346043 + + + + + + + Mozilla Bug 346043 +

+
+ + + +
+

+
+
diff --git a/layout/forms/test/test_bug348236.html b/layout/forms/test/test_bug348236.html
new file mode 100644
index 0000000000..f00f89efa7
--- /dev/null
+++ b/layout/forms/test/test_bug348236.html
@@ -0,0 +1,123 @@
+
+
+
+
+
+  Test for Bug 348236
+  
+  
+  
+  
+
+
+Mozilla Bug 348236
+

+
+ + +
+
+
+
+ + diff --git a/layout/forms/test/test_bug353539.html b/layout/forms/test/test_bug353539.html new file mode 100644 index 0000000000..593250d207 --- /dev/null +++ b/layout/forms/test/test_bug353539.html @@ -0,0 +1,52 @@ + + + + + Test for Bug 353539 + + + + +Mozilla Bug 353539 +

+ +

+ +
+
+
+ + diff --git a/layout/forms/test/test_bug365410.html b/layout/forms/test/test_bug365410.html new file mode 100644 index 0000000000..94ba8abfa0 --- /dev/null +++ b/layout/forms/test/test_bug365410.html @@ -0,0 +1,132 @@ + + + +Test for Bug 365410 + + + + + +Mozilla Bug 365410 +

+ + + + + +

+ +
+
+
+ + diff --git a/layout/forms/test/test_bug378670.html b/layout/forms/test/test_bug378670.html new file mode 100644 index 0000000000..f039cf35d6 --- /dev/null +++ b/layout/forms/test/test_bug378670.html @@ -0,0 +1,55 @@ + + + + + Test for Bug 378670 + + + + + +Mozilla Bug 378670 +

+ +Clicking on the select should not crash Mozilla + + +
+
+
+
+ + diff --git a/layout/forms/test/test_bug402198.html b/layout/forms/test/test_bug402198.html new file mode 100644 index 0000000000..f319a11976 --- /dev/null +++ b/layout/forms/test/test_bug402198.html @@ -0,0 +1,77 @@ + + + + Test for Bug 402198 + + + + + +Mozilla Bug 402198 +

+ + + + + + + + + + + + + + + + + + + +

+ +
+
+
+ + + + + diff --git a/layout/forms/test/test_bug411236.html b/layout/forms/test/test_bug411236.html new file mode 100644 index 0000000000..b2e2bee380 --- /dev/null +++ b/layout/forms/test/test_bug411236.html @@ -0,0 +1,71 @@ + + + + + Test for Bug 411236 + + + + + +Mozilla Bug 411236 +

+
+ +
+
+
+
+ + diff --git a/layout/forms/test/test_bug446663.html b/layout/forms/test/test_bug446663.html new file mode 100644 index 0000000000..bdab0b20ef --- /dev/null +++ b/layout/forms/test/test_bug446663.html @@ -0,0 +1,80 @@ + + + + + Test for Bug 446663 + + + + + +Mozilla Bug 446663 +

+ +

+ +
+
+
+ + + diff --git a/layout/forms/test/test_bug476308.html b/layout/forms/test/test_bug476308.html new file mode 100644 index 0000000000..41858d9eb8 --- /dev/null +++ b/layout/forms/test/test_bug476308.html @@ -0,0 +1,31 @@ + + + + + Test for Bug 345267 + + + + + + + + +
+
2
+ + + + + + diff --git a/layout/forms/test/test_bug477531.html b/layout/forms/test/test_bug477531.html new file mode 100644 index 0000000000..e0c7979af4 --- /dev/null +++ b/layout/forms/test/test_bug477531.html @@ -0,0 +1,65 @@ + + + + + Test for Bug 477531 + + + + + + +Mozilla Bug 477531 +

+
+ + + +
+
+
+
+ + + diff --git a/layout/forms/test/test_bug477700.html b/layout/forms/test/test_bug477700.html new file mode 100644 index 0000000000..1ab8d88321 --- /dev/null +++ b/layout/forms/test/test_bug477700.html @@ -0,0 +1,59 @@ + + + + + Test for Bug 477700 + + + + +Mozilla Bug 477700 +

+ +

+ +
+
+
+ + diff --git a/layout/forms/test/test_bug534785.html b/layout/forms/test/test_bug534785.html new file mode 100644 index 0000000000..7e43838927 --- /dev/null +++ b/layout/forms/test/test_bug534785.html @@ -0,0 +1,88 @@ + + + + + Test for Bug 534785 + + + + + +Mozilla Bug 534785 +

+ +
+ + + + +
+
+
+
+ + diff --git a/layout/forms/test/test_bug536567_perwindowpb.html b/layout/forms/test/test_bug536567_perwindowpb.html new file mode 100644 index 0000000000..224b2c74a4 --- /dev/null +++ b/layout/forms/test/test_bug536567_perwindowpb.html @@ -0,0 +1,215 @@ + + + + + Test for Bug 536567 + + + + + +Mozilla Bug 536567 +

+
+
+
+ + diff --git a/layout/forms/test/test_bug542914.html b/layout/forms/test/test_bug542914.html new file mode 100644 index 0000000000..ff7a68acea --- /dev/null +++ b/layout/forms/test/test_bug542914.html @@ -0,0 +1,115 @@ + + + + + Test for Bug 542914 + + + + + +Mozilla Bug 542914 +

+ + + +

+ +
+
+
+ + diff --git a/layout/forms/test/test_bug549170.html b/layout/forms/test/test_bug549170.html new file mode 100644 index 0000000000..a8a3f6d508 --- /dev/null +++ b/layout/forms/test/test_bug549170.html @@ -0,0 +1,77 @@ + + + + + Test for Bug 549170 + + + + + +Mozilla Bug 549170 +

+ + +
+ + +
+
+
+
+ + diff --git a/layout/forms/test/test_bug562447.html b/layout/forms/test/test_bug562447.html new file mode 100644 index 0000000000..86042bb485 --- /dev/null +++ b/layout/forms/test/test_bug562447.html @@ -0,0 +1,62 @@ + + + + + Test for Bug 562447 + + + + + +

Mozilla Bug 562447 + + + +

+
+
+ + + diff --git a/layout/forms/test/test_bug563642.html b/layout/forms/test/test_bug563642.html new file mode 100644 index 0000000000..72bb4c6858 --- /dev/null +++ b/layout/forms/test/test_bug563642.html @@ -0,0 +1,82 @@ + + + + + Test for Bug 563642 + + + + + +Mozilla Bug 563642 +

+ + + + +

+ +
+
+
+ + diff --git a/layout/forms/test/test_bug564115.html b/layout/forms/test/test_bug564115.html new file mode 100644 index 0000000000..16fb423341 --- /dev/null +++ b/layout/forms/test/test_bug564115.html @@ -0,0 +1,57 @@ + + + + + Test for Bug 564115 + + + + + +

Mozilla Bug 564115 + +

+
+
+ + + diff --git a/layout/forms/test/test_bug571352.html b/layout/forms/test/test_bug571352.html new file mode 100644 index 0000000000..73ad7454f3 --- /dev/null +++ b/layout/forms/test/test_bug571352.html @@ -0,0 +1,86 @@ + + + + + Test for Bug 571352 + + + + + +Mozilla Bug 571352 +

+ +
+
+
+ + diff --git a/layout/forms/test/test_bug572406.html b/layout/forms/test/test_bug572406.html new file mode 100644 index 0000000000..8b4d0b81ce --- /dev/null +++ b/layout/forms/test/test_bug572406.html @@ -0,0 +1,48 @@ + + + + + Test for Bug 572406 + + + + +Mozilla Bug 572406 +

+
+ +
+
+
+
+ + diff --git a/layout/forms/test/test_bug572649.html b/layout/forms/test/test_bug572649.html new file mode 100644 index 0000000000..10fa6e63f7 --- /dev/null +++ b/layout/forms/test/test_bug572649.html @@ -0,0 +1,63 @@ + + + + + Test for Bug 572649 + + + + + +Mozilla Bug 572649 +

+ +

+ +
+
+
+ + diff --git a/layout/forms/test/test_bug595310.html b/layout/forms/test/test_bug595310.html new file mode 100644 index 0000000000..47db981167 --- /dev/null +++ b/layout/forms/test/test_bug595310.html @@ -0,0 +1,64 @@ + + + + + Test for Bug 595310 + + + + + +Mozilla Bug 595310 +

+
+ + +
+
+
+
+ + diff --git a/layout/forms/test/test_bug620936.html b/layout/forms/test/test_bug620936.html new file mode 100644 index 0000000000..aa1beed01a --- /dev/null +++ b/layout/forms/test/test_bug620936.html @@ -0,0 +1,35 @@ + + + + + Test for Bug 620936 + + + + + +Mozilla Bug 620936 +

+
+ +
+
+
+
+ + diff --git a/layout/forms/test/test_bug644542.html b/layout/forms/test/test_bug644542.html new file mode 100644 index 0000000000..6c33cbe958 --- /dev/null +++ b/layout/forms/test/test_bug644542.html @@ -0,0 +1,63 @@ + + + + + Test for Bug 644542 + + + + + + +Mozilla Bug 644542 +

+ + +

+

+ +
+
+
+ + diff --git a/layout/forms/test/test_bug672810.html b/layout/forms/test/test_bug672810.html new file mode 100644 index 0000000000..991fe57389 --- /dev/null +++ b/layout/forms/test/test_bug672810.html @@ -0,0 +1,120 @@ + + + + + Test for Bug 672810 + + + + + +Mozilla Bug 672810 +

+
+ + + +
+
+
+
+ + diff --git a/layout/forms/test/test_bug704049.html b/layout/forms/test/test_bug704049.html new file mode 100644 index 0000000000..1da2badea2 --- /dev/null +++ b/layout/forms/test/test_bug704049.html @@ -0,0 +1,50 @@ + + + + + Test for Bug 704049 + + + + + +Mozilla Bug 704049 +

+ + + + +
+
+
+ + diff --git a/layout/forms/test/test_bug717878_input_scroll.html b/layout/forms/test/test_bug717878_input_scroll.html new file mode 100644 index 0000000000..6bd27ddacc --- /dev/null +++ b/layout/forms/test/test_bug717878_input_scroll.html @@ -0,0 +1,107 @@ + + + + + + Test for Bug 717878 + + + + +Mozilla Bug 717878 +

+ + + + + + + + + +
+
+
+ + diff --git a/layout/forms/test/test_bug869314.html b/layout/forms/test/test_bug869314.html new file mode 100644 index 0000000000..7c786fccfc --- /dev/null +++ b/layout/forms/test/test_bug869314.html @@ -0,0 +1,55 @@ + + + + + + Test for Bug 869314 + + + + + + + +Mozilla Bug 869314 +

+
+ + + + + + + +
+
+
+ + diff --git a/layout/forms/test/test_bug903715.html b/layout/forms/test/test_bug903715.html new file mode 100644 index 0000000000..b887b2cd01 --- /dev/null +++ b/layout/forms/test/test_bug903715.html @@ -0,0 +1,81 @@ + + + + + + Test for Bug 903715 + + + + + +Mozilla Bug 903715 +

+
+
+ + + +
+
+
+
+ + + diff --git a/layout/forms/test/test_bug935876.html b/layout/forms/test/test_bug935876.html new file mode 100644 index 0000000000..4488fdf962 --- /dev/null +++ b/layout/forms/test/test_bug935876.html @@ -0,0 +1,502 @@ + + + + + + Test for Bug 935876 + + + + + +Mozilla Bug 935876 +

+
+ + + +
+
+
+ + + diff --git a/layout/forms/test/test_bug957562.html b/layout/forms/test/test_bug957562.html new file mode 100644 index 0000000000..52821d8757 --- /dev/null +++ b/layout/forms/test/test_bug957562.html @@ -0,0 +1,43 @@ + + + + + + Test for Bug 903715 + + + + + +Mozilla Bug 957562 +

+ +
+
+ + + diff --git a/layout/forms/test/test_bug960277.html b/layout/forms/test/test_bug960277.html new file mode 100644 index 0000000000..28981b121a --- /dev/null +++ b/layout/forms/test/test_bug960277.html @@ -0,0 +1,29 @@ + + + + + + Test for Bug 903715 + + + + + +Mozilla Bug 960277 +

+
+ +
+
+
+
+
+ + + diff --git a/layout/forms/test/test_listcontrol_search.html b/layout/forms/test/test_listcontrol_search.html new file mode 100644 index 0000000000..d696edae66 --- /dev/null +++ b/layout/forms/test/test_listcontrol_search.html @@ -0,0 +1,46 @@ + + + + + + Test for <select> list control search + + + + + + +Mozilla Bug 849438 +

+
+ +
+
+
+ + diff --git a/layout/forms/test/test_readonly.html b/layout/forms/test/test_readonly.html new file mode 100644 index 0000000000..3bfb768f9c --- /dev/null +++ b/layout/forms/test/test_readonly.html @@ -0,0 +1,58 @@ + + + + +
+ + + + + + + + + + + + + +
+
+ + + + + + + + + + +
+ diff --git a/layout/forms/test/test_select_collapsed_page_keys.html b/layout/forms/test/test_select_collapsed_page_keys.html new file mode 100644 index 0000000000..08c1615152 --- /dev/null +++ b/layout/forms/test/test_select_collapsed_page_keys.html @@ -0,0 +1,43 @@ + + + + +Test for page up/down in collapsed select (bug 1488828) + + + + + + +
+ + +
+
+
+ + diff --git a/layout/forms/test/test_select_key_navigation_bug1498769.html b/layout/forms/test/test_select_key_navigation_bug1498769.html new file mode 100644 index 0000000000..3574761e66 --- /dev/null +++ b/layout/forms/test/test_select_key_navigation_bug1498769.html @@ -0,0 +1,123 @@ + + + + + +Test for Bug 1498769 + + + + + + +Mozilla Bug 1498769 +
+ + + + + + + + + + + + + +
+
+
+ + diff --git a/layout/forms/test/test_select_key_navigation_bug961363.html b/layout/forms/test/test_select_key_navigation_bug961363.html new file mode 100644 index 0000000000..7815149778 --- /dev/null +++ b/layout/forms/test/test_select_key_navigation_bug961363.html @@ -0,0 +1,131 @@ + + + + + +Test for Bug 961363 + + + + + + +Mozilla Bug 961363 +
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+ + diff --git a/layout/forms/test/test_select_prevent_default.html b/layout/forms/test/test_select_prevent_default.html new file mode 100644 index 0000000000..4ad9db0580 --- /dev/null +++ b/layout/forms/test/test_select_prevent_default.html @@ -0,0 +1,116 @@ + + + + + +Test for Bug 291082 + + + + + + +Mozilla Bug 291082 +
+
    +
  • + + select onkeydown="event.preventDefault();" +
  • +
  • + + select onkeypress="event.preventDefault();" +
  • +
  • + + li onkeydown="event.preventDefault();" +
  • +
  • + + li onkeypress="event.preventDefault();" +
  • +
  • + + select.addEventListener("keydown", function(event) { event.preventDefault(); }); +
  • +
  • + + select.addEventListener("keypress", function(event) { event.preventDefault(); }); +
  • +
+
+
+
+ + diff --git a/layout/forms/test/test_select_reframe.html b/layout/forms/test/test_select_reframe.html new file mode 100644 index 0000000000..666d8074b8 --- /dev/null +++ b/layout/forms/test/test_select_reframe.html @@ -0,0 +1,52 @@ + + +Test for page up/down in collapsed select (bug 1488828) + + + + +
+ +
+ diff --git a/layout/forms/test/test_select_vertical.html b/layout/forms/test/test_select_vertical.html new file mode 100644 index 0000000000..51c6208bd3 --- /dev/null +++ b/layout/forms/test/test_select_vertical.html @@ -0,0 +1,75 @@ + + +Test for select popup in vertical writing mode + + + + + +Mozilla Bug 1113206 +
+ + +
diff --git a/layout/forms/test/test_textarea_resize.html b/layout/forms/test/test_textarea_resize.html new file mode 100644 index 0000000000..93889a0d17 --- /dev/null +++ b/layout/forms/test/test_textarea_resize.html @@ -0,0 +1,102 @@ + + + + Test for Bug 477700 + + + + + + + + + +
+
+
+ + diff --git a/layout/forms/test/test_unstyled_control_height.html b/layout/forms/test/test_unstyled_control_height.html new file mode 100644 index 0000000000..ad5cd125d2 --- /dev/null +++ b/layout/forms/test/test_unstyled_control_height.html @@ -0,0 +1,72 @@ + + + + + + +
+ + + + + + +
+ +
+
+ + +
+ + -- cgit v1.2.3