summaryrefslogtreecommitdiffstats
path: root/layout/xul
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /layout/xul
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'layout/xul')
-rw-r--r--layout/xul/crashtests/131008-1.xhtml11
-rw-r--r--layout/xul/crashtests/137216-1.xhtml4
-rw-r--r--layout/xul/crashtests/1379332-2.xhtml9
-rw-r--r--layout/xul/crashtests/140218-1.xml4
-rw-r--r--layout/xul/crashtests/151826-1.xhtml27
-rw-r--r--layout/xul/crashtests/168724-1.xhtml18
-rw-r--r--layout/xul/crashtests/189814-1.xhtml21
-rw-r--r--layout/xul/crashtests/289410-1.xhtml14
-rw-r--r--layout/xul/crashtests/291702-1.xhtml11
-rw-r--r--layout/xul/crashtests/291702-2.xhtml11
-rw-r--r--layout/xul/crashtests/291702-3.xhtml137
-rw-r--r--layout/xul/crashtests/294371-1.xhtml53
-rw-r--r--layout/xul/crashtests/322786-1.xhtml6
-rw-r--r--layout/xul/crashtests/325377.xhtml16
-rw-r--r--layout/xul/crashtests/326879-1.xhtml31
-rw-r--r--layout/xul/crashtests/329327-1.xhtml2
-rw-r--r--layout/xul/crashtests/329407-1.xml14
-rw-r--r--layout/xul/crashtests/336962-1.xhtml18
-rw-r--r--layout/xul/crashtests/344228-1.xhtml27
-rw-r--r--layout/xul/crashtests/365151.xhtml39
-rw-r--r--layout/xul/crashtests/366112-1.xhtml9
-rw-r--r--layout/xul/crashtests/366203-1.xhtml40
-rw-r--r--layout/xul/crashtests/367185-1.xhtml11
-rw-r--r--layout/xul/crashtests/369942-1.xhtml36
-rw-r--r--layout/xul/crashtests/376137-1.html18
-rw-r--r--layout/xul/crashtests/376137-2.html11
-rw-r--r--layout/xul/crashtests/378961.html9
-rw-r--r--layout/xul/crashtests/381862.html23
-rw-r--r--layout/xul/crashtests/382746-1.xhtml15
-rw-r--r--layout/xul/crashtests/382899-1.xhtml9
-rw-r--r--layout/xul/crashtests/384037-1.xhtml9
-rw-r--r--layout/xul/crashtests/384105-1-inner.xhtml21
-rw-r--r--layout/xul/crashtests/384105-1.html9
-rw-r--r--layout/xul/crashtests/384373-1.xhtml10
-rw-r--r--layout/xul/crashtests/384373-2.xhtml4
-rw-r--r--layout/xul/crashtests/384373.html23
-rw-r--r--layout/xul/crashtests/384871-1-inner.xhtml9
-rw-r--r--layout/xul/crashtests/384871-1.html9
-rw-r--r--layout/xul/crashtests/386642.xhtml31
-rw-r--r--layout/xul/crashtests/387080-1.xhtml6
-rw-r--r--layout/xul/crashtests/391974-1-inner.xhtml19
-rw-r--r--layout/xul/crashtests/391974-1.html9
-rw-r--r--layout/xul/crashtests/402912-1.xhtml5
-rw-r--r--layout/xul/crashtests/404192.xhtml12
-rw-r--r--layout/xul/crashtests/408904-1.xhtml1
-rw-r--r--layout/xul/crashtests/412479-1.xhtml4
-rw-r--r--layout/xul/crashtests/417509.xhtml7
-rw-r--r--layout/xul/crashtests/430356-1.xhtml5
-rw-r--r--layout/xul/crashtests/464407-1.xhtml9
-rw-r--r--layout/xul/crashtests/470063-1.html15
-rw-r--r--layout/xul/crashtests/470272.html21
-rw-r--r--layout/xul/crashtests/538308-1.xhtml32
-rw-r--r--layout/xul/crashtests/557174-1.xml1
-rw-r--r--layout/xul/crashtests/564705-1.xhtml6
-rw-r--r--layout/xul/crashtests/583957-1.html20
-rw-r--r--layout/xul/crashtests/617089.html9
-rw-r--r--layout/xul/crashtests/716503.html11
-rw-r--r--layout/xul/crashtests/crashtests.list53
-rw-r--r--layout/xul/crashtests/menulist-focused.xhtml5
-rw-r--r--layout/xul/moz.build58
-rw-r--r--layout/xul/nsBox.cpp607
-rw-r--r--layout/xul/nsBoxFrame.cpp1035
-rw-r--r--layout/xul/nsBoxFrame.h185
-rw-r--r--layout/xul/nsBoxLayout.cpp74
-rw-r--r--layout/xul/nsBoxLayout.h67
-rw-r--r--layout/xul/nsBoxLayoutState.cpp37
-rw-r--r--layout/xul/nsBoxLayoutState.h79
-rw-r--r--layout/xul/nsIPopupContainer.h29
-rw-r--r--layout/xul/nsIScrollbarMediator.h101
-rw-r--r--layout/xul/nsImageBoxFrame.cpp805
-rw-r--r--layout/xul/nsImageBoxFrame.h184
-rw-r--r--layout/xul/nsLeafBoxFrame.cpp306
-rw-r--r--layout/xul/nsLeafBoxFrame.h80
-rw-r--r--layout/xul/nsMenuBarFrame.cpp136
-rw-r--r--layout/xul/nsMenuBarFrame.h83
-rw-r--r--layout/xul/nsMenuBarListener.cpp564
-rw-r--r--layout/xul/nsMenuBarListener.h121
-rw-r--r--layout/xul/nsMenuPopupFrame.cpp2479
-rw-r--r--layout/xul/nsMenuPopupFrame.h638
-rw-r--r--layout/xul/nsRepeatService.cpp93
-rw-r--r--layout/xul/nsRepeatService.h73
-rw-r--r--layout/xul/nsScrollbarButtonFrame.cpp274
-rw-r--r--layout/xul/nsScrollbarButtonFrame.h85
-rw-r--r--layout/xul/nsScrollbarFrame.cpp546
-rw-r--r--layout/xul/nsScrollbarFrame.h159
-rw-r--r--layout/xul/nsSliderFrame.cpp1626
-rw-r--r--layout/xul/nsSliderFrame.h226
-rw-r--r--layout/xul/nsSplitterFrame.cpp956
-rw-r--r--layout/xul/nsSplitterFrame.h83
-rw-r--r--layout/xul/nsSprocketLayout.cpp1372
-rw-r--r--layout/xul/nsSprocketLayout.h156
-rw-r--r--layout/xul/nsTextBoxFrame.cpp1055
-rw-r--r--layout/xul/nsTextBoxFrame.h128
-rw-r--r--layout/xul/nsXULPopupManager.cpp2800
-rw-r--r--layout/xul/nsXULPopupManager.h881
-rw-r--r--layout/xul/nsXULTooltipListener.cpp678
-rw-r--r--layout/xul/nsXULTooltipListener.h98
-rw-r--r--layout/xul/reftest/checkbox-dynamic-change-ref.xhtml6
-rw-r--r--layout/xul/reftest/checkbox-dynamic-change.xhtml17
-rw-r--r--layout/xul/reftest/image-scaling-min-height-1-ref.xhtml14
-rw-r--r--layout/xul/reftest/image-scaling-min-height-1.xhtml14
-rw-r--r--layout/xul/reftest/image-size-ref.xhtml115
-rw-r--r--layout/xul/reftest/image-size.xhtml123
-rw-r--r--layout/xul/reftest/image4x3.pngbin0 -> 176 bytes
-rw-r--r--layout/xul/reftest/popup-explicit-size-ref.xhtml6
-rw-r--r--layout/xul/reftest/popup-explicit-size.xhtml7
-rw-r--r--layout/xul/reftest/radio-dynamic-change-ref.xhtml6
-rw-r--r--layout/xul/reftest/radio-dynamic-change.xhtml17
-rw-r--r--layout/xul/reftest/reftest.list14
-rw-r--r--layout/xul/reftest/scrollbar-marks-overlay-ref.html64
-rw-r--r--layout/xul/reftest/scrollbar-marks-overlay.html18
-rw-r--r--layout/xul/reftest/scrollbar-marks-ref.html13
-rw-r--r--layout/xul/reftest/scrollbar-marks.html18
-rw-r--r--layout/xul/reftest/scrollbar-marks2.html19
-rw-r--r--layout/xul/reftest/textbox-text-transform-ref.xhtml6
-rw-r--r--layout/xul/reftest/textbox-text-transform.xhtml6
-rw-r--r--layout/xul/test/browser.ini11
-rw-r--r--layout/xul/test/browser_bug1163304.js83
-rw-r--r--layout/xul/test/browser_bug1754298.js35
-rw-r--r--layout/xul/test/browser_bug685470.js38
-rw-r--r--layout/xul/test/browser_bug703210.js56
-rw-r--r--layout/xul/test/browser_bug706743.js158
-rw-r--r--layout/xul/test/chrome.ini39
-rw-r--r--layout/xul/test/file_bug386386.sjs14
-rw-r--r--layout/xul/test/mochitest.ini13
-rw-r--r--layout/xul/test/test_blockify_moz_box.html114
-rw-r--r--layout/xul/test/test_bug1197913.xhtml63
-rw-r--r--layout/xul/test/test_bug159346.xhtml143
-rw-r--r--layout/xul/test/test_bug381167.xhtml52
-rw-r--r--layout/xul/test/test_bug386386.html34
-rw-r--r--layout/xul/test/test_bug394800.xhtml39
-rw-r--r--layout/xul/test/test_bug398982-1.xhtml31
-rw-r--r--layout/xul/test/test_bug398982-2.xhtml33
-rw-r--r--layout/xul/test/test_bug467442.xhtml53
-rw-r--r--layout/xul/test/test_bug477754.xhtml51
-rw-r--r--layout/xul/test/test_bug511075.html121
-rw-r--r--layout/xul/test/test_bug563416.html53
-rw-r--r--layout/xul/test/test_bug703150.xhtml74
-rw-r--r--layout/xul/test/test_bug987230.xhtml109
-rw-r--r--layout/xul/test/test_drag_thumb_in_link.html76
-rw-r--r--layout/xul/test/test_menuitem_ctrl_click.xhtml80
-rw-r--r--layout/xul/test/test_popupReflowPos.xhtml77
-rw-r--r--layout/xul/test/test_popupSizeTo.xhtml55
-rw-r--r--layout/xul/test/test_popupZoom.xhtml53
-rw-r--r--layout/xul/test/test_resizer_ctrl_click.xhtml51
-rw-r--r--layout/xul/test/test_resizer_incontent.xhtml42
-rw-r--r--layout/xul/test/test_splitter.xhtml131
-rw-r--r--layout/xul/test/test_splitter_sibling.xhtml88
-rw-r--r--layout/xul/test/test_submenuClose.xhtml91
-rw-r--r--layout/xul/test/test_toolbarbutton_ctrl_click.xhtml51
-rw-r--r--layout/xul/test/test_windowminmaxsize.xhtml191
-rw-r--r--layout/xul/test/titledpanelwindow.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize1.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize10.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize2.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize3.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize4.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize5.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize6.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize7.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize8.xhtml4
-rw-r--r--layout/xul/test/windowminmaxsize9.xhtml4
-rw-r--r--layout/xul/tree/crashtests/307298-1.xhtml21
-rw-r--r--layout/xul/tree/crashtests/309732-1.xhtml30
-rw-r--r--layout/xul/tree/crashtests/309732-2.xhtml31
-rw-r--r--layout/xul/tree/crashtests/366583-1.xhtml43
-rw-r--r--layout/xul/tree/crashtests/380217-1.xhtml31
-rw-r--r--layout/xul/tree/crashtests/382444-1-inner.html15
-rw-r--r--layout/xul/tree/crashtests/382444-1.html9
-rw-r--r--layout/xul/tree/crashtests/391178-1.xhtml41
-rw-r--r--layout/xul/tree/crashtests/391178-2.xhtml20
-rw-r--r--layout/xul/tree/crashtests/393665-1.xhtml3
-rw-r--r--layout/xul/tree/crashtests/399227-1.xhtml44
-rw-r--r--layout/xul/tree/crashtests/399692-1.xhtml10
-rw-r--r--layout/xul/tree/crashtests/399715-1.xhtml9
-rw-r--r--layout/xul/tree/crashtests/409807-1.xhtml25
-rw-r--r--layout/xul/tree/crashtests/414170-1.xhtml20
-rw-r--r--layout/xul/tree/crashtests/479931-1.xhtml19
-rw-r--r--layout/xul/tree/crashtests/585815-iframe.xhtml72
-rw-r--r--layout/xul/tree/crashtests/585815.html18
-rw-r--r--layout/xul/tree/crashtests/601427.html30
-rw-r--r--layout/xul/tree/crashtests/730441-3.xhtml38
-rw-r--r--layout/xul/tree/crashtests/crashtests.list18
-rw-r--r--layout/xul/tree/moz.build46
-rw-r--r--layout/xul/tree/nsITreeSelection.idl119
-rw-r--r--layout/xul/tree/nsITreeView.idl173
-rw-r--r--layout/xul/tree/nsTreeBodyFrame.cpp4348
-rw-r--r--layout/xul/tree/nsTreeBodyFrame.h614
-rw-r--r--layout/xul/tree/nsTreeColFrame.cpp169
-rw-r--r--layout/xul/tree/nsTreeColFrame.h60
-rw-r--r--layout/xul/tree/nsTreeColumns.cpp464
-rw-r--r--layout/xul/tree/nsTreeColumns.h214
-rw-r--r--layout/xul/tree/nsTreeContentView.cpp1269
-rw-r--r--layout/xul/tree/nsTreeContentView.h164
-rw-r--r--layout/xul/tree/nsTreeImageListener.cpp115
-rw-r--r--layout/xul/tree/nsTreeImageListener.h67
-rw-r--r--layout/xul/tree/nsTreeSelection.cpp723
-rw-r--r--layout/xul/tree/nsTreeSelection.h56
-rw-r--r--layout/xul/tree/nsTreeStyleCache.cpp102
-rw-r--r--layout/xul/tree/nsTreeStyleCache.h82
-rw-r--r--layout/xul/tree/nsTreeUtils.cpp135
-rw-r--r--layout/xul/tree/nsTreeUtils.h43
202 files changed, 32426 insertions, 0 deletions
diff --git a/layout/xul/crashtests/131008-1.xhtml b/layout/xul/crashtests/131008-1.xhtml
new file mode 100644
index 0000000000..fe15a46aa6
--- /dev/null
+++ b/layout/xul/crashtests/131008-1.xhtml
@@ -0,0 +1,11 @@
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="MainWindow"
+ title="IWindow Test">
+<div style="display: block; position:absolute">abc</div>
+
+
+</window>
diff --git a/layout/xul/crashtests/137216-1.xhtml b/layout/xul/crashtests/137216-1.xhtml
new file mode 100644
index 0000000000..e01541c622
--- /dev/null
+++ b/layout/xul/crashtests/137216-1.xhtml
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <iframe style="position:absolute; display: block;"/>
+</window>
diff --git a/layout/xul/crashtests/1379332-2.xhtml b/layout/xul/crashtests/1379332-2.xhtml
new file mode 100644
index 0000000000..cab6145c44
--- /dev/null
+++ b/layout/xul/crashtests/1379332-2.xhtml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <hbox style="position: relative;visibility: collapse;">
+ <hbox style="padding:5px; border: 5px solid black">
+ <hbox style="position: absolute; display: block; width: 10px; height: 10px">
+ </hbox>
+ </hbox>
+ </hbox>
+</window>
diff --git a/layout/xul/crashtests/140218-1.xml b/layout/xul/crashtests/140218-1.xml
new file mode 100644
index 0000000000..311afc2188
--- /dev/null
+++ b/layout/xul/crashtests/140218-1.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <treechildren style = " display: block; " />
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/151826-1.xhtml b/layout/xul/crashtests/151826-1.xhtml
new file mode 100644
index 0000000000..bb8ee2e200
--- /dev/null
+++ b/layout/xul/crashtests/151826-1.xhtml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window
+ title = "Arrowscrollbox->Splitter Crash Testcase"
+ xmlns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ width = "300"
+ height = "200"
+ orient = "vertical"
+>
+<vbox flex="1">
+
+<scrollbox flex="1">
+<vbox flex="1">
+<vbox id="box_1">
+<hbox><label value="Test"/></hbox>
+</vbox>
+<splitter collapse="none"/>
+<vbox id="box_2">
+<hbox><label value="Test"/></hbox>
+</vbox>
+</vbox>
+</scrollbox>
+
+</vbox>
+</window>
diff --git a/layout/xul/crashtests/168724-1.xhtml b/layout/xul/crashtests/168724-1.xhtml
new file mode 100644
index 0000000000..61d4f48327
--- /dev/null
+++ b/layout/xul/crashtests/168724-1.xhtml
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css" ?>
+
+<window
+ id="nodeCreator" title="Node Creator"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+<description context="context">Right-click here, and expect a crash.</description>
+
+<popupset id="context-set">
+<popup id="context">
+<deck selectedItem="0">
+<menuitem label="You should never see this" />
+</deck>
+</popup>
+</popupset>
+</window>
diff --git a/layout/xul/crashtests/189814-1.xhtml b/layout/xul/crashtests/189814-1.xhtml
new file mode 100644
index 0000000000..79462348c6
--- /dev/null
+++ b/layout/xul/crashtests/189814-1.xhtml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<window
+ id="sliderprint" title="Print These Sliders"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ style="background-color: white">
+
+ <label>
+ With the Classic theme, printing causes the browser to crash. adding style="-moz-appearance: none" to the
+ thumb prevents the crash. The crash doesn't happen at all with Modern.
+ </label>
+ <spacer height="10"/>
+ <hbox>
+
+ <slider style="height: 174px; width: 24px" orient="vertical">
+ <thumb/>
+ </slider>
+
+ </hbox>
+
+</window>
diff --git a/layout/xul/crashtests/289410-1.xhtml b/layout/xul/crashtests/289410-1.xhtml
new file mode 100644
index 0000000000..fa235b607e
--- /dev/null
+++ b/layout/xul/crashtests/289410-1.xhtml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window id="crash-window"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <scrollbox>
+ <tree id="crash-tree">
+ <treecols/>
+ <treechildren/>
+ </tree>
+ </scrollbox>
+
+</window>
diff --git a/layout/xul/crashtests/291702-1.xhtml b/layout/xul/crashtests/291702-1.xhtml
new file mode 100644
index 0000000000..6b36046d16
--- /dev/null
+++ b/layout/xul/crashtests/291702-1.xhtml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window title="Negative flex bug #2"
+ orient="horizontal"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <button label="Button" style="-moz-box-flex: 2"/>
+ <label value="This is a label" style="-moz-box-flex: 1"/>
+ <label value="This is the second label" style="-moz-box-flex: -2"/>
+ <label value="This is another label" style="-moz-box-flex: -1"/>
+</window>
diff --git a/layout/xul/crashtests/291702-2.xhtml b/layout/xul/crashtests/291702-2.xhtml
new file mode 100644
index 0000000000..a47dbbdd41
--- /dev/null
+++ b/layout/xul/crashtests/291702-2.xhtml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window title="Negative flex bug #2"
+ orient="horizontal"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is a label" style="-moz-box-flex: 1073741824"/>
+ <label value="This is the second label" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+</window>
diff --git a/layout/xul/crashtests/291702-3.xhtml b/layout/xul/crashtests/291702-3.xhtml
new file mode 100644
index 0000000000..6f947f4887
--- /dev/null
+++ b/layout/xul/crashtests/291702-3.xhtml
@@ -0,0 +1,137 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window title="Negative flex bug #2"
+ orient="vertical"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ </hbox>
+
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ </hbox>
+
+
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 2;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 2;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 2;"/>
+ </hbox>
+
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 1"/>
+ <label value="This is another label" style="-moz-box-flex: 1;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 1"/>
+ <label value="This is another label" style="-moz-box-flex: 1;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 1"/>
+ <label value="This is another label" style="-moz-box-flex: 1;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741823"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741823;"/>
+ <button label="Button" style="-moz-box-flex: 2"/>
+ <label value="This is another label" style="-moz-box-flex: 2;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741824"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741824;"/>
+ <button label="Button" style="-moz-box-flex: 2"/>
+ <label value="This is another label" style="-moz-box-flex: 2;"/>
+ </hbox>
+ <hbox>
+ <button label="Button" style="-moz-box-flex: 1073741825"/>
+ <label value="This is another label" style="-moz-box-flex: 1073741825;"/>
+ <button label="Button" style="-moz-box-flex: 2"/>
+ <label value="This is another label" style="-moz-box-flex: 2;"/>
+ </hbox>
+</window>
diff --git a/layout/xul/crashtests/294371-1.xhtml b/layout/xul/crashtests/294371-1.xhtml
new file mode 100644
index 0000000000..ca5b54914a
--- /dev/null
+++ b/layout/xul/crashtests/294371-1.xhtml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window
+ id = "overflow crash"
+ title = "scrollbox crasher"
+ xmlns = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ persist="sizemode width height screenX screenY"
+ width="320"
+ height="240">
+
+ <scrollbox flex="1">
+ <grid style="overflow: auto">
+ <columns>
+ <column flex="0"/>
+ </columns>
+ <rows>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ <row><label value="Date"/></row>
+ </rows>
+ </grid>
+ </scrollbox>
+
+</window>
diff --git a/layout/xul/crashtests/322786-1.xhtml b/layout/xul/crashtests/322786-1.xhtml
new file mode 100644
index 0000000000..79bb092c4b
--- /dev/null
+++ b/layout/xul/crashtests/322786-1.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <foo style="display: inline;">
+ <scrollbox/>
+ </foo>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/325377.xhtml b/layout/xul/crashtests/325377.xhtml
new file mode 100644
index 0000000000..8ea30473d8
--- /dev/null
+++ b/layout/xul/crashtests/325377.xhtml
@@ -0,0 +1,16 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Testcase bug 325377 - Crash on reload with evil xul textcase, using menulist and nested tooltips">
+<menulist style="display: table-cell;">
+<tooltip style="display: none;">
+ <tooltip/>
+</tooltip>
+</menulist>
+
+<html:script>
+function removestyles(){
+document.getElementsByTagName('tooltip')[0].removeAttribute('style');
+}
+try { document.getElementsByTagName('tooltip')[0].offsetHeight; } catch(e) {}
+setTimeout(removestyles,0);
+</html:script>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/326879-1.xhtml b/layout/xul/crashtests/326879-1.xhtml
new file mode 100644
index 0000000000..26965ae65e
--- /dev/null
+++ b/layout/xul/crashtests/326879-1.xhtml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+
+
+<script>
+
+
+function init() {
+
+ var menupopup = document.getElementsByTagName("menupopup")[0];
+ menupopup.style.MozBoxOrdinalGroup = null;
+};
+
+
+window.addEventListener("load", init, false);
+
+</script>
+
+
+<menulist>
+ <menupopup>
+ <menuitem label="Foo"/>
+ </menupopup>
+</menulist>
+
+
+
+</window>
diff --git a/layout/xul/crashtests/329327-1.xhtml b/layout/xul/crashtests/329327-1.xhtml
new file mode 100644
index 0000000000..fcfed07c4c
--- /dev/null
+++ b/layout/xul/crashtests/329327-1.xhtml
@@ -0,0 +1,2 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"><menulist equalsize="always"><y/> <z width="-444981589286"/> </menulist></window>
diff --git a/layout/xul/crashtests/329407-1.xml b/layout/xul/crashtests/329407-1.xml
new file mode 100644
index 0000000000..0d41c0185f
--- /dev/null
+++ b/layout/xul/crashtests/329407-1.xml
@@ -0,0 +1,14 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+
+<body>
+
+ <xul:hbox>
+ <select/>
+ <select/>
+ </xul:hbox>
+
+</body>
+
+</html>
diff --git a/layout/xul/crashtests/336962-1.xhtml b/layout/xul/crashtests/336962-1.xhtml
new file mode 100644
index 0000000000..bd2129a853
--- /dev/null
+++ b/layout/xul/crashtests/336962-1.xhtml
@@ -0,0 +1,18 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script>
+
+function init() {
+ document.getElementById("foopy").style.display = "block";
+ document.getElementById("foopy").style.position = "absolute";
+}
+
+window.addEventListener("load", init, 0);
+
+</script>
+
+
+<box id="foopy" />
+
+
+</window>
diff --git a/layout/xul/crashtests/344228-1.xhtml b/layout/xul/crashtests/344228-1.xhtml
new file mode 100644
index 0000000000..d6015707bd
--- /dev/null
+++ b/layout/xul/crashtests/344228-1.xhtml
@@ -0,0 +1,27 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="setTimeout(boom, 30);" class="reftest-wait">
+
+<script>
+
+function remove(q1) { q1.parentNode.removeChild(q1); }
+
+function boom()
+{
+ var x = document.getElementById("x");
+ var y = document.getElementById("y");
+ remove(x);
+ remove(y);
+
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+
+<tree>
+ <treechildren id="y"/>
+ <richlistbox>
+ <hbox id="x"/>
+ <menulist/>
+ </richlistbox>
+</tree>
+
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/365151.xhtml b/layout/xul/crashtests/365151.xhtml
new file mode 100644
index 0000000000..001707f4eb
--- /dev/null
+++ b/layout/xul/crashtests/365151.xhtml
@@ -0,0 +1,39 @@
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="boom()" class="reftest-wait">
+
+
+<script>
+function boom()
+{
+ try {
+ var tree = document.getElementById("tree");
+ var col = tree.columns.getFirstColumn();
+ var treecols = document.getElementById("treecols");
+ treecols.parentNode.removeChild(treecols);
+ var x = col.x;
+ } finally {
+ document.documentElement.removeAttribute("class");
+ }
+}
+</script>
+
+
+<tree rows="6" id="tree">
+
+ <treecols id="treecols">
+ <treecol id="firstname" label="First Name"/>
+ </treecols>
+
+ <treechildren id="treechildren">
+ <treeitem>
+ <treerow>
+ <treecell label="Bob"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+
+</tree>
+
+</window>
diff --git a/layout/xul/crashtests/366112-1.xhtml b/layout/xul/crashtests/366112-1.xhtml
new file mode 100644
index 0000000000..ff95a722f3
--- /dev/null
+++ b/layout/xul/crashtests/366112-1.xhtml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <nativescrollbar />
+
+</window>
diff --git a/layout/xul/crashtests/366203-1.xhtml b/layout/xul/crashtests/366203-1.xhtml
new file mode 100644
index 0000000000..5d97782ea3
--- /dev/null
+++ b/layout/xul/crashtests/366203-1.xhtml
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="setTimeout(boom, 500);">
+
+<script>
+function boom()
+{
+ tc1 = document.getElementById("tc1");
+ tc1.parentNode.removeChild(tc1);
+}
+</script>
+
+<tree rows="6">
+ <treecols>
+ <treecol id="firstname" label="First Name" primary="true" style="-moz-box-flex: 3"/>
+ <treecol id="lastname" label="Last Name" style="-moz-box-flex: 7"/>
+ </treecols>
+
+ <treechildren id="tc1">
+ <treeitem container="true" open="true">
+ <treerow>
+ <treecell label="Foo"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+
+ <treechildren>
+ <treeitem container="true" open="true">
+ <treerow>
+ <treecell label="Bar"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+</tree>
+
+
+</window>
+
diff --git a/layout/xul/crashtests/367185-1.xhtml b/layout/xul/crashtests/367185-1.xhtml
new file mode 100644
index 0000000000..08fd39fa11
--- /dev/null
+++ b/layout/xul/crashtests/367185-1.xhtml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+<title>Testcase bug - ASSERTION: shouldn't use unconstrained widths anymore with nested marquees</title>
+</head>
+<body>
+<xul:hbox style="margin: 0 100%;"><span><xul:hbox style="margin: 0 100%;"></xul:hbox></span></xul:hbox>
+</body>
+</html>
diff --git a/layout/xul/crashtests/369942-1.xhtml b/layout/xul/crashtests/369942-1.xhtml
new file mode 100644
index 0000000000..a05705843d
--- /dev/null
+++ b/layout/xul/crashtests/369942-1.xhtml
@@ -0,0 +1,36 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ class="reftest-wait">
+<head>
+
+<script>
+function boom()
+{
+ var span = document.getElementById("span");
+ var radio = document.getElementById("radio");
+
+ radio.appendChild(span);
+
+ document.documentElement.removeAttribute("class");
+}
+</script>
+
+
+<style>
+body {
+ text-align: center;
+ font-size: 9px;
+}
+</style>
+
+</head>
+
+
+<body onload="setTimeout(boom, 30);">
+
+<span id="span"><xul:wizard/><div>Industries</div></span>
+
+<xul:radio id="radio"/>
+
+</body>
+</html>
diff --git a/layout/xul/crashtests/376137-1.html b/layout/xul/crashtests/376137-1.html
new file mode 100644
index 0000000000..33d706f9c1
--- /dev/null
+++ b/layout/xul/crashtests/376137-1.html
@@ -0,0 +1,18 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<style>
+span { display:block; outline: 10px solid yellow; }
+</style>
+</head>
+
+<body>
+
+<div>
+ <div style="display: -moz-inline-box">
+ <span>M</span>
+ <span>N</span>
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/layout/xul/crashtests/376137-2.html b/layout/xul/crashtests/376137-2.html
new file mode 100644
index 0000000000..d3abb2d838
--- /dev/null
+++ b/layout/xul/crashtests/376137-2.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>Bug 376137</title>
+<style>
+p { width: 100%; border: solid 1px;}
+</style>
+
+<div style="display: -moz-inline-box">
+ <div><p>M</p></div>
+ <div><p>N</p></div>
+</div>
+
diff --git a/layout/xul/crashtests/378961.html b/layout/xul/crashtests/378961.html
new file mode 100644
index 0000000000..42e9a64bd8
--- /dev/null
+++ b/layout/xul/crashtests/378961.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Testcase bug 378961 - Crash [@ nsSplitterFrameInner::RemoveListener] when dragging splitter and DOMAttrModified event removing window</title>
+</head>
+<body>
+<iframe src="data:application/xhtml+xml;charset=utf-8,%3C%3Fxml%20version%3D%221.0%22%3F%3E%0A%3C%3Fxml-stylesheet%20href%3D%22chrome%3A//global/skin%22%20type%3D%22text/css%22%3F%3E%0A%3Cwindow%20xmlns%3D%22http%3A//www.mozilla.org/keymaster/gatekeeper/there.is.only.xul%22%20orient%3D%22horizontal%22%3E%0A%3Ctextbox/%3E%3Csplitter/%3E%3Cbox/%3E%0A%0A%3Cscript%20xmlns%3D%22http%3A//www.w3.org/1999/xhtml%22%3E%0Afunction%20doe%28%29%20%7B%0Awindow.frameElement.parentNode.removeChild%28window.frameElement%29%3B%0A%7D%0Adocument.addEventListener%28%27DOMAttrModified%27%2C%20doe%2C%20true%29%3B%0A%3C/script%3E%0A%3C/window%3E" style="width: 500px;height:200px;"></iframe>
+
+</body>
+</html>
diff --git a/layout/xul/crashtests/381862.html b/layout/xul/crashtests/381862.html
new file mode 100644
index 0000000000..65721d1a3f
--- /dev/null
+++ b/layout/xul/crashtests/381862.html
@@ -0,0 +1,23 @@
+<html><head>
+<title>Testcase bug - Crash [@ nsBoxFrame::BuildDisplayListForChildren] with tree stuff in iframe toggling display</title>
+</head>
+<body>
+<iframe src="data:application/xhtml+xml;charset=utf-8,%3Cwindow%20xmlns%3D%22http%3A//www.mozilla.org/keymaster/gatekeeper/there.is.only.xul%22%3E%0A%20%20%3Ctree%20style%3D%22display%3A%20block%3B%20position%3A%20absolute%3B%22%3E%0A%20%20%20%20%3Ctree%20style%3D%22display%3A%20table%3B%22%3E%0A%20%20%20%20%20%20%3Ctreeseparator%20style%3D%22display%3A%20block%3B%20position%3A%20absolute%3B%22%3E%0A%20%20%20%20%20%20%20%20%3Ctreechildren%20style%3D%22display%3A%20block%3B%22/%3E%0A%20%20%20%20%20%20%3C/treeseparator%3E%0A%20%20%20%20%20%20%3Ctreechildren%20style%3D%22display%3A%20none%3B%22/%3E%0A%20%20%20%20%3C/tree%3E%0A%20%20%3C/tree%3E%0A%3C/window%3E" id="content"></iframe>
+
+<script>
+function toggleIframe(){
+var x=document.getElementById('content');
+x.style.display = x.style.display == 'none' ? x.style.display = 'block' : x.style.display = 'none';
+setTimeout(toggleIframe,200);
+}
+setTimeout(toggleIframe,500);
+
+function removestyles(i){
+window.frames[0].document.getElementsByTagName('*')[1].removeAttribute('style');
+}
+
+setTimeout(removestyles,500,1);
+/*template*/
+</script>
+</body>
+</html>
diff --git a/layout/xul/crashtests/382746-1.xhtml b/layout/xul/crashtests/382746-1.xhtml
new file mode 100644
index 0000000000..c76a1531cd
--- /dev/null
+++ b/layout/xul/crashtests/382746-1.xhtml
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<grid>
+ <rows>
+ <column>
+ <hbox/>
+ <hbox/>
+ </column>
+ <hbox/>
+ </rows>
+</grid>
+
+</window>
diff --git a/layout/xul/crashtests/382899-1.xhtml b/layout/xul/crashtests/382899-1.xhtml
new file mode 100644
index 0000000000..4b48eac240
--- /dev/null
+++ b/layout/xul/crashtests/382899-1.xhtml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<hbox equalsize="always"><grid/>x</hbox>
+
+</window>
diff --git a/layout/xul/crashtests/384037-1.xhtml b/layout/xul/crashtests/384037-1.xhtml
new file mode 100644
index 0000000000..04bac671cc
--- /dev/null
+++ b/layout/xul/crashtests/384037-1.xhtml
@@ -0,0 +1,9 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<body>
+
+<xul:splitter id="s" collapse="both" state="collapsed" />
+
+</body>
+</html>
+
diff --git a/layout/xul/crashtests/384105-1-inner.xhtml b/layout/xul/crashtests/384105-1-inner.xhtml
new file mode 100644
index 0000000000..ea9c0be8ad
--- /dev/null
+++ b/layout/xul/crashtests/384105-1-inner.xhtml
@@ -0,0 +1,21 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<script id="script" xmlns="http://www.w3.org/1999/xhtml">
+function doe(){
+document.getElementById('a').removeAttribute('style');
+}
+setTimeout(doe,100);
+</script>
+<box id="a" style="position: absolute; display: block;">
+ <menuitem sizetopopup="always">
+ <menupopup style="position: absolute; display: block;"/>
+ </menuitem>
+
+ <box style="position: fixed; display: block;">
+ <tree>
+ <treecol>
+ <treecol/>
+ </treecol>
+ </tree>
+ </box>
+</box>
+</window>
diff --git a/layout/xul/crashtests/384105-1.html b/layout/xul/crashtests/384105-1.html
new file mode 100644
index 0000000000..8161342ec8
--- /dev/null
+++ b/layout/xul/crashtests/384105-1.html
@@ -0,0 +1,9 @@
+<html class="reftest-wait">
+<head>
+<script>
+setTimeout('document.documentElement.className = ""', 1000);
+</script>
+<body>
+<iframe src="384105-1-inner.xhtml"></iframe>
+</body>
+</html>
diff --git a/layout/xul/crashtests/384373-1.xhtml b/layout/xul/crashtests/384373-1.xhtml
new file mode 100644
index 0000000000..603b53cdea
--- /dev/null
+++ b/layout/xul/crashtests/384373-1.xhtml
@@ -0,0 +1,10 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+onerror="var x=document.getElementsByTagName('*');x[Math.floor(Math.random()*x.length)].focus()"
+onblur="event.originalTarget.parentNode.parentNode.removeChild(event.originalTarget.parentNode)">
+<script xmlns="http://www.w3.org/1999/xhtml">setTimeout(function() {window.location.reload()}, 200);</script>
+
+<broadcasterset style="display: block;">
+ <broadcaster style="display: block;"></broadcaster>
+</broadcasterset>
+<preferences></preferences>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/384373-2.xhtml b/layout/xul/crashtests/384373-2.xhtml
new file mode 100644
index 0000000000..1d56394e31
--- /dev/null
+++ b/layout/xul/crashtests/384373-2.xhtml
@@ -0,0 +1,4 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onerror="document.getElementsByTagName('*')[1].focus()" onfocus="event.target.parentNode.removeChild(event.target)">
+<broadcaster style="display: block;"/>
+<preferences/>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/384373.html b/layout/xul/crashtests/384373.html
new file mode 100644
index 0000000000..a3658b86f8
--- /dev/null
+++ b/layout/xul/crashtests/384373.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait"><head>
+ <meta charset="utf-8">
+ <title>Testcase for bug 384373</title>
+<script>
+function reload() {
+ this.location.reload();
+}
+// Run the test for 1 second
+setTimeout(function() {
+ document.body.getBoundingClientRect();
+ document.documentElement.removeChild(document.body);
+ document.documentElement.className = "";
+ }, 2000);
+</script>
+</head>
+<body onload="document.body.getBoundingClientRect()">
+
+<iframe src="384373-1.xhtml"></iframe>
+<iframe onload="this.contentWindow.setTimeout(reload,500)" src="384373-2.xhtml"></iframe>
+
+</body>
+</html>
diff --git a/layout/xul/crashtests/384871-1-inner.xhtml b/layout/xul/crashtests/384871-1-inner.xhtml
new file mode 100644
index 0000000000..62efdb2608
--- /dev/null
+++ b/layout/xul/crashtests/384871-1-inner.xhtml
@@ -0,0 +1,9 @@
+<popup xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<script xmlns="http://www.w3.org/1999/xhtml">
+function doe(){
+document.documentElement.autoPosition = 'on';
+window.location.reload();
+}
+setTimeout(doe, 300);
+</script>
+</popup> \ No newline at end of file
diff --git a/layout/xul/crashtests/384871-1.html b/layout/xul/crashtests/384871-1.html
new file mode 100644
index 0000000000..bcd9f98bc8
--- /dev/null
+++ b/layout/xul/crashtests/384871-1.html
@@ -0,0 +1,9 @@
+<html class="reftest-wait">
+<head>
+<script>
+setTimeout('document.documentElement.className = ""', 500);
+</script>
+<body>
+<iframe src="384871-1-inner.xhtml"></iframe>
+</body>
+</html>
diff --git a/layout/xul/crashtests/386642.xhtml b/layout/xul/crashtests/386642.xhtml
new file mode 100644
index 0000000000..50db21a095
--- /dev/null
+++ b/layout/xul/crashtests/386642.xhtml
@@ -0,0 +1,31 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Bug 386642 Crash [@ IsCanvasFrame] while opening context menu or changing styles">
+<toolbarbutton type="menu" id="a">
+<menupopup id="b"/>
+</toolbarbutton>
+
+<style xmlns="http://www.w3.org/1999/xhtml">
+.one image {
+display: -moz-box;
+}
+image{
+display: none;
+}
+
+</style>
+<script><![CDATA[
+var gg=0;
+function doe() {
+ var a = document.getElementById('a');
+ if (!a.hasAttribute('class')) {
+ a.setAttribute('class', 'one');
+ } else {
+ a.removeAttribute('class');
+ }
+document.getElementById('b').hidePopup();
+}
+
+doe();
+setInterval(doe, 200);
+]]></script>
+</window>
diff --git a/layout/xul/crashtests/387080-1.xhtml b/layout/xul/crashtests/387080-1.xhtml
new file mode 100644
index 0000000000..4eb9bd784b
--- /dev/null
+++ b/layout/xul/crashtests/387080-1.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <description>
+ <foo height="1793689537164611773" width="20000238421986669650" />
+ </description>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/391974-1-inner.xhtml b/layout/xul/crashtests/391974-1-inner.xhtml
new file mode 100644
index 0000000000..f13aa2110f
--- /dev/null
+++ b/layout/xul/crashtests/391974-1-inner.xhtml
@@ -0,0 +1,19 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<menuitem>
+<tooltip/>
+<box/>
+</menuitem>
+
+<script xmlns="http://www.w3.org/1999/xhtml">
+function doe2() {
+document.getElementsByTagName('menuitem')[0].setAttribute('description', 'tetx');
+}
+
+function doe3() {
+document.getElementsByTagName('menuitem')[0].removeAttribute('description');
+document.getElementsByTagName('tooltip')[0].setAttribute('ordinal', '0');
+}
+setTimeout(doe2,150);
+setTimeout(doe3,200);
+</script>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/391974-1.html b/layout/xul/crashtests/391974-1.html
new file mode 100644
index 0000000000..6946d66182
--- /dev/null
+++ b/layout/xul/crashtests/391974-1.html
@@ -0,0 +1,9 @@
+<html class="reftest-wait">
+<head>
+<script>
+setTimeout('document.documentElement.className = ""', 1000);
+</script>
+<body>
+<iframe src="391974-1-inner.xhtml"></iframe>
+</body>
+</html>
diff --git a/layout/xul/crashtests/402912-1.xhtml b/layout/xul/crashtests/402912-1.xhtml
new file mode 100644
index 0000000000..b2cb98dc5a
--- /dev/null
+++ b/layout/xul/crashtests/402912-1.xhtml
@@ -0,0 +1,5 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<body>
+<xul:vbox equalsize="always"><xul:hbox flex="1"><span><xul:hbox width="10" height="10"/></span><xul:button /></xul:hbox><xul:hbox maxheight="0"/></xul:vbox>
+</body>
+</html>
diff --git a/layout/xul/crashtests/404192.xhtml b/layout/xul/crashtests/404192.xhtml
new file mode 100644
index 0000000000..4ad5af348b
--- /dev/null
+++ b/layout/xul/crashtests/404192.xhtml
@@ -0,0 +1,12 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait">
+<xul:titlebar id="a" style="overflow: auto;"/>
+
+<script>
+function doe() {
+document.getElementsByTagName('*')[1].focus();
+document.getElementsByTagName('*')[0].focus();
+document.documentElement.removeAttribute("class");
+}
+setTimeout(doe, 200);
+</script>
+</html>
diff --git a/layout/xul/crashtests/408904-1.xhtml b/layout/xul/crashtests/408904-1.xhtml
new file mode 100644
index 0000000000..59f215c73b
--- /dev/null
+++ b/layout/xul/crashtests/408904-1.xhtml
@@ -0,0 +1 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"><grid><rows><label/></rows><columns><column><label/></column></columns></grid></window>
diff --git a/layout/xul/crashtests/412479-1.xhtml b/layout/xul/crashtests/412479-1.xhtml
new file mode 100644
index 0000000000..b1086a816e
--- /dev/null
+++ b/layout/xul/crashtests/412479-1.xhtml
@@ -0,0 +1,4 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head></head>
+<body><xul:menubar style="display: table-column; padding: 10px 3000px;"/></body>
+</html>
diff --git a/layout/xul/crashtests/417509.xhtml b/layout/xul/crashtests/417509.xhtml
new file mode 100644
index 0000000000..81703ada37
--- /dev/null
+++ b/layout/xul/crashtests/417509.xhtml
@@ -0,0 +1,7 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<span id="a" datasources="" xmlns="http://www.w3.org/1999/xhtml"/>
+<script xmlns="http://www.w3.org/1999/xhtml">
+document.documentElement.appendChild(document.getElementById('a'));
+
+</script>
+</window> \ No newline at end of file
diff --git a/layout/xul/crashtests/430356-1.xhtml b/layout/xul/crashtests/430356-1.xhtml
new file mode 100644
index 0000000000..8e7858904f
--- /dev/null
+++ b/layout/xul/crashtests/430356-1.xhtml
@@ -0,0 +1,5 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<body style="visibility: collapse;">
+<tabpanels xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" style="width: max-content;"></tabpanels>
+</body>
+</html>
diff --git a/layout/xul/crashtests/464407-1.xhtml b/layout/xul/crashtests/464407-1.xhtml
new file mode 100644
index 0000000000..83666a6a46
--- /dev/null
+++ b/layout/xul/crashtests/464407-1.xhtml
@@ -0,0 +1,9 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+</head>
+<body>
+
+<xul:radio style="overflow: auto; height: 72057594037927940pt; display: table-cell;"/>
+
+</body>
+</html>
diff --git a/layout/xul/crashtests/470063-1.html b/layout/xul/crashtests/470063-1.html
new file mode 100644
index 0000000000..11c01b30e4
--- /dev/null
+++ b/layout/xul/crashtests/470063-1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.removeChild(document.documentElement)
+ document.appendChild(document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "hbox"));
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/layout/xul/crashtests/470272.html b/layout/xul/crashtests/470272.html
new file mode 100644
index 0000000000..5caf12d636
--- /dev/null
+++ b/layout/xul/crashtests/470272.html
@@ -0,0 +1,21 @@
+<html>
+<head>
+<script>
+function doe2(i) {
+document.documentElement.offsetHeight;
+document.getElementById('a').setAttribute('style', 'display: -moz-inline-box;');
+document.documentElement.offsetHeight;
+}
+</script>
+</head>
+<body style="float: right; column-count: 2; height: 20%;" onload="setTimeout(doe2,0);">
+ <div style="display: none;"></div>
+ <ul style="display: -moz-inline-box;"></ul>
+ <span id="a">
+ <ul style="display: -moz-box; overflow: scroll;"></ul>
+ <span style="display: -moz-inline-box; height: 10px;">
+ <span style="position: absolute;"></span>
+ </span>
+ </span>
+</body>
+</html>
diff --git a/layout/xul/crashtests/538308-1.xhtml b/layout/xul/crashtests/538308-1.xhtml
new file mode 100644
index 0000000000..477c725ed1
--- /dev/null
+++ b/layout/xul/crashtests/538308-1.xhtml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="run()">
+
+ <tree id="tr" flex="1">
+ <treecols>
+ <treecol/>
+ </treecols>
+ <treechildren>
+ <html:optgroup id="group">
+ <html:option id="victim" label="never see this"/>
+ </html:optgroup>
+ </treechildren>
+ </tree>
+
+ <script type="text/javascript"><![CDATA[
+ function run() {
+ group = document.getElementById("group");
+ tc = document.createXULElement("treechildren");
+ group.appendChild(tc);
+
+ v = document.getElementById("victim");
+ v.remove();
+ v = null;
+
+ tree = document.getElementById("tr");
+ col = tree.columns[0];
+ alert(tree.view.getItemAtIndex(1, col));
+ }
+ ]]></script>
+</window>
diff --git a/layout/xul/crashtests/557174-1.xml b/layout/xul/crashtests/557174-1.xml
new file mode 100644
index 0000000000..02850a2db9
--- /dev/null
+++ b/layout/xul/crashtests/557174-1.xml
@@ -0,0 +1 @@
+<ther:window xmlns:ther="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" a="" e=""><HTML><ther:statusbar l="" c=""><ther:menulist d=""><ther:menu t="" i="" l=""><mat:h xmlns:mat="http://www.w3.org/1998/Math/MathML" w=""/></ther:menu><ther:menupopup p=""/><ther:menu a="" t="" l=""><ther:menuseparator u="" x=""><xht:html xmlns:xht="http://www.w3.org/1999/xhtml" x=""><xht:body d=""><xht:abbr d=""><xht:abbr p=""><xht:small s=""><xht:a s=""><xht:var e=""><xht:samp e=""><xht:code p=""><xht:b e=""><xht:b d=""><xht:del t=""><xht:h4 r=""><xht:var l=""><xht:i r=""><xht:em r=""><xht:em n=""><xht:map g=""><xht:isindex d=""/></xht:map></xht:em></xht:em></xht:i></xht:var></xht:h4></xht:del></xht:b></xht:b></xht:code></xht:samp></xht:var></xht:a></xht:small></xht:abbr></xht:abbr></xht:body></xht:html></ther:menuseparator></ther:menu></ther:menulist></ther:statusbar></HTML></ther:window> \ No newline at end of file
diff --git a/layout/xul/crashtests/564705-1.xhtml b/layout/xul/crashtests/564705-1.xhtml
new file mode 100644
index 0000000000..b0f29bef7a
--- /dev/null
+++ b/layout/xul/crashtests/564705-1.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"><label value="&#x2026;" accesskey="b"></label></window>
+
diff --git a/layout/xul/crashtests/583957-1.html b/layout/xul/crashtests/583957-1.html
new file mode 100644
index 0000000000..48d29fc1c6
--- /dev/null
+++ b/layout/xul/crashtests/583957-1.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+<script>
+
+function boom()
+{
+ window.addEventListener("DOMSubtreeModified", function(){});
+
+ var m = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "menuitem");
+ document.body.appendChild(m);
+ m.setAttribute("type", "checkbox");
+ m.setAttribute("checked", "true");
+ m.removeAttribute("type");
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/layout/xul/crashtests/617089.html b/layout/xul/crashtests/617089.html
new file mode 100644
index 0000000000..22e5f6d535
--- /dev/null
+++ b/layout/xul/crashtests/617089.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <body>
+ <div style="display: -moz-inline-box;">
+ <table style="height: 101%;"><tbody><tr><td><div></div></td></tr></tbody></table>
+ <table style="height: 101%;"><tbody><tr><td><div></div></td></tr></tbody></table>
+ </div>
+ </body>
+</html>
diff --git a/layout/xul/crashtests/716503.html b/layout/xul/crashtests/716503.html
new file mode 100644
index 0000000000..250ad2ba40
--- /dev/null
+++ b/layout/xul/crashtests/716503.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<style>
+div::before {
+ content: "j";
+ display:-moz-inline-box;
+}
+</style>
+<body><div></div></body>
+</html>
diff --git a/layout/xul/crashtests/crashtests.list b/layout/xul/crashtests/crashtests.list
new file mode 100644
index 0000000000..ef7942ba6b
--- /dev/null
+++ b/layout/xul/crashtests/crashtests.list
@@ -0,0 +1,53 @@
+load chrome://reftest/content/crashtests/layout/xul/crashtests/131008-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/137216-1.xhtml
+load 140218-1.xml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/151826-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/168724-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/189814-1.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/289410-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/291702-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/291702-2.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/291702-3.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/294371-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/322786-1.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/325377.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/326879-1.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/329327-1.xhtml
+load 329407-1.xml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/336962-1.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/344228-1.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/365151.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/366112-1.xhtml
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/366203-1.xhtml
+load 367185-1.xhtml
+load 369942-1.xhtml
+load 376137-1.html
+load 376137-2.html
+load 378961.html
+load 381862.html
+load chrome://reftest/content/crashtests/layout/xul/crashtests/382746-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/382899-1.xhtml
+load 384037-1.xhtml
+load 384105-1.html
+load 384373.html
+load 384871-1.html
+load chrome://reftest/content/crashtests/layout/xul/crashtests/386642.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/387080-1.xhtml
+load 391974-1.html
+load 402912-1.xhtml
+load 404192.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/408904-1.xhtml
+load 412479-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/417509.xhtml
+load 430356-1.xhtml
+asserts(0-1) load 464407-1.xhtml # Bugs 450974, 1267054, 718883
+load 470063-1.html
+load 470272.html
+skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/538308-1.xhtml
+load 557174-1.xml
+load chrome://reftest/content/crashtests/layout/xul/crashtests/564705-1.xhtml
+load 583957-1.html
+load 617089.html
+load menulist-focused.xhtml
+load 716503.html
+load chrome://reftest/content/crashtests/layout/xul/crashtests/1379332-2.xhtml
diff --git a/layout/xul/crashtests/menulist-focused.xhtml b/layout/xul/crashtests/menulist-focused.xhtml
new file mode 100644
index 0000000000..7a09a838d7
--- /dev/null
+++ b/layout/xul/crashtests/menulist-focused.xhtml
@@ -0,0 +1,5 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<body>
+<xul:menulist focused="true"/>
+</body>
+</html>
diff --git a/layout/xul/moz.build b/layout/xul/moz.build
new file mode 100644
index 0000000000..9acbdf4826
--- /dev/null
+++ b/layout/xul/moz.build
@@ -0,0 +1,58 @@
+# -*- 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", "XUL")
+
+if CONFIG["ENABLE_TESTS"]:
+ MOCHITEST_MANIFESTS += ["test/mochitest.ini"]
+ MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"]
+ BROWSER_CHROME_MANIFESTS += ["test/browser.ini"]
+
+EXPORTS += [
+ "nsIScrollbarMediator.h",
+ "nsXULPopupManager.h",
+ "nsXULTooltipListener.h",
+]
+
+UNIFIED_SOURCES += [
+ "nsBox.cpp",
+ "nsBoxFrame.cpp",
+ "nsBoxLayout.cpp",
+ "nsBoxLayoutState.cpp",
+ "nsImageBoxFrame.cpp",
+ "nsLeafBoxFrame.cpp",
+ "nsMenuBarFrame.cpp",
+ "nsMenuBarListener.cpp",
+ "nsMenuPopupFrame.cpp",
+ "nsRepeatService.cpp",
+ "nsScrollbarButtonFrame.cpp",
+ "nsScrollbarFrame.cpp",
+ "nsSliderFrame.cpp",
+ "nsSplitterFrame.cpp",
+ "nsSprocketLayout.cpp",
+ "nsTextBoxFrame.cpp",
+ "nsXULPopupManager.cpp",
+ "nsXULTooltipListener.cpp",
+]
+
+DIRS += ["tree"]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ CFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "../base",
+ "../generic",
+ "../painting",
+ "../style",
+ "/dom/base",
+ "/dom/xul",
+]
diff --git a/layout/xul/nsBox.cpp b/layout/xul/nsBox.cpp
new file mode 100644
index 0000000000..3dd11d8a3f
--- /dev/null
+++ b/layout/xul/nsBox.cpp
@@ -0,0 +1,607 @@
+/* -*- 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/Attributes.h"
+#include "mozilla/StaticPtr.h"
+#include "nsIFrame.h"
+
+#include "nsBoxLayoutState.h"
+#include "nsBoxFrame.h"
+#include "nsDOMAttributeMap.h"
+#include "nsPresContext.h"
+#include "nsCOMPtr.h"
+#include "nsIContent.h"
+#include "nsContainerFrame.h"
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsITheme.h"
+#include "nsBoxLayout.h"
+#include "nsLayoutUtils.h"
+#include "mozilla/dom/Attr.h"
+#include "mozilla/dom/Element.h"
+#include <algorithm>
+
+using namespace mozilla;
+
+nsresult nsIFrame::BeginXULLayout(nsBoxLayoutState& aState) {
+ // mark ourselves as dirty so no child under us
+ // can post an incremental layout.
+ // XXXldb Is this still needed?
+ AddStateBits(NS_FRAME_HAS_DIRTY_CHILDREN);
+
+ if (HasAnyStateBits(NS_FRAME_IS_DIRTY)) {
+ // If the parent is dirty, all the children are dirty (ReflowInput
+ // does this too).
+ nsIFrame* box;
+ for (box = GetChildXULBox(this); box; box = GetNextXULBox(box))
+ box->MarkSubtreeDirty();
+ }
+
+ // Another copy-over from ReflowInput.
+ // Since we are in reflow, we don't need to store these properties anymore.
+ RemoveProperty(UsedBorderProperty());
+ RemoveProperty(UsedPaddingProperty());
+ RemoveProperty(UsedMarginProperty());
+
+ return NS_OK;
+}
+
+nsresult nsIFrame::EndXULLayout(nsBoxLayoutState& aState) {
+ return SyncXULLayout(aState);
+}
+
+nsresult nsIFrame::GetXULClientRect(nsRect& aClientRect) {
+ aClientRect = mRect;
+ aClientRect.MoveTo(0, 0);
+
+ nsMargin borderPadding;
+ GetXULBorderAndPadding(borderPadding);
+
+ aClientRect.Deflate(borderPadding);
+
+ if (aClientRect.width < 0) aClientRect.width = 0;
+
+ if (aClientRect.height < 0) aClientRect.height = 0;
+
+ return NS_OK;
+}
+
+void nsIFrame::SetXULBounds(nsBoxLayoutState& aState, const nsRect& aRect,
+ bool aRemoveOverflowAreas) {
+ nsRect rect(mRect);
+
+ ReflowChildFlags flags = GetXULLayoutFlags() | aState.LayoutFlags();
+
+ if ((flags & ReflowChildFlags::NoMoveFrame) ==
+ ReflowChildFlags::NoMoveFrame) {
+ SetSize(aRect.Size());
+ } else {
+ SetRect(aRect);
+ }
+
+ // Nuke the overflow area. The caller is responsible for restoring
+ // it if necessary.
+ if (aRemoveOverflowAreas) {
+ // remove the previously stored overflow area
+ ClearOverflowRects();
+ }
+
+ if (!(flags & ReflowChildFlags::NoMoveView)) {
+ nsContainerFrame::PositionFrameView(this);
+ if ((rect.x != aRect.x) || (rect.y != aRect.y))
+ nsContainerFrame::PositionChildViews(this);
+ }
+}
+
+nsresult nsIFrame::GetXULBorderAndPadding(nsMargin& aBorderAndPadding) {
+ aBorderAndPadding.SizeTo(0, 0, 0, 0);
+ nsresult rv = GetXULBorder(aBorderAndPadding);
+ if (NS_FAILED(rv)) return rv;
+
+ nsMargin padding;
+ rv = GetXULPadding(padding);
+ if (NS_FAILED(rv)) return rv;
+
+ aBorderAndPadding += padding;
+
+ return rv;
+}
+
+nsresult nsIFrame::GetXULBorder(nsMargin& aBorder) {
+ aBorder.SizeTo(0, 0, 0, 0);
+
+ StyleAppearance appearance = StyleDisplay()->EffectiveAppearance();
+ if (appearance != StyleAppearance::None) {
+ // Go to the theme for the border.
+ nsPresContext* pc = PresContext();
+ nsITheme* theme = pc->Theme();
+ if (theme->ThemeSupportsWidget(pc, this, appearance)) {
+ LayoutDeviceIntMargin margin =
+ theme->GetWidgetBorder(pc->DeviceContext(), this, appearance);
+ aBorder =
+ LayoutDevicePixel::ToAppUnits(margin, pc->AppUnitsPerDevPixel());
+ return NS_OK;
+ }
+ }
+
+ aBorder = StyleBorder()->GetComputedBorder();
+
+ return NS_OK;
+}
+
+nsresult nsIFrame::GetXULPadding(nsMargin& aBorderAndPadding) {
+ StyleAppearance appearance = StyleDisplay()->EffectiveAppearance();
+ if (appearance != StyleAppearance::None) {
+ // Go to the theme for the padding.
+ nsPresContext* pc = PresContext();
+ nsITheme* theme = pc->Theme();
+ if (theme->ThemeSupportsWidget(pc, this, appearance)) {
+ LayoutDeviceIntMargin padding;
+ bool useThemePadding = theme->GetWidgetPadding(pc->DeviceContext(), this,
+ appearance, &padding);
+ if (useThemePadding) {
+ aBorderAndPadding =
+ LayoutDevicePixel::ToAppUnits(padding, pc->AppUnitsPerDevPixel());
+ return NS_OK;
+ }
+ }
+ }
+
+ aBorderAndPadding.SizeTo(0, 0, 0, 0);
+ StylePadding()->GetPadding(aBorderAndPadding);
+
+ return NS_OK;
+}
+
+nsresult nsIFrame::GetXULMargin(nsMargin& aMargin) {
+ aMargin.SizeTo(0, 0, 0, 0);
+ StyleMargin()->GetMargin(aMargin);
+
+ return NS_OK;
+}
+
+void nsIFrame::XULSizeNeedsRecalc(nsSize& aSize) {
+ aSize.width = -1;
+ aSize.height = -1;
+}
+
+void nsIFrame::XULCoordNeedsRecalc(nscoord& aCoord) { aCoord = -1; }
+
+bool nsIFrame::XULNeedsRecalc(const nsSize& aSize) {
+ return (aSize.width == -1 || aSize.height == -1);
+}
+
+bool nsIFrame::XULNeedsRecalc(nscoord aCoord) { return (aCoord == -1); }
+
+nsSize nsIFrame::GetUncachedXULPrefSize(nsBoxLayoutState& aBoxLayoutState) {
+ NS_ASSERTION(aBoxLayoutState.GetRenderingContext(),
+ "must have rendering context");
+
+ nsSize pref(0, 0);
+ DISPLAY_PREF_SIZE(this, pref);
+
+ if (IsXULCollapsed()) {
+ return pref;
+ }
+
+ AddXULBorderAndPadding(pref);
+ bool widthSet, heightSet;
+ nsIFrame::AddXULPrefSize(this, pref, widthSet, heightSet);
+
+ nsSize minSize = GetXULMinSize(aBoxLayoutState);
+ nsSize maxSize = GetXULMaxSize(aBoxLayoutState);
+ return XULBoundsCheck(minSize, pref, maxSize);
+}
+
+nsSize nsIFrame::GetUncachedXULMinSize(nsBoxLayoutState& aBoxLayoutState) {
+ NS_ASSERTION(aBoxLayoutState.GetRenderingContext(),
+ "must have rendering context");
+
+ nsSize min(0, 0);
+ DISPLAY_MIN_SIZE(this, min);
+
+ if (IsXULCollapsed()) {
+ return min;
+ }
+
+ AddXULBorderAndPadding(min);
+ bool widthSet, heightSet;
+ nsIFrame::AddXULMinSize(this, min, widthSet, heightSet);
+ return min;
+}
+
+nsSize nsIFrame::GetUncachedXULMaxSize(nsBoxLayoutState& aBoxLayoutState) {
+ NS_ASSERTION(aBoxLayoutState.GetRenderingContext(),
+ "must have rendering context");
+
+ nsSize maxSize(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE);
+ DISPLAY_MAX_SIZE(this, maxSize);
+
+ if (IsXULCollapsed()) {
+ return maxSize;
+ }
+
+ AddXULBorderAndPadding(maxSize);
+ bool widthSet, heightSet;
+ nsIFrame::AddXULMaxSize(this, maxSize, widthSet, heightSet);
+ return maxSize;
+}
+
+bool nsIFrame::IsXULCollapsed() {
+ return StyleVisibility()->mVisible == StyleVisibility::Collapse;
+}
+
+nsresult nsIFrame::XULLayout(nsBoxLayoutState& aState) {
+ NS_ASSERTION(aState.GetRenderingContext(), "must have rendering context");
+
+ nsIFrame* box = static_cast<nsIFrame*>(this);
+ DISPLAY_LAYOUT(box);
+
+ box->BeginXULLayout(aState);
+
+ box->DoXULLayout(aState);
+
+ box->EndXULLayout(aState);
+
+ return NS_OK;
+}
+
+bool nsIFrame::DoesClipChildrenInBothAxes() {
+ const nsStyleDisplay* display = StyleDisplay();
+ return display->mOverflowX == StyleOverflow::Clip &&
+ display->mOverflowY == StyleOverflow::Clip;
+}
+
+nsresult nsIFrame::SyncXULLayout(nsBoxLayoutState& aBoxLayoutState) {
+ /*
+ if (IsXULCollapsed()) {
+ CollapseChild(aBoxLayoutState, this, true);
+ return NS_OK;
+ }
+ */
+
+ if (HasAnyStateBits(NS_FRAME_IS_DIRTY)) {
+ XULRedraw(aBoxLayoutState);
+ }
+
+ RemoveStateBits(NS_FRAME_HAS_DIRTY_CHILDREN | NS_FRAME_IS_DIRTY |
+ NS_FRAME_FIRST_REFLOW | NS_FRAME_IN_REFLOW);
+
+ nsPresContext* presContext = aBoxLayoutState.PresContext();
+
+ ReflowChildFlags flags = GetXULLayoutFlags() | aBoxLayoutState.LayoutFlags();
+
+ nsRect inkOverflow;
+
+ if (XULComputesOwnOverflowArea()) {
+ inkOverflow = InkOverflowRect();
+ } else {
+ nsRect rect(nsPoint(0, 0), GetSize());
+ OverflowAreas overflowAreas(rect, rect);
+ if (!DoesClipChildrenInBothAxes() && !IsXULCollapsed()) {
+ // See if our child frames caused us to overflow after being laid
+ // out. If so, store the overflow area. This normally can't happen
+ // in XUL, but it can happen with the CSS 'outline' property and
+ // possibly with other exotic stuff (e.g. relatively positioned
+ // frames in HTML inside XUL).
+ nsLayoutUtils::UnionChildOverflow(this, overflowAreas);
+ }
+
+ FinishAndStoreOverflow(overflowAreas, GetSize());
+ inkOverflow = overflowAreas.InkOverflow();
+ }
+
+ nsView* view = GetView();
+ if (view) {
+ // Make sure the frame's view is properly sized and positioned and has
+ // things like opacity correct
+ nsContainerFrame::SyncFrameViewAfterReflow(presContext, this, view,
+ inkOverflow, flags);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsIFrame::XULRedraw(nsBoxLayoutState& aState) {
+ if (aState.PaintingDisabled()) return NS_OK;
+
+ // Unclear whether we could get away with just InvalidateFrame().
+ InvalidateFrameSubtree();
+
+ return NS_OK;
+}
+
+bool nsIFrame::AddXULPrefSize(nsIFrame* aBox, nsSize& aSize, bool& aWidthSet,
+ bool& aHeightSet) {
+ aWidthSet = false;
+ aHeightSet = false;
+
+ // add in the css min, max, pref
+ const nsStylePosition* position = aBox->StylePosition();
+
+ // see if the width or height was specifically set
+ // XXX Handle eStyleUnit_Enumerated?
+ // (Handling the eStyleUnit_Enumerated types requires
+ // GetXULPrefSize/GetXULMinSize methods that don't consider
+ // (min-/max-/)(width/height) properties.)
+ const auto& width = position->mWidth;
+ if (width.ConvertsToLength()) {
+ aSize.width = std::max(0, width.ToLength());
+ aWidthSet = true;
+ }
+
+ const auto& height = position->mHeight;
+ if (height.ConvertsToLength()) {
+ aSize.height = std::max(0, height.ToLength());
+ aHeightSet = true;
+ }
+
+ nsIContent* content = aBox->GetContent();
+ // ignore 'height' and 'width' attributes if the actual element is not XUL
+ // For example, we might be magic XUL frames whose primary content is an HTML
+ // <select>
+ if (content && content->IsXULElement()) {
+ nsAutoString value;
+ nsresult error;
+
+ content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::width, value);
+ if (!value.IsEmpty()) {
+ value.Trim("%");
+
+ aSize.width = nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error));
+ aWidthSet = true;
+ }
+
+ content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::height, value);
+ if (!value.IsEmpty()) {
+ value.Trim("%");
+
+ aSize.height =
+ nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error));
+ aHeightSet = true;
+ }
+ }
+
+ return (aWidthSet && aHeightSet);
+}
+
+bool nsIFrame::AddXULMinSize(nsIFrame* aBox, nsSize& aSize, bool& aWidthSet,
+ bool& aHeightSet) {
+ aWidthSet = false;
+ aHeightSet = false;
+
+ nsPresContext* pc = aBox->PresContext();
+
+ // See if a native theme wants to supply a minimum size.
+ const nsStyleDisplay* display = aBox->StyleDisplay();
+ if (display->HasAppearance()) {
+ nsITheme* theme = pc->Theme();
+ StyleAppearance appearance = display->EffectiveAppearance();
+ if (theme->ThemeSupportsWidget(pc, aBox, appearance)) {
+ LayoutDeviceIntSize size =
+ theme->GetMinimumWidgetSize(pc, aBox, appearance);
+ if (size.width) {
+ aSize.width = pc->DevPixelsToAppUnits(size.width);
+ aWidthSet = true;
+ }
+ if (size.height) {
+ aSize.height = pc->DevPixelsToAppUnits(size.height);
+ aHeightSet = true;
+ }
+ } else {
+ switch (appearance) {
+ case StyleAppearance::ScrollbarVertical:
+ case StyleAppearance::ScrollbarHorizontal: {
+ ComputedStyle* style = nsLayoutUtils::StyleForScrollbar(aBox);
+ auto sizes = theme->GetScrollbarSizes(
+ pc, style->StyleUIReset()->ScrollbarWidth(),
+ nsITheme::Overlay::No);
+ if (appearance == StyleAppearance::ScrollbarVertical) {
+ aSize.width = pc->DevPixelsToAppUnits(sizes.mVertical);
+ aWidthSet = true;
+ } else {
+ aSize.height = pc->DevPixelsToAppUnits(sizes.mHorizontal);
+ aHeightSet = true;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+
+ // add in the css min, max, pref
+ const nsStylePosition* position = aBox->StylePosition();
+ const auto& minWidth = position->mMinWidth;
+ if (minWidth.ConvertsToLength()) {
+ nscoord min = minWidth.ToLength();
+ if (!aWidthSet || min > aSize.width) {
+ aSize.width = min;
+ aWidthSet = true;
+ }
+ } else if (minWidth.ConvertsToPercentage()) {
+ NS_ASSERTION(minWidth.ToPercentage() == 0.0f,
+ "Non-zero percentage values not currently supported");
+ aSize.width = 0;
+ aWidthSet = true; // FIXME: should we really do this for
+ // nonzero values?
+ }
+ // XXX Handle ExtremumLength?
+ // (Handling them requires GetXULPrefSize/GetXULMinSize methods that don't
+ // consider (min-/max-/)(width/height) properties.
+ // calc() with percentage is treated like '0' (unset)
+
+ const auto& minHeight = position->mMinHeight;
+ if (minHeight.ConvertsToLength()) {
+ nscoord min = minHeight.ToLength();
+ if (!aHeightSet || min > aSize.height) {
+ aSize.height = min;
+ aHeightSet = true;
+ }
+ } else if (minHeight.ConvertsToPercentage()) {
+ NS_ASSERTION(position->mMinHeight.ToPercentage() == 0.0f,
+ "Non-zero percentage values not currently supported");
+ aSize.height = 0;
+ aHeightSet = true; // FIXME: should we really do this for
+ // nonzero values?
+ }
+ // calc() with percentage is treated like '0' (unset)
+
+ nsIContent* content = aBox->GetContent();
+ if (content && content->IsXULElement()) {
+ nsAutoString value;
+ nsresult error;
+
+ content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::minwidth,
+ value);
+ if (!value.IsEmpty()) {
+ value.Trim("%");
+
+ nscoord val = nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error));
+ if (val > aSize.width) aSize.width = val;
+ aWidthSet = true;
+ }
+
+ content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::minheight,
+ value);
+ if (!value.IsEmpty()) {
+ value.Trim("%");
+
+ nscoord val = nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error));
+ if (val > aSize.height) aSize.height = val;
+
+ aHeightSet = true;
+ }
+ }
+
+ return (aWidthSet && aHeightSet);
+}
+
+bool nsIFrame::AddXULMaxSize(nsIFrame* aBox, nsSize& aSize, bool& aWidthSet,
+ bool& aHeightSet) {
+ aWidthSet = false;
+ aHeightSet = false;
+
+ // add in the css min, max, pref
+ const nsStylePosition* position = aBox->StylePosition();
+
+ // and max
+ // see if the width or height was specifically set
+ // XXX Handle eStyleUnit_Enumerated?
+ // (Handling the eStyleUnit_Enumerated types requires
+ // GetXULPrefSize/GetXULMinSize methods that don't consider
+ // (min-/max-/)(width/height) properties.)
+ const auto& maxWidth = position->mMaxWidth;
+ if (maxWidth.ConvertsToLength()) {
+ aSize.width = maxWidth.ToLength();
+ aWidthSet = true;
+ }
+ // percentages and calc() with percentages are treated like 'none'
+
+ const auto& maxHeight = position->mMaxHeight;
+ if (maxHeight.ConvertsToLength()) {
+ aSize.height = maxHeight.ToLength();
+ aHeightSet = true;
+ }
+ // percentages and calc() with percentages are treated like 'none'
+
+ nsIContent* content = aBox->GetContent();
+ if (content && content->IsXULElement()) {
+ nsAutoString value;
+ nsresult error;
+
+ content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::maxwidth,
+ value);
+ if (!value.IsEmpty()) {
+ value.Trim("%");
+
+ nscoord val = nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error));
+ aSize.width = val;
+ aWidthSet = true;
+ }
+
+ content->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::maxheight,
+ value);
+ if (!value.IsEmpty()) {
+ value.Trim("%");
+
+ nscoord val = nsPresContext::CSSPixelsToAppUnits(value.ToInteger(&error));
+ aSize.height = val;
+
+ aHeightSet = true;
+ }
+ }
+
+ return (aWidthSet || aHeightSet);
+}
+
+void nsIFrame::AddXULBorderAndPadding(nsSize& aSize) {
+ AddXULBorderAndPadding(this, aSize);
+}
+
+void nsIFrame::AddXULBorderAndPadding(nsIFrame* aBox, nsSize& aSize) {
+ nsMargin borderPadding(0, 0, 0, 0);
+ aBox->GetXULBorderAndPadding(borderPadding);
+ AddXULMargin(aSize, borderPadding);
+}
+
+void nsIFrame::AddXULMargin(nsIFrame* aChild, nsSize& aSize) {
+ nsMargin margin(0, 0, 0, 0);
+ aChild->GetXULMargin(margin);
+ AddXULMargin(aSize, margin);
+}
+
+void nsIFrame::AddXULMargin(nsSize& aSize, const nsMargin& aMargin) {
+ if (aSize.width != NS_UNCONSTRAINEDSIZE)
+ aSize.width += aMargin.left + aMargin.right;
+
+ if (aSize.height != NS_UNCONSTRAINEDSIZE)
+ aSize.height += aMargin.top + aMargin.bottom;
+}
+
+nscoord nsIFrame::XULBoundsCheck(nscoord aMin, nscoord aPref, nscoord aMax) {
+ if (aPref > aMax) aPref = aMax;
+
+ if (aPref < aMin) aPref = aMin;
+
+ return aPref;
+}
+
+nsSize nsIFrame::XULBoundsCheckMinMax(const nsSize& aMinSize,
+ const nsSize& aMaxSize) {
+ return nsSize(std::max(aMaxSize.width, aMinSize.width),
+ std::max(aMaxSize.height, aMinSize.height));
+}
+
+nsSize nsIFrame::XULBoundsCheck(const nsSize& aMinSize, const nsSize& aPrefSize,
+ const nsSize& aMaxSize) {
+ return nsSize(
+ XULBoundsCheck(aMinSize.width, aPrefSize.width, aMaxSize.width),
+ XULBoundsCheck(aMinSize.height, aPrefSize.height, aMaxSize.height));
+}
+
+/*static*/
+nsIFrame* nsIFrame::GetChildXULBox(const nsIFrame* aFrame) {
+ // box layout ends at box-wrapped frames, so don't allow these frames
+ // to report child boxes.
+ return aFrame->IsXULBoxFrame() ? aFrame->PrincipalChildList().FirstChild()
+ : nullptr;
+}
+
+/*static*/
+nsIFrame* nsIFrame::GetNextXULBox(const nsIFrame* aFrame) {
+ return aFrame->GetParent() && aFrame->GetParent()->IsXULBoxFrame()
+ ? aFrame->GetNextSibling()
+ : nullptr;
+}
+
+/*static*/
+nsIFrame* nsIFrame::GetParentXULBox(const nsIFrame* aFrame) {
+ return aFrame->GetParent() && aFrame->GetParent()->IsXULBoxFrame()
+ ? aFrame->GetParent()
+ : nullptr;
+}
diff --git a/layout/xul/nsBoxFrame.cpp b/layout/xul/nsBoxFrame.cpp
new file mode 100644
index 0000000000..4a547dda98
--- /dev/null
+++ b/layout/xul/nsBoxFrame.cpp
@@ -0,0 +1,1035 @@
+/* -*- 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/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+// How boxes layout
+// ----------------
+// Boxes layout a bit differently than html. html does a bottom up layout. Where
+// boxes do a top down.
+//
+// 1) First thing a box does it goes out and askes each child for its min, max,
+// and preferred sizes.
+//
+// 2) It then adds them up to determine its size.
+//
+// 3) If the box was asked to layout it self intrinically it will layout its
+// children at their preferred size otherwise it will layout the child at
+// the size it was told to. It will squeeze or stretch its children if
+// Necessary.
+//
+// However there is a catch. Some html components like block frames can not
+// determine their preferred size. this is their size if they were laid out
+// intrinsically. So the box will flow the child to determine this can cache the
+// value.
+
+// Boxes and Incremental Reflow
+// ----------------------------
+// Boxes layout out top down by adding up their children's min, max, and
+// preferred sizes. Only problem is if a incremental reflow occurs. The
+// preferred size of a child deep in the hierarchy could change. And this could
+// change any number of syblings around the box. Basically any children in the
+// reflow chain must have their caches cleared so when asked for there current
+// size they can relayout themselves.
+
+#include "nsBoxFrame.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "gfxUtils.h"
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/CSSOrderAwareFrameIterator.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Touch.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "nsBoxLayout.h"
+#include "nsBoxLayoutState.h"
+#include "nsCOMPtr.h"
+#include "nsCSSAnonBoxes.h"
+#include "nsCSSRendering.h"
+#include "nsContainerFrame.h"
+#include "nsDisplayList.h"
+#include "nsGkAtoms.h"
+#include "nsHTMLParts.h"
+#include "nsIContent.h"
+#include "nsIFrameInlines.h"
+#include "nsIScrollableFrame.h"
+#include "nsITheme.h"
+#include "nsIWidget.h"
+#include "nsLayoutUtils.h"
+#include "nsNameSpaceManager.h"
+#include "nsPlaceholderFrame.h"
+#include "nsPresContext.h"
+#include "nsSliderFrame.h"
+#include "nsSprocketLayout.h"
+#include "nsStyleConsts.h"
+#include "nsTransform2D.h"
+#include "nsView.h"
+#include "nsViewManager.h"
+#include "nsWidgetsCID.h"
+
+// Needed for Print Preview
+
+#include "mozilla/TouchEvents.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::gfx;
+
+nsContainerFrame* NS_NewBoxFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsBoxFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsBoxFrame)
+
+#ifdef DEBUG
+NS_QUERYFRAME_HEAD(nsBoxFrame)
+ NS_QUERYFRAME_ENTRY(nsBoxFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+#endif
+
+nsBoxFrame::nsBoxFrame(ComputedStyle* aStyle, nsPresContext* aPresContext,
+ ClassID aID)
+ : nsContainerFrame(aStyle, aPresContext, aID), mAscent(0) {
+ AddStateBits(NS_STATE_IS_HORIZONTAL | NS_STATE_AUTO_STRETCH);
+
+ mValign = vAlign_Top;
+ mHalign = hAlign_Left;
+
+ // Use the static sprocket layout
+ nsCOMPtr<nsBoxLayout> layout;
+ NS_NewSprocketLayout(layout);
+ SetXULLayoutManager(layout);
+}
+
+nsBoxFrame::~nsBoxFrame() = default;
+
+nsIFrame* nsBoxFrame::SlowOrdinalGroupAwareSibling(nsIFrame* aBox, bool aNext) {
+ nsIFrame* parent = aBox->GetParent();
+ if (!parent) {
+ return nullptr;
+ }
+ CSSOrderAwareFrameIterator iter(
+ parent, FrameChildListID::Principal,
+ CSSOrderAwareFrameIterator::ChildFilter::IncludeAll,
+ CSSOrderAwareFrameIterator::OrderState::Unknown,
+ CSSOrderAwareFrameIterator::OrderingProperty::BoxOrdinalGroup);
+
+ nsIFrame* prevSibling = nullptr;
+ for (; !iter.AtEnd(); iter.Next()) {
+ nsIFrame* current = iter.get();
+ if (!aNext && current == aBox) {
+ return prevSibling;
+ }
+ if (aNext && prevSibling == aBox) {
+ return current;
+ }
+ prevSibling = current;
+ }
+ return nullptr;
+}
+
+void nsBoxFrame::SetInitialChildList(ChildListID aListID,
+ nsFrameList&& aChildList) {
+ nsContainerFrame::SetInitialChildList(aListID, std::move(aChildList));
+ if (aListID == FrameChildListID::Principal) {
+ // initialize our list of infos.
+ nsBoxLayoutState state(PresContext());
+ if (mLayoutManager)
+ mLayoutManager->ChildrenSet(this, state, mFrames.FirstChild());
+ }
+}
+
+/* virtual */
+void nsBoxFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) {
+ nsContainerFrame::DidSetComputedStyle(aOldComputedStyle);
+
+ // The values that CacheAttributes() computes depend on our style,
+ // so we need to recompute them here...
+ CacheAttributes();
+}
+
+/**
+ * Initialize us. This is a good time to get the alignment of the box
+ */
+void nsBoxFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsContainerFrame::Init(aContent, aParent, aPrevInFlow);
+
+ if (HasAnyStateBits(NS_FRAME_FONT_INFLATION_CONTAINER)) {
+ AddStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT);
+ }
+
+ MarkIntrinsicISizesDirty();
+
+ CacheAttributes();
+}
+
+void nsBoxFrame::CacheAttributes() {
+ /*
+ printf("Caching: ");
+ XULDumpBox(stdout);
+ printf("\n");
+ */
+
+ mValign = vAlign_Top;
+ mHalign = hAlign_Left;
+
+ bool orient = false;
+ GetInitialOrientation(orient);
+ if (orient)
+ AddStateBits(NS_STATE_IS_HORIZONTAL);
+ else
+ RemoveStateBits(NS_STATE_IS_HORIZONTAL);
+
+ bool normal = true;
+ GetInitialDirection(normal);
+ if (normal)
+ AddStateBits(NS_STATE_IS_DIRECTION_NORMAL);
+ else
+ RemoveStateBits(NS_STATE_IS_DIRECTION_NORMAL);
+
+ GetInitialVAlignment(mValign);
+ GetInitialHAlignment(mHalign);
+
+ bool autostretch = !!(mState & NS_STATE_AUTO_STRETCH);
+ GetInitialAutoStretch(autostretch);
+ if (autostretch)
+ AddStateBits(NS_STATE_AUTO_STRETCH);
+ else
+ RemoveStateBits(NS_STATE_AUTO_STRETCH);
+}
+
+bool nsBoxFrame::GetInitialHAlignment(nsBoxFrame::Halignment& aHalign) {
+ if (!GetContent()) return false;
+
+ // For horizontal boxes we're checking PACK. For vertical boxes we are
+ // checking ALIGN.
+ const nsStyleXUL* boxInfo = StyleXUL();
+ if (IsXULHorizontal()) {
+ switch (boxInfo->mBoxPack) {
+ case StyleBoxPack::Start:
+ aHalign = nsBoxFrame::hAlign_Left;
+ return true;
+ case StyleBoxPack::Center:
+ aHalign = nsBoxFrame::hAlign_Center;
+ return true;
+ case StyleBoxPack::End:
+ aHalign = nsBoxFrame::hAlign_Right;
+ return true;
+ default: // Nonsensical value. Just bail.
+ return false;
+ }
+ } else {
+ switch (boxInfo->mBoxAlign) {
+ case StyleBoxAlign::Start:
+ aHalign = nsBoxFrame::hAlign_Left;
+ return true;
+ case StyleBoxAlign::Center:
+ aHalign = nsBoxFrame::hAlign_Center;
+ return true;
+ case StyleBoxAlign::End:
+ aHalign = nsBoxFrame::hAlign_Right;
+ return true;
+ default: // Nonsensical value. Just bail.
+ return false;
+ }
+ }
+}
+
+bool nsBoxFrame::GetInitialVAlignment(nsBoxFrame::Valignment& aValign) {
+ if (!GetContent()) return false;
+ // For horizontal boxes we're checking ALIGN. For vertical boxes we are
+ // checking PACK.
+ const nsStyleXUL* boxInfo = StyleXUL();
+ if (IsXULHorizontal()) {
+ switch (boxInfo->mBoxAlign) {
+ case StyleBoxAlign::Start:
+ aValign = nsBoxFrame::vAlign_Top;
+ return true;
+ case StyleBoxAlign::Center:
+ aValign = nsBoxFrame::vAlign_Middle;
+ return true;
+ case StyleBoxAlign::Baseline:
+ aValign = nsBoxFrame::vAlign_BaseLine;
+ return true;
+ case StyleBoxAlign::End:
+ aValign = nsBoxFrame::vAlign_Bottom;
+ return true;
+ default: // Nonsensical value. Just bail.
+ return false;
+ }
+ } else {
+ switch (boxInfo->mBoxPack) {
+ case StyleBoxPack::Start:
+ aValign = nsBoxFrame::vAlign_Top;
+ return true;
+ case StyleBoxPack::Center:
+ aValign = nsBoxFrame::vAlign_Middle;
+ return true;
+ case StyleBoxPack::End:
+ aValign = nsBoxFrame::vAlign_Bottom;
+ return true;
+ default: // Nonsensical value. Just bail.
+ return false;
+ }
+ }
+}
+
+void nsBoxFrame::GetInitialOrientation(bool& aIsHorizontal) {
+ // see if we are a vertical or horizontal box.
+ if (!GetContent()) return;
+
+ const nsStyleXUL* boxInfo = StyleXUL();
+ if (boxInfo->mBoxOrient == StyleBoxOrient::Horizontal) {
+ aIsHorizontal = true;
+ } else {
+ aIsHorizontal = false;
+ }
+}
+
+void nsBoxFrame::GetInitialDirection(bool& aIsNormal) {
+ if (!GetContent()) return;
+
+ if (IsXULHorizontal()) {
+ // For horizontal boxes only, we initialize our value based off the CSS
+ // 'direction' property. This means that BiDI users will end up with
+ // horizontally inverted chrome.
+ //
+ // If text runs RTL then so do we.
+ aIsNormal = StyleVisibility()->mDirection == StyleDirection::Ltr;
+ if (GetContent()->IsElement()) {
+ Element* element = GetContent()->AsElement();
+
+ // Now see if we have an attribute. The attribute overrides
+ // the style system 'direction' property.
+ static Element::AttrValuesArray strings[] = {nsGkAtoms::ltr,
+ nsGkAtoms::rtl, nullptr};
+ int32_t index = element->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::dir, strings, eCaseMatters);
+ if (index >= 0) {
+ bool values[] = {true, false};
+ aIsNormal = values[index];
+ }
+ }
+ } else {
+ aIsNormal = true; // Assume a normal direction in the vertical case.
+ }
+
+ // Now check the style system to see if we should invert aIsNormal.
+ const nsStyleXUL* boxInfo = StyleXUL();
+ if (boxInfo->mBoxDirection == StyleBoxDirection::Reverse) {
+ aIsNormal = !aIsNormal; // Invert our direction.
+ }
+}
+
+/* Returns true if it was set.
+ */
+bool nsBoxFrame::GetInitialAutoStretch(bool& aStretch) {
+ if (!GetContent()) return false;
+
+ // Check the CSS box-align property.
+ const nsStyleXUL* boxInfo = StyleXUL();
+ aStretch = (boxInfo->mBoxAlign == StyleBoxAlign::Stretch);
+
+ return true;
+}
+
+void nsBoxFrame::DidReflow(nsPresContext* aPresContext,
+ const ReflowInput* aReflowInput) {
+ nsFrameState preserveBits =
+ mState & (NS_FRAME_IS_DIRTY | NS_FRAME_HAS_DIRTY_CHILDREN);
+ nsIFrame::DidReflow(aPresContext, aReflowInput);
+ AddStateBits(preserveBits);
+ if (preserveBits & NS_FRAME_IS_DIRTY) {
+ this->MarkSubtreeDirty();
+ }
+}
+
+bool nsBoxFrame::HonorPrintBackgroundSettings() const {
+ return !mContent->IsInNativeAnonymousSubtree() &&
+ nsContainerFrame::HonorPrintBackgroundSettings();
+}
+
+#ifdef DO_NOISY_REFLOW
+static int myCounter = 0;
+static void printSize(char* aDesc, nscoord aSize) {
+ printf(" %s: ", aDesc);
+ if (aSize == NS_UNCONSTRAINEDSIZE) {
+ printf("UC");
+ } else {
+ printf("%d", aSize);
+ }
+}
+#endif
+
+/* virtual */
+nscoord nsBoxFrame::GetMinISize(gfxContext* aRenderingContext) {
+ nscoord result;
+ DISPLAY_MIN_INLINE_SIZE(this, result);
+
+ nsBoxLayoutState state(PresContext(), aRenderingContext);
+ nsSize minSize = GetXULMinSize(state);
+
+ // GetXULMinSize returns border-box width, and we want to return content
+ // width. Since Reflow uses the reflow input's border and padding, we
+ // actually just want to subtract what GetXULMinSize added, which is the
+ // result of GetXULBorderAndPadding.
+ nsMargin bp;
+ GetXULBorderAndPadding(bp);
+
+ result = minSize.width - bp.LeftRight();
+ result = std::max(result, 0);
+
+ return result;
+}
+
+/* virtual */
+nscoord nsBoxFrame::GetPrefISize(gfxContext* aRenderingContext) {
+ nscoord result;
+ DISPLAY_PREF_INLINE_SIZE(this, result);
+
+ nsBoxLayoutState state(PresContext(), aRenderingContext);
+ nsSize prefSize = GetXULPrefSize(state);
+
+ // GetXULPrefSize returns border-box width, and we want to return content
+ // width. Since Reflow uses the reflow input's border and padding, we
+ // actually just want to subtract what GetXULPrefSize added, which is the
+ // result of GetXULBorderAndPadding.
+ nsMargin bp;
+ GetXULBorderAndPadding(bp);
+
+ result = prefSize.width - bp.LeftRight();
+ result = std::max(result, 0);
+
+ return result;
+}
+
+void nsBoxFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ // If you make changes to this method, please keep nsLeafBoxFrame::Reflow
+ // in sync, if the changes are applicable there.
+
+ DO_GLOBAL_REFLOW_COUNT("nsBoxFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ NS_ASSERTION(
+ aReflowInput.ComputedWidth() >= 0 && aReflowInput.ComputedHeight() >= 0,
+ "Computed Size < 0");
+
+#ifdef DO_NOISY_REFLOW
+ printf(
+ "\n-------------Starting BoxFrame Reflow ----------------------------\n");
+ printf("%p ** nsBF::Reflow %d ", this, myCounter++);
+
+ printSize("AW", aReflowInput.AvailableWidth());
+ printSize("AH", aReflowInput.AvailableHeight());
+ printSize("CW", aReflowInput.ComputedWidth());
+ printSize("CH", aReflowInput.ComputedHeight());
+
+ printf(" *\n");
+
+#endif
+
+ // create the layout state
+ nsBoxLayoutState state(aPresContext, aReflowInput.mRenderingContext,
+ &aReflowInput, aReflowInput.mReflowDepth);
+
+ WritingMode wm = aReflowInput.GetWritingMode();
+ LogicalSize computedSize = aReflowInput.ComputedSize();
+
+ LogicalMargin m = aReflowInput.ComputedLogicalBorderPadding(wm);
+ // GetXULBorderAndPadding(m);
+
+ LogicalSize prefSize(wm);
+
+ // if we are told to layout intrinsic then get our preferred size.
+ NS_ASSERTION(computedSize.ISize(wm) != NS_UNCONSTRAINEDSIZE,
+ "computed inline size should always be computed");
+ if (computedSize.BSize(wm) == NS_UNCONSTRAINEDSIZE) {
+ nsSize physicalPrefSize = GetXULPrefSize(state);
+ nsSize minSize = GetXULMinSize(state);
+ nsSize maxSize = GetXULMaxSize(state);
+ // XXXbz isn't GetXULPrefSize supposed to bounds-check for us?
+ physicalPrefSize = XULBoundsCheck(minSize, physicalPrefSize, maxSize);
+ prefSize = LogicalSize(wm, physicalPrefSize);
+ }
+
+ // get our desiredSize
+ computedSize.ISize(wm) += m.IStart(wm) + m.IEnd(wm);
+
+ if (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE) {
+ computedSize.BSize(wm) = prefSize.BSize(wm);
+ // prefSize is border-box but min/max constraints are content-box.
+ nscoord blockDirBorderPadding =
+ aReflowInput.ComputedLogicalBorderPadding(wm).BStartEnd(wm);
+ nscoord contentBSize = computedSize.BSize(wm) - blockDirBorderPadding;
+ // Note: contentHeight might be negative, but that's OK because min-height
+ // is never negative.
+ computedSize.BSize(wm) =
+ aReflowInput.ApplyMinMaxHeight(contentBSize) + blockDirBorderPadding;
+ } else {
+ computedSize.BSize(wm) += m.BStart(wm) + m.BEnd(wm);
+ }
+
+ nsSize physicalSize = computedSize.GetPhysicalSize(wm);
+ nsRect r(mRect.x, mRect.y, physicalSize.width, physicalSize.height);
+
+ SetXULBounds(state, r);
+
+ // layout our children
+ XULLayout(state);
+
+ // ok our child could have gotten bigger. So lets get its bounds
+
+ // get the ascent
+ LogicalSize boxSize = GetLogicalSize(wm);
+ nscoord ascent = boxSize.BSize(wm);
+
+ // getting the ascent could be a lot of work. Don't get it if
+ // we are the root. The viewport doesn't care about it.
+ if (!Style()->IsRootElementStyle()) {
+ ascent = GetXULBoxAscent(state);
+ }
+
+ aDesiredSize.SetSize(wm, boxSize);
+ aDesiredSize.SetBlockStartAscent(ascent);
+
+ aDesiredSize.mOverflowAreas = GetOverflowAreas();
+
+#ifdef DO_NOISY_REFLOW
+ {
+ printf("%p ** nsBF(done) W:%d H:%d ", this, aDesiredSize.Width(),
+ aDesiredSize.Height());
+
+ if (maxElementSize) {
+ printf("MW:%d\n", *maxElementWidth);
+ } else {
+ printf("MW:?\n");
+ }
+ }
+#endif
+
+ ReflowAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput, aStatus);
+}
+
+nsSize nsBoxFrame::GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) {
+ NS_ASSERTION(aBoxLayoutState.GetRenderingContext(),
+ "must have rendering context");
+
+ nsSize size(0, 0);
+ DISPLAY_PREF_SIZE(this, size);
+ if (!XULNeedsRecalc(mPrefSize)) {
+ size = mPrefSize;
+ return size;
+ }
+
+ if (IsXULCollapsed()) return size;
+
+ // if the size was not completely redefined in CSS then ask our children
+ bool widthSet, heightSet;
+ if (!nsIFrame::AddXULPrefSize(this, size, widthSet, heightSet)) {
+ if (mLayoutManager) {
+ nsSize layoutSize = mLayoutManager->GetXULPrefSize(this, aBoxLayoutState);
+ if (!widthSet) size.width = layoutSize.width;
+ if (!heightSet) size.height = layoutSize.height;
+ } else {
+ size = nsIFrame::GetUncachedXULPrefSize(aBoxLayoutState);
+ }
+ }
+
+ nsSize minSize = GetXULMinSize(aBoxLayoutState);
+ nsSize maxSize = GetXULMaxSize(aBoxLayoutState);
+ mPrefSize = XULBoundsCheck(minSize, size, maxSize);
+
+ return mPrefSize;
+}
+
+nscoord nsBoxFrame::GetXULBoxAscent(nsBoxLayoutState& aBoxLayoutState) {
+ if (!XULNeedsRecalc(mAscent)) {
+ return mAscent;
+ }
+
+ if (IsXULCollapsed()) {
+ return 0;
+ }
+
+ if (mLayoutManager) {
+ mAscent = mLayoutManager->GetAscent(this, aBoxLayoutState);
+ } else {
+ mAscent = GetXULPrefSize(aBoxLayoutState).height;
+ }
+
+ return mAscent;
+}
+
+nsSize nsBoxFrame::GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) {
+ NS_ASSERTION(aBoxLayoutState.GetRenderingContext(),
+ "must have rendering context");
+
+ nsSize size(0, 0);
+ DISPLAY_MIN_SIZE(this, size);
+ if (!XULNeedsRecalc(mMinSize)) {
+ size = mMinSize;
+ return size;
+ }
+
+ if (IsXULCollapsed()) return size;
+
+ // if the size was not completely redefined in CSS then ask our children
+ bool widthSet, heightSet;
+ if (!nsIFrame::AddXULMinSize(this, size, widthSet, heightSet)) {
+ if (mLayoutManager) {
+ nsSize layoutSize = mLayoutManager->GetXULMinSize(this, aBoxLayoutState);
+ if (!widthSet) size.width = layoutSize.width;
+ if (!heightSet) size.height = layoutSize.height;
+ } else {
+ size = nsIFrame::GetUncachedXULMinSize(aBoxLayoutState);
+ }
+ }
+
+ mMinSize = size;
+
+ return size;
+}
+
+nsSize nsBoxFrame::GetXULMaxSize(nsBoxLayoutState& aBoxLayoutState) {
+ NS_ASSERTION(aBoxLayoutState.GetRenderingContext(),
+ "must have rendering context");
+
+ nsSize size(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE);
+ DISPLAY_MAX_SIZE(this, size);
+ if (!XULNeedsRecalc(mMaxSize)) {
+ size = mMaxSize;
+ return size;
+ }
+
+ if (IsXULCollapsed()) return size;
+
+ // if the size was not completely redefined in CSS then ask our children
+ bool widthSet, heightSet;
+ if (!nsIFrame::AddXULMaxSize(this, size, widthSet, heightSet)) {
+ if (mLayoutManager) {
+ nsSize layoutSize = mLayoutManager->GetXULMaxSize(this, aBoxLayoutState);
+ if (!widthSet) size.width = layoutSize.width;
+ if (!heightSet) size.height = layoutSize.height;
+ } else {
+ size = nsIFrame::GetUncachedXULMaxSize(aBoxLayoutState);
+ }
+ }
+
+ mMaxSize = size;
+
+ return size;
+}
+
+/**
+ * If subclassing please subclass this method not layout.
+ * layout will call this method.
+ */
+NS_IMETHODIMP
+nsBoxFrame::DoXULLayout(nsBoxLayoutState& aState) {
+ ReflowChildFlags oldFlags = aState.LayoutFlags();
+ aState.SetLayoutFlags(ReflowChildFlags::Default);
+
+ nsresult rv = NS_OK;
+ if (mLayoutManager) {
+ XULCoordNeedsRecalc(mAscent);
+ rv = mLayoutManager->XULLayout(this, aState);
+ }
+
+ aState.SetLayoutFlags(oldFlags);
+
+ if (HasAbsolutelyPositionedChildren()) {
+ // Set up a |reflowInput| to pass into ReflowAbsoluteFrames
+ WritingMode wm = GetWritingMode();
+ ReflowInput reflowInput(
+ aState.PresContext(), this, aState.GetRenderingContext(),
+ LogicalSize(wm, GetLogicalSize().ISize(wm), NS_UNCONSTRAINEDSIZE));
+
+ // Set up a |desiredSize| to pass into ReflowAbsoluteFrames
+ ReflowOutput desiredSize(reflowInput);
+ desiredSize.Width() = mRect.width;
+ desiredSize.Height() = mRect.height;
+
+ // get the ascent (cribbed from ::Reflow)
+ nscoord ascent = mRect.height;
+
+ // getting the ascent could be a lot of work. Don't get it if
+ // we are the root. The viewport doesn't care about it.
+ if (!Style()->IsRootElementStyle()) {
+ ascent = GetXULBoxAscent(aState);
+ }
+ desiredSize.SetBlockStartAscent(ascent);
+ desiredSize.mOverflowAreas = GetOverflowAreas();
+
+ AddStateBits(NS_FRAME_IN_REFLOW);
+ // Set up a |reflowStatus| to pass into ReflowAbsoluteFrames
+ // (just a dummy value; hopefully that's OK)
+ nsReflowStatus reflowStatus;
+ ReflowAbsoluteFrames(aState.PresContext(), desiredSize, reflowInput,
+ reflowStatus);
+ RemoveStateBits(NS_FRAME_IN_REFLOW);
+ }
+
+ return rv;
+}
+
+void nsBoxFrame::DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) {
+ // clean up the container box's layout manager and child boxes
+ SetXULLayoutManager(nullptr);
+
+ nsContainerFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
+}
+
+/* virtual */
+void nsBoxFrame::MarkIntrinsicISizesDirty() {
+ XULSizeNeedsRecalc(mPrefSize);
+ XULSizeNeedsRecalc(mMinSize);
+ XULSizeNeedsRecalc(mMaxSize);
+ XULCoordNeedsRecalc(mAscent);
+
+ if (mLayoutManager) {
+ nsBoxLayoutState state(PresContext());
+ mLayoutManager->IntrinsicISizesDirty(this, state);
+ }
+
+ nsContainerFrame::MarkIntrinsicISizesDirty();
+}
+
+void nsBoxFrame::RemoveFrame(ChildListID aListID, nsIFrame* aOldFrame) {
+ MOZ_ASSERT(aListID == FrameChildListID::Principal,
+ "We don't support out-of-flow kids");
+
+ nsPresContext* presContext = PresContext();
+ nsBoxLayoutState state(presContext);
+
+ // remove the child frame
+ mFrames.RemoveFrame(aOldFrame);
+
+ // notify the layout manager
+ if (mLayoutManager) mLayoutManager->ChildrenRemoved(this, state, aOldFrame);
+
+ // destroy the child frame
+ aOldFrame->Destroy();
+
+ // mark us dirty and generate a reflow command
+ PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors,
+ NS_FRAME_HAS_DIRTY_CHILDREN);
+}
+
+void nsBoxFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame,
+ const nsLineList::iterator* aPrevFrameLine,
+ nsFrameList&& aFrameList) {
+ NS_ASSERTION(!aPrevFrame || aPrevFrame->GetParent() == this,
+ "inserting after sibling frame with different parent");
+ NS_ASSERTION(!aPrevFrame || mFrames.ContainsFrame(aPrevFrame),
+ "inserting after sibling frame not in our child list");
+ MOZ_ASSERT(aListID == FrameChildListID::Principal,
+ "We don't support out-of-flow kids");
+
+ nsBoxLayoutState state(PresContext());
+
+ // insert the child frames
+ const nsFrameList::Slice& newFrames =
+ mFrames.InsertFrames(this, aPrevFrame, std::move(aFrameList));
+
+ // notify the layout manager
+ if (mLayoutManager)
+ mLayoutManager->ChildrenInserted(this, state, aPrevFrame, newFrames);
+
+ PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors,
+ NS_FRAME_HAS_DIRTY_CHILDREN);
+}
+
+void nsBoxFrame::AppendFrames(ChildListID aListID, nsFrameList&& aFrameList) {
+ MOZ_ASSERT(aListID == FrameChildListID::Principal,
+ "We don't support out-of-flow kids");
+
+ nsBoxLayoutState state(PresContext());
+
+ // append the new frames
+ const nsFrameList::Slice& newFrames =
+ mFrames.AppendFrames(this, std::move(aFrameList));
+
+ // notify the layout manager
+ if (mLayoutManager) mLayoutManager->ChildrenAppended(this, state, newFrames);
+
+ // XXXbz why is this NS_FRAME_FIRST_REFLOW check here?
+ if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) {
+ PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors,
+ NS_FRAME_HAS_DIRTY_CHILDREN);
+ }
+}
+
+nsresult nsBoxFrame::AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) {
+ nsresult rv =
+ nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+
+ // Ignore 'width', 'height', 'screenX', 'screenY' and 'sizemode' on a
+ // <window>.
+ if (mContent->IsXULElement(nsGkAtoms::window) &&
+ (nsGkAtoms::width == aAttribute || nsGkAtoms::height == aAttribute ||
+ nsGkAtoms::screenX == aAttribute || nsGkAtoms::screenY == aAttribute ||
+ nsGkAtoms::sizemode == aAttribute)) {
+ return rv;
+ }
+
+ if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height ||
+ aAttribute == nsGkAtoms::align || aAttribute == nsGkAtoms::valign ||
+ aAttribute == nsGkAtoms::minwidth || aAttribute == nsGkAtoms::maxwidth ||
+ aAttribute == nsGkAtoms::minheight ||
+ aAttribute == nsGkAtoms::maxheight || aAttribute == nsGkAtoms::orient ||
+ aAttribute == nsGkAtoms::pack || aAttribute == nsGkAtoms::dir) {
+ if (aAttribute == nsGkAtoms::align || aAttribute == nsGkAtoms::valign ||
+ aAttribute == nsGkAtoms::orient || aAttribute == nsGkAtoms::pack ||
+ aAttribute == nsGkAtoms::dir) {
+ mValign = nsBoxFrame::vAlign_Top;
+ mHalign = nsBoxFrame::hAlign_Left;
+
+ bool orient = true;
+ GetInitialOrientation(orient);
+ if (orient)
+ AddStateBits(NS_STATE_IS_HORIZONTAL);
+ else
+ RemoveStateBits(NS_STATE_IS_HORIZONTAL);
+
+ bool normal = true;
+ GetInitialDirection(normal);
+ if (normal)
+ AddStateBits(NS_STATE_IS_DIRECTION_NORMAL);
+ else
+ RemoveStateBits(NS_STATE_IS_DIRECTION_NORMAL);
+
+ GetInitialVAlignment(mValign);
+ GetInitialHAlignment(mHalign);
+
+ bool autostretch = !!(mState & NS_STATE_AUTO_STRETCH);
+ GetInitialAutoStretch(autostretch);
+ if (autostretch)
+ AddStateBits(NS_STATE_AUTO_STRETCH);
+ else
+ RemoveStateBits(NS_STATE_AUTO_STRETCH);
+ }
+
+ PresShell()->FrameNeedsReflow(
+ this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
+ } else if (aAttribute == nsGkAtoms::rows &&
+ mContent->IsXULElement(nsGkAtoms::tree)) {
+ // Reflow ourselves and all our children if "rows" changes, since
+ // nsTreeBodyFrame's layout reads this from its parent (this frame).
+ PresShell()->FrameNeedsReflow(
+ this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
+ }
+
+ return rv;
+}
+
+void nsBoxFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ nsDisplayListCollection tempLists(aBuilder);
+ DisplayBorderBackgroundOutline(aBuilder, aLists);
+
+ BuildDisplayListForChildren(aBuilder, aLists);
+
+ // see if we have to draw a selection frame around this container
+ DisplaySelectionOverlay(aBuilder, aLists.Content());
+}
+
+void nsBoxFrame::BuildDisplayListForChildren(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ // Iterate over the children in CSS order.
+ auto iter = CSSOrderAwareFrameIterator(
+ this, FrameChildListID::Principal,
+ CSSOrderAwareFrameIterator::ChildFilter::IncludeAll,
+ CSSOrderAwareFrameIterator::OrderState::Unknown,
+ CSSOrderAwareFrameIterator::OrderingProperty::BoxOrdinalGroup);
+ // Put each child's background onto the BlockBorderBackgrounds list
+ // to emulate the existing two-layer XUL painting scheme.
+ nsDisplayListSet set(aLists, aLists.BlockBorderBackgrounds());
+ for (; !iter.AtEnd(); iter.Next()) {
+ BuildDisplayListForChild(aBuilder, iter.get(), set);
+ }
+}
+
+#ifdef DEBUG_FRAME_DUMP
+nsresult nsBoxFrame::GetFrameName(nsAString& aResult) const {
+ return MakeFrameName(u"Box"_ns, aResult);
+}
+#endif
+
+nsresult nsBoxFrame::LayoutChildAt(nsBoxLayoutState& aState, nsIFrame* aBox,
+ const nsRect& aRect) {
+ // get the current rect
+ nsRect oldRect(aBox->GetRect());
+ aBox->SetXULBounds(aState, aRect);
+
+ bool layout = aBox->IsSubtreeDirty();
+
+ if (layout ||
+ (oldRect.width != aRect.width || oldRect.height != aRect.height)) {
+ return aBox->XULLayout(aState);
+ }
+
+ return NS_OK;
+}
+
+namespace mozilla {
+
+/**
+ * This wrapper class lets us redirect mouse hits from descendant frames
+ * of a menu to the menu itself, if they didn't specify 'allowevents'.
+ *
+ * The wrapper simply turns a hit on a descendant element
+ * into a hit on the menu itself, unless there is an element between the target
+ * and the menu with the "allowevents" attribute.
+ *
+ * This is used by nsMenuFrame and nsTreeColFrame.
+ *
+ * Note that turning a hit on a descendant element into nullptr, so events
+ * could fall through to the menu background, might be an appealing
+ * simplification but it would mean slightly strange behaviour in some cases,
+ * because grabber wrappers can be created for many individual lists and items,
+ * so the exact fallthrough behaviour would be complex. E.g. an element with
+ * "allowevents" on top of the Content() list could receive the event even if it
+ * was covered by a PositionedDescenants() element without "allowevents". It is
+ * best to never convert a non-null hit into null.
+ */
+// REVIEW: This is roughly of what nsMenuFrame::GetFrameForPoint used to do.
+// I've made 'allowevents' affect child elements because that seems the only
+// reasonable thing to do.
+class nsDisplayXULEventRedirector final : public nsDisplayWrapList {
+ public:
+ nsDisplayXULEventRedirector(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
+ nsDisplayItem* aItem, nsIFrame* aTargetFrame)
+ : nsDisplayWrapList(aBuilder, aFrame, aItem),
+ mTargetFrame(aTargetFrame) {}
+ nsDisplayXULEventRedirector(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame,
+ nsDisplayList* aList, nsIFrame* aTargetFrame)
+ : nsDisplayWrapList(aBuilder, aFrame, aList),
+ mTargetFrame(aTargetFrame) {}
+ virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect,
+ HitTestState* aState,
+ nsTArray<nsIFrame*>* 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("XULEventRedirector", TYPE_XUL_EVENT_REDIRECTOR)
+ private:
+ nsIFrame* mTargetFrame;
+};
+
+void nsDisplayXULEventRedirector::HitTest(nsDisplayListBuilder* aBuilder,
+ const nsRect& aRect,
+ HitTestState* aState,
+ nsTArray<nsIFrame*>* aOutFrames) {
+ nsTArray<nsIFrame*> outFrames;
+ mList.HitTest(aBuilder, aRect, aState, &outFrames);
+
+ bool topMostAdded = false;
+ uint32_t localLength = outFrames.Length();
+
+ for (uint32_t i = 0; i < localLength; i++) {
+ for (nsIContent* content = outFrames.ElementAt(i)->GetContent();
+ content && content != mTargetFrame->GetContent();
+ content = content->GetParent()) {
+ if (!content->IsElement() ||
+ !content->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::allowevents,
+ nsGkAtoms::_true, eCaseMatters)) {
+ continue;
+ }
+
+ // Events are allowed on 'frame', so let it go.
+ aOutFrames->AppendElement(outFrames.ElementAt(i));
+ topMostAdded = true;
+ }
+
+ // If there was no hit on the topmost frame or its ancestors,
+ // add the target frame itself as the first candidate (see bug 562554).
+ if (!topMostAdded) {
+ topMostAdded = true;
+ aOutFrames->AppendElement(mTargetFrame);
+ }
+ }
+}
+
+} // namespace mozilla
+
+class nsXULEventRedirectorWrapper final : public nsDisplayItemWrapper {
+ public:
+ explicit nsXULEventRedirectorWrapper(nsIFrame* aTargetFrame)
+ : mTargetFrame(aTargetFrame) {}
+ virtual nsDisplayItem* WrapList(nsDisplayListBuilder* aBuilder,
+ nsIFrame* aFrame,
+ nsDisplayList* aList) override {
+ return MakeDisplayItem<nsDisplayXULEventRedirector>(aBuilder, aFrame, aList,
+ mTargetFrame);
+ }
+ virtual nsDisplayItem* WrapItem(nsDisplayListBuilder* aBuilder,
+ nsDisplayItem* aItem) override {
+ return MakeDisplayItem<nsDisplayXULEventRedirector>(
+ aBuilder, aItem->Frame(), aItem, mTargetFrame);
+ }
+
+ private:
+ nsIFrame* mTargetFrame;
+};
+
+void nsBoxFrame::WrapListsInRedirector(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aIn,
+ const nsDisplayListSet& aOut) {
+ nsXULEventRedirectorWrapper wrapper(this);
+ wrapper.WrapLists(aBuilder, this, aIn, aOut);
+}
+
+bool nsBoxFrame::GetEventPoint(WidgetGUIEvent* aEvent, nsPoint& aPoint) {
+ LayoutDeviceIntPoint refPoint;
+ bool res = GetEventPoint(aEvent, refPoint);
+ aPoint = nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, refPoint,
+ RelativeTo{this});
+ return res;
+}
+
+bool nsBoxFrame::GetEventPoint(WidgetGUIEvent* aEvent,
+ LayoutDeviceIntPoint& aPoint) {
+ NS_ENSURE_TRUE(aEvent, false);
+
+ WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent();
+ if (touchEvent) {
+ // return false if there is more than one touch on the page, or if
+ // we can't find a touch point
+ if (touchEvent->mTouches.Length() != 1) {
+ return false;
+ }
+
+ dom::Touch* touch = touchEvent->mTouches.SafeElementAt(0);
+ if (!touch) {
+ return false;
+ }
+ aPoint = touch->mRefPoint;
+ } else {
+ aPoint = aEvent->mRefPoint;
+ }
+ return true;
+}
diff --git a/layout/xul/nsBoxFrame.h b/layout/xul/nsBoxFrame.h
new file mode 100644
index 0000000000..c2d34bf3dd
--- /dev/null
+++ b/layout/xul/nsBoxFrame.h
@@ -0,0 +1,185 @@
+/* -*- 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/. */
+
+/**
+
+ Eric D Vaughan
+ nsBoxFrame is a frame that can lay its children out either vertically or
+horizontally. It lays them out according to a min max or preferred size.
+
+**/
+
+#ifndef nsBoxFrame_h___
+#define nsBoxFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "nsCOMPtr.h"
+#include "nsContainerFrame.h"
+#include "nsBoxLayout.h"
+
+class nsBoxLayoutState;
+
+namespace mozilla {
+class PresShell;
+namespace gfx {
+class DrawTarget;
+} // namespace gfx
+} // namespace mozilla
+
+nsContainerFrame* NS_NewBoxFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle);
+
+class nsBoxFrame : public nsContainerFrame {
+ protected:
+ typedef mozilla::gfx::DrawTarget DrawTarget;
+
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsBoxFrame)
+#ifdef DEBUG
+ NS_DECL_QUERYFRAME
+#endif
+
+ friend nsContainerFrame* NS_NewBoxFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+
+ // gets the rect inside our border and debug border. If you wish to paint
+ // inside a box call this method to get the rect so you don't draw on the
+ // debug border or outer border.
+
+ virtual void SetXULLayoutManager(nsBoxLayout* aLayout) override {
+ mLayoutManager = aLayout;
+ }
+ virtual nsBoxLayout* GetXULLayoutManager() override { return mLayoutManager; }
+
+ virtual nsSize GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual nsSize GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual nsSize GetXULMaxSize(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual nscoord GetXULBoxAscent(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual Valignment GetXULVAlign() const override { return mValign; }
+ virtual Halignment GetXULHAlign() const override { return mHalign; }
+ NS_IMETHOD DoXULLayout(nsBoxLayoutState& aBoxLayoutState) override;
+
+ virtual bool XULComputesOwnOverflowArea() override { return false; }
+
+ // ----- child and sibling operations ---
+
+ // ----- public methods -------
+
+ virtual void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ virtual void MarkIntrinsicISizesDirty() override;
+ virtual nscoord GetMinISize(gfxContext* aRenderingContext) override;
+ virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override;
+
+ virtual void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ 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;
+ virtual void RemoveFrame(ChildListID aListID, nsIFrame* aOldFrame) override;
+
+ virtual void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override;
+
+ virtual bool IsFrameOfType(uint32_t aFlags) const override {
+ // record that children that are ignorable whitespace should be excluded
+ // (When content was loaded via the XUL content sink, it's already
+ // been excluded, but we need this for when the XUL namespace is used
+ // in other MIME types or when the XUL CSS display types are used with
+ // non-XUL elements.)
+
+ // This is bogus, but it's what we've always done.
+ // (Given that we're replaced, we need to say we're a replaced element
+ // that contains a block so ReflowInput doesn't tell us to be
+ // NS_UNCONSTRAINEDSIZE wide.)
+ return nsContainerFrame::IsFrameOfType(
+ aFlags &
+ ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock | eXULBox));
+ }
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override;
+#endif
+
+ virtual void DidReflow(nsPresContext* aPresContext,
+ const ReflowInput* aReflowInput) override;
+
+ virtual bool HonorPrintBackgroundSettings() const override;
+
+ // virtual so nsButtonBoxFrame, nsSliderFrame and nsMenuFrame
+ // can override it
+ virtual void BuildDisplayListForChildren(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists);
+
+ virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ static nsresult LayoutChildAt(nsBoxLayoutState& aState, nsIFrame* aBox,
+ const nsRect& aRect);
+
+ /**
+ * Utility method to redirect events on descendants to this frame.
+ * Supports 'allowevents' attribute on descendant elements to allow those
+ * elements and their descendants to receive events.
+ */
+ void WrapListsInRedirector(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aIn,
+ const nsDisplayListSet& aOut);
+
+ // Gets a next / prev sibling accounting for ordinal group. Slow, please avoid
+ // usage if possible.
+ static nsIFrame* SlowOrdinalGroupAwareSibling(nsIFrame*, bool aNext);
+
+ private:
+ explicit nsBoxFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
+ : nsBoxFrame(aStyle, aPresContext, kClassID) {}
+
+ protected:
+ nsBoxFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, ClassID aID);
+ virtual ~nsBoxFrame();
+
+ virtual void GetInitialOrientation(bool& aIsHorizontal);
+ virtual void GetInitialDirection(bool& aIsNormal);
+ virtual bool GetInitialHAlignment(Halignment& aHalign);
+ virtual bool GetInitialVAlignment(Valignment& aValign);
+ virtual bool GetInitialAutoStretch(bool& aStretch);
+
+ virtual void DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) override;
+
+ nsSize mPrefSize;
+ nsSize mMinSize;
+ nsSize mMaxSize;
+ nscoord mAscent;
+
+ nsCOMPtr<nsBoxLayout> mLayoutManager;
+
+ // Get the point associated with this event. Returns true if a single valid
+ // point was found. Otherwise false.
+ bool GetEventPoint(mozilla::WidgetGUIEvent* aEvent, nsPoint& aPoint);
+ // Gets the event coordinates relative to the widget offset associated with
+ // this frame. Return true if a single valid point was found.
+ bool GetEventPoint(mozilla::WidgetGUIEvent* aEvent,
+ mozilla::LayoutDeviceIntPoint& aPoint);
+
+ private:
+ void CacheAttributes();
+
+ // instance variables.
+ Halignment mHalign;
+ Valignment mValign;
+
+}; // class nsBoxFrame
+
+#endif
diff --git a/layout/xul/nsBoxLayout.cpp b/layout/xul/nsBoxLayout.cpp
new file mode 100644
index 0000000000..81ee41103f
--- /dev/null
+++ b/layout/xul/nsBoxLayout.cpp
@@ -0,0 +1,74 @@
+/* -*- 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/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "nsCOMPtr.h"
+#include "nsContainerFrame.h"
+#include "nsBoxLayout.h"
+
+void nsBoxLayout::AddXULBorderAndPadding(nsIFrame* aBox, nsSize& aSize) {
+ nsIFrame::AddXULBorderAndPadding(aBox, aSize);
+}
+
+void nsBoxLayout::AddXULMargin(nsIFrame* aChild, nsSize& aSize) {
+ nsIFrame::AddXULMargin(aChild, aSize);
+}
+
+void nsBoxLayout::AddXULMargin(nsSize& aSize, const nsMargin& aMargin) {
+ nsIFrame::AddXULMargin(aSize, aMargin);
+}
+
+nsSize nsBoxLayout::GetXULPrefSize(nsIFrame* aBox,
+ nsBoxLayoutState& aBoxLayoutState) {
+ nsSize pref(0, 0);
+ AddXULBorderAndPadding(aBox, pref);
+
+ return pref;
+}
+
+nsSize nsBoxLayout::GetXULMinSize(nsIFrame* aBox,
+ nsBoxLayoutState& aBoxLayoutState) {
+ nsSize minSize(0, 0);
+ AddXULBorderAndPadding(aBox, minSize);
+ return minSize;
+}
+
+nsSize nsBoxLayout::GetXULMaxSize(nsIFrame* aBox,
+ nsBoxLayoutState& aBoxLayoutState) {
+ // AddXULBorderAndPadding () never changes maxSize (NS_UNCONSTRAINEDSIZE)
+ // AddXULBorderAndPadding(aBox, maxSize);
+ return nsSize(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE);
+}
+
+nscoord nsBoxLayout::GetAscent(nsIFrame* aBox,
+ nsBoxLayoutState& aBoxLayoutState) {
+ return 0;
+}
+
+NS_IMETHODIMP
+nsBoxLayout::XULLayout(nsIFrame* aBox, nsBoxLayoutState& aBoxLayoutState) {
+ return NS_OK;
+}
+
+void nsBoxLayout::AddLargestSize(nsSize& aSize, const nsSize& aSize2) {
+ if (aSize2.width > aSize.width) aSize.width = aSize2.width;
+
+ if (aSize2.height > aSize.height) aSize.height = aSize2.height;
+}
+
+void nsBoxLayout::AddSmallestSize(nsSize& aSize, const nsSize& aSize2) {
+ if (aSize2.width < aSize.width) aSize.width = aSize2.width;
+
+ if (aSize2.height < aSize.height) aSize.height = aSize2.height;
+}
+
+NS_IMPL_ISUPPORTS(nsBoxLayout, nsBoxLayout)
diff --git a/layout/xul/nsBoxLayout.h b/layout/xul/nsBoxLayout.h
new file mode 100644
index 0000000000..404d7e768d
--- /dev/null
+++ b/layout/xul/nsBoxLayout.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 nsBoxLayout_h___
+#define nsBoxLayout_h___
+
+#include "nsISupports.h"
+#include "nsCoord.h"
+#include "nsFrameList.h"
+
+class nsIFrame;
+class nsBoxLayoutState;
+struct nsSize;
+struct nsMargin;
+
+#define NS_BOX_LAYOUT_IID \
+ { \
+ 0x09d522a7, 0x304c, 0x4137, { \
+ 0xaf, 0xc9, 0xe0, 0x80, 0x2e, 0x89, 0xb7, 0xe8 \
+ } \
+ }
+
+class nsBoxLayout : public nsISupports {
+ protected:
+ virtual ~nsBoxLayout() = default;
+
+ public:
+ nsBoxLayout() = default;
+
+ NS_DECL_ISUPPORTS
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_BOX_LAYOUT_IID)
+
+ NS_IMETHOD XULLayout(nsIFrame* aBox, nsBoxLayoutState& aState);
+
+ virtual nsSize GetXULPrefSize(nsIFrame* aBox,
+ nsBoxLayoutState& aBoxLayoutState);
+ virtual nsSize GetXULMinSize(nsIFrame* aBox,
+ nsBoxLayoutState& aBoxLayoutState);
+ virtual nsSize GetXULMaxSize(nsIFrame* aBox,
+ nsBoxLayoutState& aBoxLayoutState);
+ virtual nscoord GetAscent(nsIFrame* aBox, nsBoxLayoutState& aBoxLayoutState);
+ virtual void ChildrenInserted(nsIFrame* aBox, nsBoxLayoutState& aState,
+ nsIFrame* aPrevBox,
+ const nsFrameList::Slice& aNewChildren) {}
+ virtual void ChildrenAppended(nsIFrame* aBox, nsBoxLayoutState& aState,
+ const nsFrameList::Slice& aNewChildren) {}
+ virtual void ChildrenRemoved(nsIFrame* aBox, nsBoxLayoutState& aState,
+ nsIFrame* aChildList) {}
+ virtual void ChildrenSet(nsIFrame* aBox, nsBoxLayoutState& aState,
+ nsIFrame* aChildList) {}
+ virtual void IntrinsicISizesDirty(nsIFrame* aBox, nsBoxLayoutState& aState) {}
+
+ virtual void AddXULBorderAndPadding(nsIFrame* aBox, nsSize& aSize);
+ virtual void AddXULMargin(nsIFrame* aChild, nsSize& aSize);
+ virtual void AddXULMargin(nsSize& aSize, const nsMargin& aMargin);
+
+ static void AddLargestSize(nsSize& aSize, const nsSize& aToAdd);
+ static void AddSmallestSize(nsSize& aSize, const nsSize& aToAdd);
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsBoxLayout, NS_BOX_LAYOUT_IID)
+
+#endif
diff --git a/layout/xul/nsBoxLayoutState.cpp b/layout/xul/nsBoxLayoutState.cpp
new file mode 100644
index 0000000000..80543036f7
--- /dev/null
+++ b/layout/xul/nsBoxLayoutState.cpp
@@ -0,0 +1,37 @@
+/* -*- 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/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "nsBoxLayoutState.h"
+
+nsBoxLayoutState::nsBoxLayoutState(nsPresContext* aPresContext,
+ gfxContext* aRenderingContext,
+ const ReflowInput* aOuterReflowInput,
+ uint16_t aReflowDepth)
+ : mPresContext(aPresContext),
+ mRenderingContext(aRenderingContext),
+ mOuterReflowInput(aOuterReflowInput),
+ mLayoutFlags(nsIFrame::ReflowChildFlags::Default),
+ mReflowDepth(aReflowDepth),
+ mPaintingDisabled(false) {
+ NS_ASSERTION(mPresContext, "PresContext must be non-null");
+}
+
+nsBoxLayoutState::nsBoxLayoutState(const nsBoxLayoutState& aState)
+ : mPresContext(aState.mPresContext),
+ mRenderingContext(aState.mRenderingContext),
+ mOuterReflowInput(aState.mOuterReflowInput),
+ mLayoutFlags(aState.mLayoutFlags),
+ mReflowDepth(aState.mReflowDepth + 1),
+ mPaintingDisabled(aState.mPaintingDisabled) {
+ NS_ASSERTION(mPresContext, "PresContext must be non-null");
+}
diff --git a/layout/xul/nsBoxLayoutState.h b/layout/xul/nsBoxLayoutState.h
new file mode 100644
index 0000000000..7f7c9d7a3a
--- /dev/null
+++ b/layout/xul/nsBoxLayoutState.h
@@ -0,0 +1,79 @@
+/* -*- 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/. */
+
+/**
+
+ Author:
+ Eric D Vaughan
+
+**/
+
+#ifndef nsBoxLayoutState_h___
+#define nsBoxLayoutState_h___
+
+#include "nsCOMPtr.h"
+#include "nsPresContext.h"
+#include "nsIFrame.h"
+
+class gfxContext;
+namespace mozilla {
+class PresShell;
+struct ReflowInput;
+} // namespace mozilla
+
+class MOZ_STACK_CLASS nsBoxLayoutState {
+ using ReflowInput = mozilla::ReflowInput;
+
+ public:
+ explicit nsBoxLayoutState(nsPresContext* aPresContext,
+ gfxContext* aRenderingContext = nullptr,
+ // see OuterReflowInput() below
+ const ReflowInput* aOuterReflowInput = nullptr,
+ uint16_t aReflowDepth = 0);
+ nsBoxLayoutState(const nsBoxLayoutState& aState);
+
+ nsPresContext* PresContext() const { return mPresContext; }
+ mozilla::PresShell* PresShell() const { return mPresContext->PresShell(); }
+
+ nsIFrame::ReflowChildFlags LayoutFlags() const { return mLayoutFlags; }
+ void SetLayoutFlags(nsIFrame::ReflowChildFlags aFlags) {
+ mLayoutFlags = aFlags;
+ }
+
+ // if true no one under us will paint during reflow.
+ void SetPaintingDisabled(bool aDisable) { mPaintingDisabled = aDisable; }
+ bool PaintingDisabled() const { return mPaintingDisabled; }
+
+ // The rendering context may be null for specialized uses of
+ // nsBoxLayoutState and should be null-checked before it is used.
+ // However, passing a null rendering context to the constructor when
+ // doing box layout or intrinsic size calculation will cause bugs.
+ gfxContext* GetRenderingContext() const { return mRenderingContext; }
+
+ struct AutoReflowDepth {
+ explicit AutoReflowDepth(nsBoxLayoutState& aState) : mState(aState) {
+ ++mState.mReflowDepth;
+ }
+ ~AutoReflowDepth() { --mState.mReflowDepth; }
+ nsBoxLayoutState& mState;
+ };
+
+ // The HTML reflow input that lives outside the box-block boundary.
+ // May not be set reliably yet.
+ const ReflowInput* OuterReflowInput() { return mOuterReflowInput; }
+
+ uint16_t GetReflowDepth() { return mReflowDepth; }
+
+ private:
+ RefPtr<nsPresContext> mPresContext;
+ gfxContext* mRenderingContext;
+ const ReflowInput* mOuterReflowInput;
+ nsIFrame::ReflowChildFlags mLayoutFlags;
+ uint16_t mReflowDepth;
+ bool mPaintingDisabled;
+};
+
+#endif
diff --git a/layout/xul/nsIPopupContainer.h b/layout/xul/nsIPopupContainer.h
new file mode 100644
index 0000000000..4870863781
--- /dev/null
+++ b/layout/xul/nsIPopupContainer.h
@@ -0,0 +1,29 @@
+/* -*- 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 nsIPopupContainer_h___
+#define nsIPopupContainer_h___
+
+#include "nsQueryFrame.h"
+class nsIContent;
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class Element;
+}
+} // namespace mozilla
+
+class nsIPopupContainer {
+ public:
+ NS_DECL_QUERYFRAME_TARGET(nsIPopupContainer)
+
+ virtual mozilla::dom::Element* GetDefaultTooltip() = 0;
+
+ static nsIPopupContainer* GetPopupContainer(mozilla::PresShell* aShell);
+};
+
+#endif
diff --git a/layout/xul/nsIScrollbarMediator.h b/layout/xul/nsIScrollbarMediator.h
new file mode 100644
index 0000000000..68f6f8b232
--- /dev/null
+++ b/layout/xul/nsIScrollbarMediator.h
@@ -0,0 +1,101 @@
+/* -*- 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 nsIScrollbarMediator_h___
+#define nsIScrollbarMediator_h___
+
+#include "mozilla/ScrollTypes.h"
+#include "nsQueryFrame.h"
+#include "nsCoord.h"
+
+class nsScrollbarFrame;
+class nsIFrame;
+
+class nsIScrollbarMediator : public nsQueryFrame {
+ public:
+ NS_DECL_QUERYFRAME_TARGET(nsIScrollbarMediator)
+
+ /**
+ * The aScrollbar argument denotes the scrollbar that's firing the
+ * notification. aScrollbar is never null. aDirection is either -1, 0, or 1.
+ */
+
+ /**
+ * When set to ENABLE_SNAP, additional scrolling will be performed after the
+ * scroll operation to maintain the constraints set by CSS Scroll snapping.
+ * The additional scrolling may include asynchronous smooth scrolls that
+ * continue to animate after the initial scroll position has been set.
+ * In case of DEFAULT, it means ENABLE_SNAP for CSS scroll snap v1,
+ * DISABLE_SNAP for the old scroll snap.
+ */
+
+ /**
+ * One of the following three methods is called when the scrollbar's button is
+ * clicked.
+ * @note These methods might destroy the frame, pres shell, and other objects.
+ */
+ virtual void ScrollByPage(nsScrollbarFrame* aScrollbar, int32_t aDirection,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) = 0;
+ virtual void ScrollByWhole(nsScrollbarFrame* aScrollbar, int32_t aDirection,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) = 0;
+ virtual void ScrollByLine(nsScrollbarFrame* aScrollbar, int32_t aDirection,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) = 0;
+
+ // Only implemented for nsGfxScrollFrame, not nsTreeBodyFrame.
+ virtual void ScrollByUnit(nsScrollbarFrame* aScrollbar,
+ mozilla::ScrollMode aMode, int32_t aDirection,
+ mozilla::ScrollUnit aUnit,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) = 0;
+
+ /**
+ * RepeatButtonScroll is called when the scrollbar's button is held down. When
+ * the button is first clicked the increment is set; RepeatButtonScroll adds
+ * this increment to the current position.
+ * @note This method might destroy the frame, pres shell, and other objects.
+ */
+ virtual void RepeatButtonScroll(nsScrollbarFrame* aScrollbar) = 0;
+ /**
+ * aOldPos and aNewPos are scroll positions.
+ * The scroll positions start with zero at the left edge; implementors that
+ * want zero at the right edge for RTL content will need to adjust
+ * accordingly. (See ScrollFrameHelper::ThumbMoved in nsGfxScrollFrame.cpp.)
+ * @note This method might destroy the frame, pres shell, and other objects.
+ */
+ virtual void ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos,
+ nscoord aNewPos) = 0;
+ /**
+ * Called when the scroll bar thumb, slider, or any other component is
+ * released.
+ */
+ virtual void ScrollbarReleased(nsScrollbarFrame* aScrollbar) = 0;
+ virtual void VisibilityChanged(bool aVisible) = 0;
+
+ /**
+ * Obtain the frame for the horizontal or vertical scrollbar, or null
+ * if there is no such box.
+ */
+ virtual nsIFrame* GetScrollbarBox(bool aVertical) = 0;
+ /**
+ * Show or hide scrollbars on 2 fingers touch.
+ * Subclasses should call their ScrollbarActivity's corresponding methods.
+ */
+ virtual void ScrollbarActivityStarted() const = 0;
+ virtual void ScrollbarActivityStopped() const = 0;
+
+ virtual bool IsScrollbarOnRight() const = 0;
+
+ /**
+ * Returns true if the mediator is asking the scrollbar to suppress
+ * repainting itself on changes.
+ */
+ virtual bool ShouldSuppressScrollbarRepaints() const = 0;
+};
+
+#endif
diff --git a/layout/xul/nsImageBoxFrame.cpp b/layout/xul/nsImageBoxFrame.cpp
new file mode 100644
index 0000000000..9e10b58f43
--- /dev/null
+++ b/layout/xul/nsImageBoxFrame.cpp
@@ -0,0 +1,805 @@
+/* -*- 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/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "gfxContext.h"
+#include "nsImageBoxFrame.h"
+#include "nsGkAtoms.h"
+#include "mozilla/ComputedStyle.h"
+#include "nsStyleConsts.h"
+#include "nsStyleUtil.h"
+#include "nsCOMPtr.h"
+#include "nsLayoutUtils.h"
+#include "nsPresContext.h"
+#include "nsBoxLayoutState.h"
+
+#include "nsHTMLParts.h"
+#include "nsString.h"
+#include "nsLeafFrame.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "nsImageMap.h"
+#include "nsContainerFrame.h"
+#include "nsCSSRendering.h"
+#include "nsNameSpaceManager.h"
+#include "nsTextFragment.h"
+#include "nsTransform2D.h"
+#include "nsITheme.h"
+
+#include "nsIURI.h"
+#include "nsThreadUtils.h"
+#include "nsDisplayList.h"
+#include "ImageRegion.h"
+#include "ImageContainer.h"
+#include "nsIContent.h"
+
+#include "nsContentUtils.h"
+
+#include "mozilla/BasicEvents.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_image.h"
+#include "mozilla/SVGImageContext.h"
+#include "Units.h"
+#include "mozilla/image/WebRenderImageProvider.h"
+#include "mozilla/layers/RenderRootStateManager.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include "mozilla/dom/ImageTracker.h"
+
+#if defined(XP_WIN)
+// Undefine LoadImage to prevent naming conflict with Windows.
+# undef LoadImage
+#endif
+
+#define ONLOAD_CALLED_TOO_EARLY 1
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+using namespace mozilla::layers;
+
+using mozilla::dom::Document;
+using mozilla::dom::Element;
+using mozilla::dom::ReferrerInfo;
+
+class nsImageBoxFrameEvent : public Runnable {
+ public:
+ nsImageBoxFrameEvent(nsIContent* content, EventMessage message)
+ : mozilla::Runnable("nsImageBoxFrameEvent"),
+ mContent(content),
+ mMessage(message) {}
+
+ NS_IMETHOD Run() override;
+
+ private:
+ const nsCOMPtr<nsIContent> mContent;
+ EventMessage mMessage;
+};
+
+// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsImageBoxFrameEvent::Run() {
+ RefPtr<nsPresContext> presContext = mContent->OwnerDoc()->GetPresContext();
+ if (!presContext) {
+ return NS_OK;
+ }
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetEvent event(true, mMessage);
+
+ event.mFlags.mBubbles = false;
+ EventDispatcher::Dispatch(mContent, presContext, &event, nullptr, &status);
+ return NS_OK;
+}
+
+// Fire off an event that'll asynchronously call the image elements
+// onload handler once handled. This is needed since the image library
+// can't decide if it wants to call its observer methods
+// synchronously or asynchronously. If an image is loaded from the
+// cache the notifications come back synchronously, but if the image
+// is loaded from the network the notifications come back
+// asynchronously.
+static void FireImageDOMEvent(nsIContent* aContent, EventMessage aMessage) {
+ NS_ASSERTION(aMessage == eLoad || aMessage == eLoadError, "invalid message");
+
+ nsCOMPtr<nsIRunnable> event = new nsImageBoxFrameEvent(aContent, aMessage);
+ nsresult rv =
+ aContent->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
+ if (NS_FAILED(rv)) {
+ NS_WARNING("failed to dispatch image event");
+ }
+}
+
+//
+// NS_NewImageBoxFrame
+//
+// Creates a new image frame and returns it
+//
+nsIFrame* NS_NewImageBoxFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsImageBoxFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsImageBoxFrame)
+NS_QUERYFRAME_HEAD(nsImageBoxFrame)
+ NS_QUERYFRAME_ENTRY(nsImageBoxFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsLeafBoxFrame)
+
+nsresult nsImageBoxFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ nsresult rv =
+ nsLeafBoxFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+
+ if (aAttribute == nsGkAtoms::src) {
+ UpdateImage();
+ PresShell()->FrameNeedsReflow(
+ this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
+ } else if (aAttribute == nsGkAtoms::validate)
+ UpdateLoadFlags();
+
+ return rv;
+}
+
+nsImageBoxFrame::nsImageBoxFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsLeafBoxFrame(aStyle, aPresContext, kClassID),
+ mIntrinsicSize(0, 0),
+ mLoadFlags(nsIRequest::LOAD_NORMAL),
+ mRequestRegistered(false),
+ mUseSrcAttr(false),
+ mSuppressStyleCheck(false) {
+ MarkIntrinsicISizesDirty();
+}
+
+nsImageBoxFrame::~nsImageBoxFrame() = default;
+
+/* virtual */
+void nsImageBoxFrame::MarkIntrinsicISizesDirty() {
+ XULSizeNeedsRecalc(mImageSize);
+ nsLeafBoxFrame::MarkIntrinsicISizesDirty();
+}
+
+void nsImageBoxFrame::DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) {
+ if (mImageRequest) {
+ nsLayoutUtils::DeregisterImageRequest(PresContext(), mImageRequest,
+ &mRequestRegistered);
+
+ mImageRequest->UnlockImage();
+
+ if (mUseSrcAttr) {
+ PresContext()->Document()->ImageTracker()->Remove(mImageRequest);
+ }
+
+ // Release image loader first so that it's refcnt can go to zero
+ mImageRequest->CancelAndForgetObserver(NS_ERROR_FAILURE);
+ }
+
+ if (mListener) {
+ // set the frame to null so we don't send messages to a dead object.
+ reinterpret_cast<nsImageBoxListener*>(mListener.get())->ClearFrame();
+ }
+
+ nsLeafBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
+}
+
+void nsImageBoxFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ if (!mListener) {
+ RefPtr<nsImageBoxListener> listener = new nsImageBoxListener(this);
+ mListener = std::move(listener);
+ }
+
+ mSuppressStyleCheck = true;
+ nsLeafBoxFrame::Init(aContent, aParent, aPrevInFlow);
+ mSuppressStyleCheck = false;
+
+ UpdateLoadFlags();
+ UpdateImage();
+}
+
+void nsImageBoxFrame::UpdateImage() {
+ nsPresContext* presContext = PresContext();
+ Document* doc = presContext->Document();
+
+ RefPtr<imgRequestProxy> oldImageRequest = mImageRequest;
+
+ if (mImageRequest) {
+ nsLayoutUtils::DeregisterImageRequest(presContext, mImageRequest,
+ &mRequestRegistered);
+ mImageRequest->CancelAndForgetObserver(NS_ERROR_FAILURE);
+ if (mUseSrcAttr) {
+ doc->ImageTracker()->Remove(mImageRequest);
+ }
+ mImageRequest = nullptr;
+ }
+
+ // get the new image src
+ nsAutoString src;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::src, src);
+ mUseSrcAttr = !src.IsEmpty();
+ if (mUseSrcAttr) {
+ nsContentPolicyType contentPolicyType;
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal;
+ uint64_t requestContextID = 0;
+ nsContentUtils::GetContentPolicyTypeForUIImageLoading(
+ mContent, getter_AddRefs(triggeringPrincipal), contentPolicyType,
+ &requestContextID);
+
+ nsCOMPtr<nsIURI> uri;
+ nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri), src, doc,
+ mContent->GetBaseURI());
+ if (uri) {
+ auto referrerInfo = MakeRefPtr<ReferrerInfo>(*mContent->AsElement());
+ nsresult rv = nsContentUtils::LoadImage(
+ uri, mContent, doc, triggeringPrincipal, requestContextID,
+ referrerInfo, mListener, mLoadFlags, u""_ns,
+ getter_AddRefs(mImageRequest), contentPolicyType);
+
+ if (NS_SUCCEEDED(rv) && mImageRequest) {
+ nsLayoutUtils::RegisterImageRequestIfAnimated(
+ presContext, mImageRequest, &mRequestRegistered);
+
+ // Add to the ImageTracker so that we can find it when media
+ // feature values change (e.g. when the system theme changes)
+ // and invalidate the image. This allows favicons to respond
+ // to these changes.
+ doc->ImageTracker()->Add(mImageRequest);
+ }
+ }
+ } else if (auto* styleImage = GetImageFromStyle()) {
+ if (auto* styleRequest = styleImage->GetImageRequest()) {
+ styleRequest->SyncClone(mListener, mContent->GetComposedDoc(),
+ getter_AddRefs(mImageRequest));
+ }
+ }
+
+ if (!mImageRequest) {
+ // We have no image, so size to 0
+ mIntrinsicSize.SizeTo(0, 0);
+ } else {
+ // We don't want discarding or decode-on-draw for xul images.
+ mImageRequest->StartDecoding(imgIContainer::FLAG_ASYNC_NOTIFY);
+ mImageRequest->LockImage();
+ }
+
+ // Do this _after_ locking the new image in case they are the same image.
+ if (oldImageRequest) {
+ oldImageRequest->UnlockImage();
+ }
+}
+
+void nsImageBoxFrame::UpdateLoadFlags() {
+ static Element::AttrValuesArray strings[] = {nsGkAtoms::always,
+ nsGkAtoms::never, nullptr};
+ switch (mContent->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::validate, strings, eCaseMatters)) {
+ case 0:
+ mLoadFlags = nsIRequest::VALIDATE_ALWAYS;
+ break;
+ case 1:
+ mLoadFlags = nsIRequest::VALIDATE_NEVER | nsIRequest::LOAD_FROM_CACHE;
+ break;
+ default:
+ mLoadFlags = nsIRequest::LOAD_NORMAL;
+ break;
+ }
+}
+
+void nsImageBoxFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ nsLeafBoxFrame::BuildDisplayList(aBuilder, aLists);
+
+ if ((0 == mRect.width) || (0 == mRect.height)) {
+ // Do not render when given a zero area. This avoids some useless
+ // scaling work while we wait for our image dimensions to arrive
+ // asynchronously.
+ return;
+ }
+
+ if (!IsVisibleForPainting()) return;
+
+ uint32_t clipFlags =
+ nsStyleUtil::ObjectPropsMightCauseOverflow(StylePosition())
+ ? 0
+ : DisplayListClipState::ASSUME_DRAWING_RESTRICTED_TO_CONTENT_RECT;
+
+ DisplayListClipState::AutoClipContainingBlockDescendantsToContentBox clip(
+ aBuilder, this, clipFlags);
+
+ aLists.Content()->AppendNewToTop<nsDisplayXULImage>(aBuilder, this);
+}
+
+already_AddRefed<imgIContainer> nsImageBoxFrame::GetImageContainerForPainting(
+ const nsPoint& aPt, ImgDrawResult& aDrawResult,
+ Maybe<nsPoint>& aAnchorPoint, nsRect& aDest) {
+ if (!mImageRequest) {
+ // This probably means we're drawn by a native theme.
+ aDrawResult = ImgDrawResult::SUCCESS;
+ return nullptr;
+ }
+
+ // Don't draw if the image's size isn't available.
+ uint32_t imgStatus;
+ if (!NS_SUCCEEDED(mImageRequest->GetImageStatus(&imgStatus)) ||
+ !(imgStatus & imgIRequest::STATUS_SIZE_AVAILABLE)) {
+ aDrawResult = ImgDrawResult::NOT_READY;
+ return nullptr;
+ }
+
+ nsCOMPtr<imgIContainer> imgCon;
+ mImageRequest->GetImage(getter_AddRefs(imgCon));
+
+ if (!imgCon) {
+ aDrawResult = ImgDrawResult::NOT_READY;
+ return nullptr;
+ }
+
+ aDest = GetDestRect(aPt, aAnchorPoint);
+ aDrawResult = ImgDrawResult::SUCCESS;
+ return imgCon.forget();
+}
+
+ImgDrawResult nsImageBoxFrame::PaintImage(gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nsPoint aPt,
+ uint32_t aFlags) {
+ ImgDrawResult result;
+ Maybe<nsPoint> anchorPoint;
+ nsRect dest;
+ nsCOMPtr<imgIContainer> imgCon =
+ GetImageContainerForPainting(aPt, result, anchorPoint, dest);
+ if (!imgCon) {
+ return result;
+ }
+
+ // don't draw if the image is not dirty
+ // XXX(seth): Can this actually happen anymore?
+ nsRect dirty;
+ if (!dirty.IntersectRect(aDirtyRect, dest)) {
+ return ImgDrawResult::TEMPORARY_ERROR;
+ }
+
+ bool hasSubRect = !mUseSrcAttr && (mSubRect.width > 0 || mSubRect.height > 0);
+
+ SVGImageContext svgContext;
+ SVGImageContext::MaybeStoreContextPaint(svgContext, this, imgCon);
+ return nsLayoutUtils::DrawSingleImage(
+ aRenderingContext, PresContext(), imgCon,
+ nsLayoutUtils::GetSamplingFilterForFrame(this), dest, dirty, svgContext,
+ aFlags, anchorPoint.ptrOr(nullptr), hasSubRect ? &mSubRect : nullptr);
+}
+
+ImgDrawResult nsImageBoxFrame::CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager, nsDisplayItem* aItem,
+ nsPoint aPt, uint32_t aFlags) {
+ ImgDrawResult result;
+ Maybe<nsPoint> anchorPoint;
+ nsRect dest;
+ nsCOMPtr<imgIContainer> imgCon =
+ GetImageContainerForPainting(aPt, result, anchorPoint, dest);
+ if (!imgCon) {
+ return result;
+ }
+
+ if (StaticPrefs::image_svg_blob_image() &&
+ imgCon->GetType() == imgIContainer::TYPE_VECTOR) {
+ aFlags |= imgIContainer::FLAG_RECORD_BLOB;
+ }
+
+ const int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel();
+ LayoutDeviceRect fillRect =
+ LayoutDeviceRect::FromAppUnits(dest, appUnitsPerDevPixel);
+
+ SVGImageContext svgContext;
+ Maybe<ImageIntRegion> region;
+ gfx::IntSize decodeSize =
+ nsLayoutUtils::ComputeImageContainerDrawingParameters(
+ imgCon, aItem->Frame(), fillRect, fillRect, aSc, aFlags, svgContext,
+ region);
+
+ RefPtr<image::WebRenderImageProvider> provider;
+ result =
+ imgCon->GetImageProvider(aManager->LayerManager(), decodeSize, svgContext,
+ region, aFlags, getter_AddRefs(provider));
+
+ Maybe<wr::ImageKey> key = aManager->CommandBuilder().CreateImageProviderKey(
+ aItem, provider, result, aResources);
+ if (key.isNothing()) {
+ return result;
+ }
+
+ auto rendering = wr::ToImageRendering(aItem->Frame()->UsedImageRendering());
+ wr::LayoutRect fill = wr::ToLayoutRect(fillRect);
+
+ aBuilder.PushImage(fill, fill, !BackfaceIsHidden(), false, rendering,
+ key.value());
+ return result;
+}
+
+nsRect nsImageBoxFrame::GetDestRect(const nsPoint& aOffset,
+ Maybe<nsPoint>& aAnchorPoint) {
+ nsCOMPtr<imgIContainer> imgCon;
+ mImageRequest->GetImage(getter_AddRefs(imgCon));
+ MOZ_ASSERT(imgCon);
+
+ nsRect clientRect;
+ GetXULClientRect(clientRect);
+ clientRect += aOffset;
+ nsRect dest;
+ if (!mUseSrcAttr) {
+ // Our image (if we have one) is coming from the CSS property
+ // 'list-style-image' (combined with '-moz-image-region'). For now, ignore
+ // 'object-fit' & 'object-position' in this case, and just fill our rect.
+ // XXXdholbert Should we even honor these properties in this case? They only
+ // apply to replaced elements, and I'm not sure we count as a replaced
+ // element when our image data is determined by CSS.
+ dest = clientRect;
+ } else {
+ // Determine dest rect based on intrinsic size & ratio, along with
+ // 'object-fit' & 'object-position' properties:
+ IntrinsicSize intrinsicSize;
+ AspectRatio intrinsicRatio;
+ if (mIntrinsicSize.width > 0 && mIntrinsicSize.height > 0) {
+ // Image has a valid size; use it as intrinsic size & ratio.
+ intrinsicSize =
+ IntrinsicSize(mIntrinsicSize.width, mIntrinsicSize.height);
+ intrinsicRatio =
+ AspectRatio::FromSize(mIntrinsicSize.width, mIntrinsicSize.height);
+ } else {
+ // Image doesn't have a (valid) intrinsic size.
+ // Try to look up intrinsic ratio and use that at least.
+ intrinsicRatio = imgCon->GetIntrinsicRatio().valueOr(AspectRatio());
+ }
+ aAnchorPoint.emplace();
+ dest = nsLayoutUtils::ComputeObjectDestRect(clientRect, intrinsicSize,
+ intrinsicRatio, StylePosition(),
+ aAnchorPoint.ptr());
+ }
+
+ return dest;
+}
+
+void nsDisplayXULImage::Paint(nsDisplayListBuilder* aBuilder,
+ gfxContext* aCtx) {
+ // Even though we call StartDecoding when we get a new image we pass
+ // FLAG_SYNC_DECODE_IF_FAST here for the case where the size we draw at is not
+ // the intrinsic size of the image and we aren't likely to implement
+ // predictive decoding at the correct size for this class like nsImageFrame
+ // has.
+ uint32_t flags = imgIContainer::FLAG_SYNC_DECODE_IF_FAST;
+ if (aBuilder->ShouldSyncDecodeImages())
+ flags |= imgIContainer::FLAG_SYNC_DECODE;
+ if (aBuilder->UseHighQualityScaling())
+ flags |= imgIContainer::FLAG_HIGH_QUALITY_SCALING;
+
+ Unused << static_cast<nsImageBoxFrame*>(mFrame)->PaintImage(
+ *aCtx, GetPaintRect(aBuilder, aCtx), ToReferenceFrame(), flags);
+}
+
+bool nsDisplayXULImage::CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) {
+ nsImageBoxFrame* imageFrame = static_cast<nsImageBoxFrame*>(mFrame);
+ if (!imageFrame->CanOptimizeToImageLayer()) {
+ return false;
+ }
+
+ if (!imageFrame->mImageRequest) {
+ return true;
+ }
+
+ uint32_t flags = imgIContainer::FLAG_SYNC_DECODE_IF_FAST |
+ imgIContainer::FLAG_ASYNC_NOTIFY;
+ if (aDisplayListBuilder->ShouldSyncDecodeImages()) {
+ flags |= imgIContainer::FLAG_SYNC_DECODE;
+ }
+ if (aDisplayListBuilder->IsPaintingToWindow()) {
+ flags |= imgIContainer::FLAG_HIGH_QUALITY_SCALING;
+ }
+
+ ImgDrawResult result = imageFrame->CreateWebRenderCommands(
+ aBuilder, aResources, aSc, aManager, this, ToReferenceFrame(), flags);
+ if (result == ImgDrawResult::NOT_SUPPORTED) {
+ return false;
+ }
+
+ return true;
+}
+
+bool nsImageBoxFrame::CanOptimizeToImageLayer() {
+ bool hasSubRect = !mUseSrcAttr && (mSubRect.width > 0 || mSubRect.height > 0);
+ if (hasSubRect) {
+ return false;
+ }
+ return true;
+}
+
+const mozilla::StyleImage* nsImageBoxFrame::GetImageFromStyle(
+ const ComputedStyle& aStyle) const {
+ const nsStyleDisplay* disp = aStyle.StyleDisplay();
+ if (disp->HasAppearance()) {
+ nsPresContext* pc = PresContext();
+ if (pc->Theme()->ThemeSupportsWidget(pc, const_cast<nsImageBoxFrame*>(this),
+ disp->EffectiveAppearance())) {
+ return nullptr;
+ }
+ }
+ auto& image = aStyle.StyleList()->mListStyleImage;
+ if (!image.IsImageRequestType()) {
+ return nullptr;
+ }
+ return &image;
+}
+
+ImageResolution nsImageBoxFrame::GetImageResolution() const {
+ if (auto* image = GetImageFromStyle()) {
+ return image->GetResolution();
+ }
+ if (!mImageRequest) {
+ return {};
+ }
+ nsCOMPtr<imgIContainer> image;
+ mImageRequest->GetImage(getter_AddRefs(image));
+ if (!image) {
+ return {};
+ }
+ return image->GetResolution();
+}
+
+/* virtual */
+void nsImageBoxFrame::DidSetComputedStyle(ComputedStyle* aOldStyle) {
+ nsLeafBoxFrame::DidSetComputedStyle(aOldStyle);
+
+ // Fetch our subrect.
+ const nsStyleList* myList = StyleList();
+ mSubRect = myList->GetImageRegion(); // before |mSuppressStyleCheck| test!
+
+ if (mUseSrcAttr || mSuppressStyleCheck) {
+ return; // No more work required, since the image isn't specified by style.
+ }
+
+ auto* oldImage = aOldStyle ? GetImageFromStyle(*aOldStyle) : nullptr;
+ auto* newImage = GetImageFromStyle();
+ if (newImage == oldImage ||
+ (newImage && oldImage && *oldImage == *newImage)) {
+ return;
+ }
+ UpdateImage();
+}
+
+void nsImageBoxFrame::GetImageSize() {
+ if (mIntrinsicSize.width > 0 && mIntrinsicSize.height > 0) {
+ mImageSize.width = mIntrinsicSize.width;
+ mImageSize.height = mIntrinsicSize.height;
+ } else {
+ mImageSize.width = 0;
+ mImageSize.height = 0;
+ }
+}
+
+/**
+ * Ok return our dimensions
+ */
+nsSize nsImageBoxFrame::GetXULPrefSize(nsBoxLayoutState& aState) {
+ nsSize size(0, 0);
+ DISPLAY_PREF_SIZE(this, size);
+ if (XULNeedsRecalc(mImageSize)) {
+ GetImageSize();
+ }
+
+ if (!mUseSrcAttr && (mSubRect.width > 0 || mSubRect.height > 0)) {
+ size = mSubRect.Size();
+ } else {
+ size = mImageSize;
+ }
+
+ nsSize intrinsicSize = size;
+
+ nsMargin borderPadding(0, 0, 0, 0);
+ GetXULBorderAndPadding(borderPadding);
+ size.width += borderPadding.LeftRight();
+ size.height += borderPadding.TopBottom();
+
+ bool widthSet, heightSet;
+ nsIFrame::AddXULPrefSize(this, size, widthSet, heightSet);
+ NS_ASSERTION(
+ size.width != NS_UNCONSTRAINEDSIZE && size.height != NS_UNCONSTRAINEDSIZE,
+ "non-intrinsic size expected");
+
+ nsSize minSize = GetXULMinSize(aState);
+ nsSize maxSize = GetXULMaxSize(aState);
+
+ if (!widthSet && !heightSet) {
+ if (minSize.width != NS_UNCONSTRAINEDSIZE)
+ minSize.width -= borderPadding.LeftRight();
+ if (minSize.height != NS_UNCONSTRAINEDSIZE)
+ minSize.height -= borderPadding.TopBottom();
+ if (maxSize.width != NS_UNCONSTRAINEDSIZE)
+ maxSize.width -= borderPadding.LeftRight();
+ if (maxSize.height != NS_UNCONSTRAINEDSIZE)
+ maxSize.height -= borderPadding.TopBottom();
+
+ size = nsLayoutUtils::ComputeAutoSizeWithIntrinsicDimensions(
+ minSize.width, minSize.height, maxSize.width, maxSize.height,
+ intrinsicSize.width, intrinsicSize.height);
+ NS_ASSERTION(size.width != NS_UNCONSTRAINEDSIZE &&
+ size.height != NS_UNCONSTRAINEDSIZE,
+ "non-intrinsic size expected");
+ size.width += borderPadding.LeftRight();
+ size.height += borderPadding.TopBottom();
+ return size;
+ }
+
+ if (!widthSet) {
+ if (intrinsicSize.height > 0) {
+ // Subtract off the border and padding from the height because the
+ // content-box needs to be used to determine the ratio
+ nscoord height = size.height - borderPadding.TopBottom();
+ size.width = nscoord(int64_t(height) * int64_t(intrinsicSize.width) /
+ int64_t(intrinsicSize.height));
+ } else {
+ size.width = intrinsicSize.width;
+ }
+
+ size.width += borderPadding.LeftRight();
+ } else if (!heightSet) {
+ if (intrinsicSize.width > 0) {
+ nscoord width = size.width - borderPadding.LeftRight();
+ size.height = nscoord(int64_t(width) * int64_t(intrinsicSize.height) /
+ int64_t(intrinsicSize.width));
+ } else {
+ size.height = intrinsicSize.height;
+ }
+
+ size.height += borderPadding.TopBottom();
+ }
+
+ return XULBoundsCheck(minSize, size, maxSize);
+}
+
+nsSize nsImageBoxFrame::GetXULMinSize(nsBoxLayoutState& aState) {
+ // An image can always scale down to (0,0).
+ nsSize size(0, 0);
+ DISPLAY_MIN_SIZE(this, size);
+ AddXULBorderAndPadding(size);
+ bool widthSet, heightSet;
+ nsIFrame::AddXULMinSize(this, size, widthSet, heightSet);
+ return size;
+}
+
+nscoord nsImageBoxFrame::GetXULBoxAscent(nsBoxLayoutState& aState) {
+ return GetXULPrefSize(aState).height;
+}
+
+#ifdef DEBUG_FRAME_DUMP
+nsresult nsImageBoxFrame::GetFrameName(nsAString& aResult) const {
+ return MakeFrameName(u"ImageBox"_ns, aResult);
+}
+#endif
+
+void nsImageBoxFrame::Notify(imgIRequest* aRequest, int32_t aType,
+ const nsIntRect* aData) {
+ if (aType == imgINotificationObserver::SIZE_AVAILABLE) {
+ nsCOMPtr<imgIContainer> image;
+ aRequest->GetImage(getter_AddRefs(image));
+ return OnSizeAvailable(aRequest, image);
+ }
+
+ if (aType == imgINotificationObserver::DECODE_COMPLETE) {
+ return OnDecodeComplete(aRequest);
+ }
+
+ if (aType == imgINotificationObserver::LOAD_COMPLETE) {
+ uint32_t imgStatus;
+ aRequest->GetImageStatus(&imgStatus);
+ nsresult status =
+ imgStatus & imgIRequest::STATUS_ERROR ? NS_ERROR_FAILURE : NS_OK;
+ return OnLoadComplete(aRequest, status);
+ }
+
+ if (aType == imgINotificationObserver::IS_ANIMATED) {
+ return OnImageIsAnimated(aRequest);
+ }
+
+ if (aType == imgINotificationObserver::FRAME_UPDATE) {
+ return OnFrameUpdate(aRequest);
+ }
+}
+
+void nsImageBoxFrame::OnSizeAvailable(imgIRequest* aRequest,
+ imgIContainer* aImage) {
+ if (NS_WARN_IF(!aImage)) {
+ return;
+ }
+
+ // Ensure the animation (if any) is started. Note: There is no
+ // corresponding call to Decrement for this. This Increment will be
+ // 'cleaned up' by the Request when it is destroyed, but only then.
+ aRequest->IncrementAnimationConsumers();
+
+ aImage->SetAnimationMode(PresContext()->ImageAnimationMode());
+
+ int32_t w = 0, h = 0;
+ aImage->GetWidth(&w);
+ aImage->GetHeight(&h);
+
+ mIntrinsicSize.SizeTo(CSSPixel::ToAppUnits(w), CSSPixel::ToAppUnits(h));
+
+ GetImageResolution().ApplyTo(mIntrinsicSize.width, mIntrinsicSize.height);
+
+ if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) {
+ PresShell()->FrameNeedsReflow(
+ this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
+ }
+}
+
+void nsImageBoxFrame::OnDecodeComplete(imgIRequest* aRequest) {
+ nsBoxLayoutState state(PresContext());
+ this->XULRedraw(state);
+}
+
+void nsImageBoxFrame::OnLoadComplete(imgIRequest* aRequest, nsresult aStatus) {
+ if (NS_SUCCEEDED(aStatus)) {
+ // Fire an onload DOM event.
+ FireImageDOMEvent(mContent, eLoad);
+ } else {
+ // Fire an onerror DOM event.
+ mIntrinsicSize.SizeTo(0, 0);
+ PresShell()->FrameNeedsReflow(
+ this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
+ FireImageDOMEvent(mContent, eLoadError);
+ }
+}
+
+void nsImageBoxFrame::OnImageIsAnimated(imgIRequest* aRequest) {
+ // Register with our refresh driver, if we're animated.
+ nsLayoutUtils::RegisterImageRequest(PresContext(), aRequest,
+ &mRequestRegistered);
+}
+
+void nsImageBoxFrame::OnFrameUpdate(imgIRequest* aRequest) {
+ if ((0 == mRect.width) || (0 == mRect.height)) {
+ return;
+ }
+
+ // Check if WebRender has interacted with this frame. If it has
+ // we need to let it know that things have changed.
+ const auto type = DisplayItemType::TYPE_XUL_IMAGE;
+ const auto providerId = aRequest->GetProviderId();
+ if (WebRenderUserData::ProcessInvalidateForImage(this, type, providerId)) {
+ return;
+ }
+
+ InvalidateLayer(type);
+}
+
+NS_IMPL_ISUPPORTS(nsImageBoxListener, imgINotificationObserver)
+
+nsImageBoxListener::nsImageBoxListener(nsImageBoxFrame* frame)
+ : mFrame(frame) {}
+
+nsImageBoxListener::~nsImageBoxListener() = default;
+
+void nsImageBoxListener::Notify(imgIRequest* request, int32_t aType,
+ const nsIntRect* aData) {
+ if (!mFrame) {
+ return;
+ }
+
+ return mFrame->Notify(request, aType, aData);
+}
diff --git a/layout/xul/nsImageBoxFrame.h b/layout/xul/nsImageBoxFrame.h
new file mode 100644
index 0000000000..e233ab9d70
--- /dev/null
+++ b/layout/xul/nsImageBoxFrame.h
@@ -0,0 +1,184 @@
+/* -*- 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 nsImageBoxFrame_h___
+#define nsImageBoxFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "nsLeafBoxFrame.h"
+
+#include "imgIRequest.h"
+#include "imgIContainer.h"
+#include "imgINotificationObserver.h"
+
+class imgRequestProxy;
+class nsImageBoxFrame;
+
+namespace mozilla {
+class nsDisplayXULImage;
+class PresShell;
+} // namespace mozilla
+
+class nsImageBoxListener final : public imgINotificationObserver {
+ public:
+ explicit nsImageBoxListener(nsImageBoxFrame* frame);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_IMGINOTIFICATIONOBSERVER
+
+ void ClearFrame() { mFrame = nullptr; }
+
+ private:
+ virtual ~nsImageBoxListener();
+
+ nsImageBoxFrame* mFrame;
+};
+
+class nsImageBoxFrame final : public nsLeafBoxFrame {
+ public:
+ typedef mozilla::image::ImgDrawResult ImgDrawResult;
+ typedef mozilla::layers::ImageContainer ImageContainer;
+ typedef mozilla::layers::LayerManager LayerManager;
+
+ friend class mozilla::nsDisplayXULImage;
+ NS_DECL_FRAMEARENA_HELPERS(nsImageBoxFrame)
+ NS_DECL_QUERYFRAME
+
+ virtual nsSize GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual nsSize GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual nscoord GetXULBoxAscent(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual void MarkIntrinsicISizesDirty() override;
+
+ void Notify(imgIRequest* aRequest, int32_t aType, const nsIntRect* aData);
+
+ friend nsIFrame* NS_NewImageBoxFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+
+ virtual void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* asPrevInFlow) override;
+
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ virtual void DidSetComputedStyle(ComputedStyle* aOldStyle) override;
+
+ virtual void DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) override;
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override;
+#endif
+
+ /**
+ * Gets the image to be loaded from the current style. May be null if themed,
+ * or if not an url image.
+ *
+ * TODO(emilio): Maybe support list-style-image: linear-gradient() etc?
+ */
+ const mozilla::StyleImage* GetImageFromStyle(const ComputedStyle&) const;
+ const mozilla::StyleImage* GetImageFromStyle() const {
+ return GetImageFromStyle(*Style());
+ }
+
+ mozilla::ImageResolution GetImageResolution() const;
+
+ /**
+ * Update mUseSrcAttr from appropriate content attributes or from
+ * style, throw away the current image, and load the appropriate
+ * image.
+ * */
+ void UpdateImage();
+
+ /**
+ * Update mLoadFlags from content attributes. Does not attempt to reload the
+ * image using the new load flags.
+ */
+ void UpdateLoadFlags();
+
+ virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ virtual ~nsImageBoxFrame();
+
+ already_AddRefed<imgIContainer> GetImageContainerForPainting(
+ const nsPoint& aPt, ImgDrawResult& aDrawResult,
+ Maybe<nsPoint>& aAnchorPoint, nsRect& aDest);
+
+ ImgDrawResult PaintImage(gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nsPoint aPt,
+ uint32_t aFlags);
+
+ ImgDrawResult CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager, nsDisplayItem* aItem,
+ nsPoint aPt, uint32_t aFlags);
+
+ bool CanOptimizeToImageLayer();
+
+ nsRect GetDestRect(const nsPoint& aOffset, Maybe<nsPoint>& aAnchorPoint);
+
+ protected:
+ explicit nsImageBoxFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+
+ virtual void GetImageSize();
+
+ private:
+ void OnSizeAvailable(imgIRequest* aRequest, imgIContainer* aImage);
+ void OnDecodeComplete(imgIRequest* aRequest);
+ void OnLoadComplete(imgIRequest* aRequest, nsresult aStatus);
+ void OnImageIsAnimated(imgIRequest* aRequest);
+ void OnFrameUpdate(imgIRequest* aRequest);
+
+ nsRect mSubRect; ///< If set, indicates that only the portion of the image
+ ///< specified by the rect should be used.
+ nsSize mIntrinsicSize;
+ nsSize mImageSize;
+
+ RefPtr<imgRequestProxy> mImageRequest;
+ nsCOMPtr<imgINotificationObserver> mListener;
+
+ int32_t mLoadFlags;
+
+ // Boolean variable to determine if the current image request has been
+ // registered with the refresh driver.
+ bool mRequestRegistered;
+
+ bool mUseSrcAttr; ///< Whether or not the image src comes from an attribute.
+ bool mSuppressStyleCheck;
+}; // class nsImageBoxFrame
+
+namespace mozilla {
+class nsDisplayXULImage final : public nsPaintedDisplayItem {
+ public:
+ nsDisplayXULImage(nsDisplayListBuilder* aBuilder, nsImageBoxFrame* aFrame)
+ : nsPaintedDisplayItem(aBuilder, aFrame) {
+ MOZ_COUNT_CTOR(nsDisplayXULImage);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayXULImage)
+
+ virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder,
+ bool* aSnap) const override {
+ *aSnap = true;
+ return nsRect(ToReferenceFrame(), Frame()->GetSize());
+ }
+ // Doesn't handle HitTest because nsLeafBoxFrame already creates an
+ // event receiver for us
+ virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) 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("XULImage", TYPE_XUL_IMAGE)
+};
+
+} // namespace mozilla
+
+#endif /* nsImageBoxFrame_h___ */
diff --git a/layout/xul/nsLeafBoxFrame.cpp b/layout/xul/nsLeafBoxFrame.cpp
new file mode 100644
index 0000000000..c7e0a0b484
--- /dev/null
+++ b/layout/xul/nsLeafBoxFrame.cpp
@@ -0,0 +1,306 @@
+/* -*- 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/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/PresShell.h"
+#include "nsLeafBoxFrame.h"
+#include "nsBoxFrame.h"
+#include "nsCOMPtr.h"
+#include "nsGkAtoms.h"
+#include "nsPresContext.h"
+#include "nsIContent.h"
+#include "nsNameSpaceManager.h"
+#include "nsBoxLayoutState.h"
+#include "nsWidgetsCID.h"
+#include "nsViewManager.h"
+#include "nsContainerFrame.h"
+#include "nsDisplayList.h"
+#include <algorithm>
+
+using namespace mozilla;
+
+//
+// NS_NewLeafBoxFrame
+//
+// Creates a new Toolbar frame and returns it
+//
+nsIFrame* NS_NewLeafBoxFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsLeafBoxFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsLeafBoxFrame)
+
+/**
+ * Initialize us. This is a good time to get the alignment of the box
+ */
+void nsLeafBoxFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsLeafFrame::Init(aContent, aParent, aPrevInFlow);
+
+ if (HasAnyStateBits(NS_FRAME_FONT_INFLATION_CONTAINER)) {
+ AddStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT);
+ }
+}
+
+void nsLeafBoxFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ // REVIEW: GetFrameForPoint used to not report events for the background
+ // layer, whereas this code will put an event receiver for this frame in the
+ // BlockBorderBackground() list. But I don't see any need to preserve
+ // that anomalous behaviour. The important thing I'm preserving is that
+ // leaf boxes continue to receive events in the foreground layer.
+ DisplayBorderBackgroundOutline(aBuilder, aLists);
+
+ if (!aBuilder->IsForEventDelivery() || !IsVisibleForPainting()) return;
+
+ aLists.Content()->AppendNewToTop<nsDisplayEventReceiver>(aBuilder, this);
+}
+
+/* virtual */
+nscoord nsLeafBoxFrame::GetMinISize(gfxContext* aRenderingContext) {
+ nscoord result;
+ DISPLAY_MIN_INLINE_SIZE(this, result);
+ nsBoxLayoutState state(PresContext(), aRenderingContext);
+
+ WritingMode wm = GetWritingMode();
+ LogicalSize minSize(wm, GetXULMinSize(state));
+
+ // GetXULMinSize returns border-box size, and we want to return content
+ // inline-size. Since Reflow uses the reflow input's border and padding, we
+ // actually just want to subtract what GetXULMinSize added, which is the
+ // result of GetXULBorderAndPadding.
+ nsMargin bp;
+ GetXULBorderAndPadding(bp);
+
+ result = minSize.ISize(wm) - LogicalMargin(wm, bp).IStartEnd(wm);
+
+ return result;
+}
+
+/* virtual */
+nscoord nsLeafBoxFrame::GetPrefISize(gfxContext* aRenderingContext) {
+ nscoord result;
+ DISPLAY_PREF_INLINE_SIZE(this, result);
+ nsBoxLayoutState state(PresContext(), aRenderingContext);
+
+ WritingMode wm = GetWritingMode();
+ LogicalSize prefSize(wm, GetXULPrefSize(state));
+
+ // GetXULPrefSize returns border-box size, and we want to return content
+ // inline-size. Since Reflow uses the reflow input's border and padding, we
+ // actually just want to subtract what GetXULPrefSize added, which is the
+ // result of GetXULBorderAndPadding.
+ nsMargin bp;
+ GetXULBorderAndPadding(bp);
+
+ result = prefSize.ISize(wm) - LogicalMargin(wm, bp).IStartEnd(wm);
+
+ return result;
+}
+
+nscoord nsLeafBoxFrame::GetIntrinsicISize() {
+ // No intrinsic width
+ return 0;
+}
+
+LogicalSize nsLeafBoxFrame::ComputeAutoSize(
+ gfxContext* aRenderingContext, WritingMode aWM, const LogicalSize& aCBSize,
+ nscoord aAvailableISize, const LogicalSize& aMargin,
+ const LogicalSize& aBorderPadding, const StyleSizeOverrides& aSizeOverrides,
+ ComputeSizeFlags aFlags) {
+ // Important: NOT calling our direct superclass here!
+ return nsIFrame::ComputeAutoSize(aRenderingContext, aWM, aCBSize,
+ aAvailableISize, aMargin, aBorderPadding,
+ aSizeOverrides, aFlags);
+}
+
+void nsLeafBoxFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ // This is mostly a copy of nsBoxFrame::Reflow().
+ // We aren't able to share an implementation because of the frame
+ // class hierarchy. If you make changes here, please keep
+ // nsBoxFrame::Reflow in sync.
+
+ MarkInReflow();
+ DO_GLOBAL_REFLOW_COUNT("nsLeafBoxFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ NS_ASSERTION(
+ aReflowInput.ComputedWidth() >= 0 && aReflowInput.ComputedHeight() >= 0,
+ "Computed Size < 0");
+
+#ifdef DO_NOISY_REFLOW
+ printf(
+ "\n-------------Starting LeafBoxFrame Reflow "
+ "----------------------------\n");
+ printf("%p ** nsLBF::Reflow %d R: ", this, myCounter++);
+ switch (aReflowInput.reason) {
+ case eReflowReason_Initial:
+ printf("Ini");
+ break;
+ case eReflowReason_Incremental:
+ printf("Inc");
+ break;
+ case eReflowReason_Resize:
+ printf("Rsz");
+ break;
+ case eReflowReason_StyleChange:
+ printf("Sty");
+ break;
+ case eReflowReason_Dirty:
+ printf("Drt ");
+ break;
+ default:
+ printf("<unknown>%d", aReflowInput.reason);
+ break;
+ }
+
+ printSize("AW", aReflowInput.AvailableWidth());
+ printSize("AH", aReflowInput.AvailableHeight());
+ printSize("CW", aReflowInput.ComputedWidth());
+ printSize("CH", aReflowInput.ComputedHeight());
+
+ printf(" *\n");
+
+#endif
+
+ // create the layout state
+ nsBoxLayoutState state(aPresContext, aReflowInput.mRenderingContext);
+
+ nsSize computedSize(aReflowInput.ComputedWidth(),
+ aReflowInput.ComputedHeight());
+
+ nsMargin m;
+ m = aReflowInput.ComputedPhysicalBorderPadding();
+
+ // GetXULBorderAndPadding(m);
+
+ // this happens sometimes. So lets handle it gracefully.
+ if (aReflowInput.ComputedHeight() == 0) {
+ nsSize minSize = GetXULMinSize(state);
+ computedSize.height = minSize.height - m.top - m.bottom;
+ }
+
+ nsSize prefSize(0, 0);
+
+ // if we are told to layout intrinic then get our preferred size.
+ if (computedSize.width == NS_UNCONSTRAINEDSIZE ||
+ computedSize.height == NS_UNCONSTRAINEDSIZE) {
+ prefSize = GetXULPrefSize(state);
+ nsSize minSize = GetXULMinSize(state);
+ nsSize maxSize = GetXULMaxSize(state);
+ prefSize = XULBoundsCheck(minSize, prefSize, maxSize);
+ }
+
+ // get our desiredSize
+ if (aReflowInput.ComputedWidth() == NS_UNCONSTRAINEDSIZE) {
+ computedSize.width = prefSize.width;
+ } else {
+ computedSize.width += m.left + m.right;
+ }
+
+ if (aReflowInput.ComputedHeight() == NS_UNCONSTRAINEDSIZE) {
+ computedSize.height = prefSize.height;
+ } else {
+ computedSize.height += m.top + m.bottom;
+ }
+
+ // handle reflow input min and max sizes
+ // XXXbz the width handling here seems to be wrong, since
+ // mComputedMin/MaxWidth is a content-box size, whole
+ // computedSize.width is a border-box size...
+ if (computedSize.width > aReflowInput.ComputedMaxWidth())
+ computedSize.width = aReflowInput.ComputedMaxWidth();
+
+ if (computedSize.width < aReflowInput.ComputedMinWidth())
+ computedSize.width = aReflowInput.ComputedMinWidth();
+
+ // Now adjust computedSize.height for our min and max computed
+ // height. The only problem is that those are content-box sizes,
+ // while computedSize.height is a border-box size. So subtract off
+ // m.TopBottom() before adjusting, then readd it.
+ computedSize.height = std::max(0, computedSize.height - m.TopBottom());
+ computedSize.height =
+ NS_CSS_MINMAX(computedSize.height, aReflowInput.ComputedMinHeight(),
+ aReflowInput.ComputedMaxHeight());
+ computedSize.height += m.TopBottom();
+
+ nsRect r(mRect.x, mRect.y, computedSize.width, computedSize.height);
+
+ SetXULBounds(state, r);
+
+ // layout our children
+ XULLayout(state);
+
+ // ok our child could have gotten bigger. So lets get its bounds
+ aDesiredSize.Width() = mRect.width;
+ aDesiredSize.Height() = mRect.height;
+ aDesiredSize.SetBlockStartAscent(GetXULBoxAscent(state));
+
+ // the overflow rect is set in SetXULBounds() above
+ aDesiredSize.mOverflowAreas = GetOverflowAreas();
+
+#ifdef DO_NOISY_REFLOW
+ {
+ printf("%p ** nsLBF(done) W:%d H:%d ", this, aDesiredSize.Width(),
+ aDesiredSize.Height());
+
+ if (maxElementWidth) {
+ printf("MW:%d\n", *maxElementWidth);
+ } else {
+ printf("MW:?\n");
+ }
+ }
+#endif
+}
+
+#ifdef DEBUG_FRAME_DUMP
+nsresult nsLeafBoxFrame::GetFrameName(nsAString& aResult) const {
+ return MakeFrameName(u"LeafBox"_ns, aResult);
+}
+#endif
+
+nsresult nsLeafBoxFrame::CharacterDataChanged(
+ const CharacterDataChangeInfo& aInfo) {
+ MarkIntrinsicISizesDirty();
+ return nsLeafFrame::CharacterDataChanged(aInfo);
+}
+
+/* virtual */
+nsSize nsLeafBoxFrame::GetXULPrefSize(nsBoxLayoutState& aState) {
+ return nsIFrame::GetUncachedXULPrefSize(aState);
+}
+
+/* virtual */
+nsSize nsLeafBoxFrame::GetXULMinSize(nsBoxLayoutState& aState) {
+ return nsIFrame::GetUncachedXULMinSize(aState);
+}
+
+/* virtual */
+nsSize nsLeafBoxFrame::GetXULMaxSize(nsBoxLayoutState& aState) {
+ return nsIFrame::GetUncachedXULMaxSize(aState);
+}
+
+/* virtual */
+nscoord nsLeafBoxFrame::GetXULBoxAscent(nsBoxLayoutState& aState) {
+ if (IsXULCollapsed()) {
+ return 0;
+ }
+ return GetXULPrefSize(aState).height;
+}
+
+NS_IMETHODIMP
+nsLeafBoxFrame::DoXULLayout(nsBoxLayoutState& aState) { return NS_OK; }
diff --git a/layout/xul/nsLeafBoxFrame.h b/layout/xul/nsLeafBoxFrame.h
new file mode 100644
index 0000000000..bf9df4c3cd
--- /dev/null
+++ b/layout/xul/nsLeafBoxFrame.h
@@ -0,0 +1,80 @@
+/* -*- 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 nsLeafBoxFrame_h___
+#define nsLeafBoxFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "nsLeafFrame.h"
+
+namespace mozilla {
+class PresShell;
+} // namespace mozilla
+
+class nsLeafBoxFrame : public nsLeafFrame {
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsLeafBoxFrame)
+
+ friend nsIFrame* NS_NewLeafBoxFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+
+ virtual nsSize GetXULPrefSize(nsBoxLayoutState& aState) override;
+ virtual nsSize GetXULMinSize(nsBoxLayoutState& aState) override;
+ virtual nsSize GetXULMaxSize(nsBoxLayoutState& aState) override;
+ virtual nscoord GetXULBoxAscent(nsBoxLayoutState& aState) override;
+
+ virtual bool IsFrameOfType(uint32_t aFlags) const override {
+ // This is bogus, but it's what we've always done.
+ // Note that nsLeafFrame is also eReplacedContainsBlock.
+ return nsLeafFrame::IsFrameOfType(
+ aFlags & ~(nsIFrame::eReplaced | nsIFrame::eReplacedContainsBlock |
+ nsIFrame::eXULBox));
+ }
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override;
+#endif
+
+ // nsIHTMLReflow overrides
+
+ virtual nscoord GetMinISize(gfxContext* aRenderingContext) override;
+ virtual nscoord GetPrefISize(gfxContext* aRenderingContext) override;
+
+ // Our auto size is that provided by nsFrame, not nsLeafFrame
+ 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 void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ virtual nsresult CharacterDataChanged(
+ const CharacterDataChangeInfo&) override;
+
+ virtual void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* asPrevInFlow) override;
+
+ virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ virtual bool XULComputesOwnOverflowArea() override { return false; }
+
+ protected:
+ NS_IMETHOD DoXULLayout(nsBoxLayoutState& aState) override;
+
+ virtual nscoord GetIntrinsicISize() override;
+
+ explicit nsLeafBoxFrame(ComputedStyle* aStyle, nsPresContext* aPresContext,
+ ClassID aID = kClassID)
+ : nsLeafFrame(aStyle, aPresContext, aID) {}
+
+}; // class nsLeafBoxFrame
+
+#endif /* nsLeafBoxFrame_h___ */
diff --git a/layout/xul/nsMenuBarFrame.cpp b/layout/xul/nsMenuBarFrame.cpp
new file mode 100644
index 0000000000..914f12085e
--- /dev/null
+++ b/layout/xul/nsMenuBarFrame.cpp
@@ -0,0 +1,136 @@
+/* -*- 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 "nsMenuBarFrame.h"
+#include "mozilla/BasicEvents.h"
+#include "nsIContent.h"
+#include "nsAtom.h"
+#include "nsPresContext.h"
+#include "nsCSSRendering.h"
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsMenuPopupFrame.h"
+#include "nsUnicharUtils.h"
+#include "nsPIDOMWindow.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsCSSFrameConstructor.h"
+#ifdef XP_WIN
+# include "nsISound.h"
+# include "nsWidgetsCID.h"
+#endif
+#include "nsUTF8Utils.h"
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/XULMenuParentElement.h"
+#include "mozilla/dom/XULButtonElement.h"
+
+using namespace mozilla;
+
+//
+// NS_NewMenuBarFrame
+//
+// Wrapper for creating a new menu Bar container
+//
+nsIFrame* NS_NewMenuBarFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsMenuBarFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsMenuBarFrame)
+
+NS_QUERYFRAME_HEAD(nsMenuBarFrame)
+ NS_QUERYFRAME_ENTRY(nsMenuBarFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame)
+
+//
+// nsMenuBarFrame cntr
+//
+nsMenuBarFrame::nsMenuBarFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsBoxFrame(aStyle, aPresContext, kClassID) {}
+
+void nsMenuBarFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsBoxFrame::Init(aContent, aParent, aPrevInFlow);
+
+ // Create the menu bar listener.
+ mMenuBarListener = new nsMenuBarListener(this, aContent);
+}
+
+dom::XULMenuParentElement& nsMenuBarFrame::MenubarElement() const {
+ auto* content = dom::XULMenuParentElement::FromNode(GetContent());
+ MOZ_DIAGNOSTIC_ASSERT(content);
+ return *content;
+}
+
+MOZ_CAN_RUN_SCRIPT void nsMenuBarFrame::SetActive(bool aActiveFlag) {
+ // If the activity is not changed, there is nothing to do.
+ if (mIsActive == aActiveFlag) {
+ return;
+ }
+
+ if (!aActiveFlag) {
+ // If there is a request to deactivate the menu bar, check to see whether
+ // there is a menu popup open for the menu bar. In this case, don't
+ // deactivate the menu bar.
+ if (auto* activeChild = MenubarElement().GetActiveMenuChild()) {
+ if (activeChild->IsMenuPopupOpen()) {
+ return;
+ }
+ }
+ }
+
+ mIsActive = aActiveFlag;
+ if (mIsActive) {
+ InstallKeyboardNavigator();
+ } else {
+ mActiveByKeyboard = false;
+ RemoveKeyboardNavigator();
+ }
+
+ RefPtr menubar = &MenubarElement();
+ if (!aActiveFlag) {
+ menubar->SetActiveMenuChild(nullptr);
+ }
+
+ constexpr auto active = u"DOMMenuBarActive"_ns;
+ constexpr auto inactive = u"DOMMenuBarInactive"_ns;
+ FireDOMEvent(aActiveFlag ? active : inactive, menubar);
+}
+
+void nsMenuBarFrame::InstallKeyboardNavigator() {
+ if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
+ pm->SetActiveMenuBar(this, true);
+ }
+}
+
+void nsMenuBarFrame::MenuClosed() { SetActive(false); }
+
+void nsMenuBarFrame::HandleEnterKeyPress(WidgetEvent& aEvent) {
+ if (RefPtr<dom::XULButtonElement> activeChild =
+ MenubarElement().GetActiveMenuChild()) {
+ activeChild->HandleEnterKeyPress(aEvent);
+ }
+}
+
+void nsMenuBarFrame::RemoveKeyboardNavigator() {
+ if (!mIsActive) {
+ if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
+ pm->SetActiveMenuBar(this, false);
+ }
+ }
+}
+
+void nsMenuBarFrame::DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) {
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (pm) pm->SetActiveMenuBar(this, false);
+
+ mMenuBarListener->OnDestroyMenuBarFrame();
+ mMenuBarListener = nullptr;
+
+ nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
+}
diff --git a/layout/xul/nsMenuBarFrame.h b/layout/xul/nsMenuBarFrame.h
new file mode 100644
index 0000000000..9601f8be2e
--- /dev/null
+++ b/layout/xul/nsMenuBarFrame.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/. */
+
+//
+// nsMenuBarFrame
+//
+
+#ifndef nsMenuBarFrame_h__
+#define nsMenuBarFrame_h__
+
+#include "nsAtom.h"
+#include "nsCOMPtr.h"
+#include "nsBoxFrame.h"
+#include "nsMenuBarListener.h"
+
+class nsIContent;
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class KeyboardEvent;
+class XULMenuParentElement;
+} // namespace dom
+} // namespace mozilla
+
+nsIFrame* NS_NewMenuBarFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle);
+
+class nsMenuBarFrame final : public nsBoxFrame {
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsMenuBarFrame)
+
+ explicit nsMenuBarFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+
+ void InstallKeyboardNavigator();
+ void RemoveKeyboardNavigator();
+ MOZ_CAN_RUN_SCRIPT void MenuClosed();
+
+ void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ void DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) override;
+
+ bool IsActiveByKeyboard() { return mActiveByKeyboard; }
+ void SetActiveByKeyboard() { mActiveByKeyboard = true; }
+ MOZ_CAN_RUN_SCRIPT void SetActive(bool aActive);
+ bool IsActive() const { return mIsActive; }
+
+ mozilla::dom::XULMenuParentElement& MenubarElement() const;
+
+ // Called when Enter is pressed while the menubar is focused. If the current
+ // menu is open, let the child handle the key.
+ MOZ_CAN_RUN_SCRIPT void HandleEnterKeyPress(mozilla::WidgetEvent&);
+
+ bool IsFrameOfType(uint32_t aFlags) const override {
+ // Override bogus IsFrameOfType in nsBoxFrame.
+ if (aFlags & (nsIFrame::eReplacedContainsBlock | nsIFrame::eReplaced))
+ return false;
+ return nsBoxFrame::IsFrameOfType(aFlags);
+ }
+
+#ifdef DEBUG_FRAME_DUMP
+ nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"MenuBar"_ns, aResult);
+ }
+#endif
+
+ protected:
+ RefPtr<nsMenuBarListener> mMenuBarListener; // The listener that tells us
+ // about key and mouse events.
+
+ bool mIsActive = false; // Whether or not the menu bar is active (a menu item
+ // is highlighted or shown).
+ // Whether the menubar was made active via the keyboard.
+ bool mActiveByKeyboard = false;
+}; // class nsMenuBarFrame
+
+#endif
diff --git a/layout/xul/nsMenuBarListener.cpp b/layout/xul/nsMenuBarListener.cpp
new file mode 100644
index 0000000000..ed20499fa1
--- /dev/null
+++ b/layout/xul/nsMenuBarListener.cpp
@@ -0,0 +1,564 @@
+/* -*- 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 "nsMenuBarListener.h"
+#include "XULButtonElement.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/XULButtonElement.h"
+#include "nsMenuBarFrame.h"
+#include "nsMenuPopupFrame.h"
+#include "nsPIWindowRoot.h"
+#include "nsISound.h"
+
+// Drag & Drop, Clipboard
+#include "nsWidgetsCID.h"
+#include "nsCOMPtr.h"
+#include "nsIContent.h"
+
+#include "nsContentUtils.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/EventBinding.h"
+#include "mozilla/dom/KeyboardEvent.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include "mozilla/dom/XULMenuParentElement.h"
+#include "nsXULPopupManager.h"
+
+using namespace mozilla;
+using mozilla::dom::Event;
+using mozilla::dom::KeyboardEvent;
+
+/*
+ * nsMenuBarListener implementation
+ */
+
+NS_IMPL_ISUPPORTS(nsMenuBarListener, nsIDOMEventListener)
+
+////////////////////////////////////////////////////////////////////////
+
+int32_t nsMenuBarListener::mAccessKey = -1;
+Modifiers nsMenuBarListener::mAccessKeyMask = 0;
+
+nsMenuBarListener::nsMenuBarListener(nsMenuBarFrame* aMenuBarFrame,
+ nsIContent* aMenuBarContent)
+ : mMenuBarFrame(aMenuBarFrame),
+ mContent(dom::XULMenuParentElement::FromNode(aMenuBarContent)),
+ mEventTarget(aMenuBarContent->GetComposedDoc()),
+ mTopWindowEventTarget(nullptr),
+ mAccessKeyDown(false),
+ mAccessKeyDownCanceled(false) {
+ MOZ_ASSERT(mEventTarget);
+ MOZ_ASSERT(mContent);
+
+ // Hook up the menubar as a key listener on the whole document. This will
+ // see every keypress that occurs, but after everyone else does.
+
+ // Also hook up the listener to the window listening for focus events. This
+ // is so we can keep proper state as the user alt-tabs through processes.
+
+ mEventTarget->AddSystemEventListener(u"keypress"_ns, this, false);
+ mEventTarget->AddSystemEventListener(u"keydown"_ns, this, false);
+ mEventTarget->AddSystemEventListener(u"keyup"_ns, this, false);
+ mEventTarget->AddSystemEventListener(u"mozaccesskeynotfound"_ns, this, false);
+ // Need a capturing event listener if the user has blocked pages from
+ // overriding system keys so that we can prevent menu accesskeys from being
+ // cancelled.
+ mEventTarget->AddEventListener(u"keydown"_ns, this, true);
+
+ // mousedown event should be handled in all phase
+ mEventTarget->AddEventListener(u"mousedown"_ns, this, true);
+ mEventTarget->AddEventListener(u"mousedown"_ns, this, false);
+ mEventTarget->AddEventListener(u"blur"_ns, this, true);
+
+ mEventTarget->AddEventListener(u"MozDOMFullscreen:Entered"_ns, this, false);
+
+ // Needs to listen to the deactivate event of the window.
+ RefPtr<dom::EventTarget> topWindowEventTarget =
+ nsContentUtils::GetWindowRoot(aMenuBarContent->GetComposedDoc());
+ mTopWindowEventTarget = topWindowEventTarget.get();
+
+ mTopWindowEventTarget->AddSystemEventListener(u"deactivate"_ns, this, true);
+}
+
+////////////////////////////////////////////////////////////////////////
+nsMenuBarListener::~nsMenuBarListener() {
+ MOZ_ASSERT(!mEventTarget,
+ "OnDestroyMenuBarFrame() should've alreay been called");
+}
+
+void nsMenuBarListener::OnDestroyMenuBarFrame() {
+ mEventTarget->RemoveSystemEventListener(u"keypress"_ns, this, false);
+ mEventTarget->RemoveSystemEventListener(u"keydown"_ns, this, false);
+ mEventTarget->RemoveSystemEventListener(u"keyup"_ns, this, false);
+ mEventTarget->RemoveSystemEventListener(u"mozaccesskeynotfound"_ns, this,
+ false);
+ mEventTarget->RemoveEventListener(u"keydown"_ns, this, true);
+
+ mEventTarget->RemoveEventListener(u"mousedown"_ns, this, true);
+ mEventTarget->RemoveEventListener(u"mousedown"_ns, this, false);
+ mEventTarget->RemoveEventListener(u"blur"_ns, this, true);
+
+ mEventTarget->RemoveEventListener(u"MozDOMFullscreen:Entered"_ns, this,
+ false);
+
+ mTopWindowEventTarget->RemoveSystemEventListener(u"deactivate"_ns, this,
+ true);
+
+ mMenuBarFrame = nullptr;
+ mEventTarget = nullptr;
+ mTopWindowEventTarget = nullptr;
+}
+
+int32_t nsMenuBarListener::GetMenuAccessKey() {
+ InitAccessKey();
+ return mAccessKey;
+}
+
+void nsMenuBarListener::InitAccessKey() {
+ if (mAccessKey >= 0) return;
+
+ // Compiled-in defaults, in case we can't get LookAndFeel --
+ // mac doesn't have menu shortcuts, other platforms use alt.
+#ifdef XP_MACOSX
+ mAccessKey = 0;
+ mAccessKeyMask = 0;
+#else
+ mAccessKey = dom::KeyboardEvent_Binding::DOM_VK_ALT;
+ mAccessKeyMask = MODIFIER_ALT;
+#endif
+
+ // Get the menu access key value from prefs, overriding the default:
+ mAccessKey = Preferences::GetInt("ui.key.menuAccessKey", mAccessKey);
+ switch (mAccessKey) {
+ case dom::KeyboardEvent_Binding::DOM_VK_SHIFT:
+ mAccessKeyMask = MODIFIER_SHIFT;
+ break;
+ case dom::KeyboardEvent_Binding::DOM_VK_CONTROL:
+ mAccessKeyMask = MODIFIER_CONTROL;
+ break;
+ case dom::KeyboardEvent_Binding::DOM_VK_ALT:
+ mAccessKeyMask = MODIFIER_ALT;
+ break;
+ case dom::KeyboardEvent_Binding::DOM_VK_META:
+ mAccessKeyMask = MODIFIER_META;
+ break;
+ case dom::KeyboardEvent_Binding::DOM_VK_WIN:
+ mAccessKeyMask = MODIFIER_OS;
+ break;
+ default:
+ // Don't touch mAccessKeyMask.
+ break;
+ }
+}
+
+void nsMenuBarListener::ToggleMenuActiveState() {
+ if (mMenuBarFrame->IsActive()) {
+ mMenuBarFrame->SetActive(false);
+ } else {
+ RefPtr content = mContent;
+ mMenuBarFrame->SetActive(true);
+ content->SelectFirstItem();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+nsresult nsMenuBarListener::KeyUp(Event* aKeyEvent) {
+ WidgetKeyboardEvent* nativeKeyEvent =
+ aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
+ if (!nativeKeyEvent) {
+ return NS_OK;
+ }
+
+ InitAccessKey();
+
+ // handlers shouldn't be triggered by non-trusted events.
+ if (!nativeKeyEvent->IsTrusted()) {
+ return NS_OK;
+ }
+
+ if (!mAccessKey || !StaticPrefs::ui_key_menuAccessKeyFocuses()) {
+ return NS_OK;
+ }
+
+ // On a press of the ALT key by itself, we toggle the menu's
+ // active/inactive state.
+ if (!nativeKeyEvent->DefaultPrevented() && mAccessKeyDown &&
+ !mAccessKeyDownCanceled &&
+ static_cast<int32_t>(nativeKeyEvent->mKeyCode) == mAccessKey) {
+ // The access key was down and is now up, and no other
+ // keys were pressed in between.
+ bool toggleMenuActiveState = true;
+ if (!mMenuBarFrame->IsActive()) {
+ // If the focused content is in a remote process, we should allow the
+ // focused web app to prevent to activate the menubar.
+ if (nativeKeyEvent->WillBeSentToRemoteProcess()) {
+ nativeKeyEvent->StopImmediatePropagation();
+ nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess();
+ return NS_OK;
+ }
+ // First, close all existing popups because other popups shouldn't
+ // handle key events when menubar is active and IME should be
+ // disabled.
+ if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
+ pm->Rollup(0, false, nullptr, nullptr);
+ }
+ // If menubar active state is changed or the menubar is destroyed
+ // during closing the popups, we should do nothing anymore.
+ toggleMenuActiveState = !Destroyed() && !mMenuBarFrame->IsActive();
+ }
+ if (toggleMenuActiveState) {
+ if (!mMenuBarFrame->IsActive()) {
+ mMenuBarFrame->SetActiveByKeyboard();
+ }
+ ToggleMenuActiveState();
+ }
+ }
+
+ mAccessKeyDown = false;
+ mAccessKeyDownCanceled = false;
+
+ if (!Destroyed() && mMenuBarFrame->IsActive()) {
+ nativeKeyEvent->StopPropagation();
+ nativeKeyEvent->PreventDefault();
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+nsresult nsMenuBarListener::KeyPress(Event* aKeyEvent) {
+ // if event has already been handled, bail
+ if (!aKeyEvent || aKeyEvent->DefaultPrevented()) {
+ return NS_OK; // don't consume event
+ }
+
+ // handlers shouldn't be triggered by non-trusted events.
+ if (!aKeyEvent->IsTrusted()) {
+ return NS_OK;
+ }
+
+ InitAccessKey();
+
+ if (mAccessKey) {
+ // If accesskey handling was forwarded to a child process, wait for
+ // the mozaccesskeynotfound event before handling accesskeys.
+ WidgetKeyboardEvent* nativeKeyEvent =
+ aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
+ if (!nativeKeyEvent) {
+ return NS_OK;
+ }
+
+ RefPtr<KeyboardEvent> keyEvent = aKeyEvent->AsKeyboardEvent();
+ uint32_t keyCode = keyEvent->KeyCode();
+
+ // Cancel the access key flag unless we are pressing the access key.
+ if (keyCode != (uint32_t)mAccessKey) {
+ mAccessKeyDownCanceled = true;
+ }
+
+#ifndef XP_MACOSX
+ // Need to handle F10 specially on Non-Mac platform.
+ if (nativeKeyEvent->mMessage == eKeyPress && keyCode == NS_VK_F10) {
+ if ((GetModifiersForAccessKey(*keyEvent) & ~MODIFIER_CONTROL) == 0) {
+ // If the keyboard event should activate the menubar and will be
+ // sent to a remote process, it should be executed with reply
+ // event from the focused remote process. Note that if the menubar
+ // is active, the event is already marked as "stop cross
+ // process dispatching". So, in that case, this won't wait
+ // reply from the remote content.
+ if (nativeKeyEvent->WillBeSentToRemoteProcess()) {
+ nativeKeyEvent->StopImmediatePropagation();
+ nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess();
+ return NS_OK;
+ }
+ // The F10 key just went down by itself or with ctrl pressed.
+ // In Windows, both of these activate the menu bar.
+ mMenuBarFrame->SetActiveByKeyboard();
+ ToggleMenuActiveState();
+
+ if (mMenuBarFrame->IsActive()) {
+# ifdef MOZ_WIDGET_GTK
+ RefPtr child = mContent->GetActiveMenuChild();
+ // In GTK, this also opens the first menu.
+ child->OpenMenuPopup(false);
+# endif
+ aKeyEvent->StopPropagation();
+ aKeyEvent->PreventDefault();
+ }
+ }
+
+ return NS_OK;
+ }
+#endif // !XP_MACOSX
+
+ RefPtr menuForKey = GetMenuForKeyEvent(*keyEvent);
+ if (!menuForKey) {
+#ifdef XP_WIN
+ // Behavior on Windows - this item is on the menu bar, beep and deactivate
+ // the menu bar.
+ // TODO(emilio): This is rather odd, and I cannot get the beep to work,
+ // but this matches what old code was doing...
+ if (mMenuBarFrame->IsActive()) {
+ if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
+ sound->Beep();
+ }
+ mMenuBarFrame->SetActive(false);
+ }
+#endif
+ return NS_OK;
+ }
+
+ // If the keyboard event matches with a menu item's accesskey and
+ // will be sent to a remote process, it should be executed with
+ // reply event from the focused remote process. Note that if the
+ // menubar is active, the event is already marked as "stop cross
+ // process dispatching". So, in that case, this won't wait
+ // reply from the remote content.
+ if (nativeKeyEvent->WillBeSentToRemoteProcess()) {
+ nativeKeyEvent->StopImmediatePropagation();
+ nativeKeyEvent->MarkAsWaitingReplyFromRemoteProcess();
+ return NS_OK;
+ }
+
+ mMenuBarFrame->SetActiveByKeyboard();
+ mMenuBarFrame->SetActive(true);
+ menuForKey->OpenMenuPopup(true);
+
+ // The opened menu will listen next keyup event.
+ // Therefore, we should clear the keydown flags here.
+ mAccessKeyDown = mAccessKeyDownCanceled = false;
+
+ aKeyEvent->StopPropagation();
+ aKeyEvent->PreventDefault();
+ }
+
+ return NS_OK;
+}
+
+bool nsMenuBarListener::IsAccessKeyPressed(KeyboardEvent& aKeyEvent) {
+ InitAccessKey();
+ // No other modifiers are allowed to be down except for Shift.
+ uint32_t modifiers = GetModifiersForAccessKey(aKeyEvent);
+
+ return (mAccessKeyMask != MODIFIER_SHIFT && (modifiers & mAccessKeyMask) &&
+ (modifiers & ~(mAccessKeyMask | MODIFIER_SHIFT)) == 0);
+}
+
+Modifiers nsMenuBarListener::GetModifiersForAccessKey(
+ KeyboardEvent& aKeyEvent) {
+ WidgetInputEvent* inputEvent = aKeyEvent.WidgetEventPtr()->AsInputEvent();
+ MOZ_ASSERT(inputEvent);
+
+ static const Modifiers kPossibleModifiersForAccessKey =
+ (MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META |
+ MODIFIER_OS);
+ return inputEvent->mModifiers & kPossibleModifiersForAccessKey;
+}
+
+dom::XULButtonElement* nsMenuBarListener::GetMenuForKeyEvent(
+ KeyboardEvent& aKeyEvent) {
+ if (!IsAccessKeyPressed(aKeyEvent)) {
+ return nullptr;
+ }
+
+ uint32_t charCode = aKeyEvent.CharCode();
+ bool hasAccessKeyCandidates = charCode != 0;
+ if (!hasAccessKeyCandidates) {
+ WidgetKeyboardEvent* nativeKeyEvent =
+ aKeyEvent.WidgetEventPtr()->AsKeyboardEvent();
+ AutoTArray<uint32_t, 10> keys;
+ nativeKeyEvent->GetAccessKeyCandidates(keys);
+ hasAccessKeyCandidates = !keys.IsEmpty();
+ }
+
+ if (!hasAccessKeyCandidates) {
+ return nullptr;
+ }
+ // Do shortcut navigation.
+ // A letter was pressed. We want to see if a shortcut gets matched. If
+ // so, we'll know the menu got activated.
+ return mMenuBarFrame->MenubarElement().FindMenuWithShortcut(aKeyEvent);
+}
+
+void nsMenuBarListener::ReserveKeyIfNeeded(Event* aKeyEvent) {
+ WidgetKeyboardEvent* nativeKeyEvent =
+ aKeyEvent->WidgetEventPtr()->AsKeyboardEvent();
+ if (nsContentUtils::ShouldBlockReservedKeys(nativeKeyEvent)) {
+ nativeKeyEvent->MarkAsReservedByChrome();
+ }
+}
+
+////////////////////////////////////////////////////////////////////////
+nsresult nsMenuBarListener::KeyDown(Event* aKeyEvent) {
+ InitAccessKey();
+
+ // handlers shouldn't be triggered by non-trusted events.
+ if (!aKeyEvent || !aKeyEvent->IsTrusted()) {
+ return NS_OK;
+ }
+
+ RefPtr<KeyboardEvent> keyEvent = aKeyEvent->AsKeyboardEvent();
+ if (!keyEvent) {
+ return NS_OK;
+ }
+
+ uint32_t theChar = keyEvent->KeyCode();
+
+ uint16_t eventPhase = keyEvent->EventPhase();
+ bool capturing = (eventPhase == dom::Event_Binding::CAPTURING_PHASE);
+
+#ifndef XP_MACOSX
+ if (capturing && !mAccessKeyDown && theChar == NS_VK_F10 &&
+ (GetModifiersForAccessKey(*keyEvent) & ~MODIFIER_CONTROL) == 0) {
+ ReserveKeyIfNeeded(aKeyEvent);
+ }
+#endif
+
+ if (mAccessKey && StaticPrefs::ui_key_menuAccessKeyFocuses()) {
+ bool defaultPrevented = aKeyEvent->DefaultPrevented();
+
+ // No other modifiers can be down.
+ // Especially CTRL. CTRL+ALT == AltGR, and we'll break on non-US
+ // enhanced 102-key keyboards if we don't check this.
+ bool isAccessKeyDownEvent =
+ ((theChar == (uint32_t)mAccessKey) &&
+ (GetModifiersForAccessKey(*keyEvent) & ~mAccessKeyMask) == 0);
+
+ if (!capturing && !mAccessKeyDown) {
+ // If accesskey isn't being pressed and the key isn't the accesskey,
+ // ignore the event.
+ if (!isAccessKeyDownEvent) {
+ return NS_OK;
+ }
+
+ // Otherwise, accept the accesskey state.
+ mAccessKeyDown = true;
+ // If default is prevented already, cancel the access key down.
+ mAccessKeyDownCanceled = defaultPrevented;
+ return NS_OK;
+ }
+
+ // If the pressed accesskey was canceled already or the event was
+ // consumed already, ignore the event.
+ if (mAccessKeyDownCanceled || defaultPrevented) {
+ return NS_OK;
+ }
+
+ // Some key other than the access key just went down,
+ // so we won't activate the menu bar when the access key is released.
+ mAccessKeyDownCanceled = !isAccessKeyDownEvent;
+ }
+
+ if (capturing && mAccessKey) {
+ if (GetMenuForKeyEvent(*keyEvent)) {
+ ReserveKeyIfNeeded(aKeyEvent);
+ }
+ }
+
+ return NS_OK; // means I am NOT consuming event
+}
+
+////////////////////////////////////////////////////////////////////////
+
+nsresult nsMenuBarListener::Blur(Event* aEvent) {
+ if (!IsMenuOpen() && mMenuBarFrame->IsActive()) {
+ ToggleMenuActiveState();
+ mAccessKeyDown = false;
+ mAccessKeyDownCanceled = false;
+ }
+ return NS_OK; // means I am NOT consuming event
+}
+
+////////////////////////////////////////////////////////////////////////
+
+nsresult nsMenuBarListener::OnWindowDeactivated(Event* aEvent) {
+ // Reset the accesskey state because we cannot receive the keyup event for
+ // the pressing accesskey.
+ mAccessKeyDown = false;
+ mAccessKeyDownCanceled = false;
+ return NS_OK; // means I am NOT consuming event
+}
+
+bool nsMenuBarListener::IsMenuOpen() const {
+ auto* activeChild = mContent->GetActiveMenuChild();
+ return activeChild && activeChild->IsMenuPopupOpen();
+}
+
+////////////////////////////////////////////////////////////////////////
+nsresult nsMenuBarListener::MouseDown(Event* aMouseEvent) {
+ // NOTE: MouseDown method listens all phases
+
+ // Even if the mousedown event is canceled, it means the user don't want
+ // to activate the menu. Therefore, we need to record it at capturing (or
+ // target) phase.
+ if (mAccessKeyDown) {
+ mAccessKeyDownCanceled = true;
+ }
+
+ // Don't do anything at capturing phase, any behavior should be cancelable.
+ if (aMouseEvent->EventPhase() == dom::Event_Binding::CAPTURING_PHASE) {
+ return NS_OK;
+ }
+
+ if (!IsMenuOpen() && mMenuBarFrame->IsActive()) {
+ ToggleMenuActiveState();
+ }
+
+ return NS_OK; // means I am NOT consuming event
+}
+
+////////////////////////////////////////////////////////////////////////
+
+nsresult nsMenuBarListener::Fullscreen(Event* aEvent) {
+ if (mMenuBarFrame->IsActive()) {
+ ToggleMenuActiveState();
+ }
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult
+nsMenuBarListener::HandleEvent(Event* aEvent) {
+ // If the menu bar is collapsed, don't do anything.
+ if (!mMenuBarFrame->StyleVisibility()->IsVisible()) {
+ return NS_OK;
+ }
+
+ nsAutoString eventType;
+ aEvent->GetType(eventType);
+
+ if (eventType.EqualsLiteral("keyup")) {
+ return KeyUp(aEvent);
+ }
+ if (eventType.EqualsLiteral("keydown")) {
+ return KeyDown(aEvent);
+ }
+ if (eventType.EqualsLiteral("keypress")) {
+ return KeyPress(aEvent);
+ }
+ if (eventType.EqualsLiteral("mozaccesskeynotfound")) {
+ return KeyPress(aEvent);
+ }
+ if (eventType.EqualsLiteral("blur")) {
+ return Blur(aEvent);
+ }
+ if (eventType.EqualsLiteral("deactivate")) {
+ return OnWindowDeactivated(aEvent);
+ }
+ if (eventType.EqualsLiteral("mousedown")) {
+ return MouseDown(aEvent);
+ }
+ if (eventType.EqualsLiteral("MozDOMFullscreen:Entered")) {
+ return Fullscreen(aEvent);
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
+ return NS_OK;
+}
diff --git a/layout/xul/nsMenuBarListener.h b/layout/xul/nsMenuBarListener.h
new file mode 100644
index 0000000000..f917e9fd36
--- /dev/null
+++ b/layout/xul/nsMenuBarListener.h
@@ -0,0 +1,121 @@
+/* -*- 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 nsMenuBarListener_h
+#define nsMenuBarListener_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/EventForwards.h"
+#include "nsIContent.h"
+#include "nsIDOMEventListener.h"
+
+// X.h defines KeyPress
+#ifdef KeyPress
+# undef KeyPress
+#endif
+
+class nsMenuFrame;
+class nsMenuBarFrame;
+
+namespace mozilla {
+namespace dom {
+class EventTarget;
+class KeyboardEvent;
+class XULMenuParentElement;
+class XULButtonElement;
+} // namespace dom
+} // namespace mozilla
+
+/**
+ * EventListener implementation for menubar.
+ */
+class nsMenuBarListener final : public nsIDOMEventListener {
+ public:
+ explicit nsMenuBarListener(nsMenuBarFrame* aMenuBarFrame,
+ nsIContent* aMenuBarContent);
+
+ NS_DECL_ISUPPORTS
+
+ /**
+ * nsIDOMEventListener interface method.
+ */
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ /**
+ * When mMenuBarFrame is being destroyed, this should be called.
+ */
+ void OnDestroyMenuBarFrame();
+
+ /**
+ * GetMenuAccessKey() returns keyCode value of a modifier key which is
+ * used for accesskey. Returns 0 if the platform doesn't support access key.
+ */
+ static int32_t GetMenuAccessKey();
+
+ /**
+ * IsAccessKeyPressed() returns true if the modifier state of the event
+ * matches the modifier state of access key.
+ */
+ static bool IsAccessKeyPressed(mozilla::dom::KeyboardEvent&);
+
+ protected:
+ virtual ~nsMenuBarListener();
+
+ bool IsMenuOpen() const;
+
+ MOZ_CAN_RUN_SCRIPT nsresult KeyUp(mozilla::dom::Event* aMouseEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult KeyDown(mozilla::dom::Event* aMouseEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult KeyPress(mozilla::dom::Event* aMouseEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult Blur(mozilla::dom::Event* aEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult OnWindowDeactivated(mozilla::dom::Event* aEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult MouseDown(mozilla::dom::Event* aMouseEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult Fullscreen(mozilla::dom::Event* aEvent);
+
+ static void InitAccessKey();
+
+ static mozilla::Modifiers GetModifiersForAccessKey(
+ mozilla::dom::KeyboardEvent& event);
+
+ /**
+ * Given a key event for an Alt+shortcut combination,
+ * return the menu, if any, that would be opened. If aPeek
+ * is false, then play a beep and deactivate the menubar on Windows.
+ */
+ mozilla::dom::XULButtonElement* GetMenuForKeyEvent(
+ mozilla::dom::KeyboardEvent& aKeyEvent);
+
+ /**
+ * Call MarkAsReservedByChrome if the user's preferences indicate that
+ * the key should be chrome-only.
+ */
+ void ReserveKeyIfNeeded(mozilla::dom::Event* aKeyEvent);
+
+ // This should only be called by the nsMenuBarListener during event dispatch,
+ // thus ensuring that this doesn't get destroyed during the process.
+ MOZ_CAN_RUN_SCRIPT void ToggleMenuActiveState();
+
+ bool Destroyed() const { return !mMenuBarFrame; }
+
+ // The menu bar object.
+ nsMenuBarFrame* mMenuBarFrame;
+ mozilla::dom::XULMenuParentElement* mContent;
+ // The event target to listen to the events.
+ // XXX Should this store this as strong reference? However,
+ // OnDestroyMenuBarFrame() should be called at destroying mMenuBarFrame.
+ // So, weak reference must be safe.
+ mozilla::dom::EventTarget* mEventTarget;
+ // The top window as EventTarget.
+ mozilla::dom::EventTarget* mTopWindowEventTarget;
+ // Whether or not the ALT key is currently down.
+ bool mAccessKeyDown;
+ // Whether or not the ALT key down is canceled by other action.
+ bool mAccessKeyDownCanceled;
+ // See KeyboardEvent for sample values (DOM_VK_* constants).
+ static int32_t mAccessKey;
+ // Modifier mask for the access key.
+ static mozilla::Modifiers mAccessKeyMask;
+};
+
+#endif // #ifndef nsMenuBarListener_h
diff --git a/layout/xul/nsMenuPopupFrame.cpp b/layout/xul/nsMenuPopupFrame.cpp
new file mode 100644
index 0000000000..3a465bfe07
--- /dev/null
+++ b/layout/xul/nsMenuPopupFrame.cpp
@@ -0,0 +1,2479 @@
+/* -*- 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 "nsMenuPopupFrame.h"
+#include "XULButtonElement.h"
+#include "XULPopupElement.h"
+#include "mozilla/dom/XULPopupElement.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsIFrameInlines.h"
+#include "nsAtom.h"
+#include "nsPresContext.h"
+#include "mozilla/ComputedStyle.h"
+#include "nsCSSRendering.h"
+#include "nsNameSpaceManager.h"
+#include "nsIFrameInlines.h"
+#include "nsViewManager.h"
+#include "nsWidgetsCID.h"
+#include "nsMenuBarFrame.h"
+#include "nsPIDOMWindow.h"
+#include "nsFrameManager.h"
+#include "mozilla/dom/Document.h"
+#include "nsRect.h"
+#include "nsBoxLayoutState.h"
+#include "nsIScrollableFrame.h"
+#include "nsIPopupContainer.h"
+#include "nsIDocShell.h"
+#include "nsReadableUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsContentUtils.h"
+#include "nsCSSFrameConstructor.h"
+#include "nsPIWindowRoot.h"
+#include "nsIReflowCallback.h"
+#include "nsIDocShellTreeOwner.h"
+#include "nsIBaseWindow.h"
+#include "nsISound.h"
+#include "nsIScreenManager.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStyleConsts.h"
+#include "nsStyleStructInlines.h"
+#include "nsTransitionManager.h"
+#include "nsDisplayList.h"
+#include "nsIDOMXULSelectCntrlEl.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/Services.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/KeyboardEvent.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include <algorithm>
+
+#include "X11UndefineNone.h"
+
+using namespace mozilla;
+using mozilla::dom::Document;
+using mozilla::dom::Element;
+using mozilla::dom::Event;
+using mozilla::dom::XULButtonElement;
+
+int8_t nsMenuPopupFrame::sDefaultLevelIsTop = -1;
+
+TimeStamp nsMenuPopupFrame::sLastKeyTime;
+
+#ifdef MOZ_WAYLAND
+# include "mozilla/WidgetUtilsGtk.h"
+# define IS_WAYLAND_DISPLAY() mozilla::widget::GdkIsWaylandDisplay()
+extern mozilla::LazyLogModule gWidgetPopupLog;
+# define LOG_WAYLAND(...) \
+ MOZ_LOG(gWidgetPopupLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+#else
+# define IS_WAYLAND_DISPLAY() false
+# define LOG_WAYLAND (...)
+#endif
+
+// NS_NewMenuPopupFrame
+//
+// Wrapper for creating a new menu popup container
+//
+nsIFrame* NS_NewMenuPopupFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsMenuPopupFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsMenuPopupFrame)
+
+NS_QUERYFRAME_HEAD(nsMenuPopupFrame)
+ NS_QUERYFRAME_ENTRY(nsMenuPopupFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame)
+
+//
+// nsMenuPopupFrame ctor
+//
+nsMenuPopupFrame::nsMenuPopupFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsBoxFrame(aStyle, aPresContext, kClassID),
+ mView(nullptr),
+ mPrefSize(-1, -1),
+ mXPos(0),
+ mYPos(0),
+ mAlignmentOffset(0),
+ mLastClientOffset(0, 0),
+ mPopupType(ePopupTypePanel),
+ mPopupState(ePopupClosed),
+ mPopupAlignment(POPUPALIGNMENT_NONE),
+ mPopupAnchor(POPUPALIGNMENT_NONE),
+ mPosition(POPUPPOSITION_UNKNOWN),
+ mFlip(FlipType_Default),
+ mIsOpenChanged(false),
+ mMenuCanOverlapOSBar(false),
+ mInContentShell(true),
+ mIsOffset(false),
+ mHFlip(false),
+ mVFlip(false),
+ mPositionedOffset(0),
+ mAnchorType(MenuPopupAnchorType_Node) {
+ // the preference name is backwards here. True means that the 'top' level is
+ // the default, and false means that the 'parent' level is the default.
+ if (sDefaultLevelIsTop >= 0) return;
+ sDefaultLevelIsTop =
+ Preferences::GetBool("ui.panel.default_level_parent", false);
+} // ctor
+
+nsMenuPopupFrame::~nsMenuPopupFrame() = default;
+
+static bool IsMouseTransparent(const ComputedStyle& aStyle) {
+ // If pointer-events: none; is set on the popup, then the widget should
+ // ignore mouse events, passing them through to the content behind.
+ return aStyle.PointerEvents() == StylePointerEvents::None;
+}
+
+static nsIWidget::InputRegion ComputeInputRegion(const ComputedStyle& aStyle,
+ const nsPresContext& aPc) {
+ return {IsMouseTransparent(aStyle),
+ (aStyle.StyleUIReset()->mMozWindowInputRegionMargin.ToCSSPixels() *
+ aPc.CSSToDevPixelScale())
+ .Truncated()};
+}
+
+bool nsMenuPopupFrame::ShouldCreateWidgetUpfront() const {
+ if (mPopupType != ePopupTypeMenu) {
+ // Any panel with a type attribute, such as the autocomplete popup, is
+ // always generated right away.
+ return mContent->AsElement()->HasAttr(nsGkAtoms::type);
+ }
+
+ // Generate the widget up-front if the parent menu is a <menulist> unless its
+ // sizetopopup is set to "none".
+ return ShouldExpandToInflowParentOrAnchor();
+}
+
+void nsMenuPopupFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsBoxFrame::Init(aContent, aParent, aPrevInFlow);
+
+ // lookup if we're allowed to overlap the OS bar (menubar/taskbar) from the
+ // look&feel object
+ mMenuCanOverlapOSBar =
+ LookAndFeel::GetInt(LookAndFeel::IntID::MenusCanOverlapOSBar) != 0;
+
+ CreatePopupView();
+
+ // XXX Hack. The popup's view should float above all other views,
+ // so we use the nsView::SetFloating() to tell the view manager
+ // about that constraint.
+ nsView* ourView = GetView();
+ nsViewManager* viewManager = ourView->GetViewManager();
+ viewManager->SetViewFloating(ourView, true);
+
+ mPopupType = ePopupTypePanel;
+ if (aContent->IsAnyOfXULElements(nsGkAtoms::menupopup, nsGkAtoms::popup)) {
+ mPopupType = ePopupTypeMenu;
+ } else if (aContent->IsXULElement(nsGkAtoms::tooltip)) {
+ mPopupType = ePopupTypeTooltip;
+ }
+
+ if (PresContext()->IsChrome()) {
+ mInContentShell = false;
+ }
+
+ // Support incontentshell=false attribute to allow popups to be displayed
+ // outside of the content shell. Chrome only.
+ if (aContent->NodePrincipal()->IsSystemPrincipal()) {
+ if (aContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::incontentshell,
+ nsGkAtoms::_true, eCaseMatters)) {
+ mInContentShell = true;
+ } else if (aContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::incontentshell,
+ nsGkAtoms::_false, eCaseMatters)) {
+ mInContentShell = false;
+ }
+ }
+
+ // To improve performance, create the widget for the popup if needed. Popups
+ // such as menus will create their widgets later when the popup opens.
+ //
+ // FIXME(emilio): Doing this up-front for all menupopups causes a bunch of
+ // assertions, while it's supposed to be just an optimization.
+ if (!ourView->HasWidget() && ShouldCreateWidgetUpfront()) {
+ CreateWidgetForView(ourView);
+ }
+
+ AddStateBits(NS_FRAME_IN_POPUP);
+}
+
+bool nsMenuPopupFrame::HasRemoteContent() const {
+ return (!mInContentShell && mPopupType == ePopupTypePanel &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::remote,
+ nsGkAtoms::_true, eIgnoreCase));
+}
+
+bool nsMenuPopupFrame::IsNoAutoHide() const {
+ // Panels with noautohide="true" don't hide when the mouse is clicked
+ // outside of them, or when another application is made active. Non-autohide
+ // panels cannot be used in content windows.
+ return (!mInContentShell && mPopupType == ePopupTypePanel &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::noautohide,
+ nsGkAtoms::_true, eIgnoreCase));
+}
+
+nsPopupLevel nsMenuPopupFrame::PopupLevel(bool aIsNoAutoHide) const {
+ // The popup level is determined as follows, in this order:
+ // 1. non-panels (menus and tooltips) are always topmost
+ // 2. any specified level attribute
+ // 3. if a titlebar attribute is set, use the 'floating' level
+ // 4. if this is a noautohide panel, use the 'parent' level
+ // 5. use the platform-specific default level
+
+ // If this is not a panel, this is always a top-most popup.
+ if (mPopupType != ePopupTypePanel) return ePopupLevelTop;
+
+ // If the level attribute has been set, use that.
+ static Element::AttrValuesArray strings[] = {
+ nsGkAtoms::top, nsGkAtoms::parent, nsGkAtoms::floating, nullptr};
+ switch (mContent->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::level, strings, eCaseMatters)) {
+ case 0:
+ return ePopupLevelTop;
+ case 1:
+ return ePopupLevelParent;
+ case 2:
+ return ePopupLevelFloating;
+ }
+
+ // Panels with titlebars most likely want to be floating popups.
+ if (mContent->AsElement()->HasAttr(nsGkAtoms::titlebar))
+ return ePopupLevelFloating;
+
+ // If this panel is a noautohide panel, the default is the parent level.
+ if (aIsNoAutoHide) return ePopupLevelParent;
+
+ // Otherwise, the result depends on the platform.
+ return sDefaultLevelIsTop ? ePopupLevelTop : ePopupLevelParent;
+}
+
+void nsMenuPopupFrame::PrepareWidget(bool aRecreate) {
+ nsView* ourView = GetView();
+ if (aRecreate) {
+ if (auto* widget = GetWidget()) {
+ // Widget's WebRender resources needs to be cleared before creating new
+ // widget.
+ widget->ClearCachedWebrenderResources();
+ }
+ ourView->DestroyWidget();
+ }
+ if (!ourView->HasWidget()) {
+ CreateWidgetForView(ourView);
+ }
+ if (nsIWidget* widget = GetWidget()) {
+ // This won't dynamically update the color scheme changes while the widget
+ // is shown, but it's good enough.
+ widget->SetColorScheme(Some(LookAndFeel::ColorSchemeForFrame(this)));
+ }
+}
+
+nsresult nsMenuPopupFrame::CreateWidgetForView(nsView* aView) {
+ // Create a widget for ourselves.
+ nsWidgetInitData widgetData;
+ widgetData.mWindowType = eWindowType_popup;
+ widgetData.mBorderStyle = eBorderStyle_default;
+ widgetData.mForMenupopupFrame = true;
+ widgetData.mClipSiblings = true;
+ widgetData.mPopupHint = mPopupType;
+ widgetData.mNoAutoHide = IsNoAutoHide();
+
+ if (!mInContentShell) {
+ // A drag popup may be used for non-static translucent drag feedback
+ if (mPopupType == ePopupTypePanel &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::drag, eIgnoreCase)) {
+ widgetData.mIsDragPopup = true;
+ }
+ }
+
+ nsAutoString title;
+ if (widgetData.mNoAutoHide) {
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::titlebar,
+ nsGkAtoms::normal, eCaseMatters)) {
+ widgetData.mBorderStyle = eBorderStyle_title;
+
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label,
+ title);
+
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::close, nsGkAtoms::_true,
+ eCaseMatters)) {
+ widgetData.mBorderStyle = static_cast<enum nsBorderStyle>(
+ widgetData.mBorderStyle | eBorderStyle_close);
+ }
+ }
+ }
+
+ bool remote = HasRemoteContent();
+
+ nsTransparencyMode mode = nsLayoutUtils::GetFrameTransparency(this, this);
+ widgetData.mHasRemoteContent = remote;
+ widgetData.mSupportTranslucency = mode == eTransparencyTransparent;
+ widgetData.mPopupLevel = PopupLevel(widgetData.mNoAutoHide);
+
+ // Panels which have a parent level need a parent widget. This allows them to
+ // always appear in front of the parent window but behind other windows that
+ // should be in front of it.
+ nsCOMPtr<nsIWidget> parentWidget;
+ if (widgetData.mPopupLevel != ePopupLevelTop) {
+ nsCOMPtr<nsIDocShellTreeItem> dsti = PresContext()->GetDocShell();
+ if (!dsti) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
+ dsti->GetTreeOwner(getter_AddRefs(treeOwner));
+ if (!treeOwner) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIBaseWindow> baseWindow(do_QueryInterface(treeOwner));
+ if (baseWindow) baseWindow->GetMainWidget(getter_AddRefs(parentWidget));
+ }
+
+ nsresult rv =
+ aView->CreateWidgetForPopup(&widgetData, parentWidget, true, true);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsIWidget* widget = aView->GetWidget();
+ widget->SetTransparencyMode(mode);
+ widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext()));
+ widget->SetWindowShadowStyle(GetShadowStyle());
+ widget->SetWindowOpacity(StyleUIReset()->mWindowOpacity);
+ widget->SetWindowTransform(ComputeWidgetTransform());
+
+ // most popups don't have a title so avoid setting the title if there isn't
+ // one
+ if (!title.IsEmpty()) {
+ widget->SetTitle(title);
+ }
+
+ return NS_OK;
+}
+
+bool nsMenuPopupFrame::IsMouseTransparent() const {
+ return ::IsMouseTransparent(*Style());
+}
+
+StyleWindowShadow nsMenuPopupFrame::GetShadowStyle() {
+ StyleWindowShadow shadow = StyleUIReset()->mWindowShadow;
+ if (shadow != StyleWindowShadow::Default) return shadow;
+
+ switch (StyleDisplay()->EffectiveAppearance()) {
+ case StyleAppearance::Tooltip:
+ return StyleWindowShadow::Tooltip;
+ case StyleAppearance::Menupopup:
+ return StyleWindowShadow::Menu;
+ default:
+ return StyleWindowShadow::Default;
+ }
+}
+
+void nsMenuPopupFrame::SetPopupState(nsPopupState aState) {
+ mPopupState = aState;
+
+ // Work around https://gitlab.gnome.org/GNOME/gtk/-/issues/4166
+ if (aState == ePopupShown && IS_WAYLAND_DISPLAY()) {
+ if (nsIWidget* widget = GetWidget()) {
+ widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext()));
+ }
+ }
+}
+
+// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsXULPopupShownEvent::Run() {
+ nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame());
+ // Set the state to visible if the popup is still open.
+ if (popup && popup->IsOpen()) {
+ popup->SetPopupState(ePopupShown);
+ }
+
+ if (!mPopup->IsXULElement(nsGkAtoms::tooltip)) {
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(mPopup, "popup-shown", nullptr);
+ }
+ }
+ WidgetMouseEvent event(true, eXULPopupShown, nullptr,
+ WidgetMouseEvent::eReal);
+ return EventDispatcher::Dispatch(mPopup, mPresContext, &event);
+}
+
+NS_IMETHODIMP nsXULPopupShownEvent::HandleEvent(Event* aEvent) {
+ nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame());
+ // Ignore events not targeted at the popup itself (ie targeted at
+ // descendants):
+ if (mPopup != aEvent->GetTarget()) {
+ return NS_OK;
+ }
+ if (popup) {
+ // ResetPopupShownDispatcher will delete the reference to this, so keep
+ // another one until Run is finished.
+ RefPtr<nsXULPopupShownEvent> event = this;
+ // Only call Run if it the dispatcher was assigned. This avoids calling the
+ // Run method if the transitionend event fires multiple times.
+ if (popup->ClearPopupShownDispatcher()) {
+ return Run();
+ }
+ }
+
+ CancelListener();
+ return NS_OK;
+}
+
+void nsXULPopupShownEvent::CancelListener() {
+ mPopup->RemoveSystemEventListener(u"transitionend"_ns, this, false);
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsXULPopupShownEvent, Runnable,
+ nsIDOMEventListener);
+
+void nsMenuPopupFrame::DidSetComputedStyle(ComputedStyle* aOldStyle) {
+ nsBoxFrame::DidSetComputedStyle(aOldStyle);
+
+ if (!aOldStyle) {
+ return;
+ }
+
+ auto& newUI = *StyleUIReset();
+ auto& oldUI = *aOldStyle->StyleUIReset();
+ if (newUI.mWindowOpacity != oldUI.mWindowOpacity) {
+ if (nsIWidget* widget = GetWidget()) {
+ widget->SetWindowOpacity(newUI.mWindowOpacity);
+ }
+ }
+
+ if (newUI.mMozWindowTransform != oldUI.mMozWindowTransform) {
+ if (nsIWidget* widget = GetWidget()) {
+ widget->SetWindowTransform(ComputeWidgetTransform());
+ }
+ }
+
+ auto oldRegion = ComputeInputRegion(*aOldStyle, *PresContext());
+ auto newRegion = ComputeInputRegion(*Style(), *PresContext());
+ if (oldRegion.mFullyTransparent != newRegion.mFullyTransparent ||
+ oldRegion.mMargin != newRegion.mMargin) {
+ if (nsIWidget* widget = GetWidget()) {
+ widget->SetInputRegion(newRegion);
+ }
+ }
+}
+
+void nsMenuPopupFrame::ConstrainSizeForWayland(nsSize& aSize) const {
+#ifdef MOZ_WAYLAND
+ if (!IS_WAYLAND_DISPLAY()) {
+ return;
+ }
+
+ // If the size is not a whole number in CSS pixels we need round it up to
+ // avoid reflow of the tooltips/popups and putting the text on two lines
+ // (usually happens with 200% scale factor and font scale factor <> 1) because
+ // GTK throws away the decimals.
+ int32_t appPerCSS = AppUnitsPerCSSPixel();
+ if (aSize.width % appPerCSS > 0) {
+ aSize.width += appPerCSS;
+ }
+ if (aSize.height % appPerCSS > 0) {
+ aSize.height += appPerCSS;
+ }
+
+ nsIWidget* widget = GetWidget();
+ if (!widget) {
+ return;
+ }
+
+ // Shrink the popup down if it's larger than popup size received from Wayland
+ // compositor. We don't know screen size on Wayland so this is the only info
+ // we have there.
+ const nsSize waylandSize = LayoutDeviceIntRect::ToAppUnits(
+ widget->GetMoveToRectPopupSize(), PresContext()->AppUnitsPerDevPixel());
+ if (waylandSize.width > 0 && aSize.width > waylandSize.width) {
+ LOG_WAYLAND("Wayland constraint width [%p]: %d to %d", widget, aSize.width,
+ waylandSize.width);
+ aSize.width = waylandSize.width;
+ }
+ if (waylandSize.height > 0 && aSize.height > waylandSize.height) {
+ LOG_WAYLAND("Wayland constraint height [%p]: %d to %d", widget,
+ aSize.height, waylandSize.height);
+ aSize.height = waylandSize.height;
+ }
+#endif
+}
+
+void nsMenuPopupFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ DO_GLOBAL_REFLOW_COUNT("nsMenuPopupFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ nsBoxLayoutState state(aPresContext, aReflowInput.mRenderingContext,
+ &aReflowInput, aReflowInput.mReflowDepth);
+ LayoutPopup(state);
+
+ const auto wm = GetWritingMode();
+ LogicalSize boxSize = GetLogicalSize(wm);
+ aDesiredSize.SetSize(wm, boxSize);
+ aDesiredSize.SetBlockStartAscent(boxSize.BSize(wm));
+ aDesiredSize.SetOverflowAreasToDesiredBounds();
+ FinishAndStoreOverflow(&aDesiredSize, aReflowInput.mStyleDisplay);
+}
+
+void nsMenuPopupFrame::EnsureActiveMenuListItemIsVisible() {
+ if (!IsMenuList() || !IsOpen()) {
+ return;
+ }
+ nsIFrame* frame = GetCurrentMenuItemFrame();
+ if (!frame) {
+ return;
+ }
+ RefPtr<mozilla::PresShell> presShell = PresShell();
+ presShell->ScrollFrameIntoView(
+ frame, Nothing(), ScrollAxis(), ScrollAxis(),
+ ScrollFlags::ScrollOverflowHidden | ScrollFlags::ScrollFirstAncestorOnly);
+}
+
+void nsMenuPopupFrame::LayoutPopup(nsBoxLayoutState& aState) {
+ if (IsNativeMenu()) {
+ return;
+ }
+
+ SchedulePaint();
+
+ bool shouldPosition = [&] {
+ if (!IsAnchored()) {
+ return true;
+ }
+ if (ShouldFollowAnchor()) {
+ return true;
+ }
+ // Don't reposition anchored popups that shouldn't follow the anchor and
+ // have already been positioned.
+ return mPopupState != ePopupShown || mUsedScreenRect.IsEmpty();
+ }();
+
+ bool isOpen = IsOpen();
+ if (!isOpen) {
+ shouldPosition =
+ mPopupState == ePopupShowing || mPopupState == ePopupPositioning;
+
+ // If the popup is not open, only do layout while showing or if we're a
+ // menulist.
+ //
+ // This is needed because the SelectParent code wants to limit the height of
+ // the popup before opening it.
+ //
+ // TODO(emilio): We should consider adding a way to do that more reliably
+ // instead, but this preserves existing behavior.
+ const bool needsLayout = shouldPosition || IsMenuList();
+ if (!needsLayout) {
+ RemoveStateBits(NS_FRAME_FIRST_REFLOW);
+ return;
+ }
+ }
+
+ // if the popup has just been opened, make sure the scrolled window is at 0,0
+ // Don't scroll menulists as they will scroll to their selected item on their
+ // own.
+ if (mIsOpenChanged && !IsMenuList()) {
+ nsIScrollableFrame* scrollframe =
+ do_QueryFrame(nsIFrame::GetChildXULBox(this));
+ if (scrollframe) {
+ AutoWeakFrame weakFrame(this);
+ scrollframe->ScrollTo(nsPoint(0, 0), ScrollMode::Instant);
+ if (!weakFrame.IsAlive()) {
+ return;
+ }
+ }
+ }
+
+ // Get the preferred, minimum and maximum size. If the menu is sized to the
+ // popup, then the popup's width is the menu's width.
+ nsSize prefSize = GetXULPrefSize(aState);
+ nsSize minSize = GetXULMinSize(aState);
+ nsSize maxSize = GetXULMaxSize(aState);
+ if (ShouldExpandToInflowParentOrAnchor()) {
+ // Make sure to accommodate for our scrollbar if needed. Do it only for
+ // menulists to match previous behavior.
+ //
+ // NOTE(emilio): This is somewhat hacky. The "right" fix (which would be
+ // using scrollbar-gutter: stable on the scroller) isn't great, because even
+ // though we want a stable gutter, we want to draw on top of the gutter when
+ // there's no scrollbar, otherwise it looks rather weird.
+ //
+ // Automatically accommodating for the scrollbar otherwise would be bug
+ // 764076, but that has its own set of problems.
+ if (nsIScrollableFrame* sf = GetScrollFrame(this)) {
+ prefSize.width += sf->GetDesiredScrollbarSizes(&aState).LeftRight();
+ }
+
+ nscoord menuListOrAnchorWidth = 0;
+ if (nsIFrame* menuList = GetInFlowParent()) {
+ menuListOrAnchorWidth = menuList->GetRect().width;
+ }
+ if (mAnchorType == MenuPopupAnchorType_Rect) {
+ menuListOrAnchorWidth =
+ std::max(menuListOrAnchorWidth, mScreenRect.width);
+ }
+ // Input margin doesn't have contents, so account for it for popup sizing
+ // purposes.
+ menuListOrAnchorWidth +=
+ 2 * StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits();
+ prefSize.width = std::max(prefSize.width, menuListOrAnchorWidth);
+ }
+
+ prefSize = XULBoundsCheck(minSize, prefSize, maxSize);
+
+ ConstrainSizeForWayland(prefSize);
+
+ const bool sizeChanged = mPrefSize != prefSize;
+ // if the size changed then set the bounds to be the preferred size, and make
+ // sure we re-position the popup too (as that can shrink or resize us again).
+ if (sizeChanged) {
+ shouldPosition = true;
+ SetXULBounds(aState, nsRect(nsPoint(), prefSize), false);
+ mPrefSize = prefSize;
+ }
+
+ bool needCallback = false;
+ if (shouldPosition) {
+ SetPopupPosition(false);
+ needCallback = true;
+ }
+
+ // First do XUL layout on our contents.
+ const nsSize preLayoutSize = GetSize();
+ XULLayout(aState);
+
+ // If the width or height changed, readjust the popup position. This is a
+ // special case for tooltips where the preferred height doesn't include the
+ // real height for its inline element, but does once it is laid out.
+ // This is bug 228673 which doesn't have a simple fix.
+ // FIXME(emilio): Unclear if this is still an issue with modern flex
+ // emulation. Perhaps we should try to remove this.
+ bool rePosition = shouldPosition && (mPosition == POPUPPOSITION_SELECTION);
+ const nsSize postLayoutSize = GetSize();
+ if (postLayoutSize.width > preLayoutSize.width ||
+ postLayoutSize.height > preLayoutSize.height) {
+ // the size after layout was larger than the preferred size, so set the
+ // preferred size accordingly.
+ mPrefSize = postLayoutSize;
+ if (isOpen) {
+ rePosition = true;
+ needCallback = true;
+ }
+ }
+
+ if (rePosition) {
+ SetPopupPosition(false);
+ }
+
+ nsPresContext* pc = PresContext();
+ nsView* view = GetView();
+
+ if (sizeChanged) {
+ // If the size of the popup changed, apply any size constraints.
+ nsIWidget* widget = view->GetWidget();
+ if (widget) {
+ SetSizeConstraints(pc, widget, minSize, maxSize);
+ }
+ }
+
+ if (isOpen) {
+ nsViewManager* viewManager = view->GetViewManager();
+ nsRect rect = GetRect();
+ rect.x = rect.y = 0;
+ rect.SizeTo(XULBoundsCheck(minSize, rect.Size(), maxSize));
+ viewManager->ResizeView(view, rect);
+
+ if (mPopupState == ePopupOpening) {
+ mPopupState = ePopupVisible;
+ }
+
+ viewManager->SetViewVisibility(view, nsViewVisibility_kShow);
+ SyncFrameViewProperties(view);
+ }
+
+ // finally, if the popup just opened, send a popupshown event
+ bool openChanged = mIsOpenChanged;
+ if (openChanged) {
+ mIsOpenChanged = false;
+
+ // Make sure the current selection in a menulist is visible.
+ EnsureActiveMenuListItemIsVisible();
+
+ // If the animate attribute is set to open, check for a transition and wait
+ // for it to finish before firing the popupshown event.
+ if (LookAndFeel::GetInt(LookAndFeel::IntID::PanelAnimations) &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::animate, nsGkAtoms::open,
+ eCaseMatters) &&
+ AnimationUtils::HasCurrentTransitions(mContent->AsElement(),
+ PseudoStyleType::NotPseudo)) {
+ mPopupShownDispatcher = new nsXULPopupShownEvent(mContent, pc);
+ mContent->AddSystemEventListener(u"transitionend"_ns,
+ mPopupShownDispatcher, false, false);
+ return;
+ }
+
+ // If there are no transitions, fire the popupshown event right away.
+ nsCOMPtr<nsIRunnable> event = new nsXULPopupShownEvent(GetContent(), pc);
+ mContent->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
+ }
+
+ if (needCallback && !mReflowCallbackData.mPosted) {
+ pc->PresShell()->PostReflowCallback(this);
+ mReflowCallbackData.MarkPosted(openChanged);
+ }
+}
+
+bool nsMenuPopupFrame::ReflowFinished() {
+ SetPopupPosition(false);
+ mReflowCallbackData.Clear();
+ return false;
+}
+
+void nsMenuPopupFrame::ReflowCallbackCanceled() { mReflowCallbackData.Clear(); }
+
+bool nsMenuPopupFrame::IsMenuList() const {
+ return PopupElement().IsInMenuList();
+}
+
+bool nsMenuPopupFrame::ShouldExpandToInflowParentOrAnchor() const {
+ return IsMenuList() && !mContent->GetParent()->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::sizetopopup,
+ nsGkAtoms::none, eCaseMatters);
+}
+
+nsIContent* nsMenuPopupFrame::GetTriggerContent(
+ nsMenuPopupFrame* aMenuPopupFrame) {
+ while (aMenuPopupFrame) {
+ if (aMenuPopupFrame->mTriggerContent) {
+ return aMenuPopupFrame->mTriggerContent;
+ }
+
+ auto* button = XULButtonElement::FromNodeOrNull(
+ aMenuPopupFrame->GetContent()->GetParent());
+ if (!button || !button->IsMenu()) {
+ break;
+ }
+
+ auto* popup = button->GetContainingPopupElement();
+ if (!popup) {
+ break;
+ }
+
+ // check up the menu hierarchy until a popup with a trigger node is found
+ aMenuPopupFrame = do_QueryFrame(popup->GetPrimaryFrame());
+ }
+
+ return nullptr;
+}
+
+void nsMenuPopupFrame::InitPositionFromAnchorAlign(const nsAString& aAnchor,
+ const nsAString& aAlign) {
+ mTriggerContent = nullptr;
+
+ if (aAnchor.EqualsLiteral("topleft"))
+ mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
+ else if (aAnchor.EqualsLiteral("topright"))
+ mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
+ else if (aAnchor.EqualsLiteral("bottomleft"))
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
+ else if (aAnchor.EqualsLiteral("bottomright"))
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
+ else if (aAnchor.EqualsLiteral("leftcenter"))
+ mPopupAnchor = POPUPALIGNMENT_LEFTCENTER;
+ else if (aAnchor.EqualsLiteral("rightcenter"))
+ mPopupAnchor = POPUPALIGNMENT_RIGHTCENTER;
+ else if (aAnchor.EqualsLiteral("topcenter"))
+ mPopupAnchor = POPUPALIGNMENT_TOPCENTER;
+ else if (aAnchor.EqualsLiteral("bottomcenter"))
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMCENTER;
+ else
+ mPopupAnchor = POPUPALIGNMENT_NONE;
+
+ if (aAlign.EqualsLiteral("topleft"))
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ else if (aAlign.EqualsLiteral("topright"))
+ mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
+ else if (aAlign.EqualsLiteral("bottomleft"))
+ mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
+ else if (aAlign.EqualsLiteral("bottomright"))
+ mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
+ else
+ mPopupAlignment = POPUPALIGNMENT_NONE;
+
+ mPosition = POPUPPOSITION_UNKNOWN;
+}
+
+void nsMenuPopupFrame::InitializePopup(nsIContent* aAnchorContent,
+ nsIContent* aTriggerContent,
+ const nsAString& aPosition,
+ int32_t aXPos, int32_t aYPos,
+ MenuPopupAnchorType aAnchorType,
+ bool aAttributesOverride) {
+ auto* widget = GetWidget();
+ bool recreateWidget = widget && widget->NeedsRecreateToReshow();
+ PrepareWidget(recreateWidget);
+
+ mPopupState = ePopupShowing;
+ mAnchorContent = aAnchorContent;
+ mTriggerContent = aTriggerContent;
+ mXPos = aXPos;
+ mYPos = aYPos;
+ mIsNativeMenu = false;
+ mIsTopLevelContextMenu = false;
+ mVFlip = false;
+ mHFlip = false;
+ mAlignmentOffset = 0;
+ mPositionedOffset = 0;
+ mPositionedByMoveToRect = false;
+
+ mAnchorType = aAnchorType;
+
+ // if aAttributesOverride is true, then the popupanchor, popupalign and
+ // position attributes on the <menupopup> override those values passed in.
+ // If false, those attributes are only used if the values passed in are empty
+ if (aAnchorContent || aAnchorType == MenuPopupAnchorType_Rect) {
+ nsAutoString anchor, align, position, flip;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::popupanchor,
+ anchor);
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::popupalign,
+ align);
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::position,
+ position);
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::flip, flip);
+
+ if (aAttributesOverride) {
+ // if the attributes are set, clear the offset position. Otherwise,
+ // the offset is used to adjust the position from the anchor point
+ if (anchor.IsEmpty() && align.IsEmpty() && position.IsEmpty())
+ position.Assign(aPosition);
+ else
+ mXPos = mYPos = 0;
+ } else if (!aPosition.IsEmpty()) {
+ position.Assign(aPosition);
+ }
+
+ if (flip.EqualsLiteral("none")) {
+ mFlip = FlipType_None;
+ } else if (flip.EqualsLiteral("both")) {
+ mFlip = FlipType_Both;
+ } else if (flip.EqualsLiteral("slide")) {
+ mFlip = FlipType_Slide;
+ }
+
+ position.CompressWhitespace();
+ int32_t spaceIdx = position.FindChar(' ');
+ // if there is a space in the position, assume it is the anchor and
+ // alignment as two separate tokens.
+ if (spaceIdx >= 0) {
+ InitPositionFromAnchorAlign(Substring(position, 0, spaceIdx),
+ Substring(position, spaceIdx + 1));
+ } else if (position.EqualsLiteral("before_start")) {
+ mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
+ mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
+ mPosition = POPUPPOSITION_BEFORESTART;
+ } else if (position.EqualsLiteral("before_end")) {
+ mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
+ mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
+ mPosition = POPUPPOSITION_BEFOREEND;
+ } else if (position.EqualsLiteral("after_start")) {
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ mPosition = POPUPPOSITION_AFTERSTART;
+ } else if (position.EqualsLiteral("after_end")) {
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
+ mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
+ mPosition = POPUPPOSITION_AFTEREND;
+ } else if (position.EqualsLiteral("start_before")) {
+ mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
+ mPopupAlignment = POPUPALIGNMENT_TOPRIGHT;
+ mPosition = POPUPPOSITION_STARTBEFORE;
+ } else if (position.EqualsLiteral("start_after")) {
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
+ mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT;
+ mPosition = POPUPPOSITION_STARTAFTER;
+ } else if (position.EqualsLiteral("end_before")) {
+ mPopupAnchor = POPUPALIGNMENT_TOPRIGHT;
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ mPosition = POPUPPOSITION_ENDBEFORE;
+ } else if (position.EqualsLiteral("end_after")) {
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT;
+ mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT;
+ mPosition = POPUPPOSITION_ENDAFTER;
+ } else if (position.EqualsLiteral("overlap")) {
+ mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ mPosition = POPUPPOSITION_OVERLAP;
+ } else if (position.EqualsLiteral("after_pointer")) {
+ mPopupAnchor = POPUPALIGNMENT_TOPLEFT;
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ mPosition = POPUPPOSITION_AFTERPOINTER;
+ // XXXndeakin this is supposed to anchor vertically after, but with the
+ // horizontal position as the mouse pointer.
+ mYPos += 21;
+ } else if (position.EqualsLiteral("selection")) {
+ mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT;
+ mPopupAlignment = POPUPALIGNMENT_TOPLEFT;
+ mPosition = POPUPPOSITION_SELECTION;
+ } else {
+ InitPositionFromAnchorAlign(anchor, align);
+ }
+ }
+ // When converted back to CSSIntRect it is (-1, -1, 0, 0) - as expected in
+ // nsXULPopupManager::Rollup
+ mScreenRect = nsRect(-AppUnitsPerCSSPixel(), -AppUnitsPerCSSPixel(), 0, 0);
+
+ if (aAttributesOverride) {
+ // Use |left| and |top| dimension attributes to position the popup if
+ // present, as they may have been persisted.
+ nsAutoString left, top;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::left, left);
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::top, top);
+
+ nsresult err;
+ if (!left.IsEmpty()) {
+ int32_t x = left.ToInteger(&err);
+ if (NS_SUCCEEDED(err)) {
+ mScreenRect.x = CSSPixel::ToAppUnits(x);
+ }
+ }
+ if (!top.IsEmpty()) {
+ int32_t y = top.ToInteger(&err);
+ if (NS_SUCCEEDED(err)) {
+ mScreenRect.y = CSSPixel::ToAppUnits(y);
+ }
+ }
+ }
+}
+
+void nsMenuPopupFrame::InitializePopupAtScreen(nsIContent* aTriggerContent,
+ int32_t aXPos, int32_t aYPos,
+ bool aIsContextMenu) {
+ auto* widget = GetWidget();
+ bool recreateWidget = widget && widget->NeedsRecreateToReshow();
+ PrepareWidget(recreateWidget);
+
+ mPopupState = ePopupShowing;
+ mAnchorContent = nullptr;
+ mTriggerContent = aTriggerContent;
+ mScreenRect =
+ nsRect(CSSPixel::ToAppUnits(aXPos), CSSPixel::ToAppUnits(aYPos), 0, 0);
+ mXPos = 0;
+ mYPos = 0;
+ mFlip = FlipType_Default;
+ mPopupAnchor = POPUPALIGNMENT_NONE;
+ mPopupAlignment = POPUPALIGNMENT_NONE;
+ mPosition = POPUPPOSITION_UNKNOWN;
+ mIsContextMenu = aIsContextMenu;
+ mIsTopLevelContextMenu = aIsContextMenu;
+ mIsNativeMenu = false;
+ mAnchorType = MenuPopupAnchorType_Point;
+ mPositionedOffset = 0;
+ mPositionedByMoveToRect = false;
+}
+
+void nsMenuPopupFrame::InitializePopupAsNativeContextMenu(
+ nsIContent* aTriggerContent, int32_t aXPos, int32_t aYPos) {
+ mTriggerContent = aTriggerContent;
+ mPopupState = ePopupShowing;
+ mAnchorContent = nullptr;
+ mScreenRect =
+ nsRect(CSSPixel::ToAppUnits(aXPos), CSSPixel::ToAppUnits(aYPos), 0, 0);
+ mXPos = 0;
+ mYPos = 0;
+ mFlip = FlipType_Default;
+ mPopupAnchor = POPUPALIGNMENT_NONE;
+ mPopupAlignment = POPUPALIGNMENT_NONE;
+ mPosition = POPUPPOSITION_UNKNOWN;
+ mIsContextMenu = true;
+ mIsTopLevelContextMenu = true;
+ mIsNativeMenu = true;
+ mAnchorType = MenuPopupAnchorType_Point;
+ mPositionedOffset = 0;
+ mPositionedByMoveToRect = false;
+}
+
+void nsMenuPopupFrame::InitializePopupAtRect(nsIContent* aTriggerContent,
+ const nsAString& aPosition,
+ const nsIntRect& aRect,
+ bool aAttributesOverride) {
+ InitializePopup(nullptr, aTriggerContent, aPosition, 0, 0,
+ MenuPopupAnchorType_Rect, aAttributesOverride);
+ mScreenRect = ToAppUnits(aRect, AppUnitsPerCSSPixel());
+}
+
+void nsMenuPopupFrame::ShowPopup(bool aIsContextMenu) {
+ mIsContextMenu = aIsContextMenu;
+
+ InvalidateFrameSubtree();
+
+ if (mPopupState == ePopupShowing || mPopupState == ePopupPositioning) {
+ mPopupState = ePopupOpening;
+ mIsOpenChanged = true;
+
+ // Clear mouse capture when a popup is opened.
+ if (mPopupType == ePopupTypeMenu) {
+ EventStateManager* activeESM = static_cast<EventStateManager*>(
+ EventStateManager::GetActiveEventStateManager());
+ if (activeESM) {
+ EventStateManager::ClearGlobalActiveContent(activeESM);
+ }
+
+ PresShell::ReleaseCapturingContent();
+ }
+
+ if (RefPtr menu = PopupElement().GetContainingMenu()) {
+ menu->PopupOpened();
+ }
+
+ // do we need an actual reflow here?
+ // is SetPopupPosition all that is needed?
+ PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors,
+ NS_FRAME_IS_DIRTY);
+
+ if (mPopupType == ePopupTypeMenu) {
+ nsCOMPtr<nsISound> sound(do_GetService("@mozilla.org/sound;1"));
+ if (sound) sound->PlayEventSound(nsISound::EVENT_MENU_POPUP);
+ }
+ }
+}
+
+void nsMenuPopupFrame::ClearTriggerContentIncludingDocument() {
+ // clear the trigger content if the popup is being closed. But don't clear
+ // it if the popup is just being made invisible as a popuphiding or command
+ if (mTriggerContent) {
+ // if the popup had a trigger node set, clear the global window popup node
+ // as well
+ Document* doc = mContent->GetUncomposedDoc();
+ if (doc) {
+ if (nsPIDOMWindowOuter* win = doc->GetWindow()) {
+ nsCOMPtr<nsPIWindowRoot> root = win->GetTopWindowRoot();
+ if (root) {
+ root->SetPopupNode(nullptr);
+ }
+ }
+ }
+ }
+ mTriggerContent = nullptr;
+}
+
+void nsMenuPopupFrame::HidePopup(bool aDeselectMenu, nsPopupState aNewState,
+ bool aFromFrameDestruction) {
+ NS_ASSERTION(aNewState == ePopupClosed || aNewState == ePopupInvisible,
+ "popup being set to unexpected state");
+
+ ClearPopupShownDispatcher();
+
+ // don't hide the popup when it isn't open
+ if (mPopupState == ePopupClosed || mPopupState == ePopupShowing ||
+ mPopupState == ePopupPositioning) {
+ return;
+ }
+
+ if (aNewState == ePopupClosed) {
+ // clear the trigger content if the popup is being closed. But don't clear
+ // it if the popup is just being made invisible as a popuphiding or command
+ // event may want to retrieve it.
+ ClearTriggerContentIncludingDocument();
+ mAnchorContent = nullptr;
+ }
+
+ // when invisible and about to be closed, HidePopup has already been called,
+ // so just set the new state to closed and return
+ if (mPopupState == ePopupInvisible) {
+ if (aNewState == ePopupClosed) {
+ mPopupState = ePopupClosed;
+ }
+ return;
+ }
+
+ mPopupState = aNewState;
+
+ mIncrementalString.Truncate();
+
+ mIsOpenChanged = false;
+ mHFlip = mVFlip = false;
+
+ if (auto* widget = GetWidget()) {
+ // Ideally we should call ClearCachedWebrenderResources but there are
+ // intermittent failures (see bug 1748788), so we currently call
+ // ClearWebrenderAnimationResources instead.
+ widget->ClearWebrenderAnimationResources();
+ }
+
+ nsView* view = GetView();
+ nsViewManager* viewManager = view->GetViewManager();
+ viewManager->SetViewVisibility(view, nsViewVisibility_kHide);
+
+ RefPtr popup = &PopupElement();
+ // XXX, bug 137033, In Windows, if mouse is outside the window when the
+ // menupopup closes, no mouse_enter/mouse_exit event will be fired to clear
+ // current hover state, we should clear it manually. This code may not the
+ // best solution, but we can leave it here until we find the better approach.
+ if (!aFromFrameDestruction &&
+ popup->State().HasState(dom::ElementState::HOVER)) {
+ EventStateManager* esm = PresContext()->EventStateManager();
+ esm->SetContentState(nullptr, dom::ElementState::HOVER);
+ }
+ popup->PopupClosed(aDeselectMenu);
+}
+
+nsIFrame::ReflowChildFlags nsMenuPopupFrame::GetXULLayoutFlags() {
+ return ReflowChildFlags::NoSizeView | ReflowChildFlags::NoMoveView;
+}
+
+nsPoint nsMenuPopupFrame::AdjustPositionForAnchorAlign(nsRect& anchorRect,
+ FlipStyle& aHFlip,
+ FlipStyle& aVFlip) {
+ // flip the anchor and alignment for right-to-left
+ int8_t popupAnchor(mPopupAnchor);
+ int8_t popupAlign(mPopupAlignment);
+ if (IsDirectionRTL()) {
+ // no need to flip the centered anchor types vertically
+ if (popupAnchor <= POPUPALIGNMENT_LEFTCENTER) {
+ popupAnchor = -popupAnchor;
+ }
+ popupAlign = -popupAlign;
+ }
+
+ nsRect originalAnchorRect(anchorRect);
+
+ // first, determine at which corner of the anchor the popup should appear
+ nsPoint pnt;
+ switch (popupAnchor) {
+ case POPUPALIGNMENT_LEFTCENTER:
+ pnt = nsPoint(anchorRect.x, anchorRect.y + anchorRect.height / 2);
+ anchorRect.y = pnt.y;
+ anchorRect.height = 0;
+ break;
+ case POPUPALIGNMENT_RIGHTCENTER:
+ pnt = nsPoint(anchorRect.XMost(), anchorRect.y + anchorRect.height / 2);
+ anchorRect.y = pnt.y;
+ anchorRect.height = 0;
+ break;
+ case POPUPALIGNMENT_TOPCENTER:
+ pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.y);
+ anchorRect.x = pnt.x;
+ anchorRect.width = 0;
+ break;
+ case POPUPALIGNMENT_BOTTOMCENTER:
+ pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.YMost());
+ anchorRect.x = pnt.x;
+ anchorRect.width = 0;
+ break;
+ case POPUPALIGNMENT_TOPRIGHT:
+ pnt = anchorRect.TopRight();
+ break;
+ case POPUPALIGNMENT_BOTTOMLEFT:
+ pnt = anchorRect.BottomLeft();
+ break;
+ case POPUPALIGNMENT_BOTTOMRIGHT:
+ pnt = anchorRect.BottomRight();
+ break;
+ case POPUPALIGNMENT_TOPLEFT:
+ default:
+ pnt = anchorRect.TopLeft();
+ break;
+ }
+
+ // If the alignment is on the right edge of the popup, move the popup left
+ // by the width. Similarly, if the alignment is on the bottom edge of the
+ // popup, move the popup up by the height. In addition, account for the
+ // margins of the popup on the edge on which it is aligned.
+ nsMargin margin = GetMargin();
+ switch (popupAlign) {
+ case POPUPALIGNMENT_TOPRIGHT:
+ pnt.MoveBy(-mRect.width - margin.right, margin.top);
+ break;
+ case POPUPALIGNMENT_BOTTOMLEFT:
+ pnt.MoveBy(margin.left, -mRect.height - margin.bottom);
+ break;
+ case POPUPALIGNMENT_BOTTOMRIGHT:
+ pnt.MoveBy(-mRect.width - margin.right, -mRect.height - margin.bottom);
+ break;
+ case POPUPALIGNMENT_TOPLEFT:
+ default:
+ pnt.MoveBy(margin.left, margin.top);
+ break;
+ }
+
+ // If we aligning to the selected item in the popup, adjust the vertical
+ // position by the height of the menulist label and the selected item's
+ // position.
+ if (mPosition == POPUPPOSITION_SELECTION) {
+ MOZ_ASSERT(popupAnchor == POPUPALIGNMENT_BOTTOMLEFT ||
+ popupAnchor == POPUPALIGNMENT_BOTTOMRIGHT);
+ MOZ_ASSERT(popupAlign == POPUPALIGNMENT_TOPLEFT ||
+ popupAlign == POPUPALIGNMENT_TOPRIGHT);
+
+ // Only adjust the popup if it just opened, otherwise the popup will move
+ // around if its gets resized or the selection changed. Cache the value in
+ // mPositionedOffset and use that instead for any future calculations.
+ if (mIsOpenChanged || mReflowCallbackData.mIsOpenChanged) {
+ if (nsIFrame* selectedItemFrame = GetSelectedItemForAlignment()) {
+ mPositionedOffset =
+ originalAnchorRect.height + selectedItemFrame->GetOffsetTo(this).y;
+ }
+ }
+
+ pnt.y -= mPositionedOffset;
+ }
+
+ // Flipping horizontally is allowed as long as the popup is above or below
+ // the anchor. This will happen if both the anchor and alignment are top or
+ // both are bottom, but different values. Similarly, flipping vertically is
+ // allowed if the popup is to the left or right of the anchor. In this case,
+ // the values of the constants are such that both must be positive or both
+ // must be negative. A special case, used for overlap, allows flipping
+ // vertically as well.
+ // If we are flipping in both directions, we want to set a flip style both
+ // horizontally and vertically. However, we want to flip on the inside edge
+ // of the anchor. Consider the example of a typical dropdown menu.
+ // Vertically, we flip the popup on the outside edges of the anchor menu,
+ // however horizontally, we want to to use the inside edges so the popup
+ // still appears underneath the anchor menu instead of floating off the
+ // side of the menu.
+ switch (popupAnchor) {
+ case POPUPALIGNMENT_LEFTCENTER:
+ case POPUPALIGNMENT_RIGHTCENTER:
+ aHFlip = FlipStyle_Outside;
+ aVFlip = FlipStyle_Inside;
+ break;
+ case POPUPALIGNMENT_TOPCENTER:
+ case POPUPALIGNMENT_BOTTOMCENTER:
+ aHFlip = FlipStyle_Inside;
+ aVFlip = FlipStyle_Outside;
+ break;
+ default: {
+ FlipStyle anchorEdge =
+ mFlip == FlipType_Both ? FlipStyle_Inside : FlipStyle_None;
+ aHFlip = (popupAnchor == -popupAlign) ? FlipStyle_Outside : anchorEdge;
+ if (((popupAnchor > 0) == (popupAlign > 0)) ||
+ (popupAnchor == POPUPALIGNMENT_TOPLEFT &&
+ popupAlign == POPUPALIGNMENT_TOPLEFT))
+ aVFlip = FlipStyle_Outside;
+ else
+ aVFlip = anchorEdge;
+ break;
+ }
+ }
+
+ return pnt;
+}
+
+nsIFrame* nsMenuPopupFrame::GetSelectedItemForAlignment() {
+ // This method adjusts a menulist's popup such that the selected item is under
+ // the cursor, aligned with the menulist label.
+ nsCOMPtr<nsIDOMXULSelectControlElement> select;
+ if (mAnchorContent) {
+ select = mAnchorContent->AsElement()->AsXULSelectControl();
+ }
+
+ if (!select) {
+ // If there isn't an anchor, then try just getting the parent of the popup.
+ select = mContent->GetParent()->AsElement()->AsXULSelectControl();
+ if (!select) {
+ return nullptr;
+ }
+ }
+
+ nsCOMPtr<Element> selectedElement;
+ select->GetSelectedItem(getter_AddRefs(selectedElement));
+ return selectedElement ? selectedElement->GetPrimaryFrame() : nullptr;
+}
+
+nscoord nsMenuPopupFrame::SlideOrResize(nscoord& aScreenPoint, nscoord aSize,
+ nscoord aScreenBegin,
+ nscoord aScreenEnd, nscoord* aOffset) {
+ // The popup may be positioned such that either the left/top or bottom/right
+ // is outside the screen - but never both.
+ nscoord newPos =
+ std::max(aScreenBegin, std::min(aScreenEnd - aSize, aScreenPoint));
+ *aOffset = newPos - aScreenPoint;
+ aScreenPoint = newPos;
+ return std::min(aSize, aScreenEnd - aScreenPoint);
+}
+
+nscoord nsMenuPopupFrame::FlipOrResize(nscoord& aScreenPoint, nscoord aSize,
+ nscoord aScreenBegin, nscoord aScreenEnd,
+ nscoord aAnchorBegin, nscoord aAnchorEnd,
+ nscoord aMarginBegin, nscoord aMarginEnd,
+ FlipStyle aFlip, bool aEndAligned,
+ bool* aFlipSide) {
+ // The flip side argument will be set to true if there wasn't room and we
+ // flipped to the opposite side.
+ *aFlipSide = false;
+
+ // all of the coordinates used here are in app units relative to the screen
+ nscoord popupSize = aSize;
+ if (aScreenPoint < aScreenBegin) {
+ // at its current position, the popup would extend past the left or top
+ // edge of the screen, so it will have to be moved or resized.
+ if (aFlip) {
+ // for inside flips, we flip on the opposite side of the anchor
+ nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd;
+ nscoord endpos = aFlip == FlipStyle_Outside ? aAnchorEnd : aAnchorBegin;
+
+ // check whether there is more room to the left and right (or top and
+ // bottom) of the anchor and put the popup on the side with more room.
+ if (startpos - aScreenBegin >= aScreenEnd - endpos) {
+ aScreenPoint = aScreenBegin;
+ popupSize = startpos - aScreenPoint - aMarginEnd;
+ *aFlipSide = !aEndAligned;
+ } else {
+ // If the newly calculated position is different than the existing
+ // position, flip such that the popup is to the right or bottom of the
+ // anchor point instead . However, when flipping use the same margin
+ // size.
+ nscoord newScreenPoint = endpos + aMarginEnd;
+ if (newScreenPoint != aScreenPoint) {
+ *aFlipSide = aEndAligned;
+ aScreenPoint = newScreenPoint;
+ // check if the new position is still off the right or bottom edge of
+ // the screen. If so, resize the popup.
+ if (aScreenPoint + aSize > aScreenEnd) {
+ popupSize = aScreenEnd - aScreenPoint;
+ }
+ }
+ }
+ } else {
+ aScreenPoint = aScreenBegin;
+ }
+ } else if (aScreenPoint + aSize > aScreenEnd) {
+ // at its current position, the popup would extend past the right or
+ // bottom edge of the screen, so it will have to be moved or resized.
+ if (aFlip) {
+ // for inside flips, we flip on the opposite side of the anchor
+ nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd;
+ nscoord endpos = aFlip == FlipStyle_Outside ? aAnchorEnd : aAnchorBegin;
+
+ // check whether there is more room to the left and right (or top and
+ // bottom) of the anchor and put the popup on the side with more room.
+ if (aScreenEnd - endpos >= startpos - aScreenBegin) {
+ *aFlipSide = aEndAligned;
+ if (mIsContextMenu) {
+ aScreenPoint = aScreenEnd - aSize;
+ } else {
+ aScreenPoint = endpos + aMarginBegin;
+ popupSize = aScreenEnd - aScreenPoint;
+ }
+ } else {
+ // if the newly calculated position is different than the existing
+ // position, we flip such that the popup is to the left or top of the
+ // anchor point instead.
+ nscoord newScreenPoint = startpos - aSize - aMarginBegin;
+ if (newScreenPoint != aScreenPoint) {
+ *aFlipSide = !aEndAligned;
+ aScreenPoint = newScreenPoint;
+
+ // check if the new position is still off the left or top edge of the
+ // screen. If so, resize the popup.
+ if (aScreenPoint < aScreenBegin) {
+ aScreenPoint = aScreenBegin;
+ if (!mIsContextMenu) {
+ popupSize = startpos - aScreenPoint - aMarginBegin;
+ }
+ }
+ }
+ }
+ } else {
+ aScreenPoint = aScreenEnd - aSize;
+ }
+ }
+
+ // Make sure that the point is within the screen boundaries and that the
+ // size isn't off the edge of the screen. This can happen when a large
+ // positive or negative margin is used.
+ if (aScreenPoint < aScreenBegin) {
+ aScreenPoint = aScreenBegin;
+ }
+ if (aScreenPoint > aScreenEnd) {
+ aScreenPoint = aScreenEnd - aSize;
+ }
+
+ // If popupSize ended up being negative, or the original size was actually
+ // smaller than the calculated popup size, just use the original size instead.
+ if (popupSize <= 0 || aSize < popupSize) {
+ popupSize = aSize;
+ }
+
+ return std::min(popupSize, aScreenEnd - aScreenPoint);
+}
+
+nsRect nsMenuPopupFrame::ComputeAnchorRect(nsPresContext* aRootPresContext,
+ nsIFrame* aAnchorFrame) {
+ // Get the root frame for a reference
+ nsIFrame* rootFrame = aRootPresContext->PresShell()->GetRootFrame();
+
+ // The dimensions of the anchor
+ nsRect anchorRect = aAnchorFrame->GetRectRelativeToSelf();
+
+ // Relative to the root
+ anchorRect = nsLayoutUtils::TransformFrameRectToAncestor(
+ aAnchorFrame, anchorRect, rootFrame);
+ // Relative to the screen
+ anchorRect.MoveBy(rootFrame->GetScreenRectInAppUnits().TopLeft());
+
+ // In its own app units
+ return anchorRect.ScaleToOtherAppUnitsRoundOut(
+ aRootPresContext->AppUnitsPerDevPixel(),
+ PresContext()->AppUnitsPerDevPixel());
+}
+
+static nsIFrame* MaybeDelegatedAnchorFrame(nsIFrame* aFrame) {
+ if (!aFrame) {
+ return nullptr;
+ }
+ if (auto* element = Element::FromNodeOrNull(aFrame->GetContent())) {
+ if (element->HasAttr(nsGkAtoms::delegatesanchor)) {
+ for (nsIFrame* f : aFrame->PrincipalChildList()) {
+ if (!f->IsPlaceholderFrame()) {
+ return f;
+ }
+ }
+ }
+ }
+ return aFrame;
+}
+
+nsresult nsMenuPopupFrame::SetPopupPosition(bool aIsMove) {
+ // If this is due to a move, return early if the popup hasn't been laid out
+ // yet. On Windows, this can happen when using a drag popup before it opens.
+ if (aIsMove && (mPrefSize.width == -1 || mPrefSize.height == -1)) {
+ return NS_OK;
+ }
+
+ nsPresContext* presContext = PresContext();
+ nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame();
+ NS_ASSERTION(rootFrame->GetView() && GetView() &&
+ rootFrame->GetView() == GetView()->GetParent(),
+ "rootFrame's view is not our view's parent???");
+
+ // For anchored popups, the anchor rectangle. For non-anchored popups, the
+ // size will be 0.
+ nsRect anchorRect;
+
+ bool anchored = IsAnchored();
+ if (anchored) {
+ // In order to deal with transforms, we need the root prescontext:
+ nsPresContext* rootPresContext = presContext->GetRootPresContext();
+
+ // If we can't reach a root pres context, don't bother continuing:
+ if (!rootPresContext) {
+ return NS_OK;
+ }
+
+ // If anchored to a rectangle, use that rectangle. Otherwise, determine the
+ // rectangle from the anchor.
+ if (mAnchorType == MenuPopupAnchorType_Rect) {
+ anchorRect = mScreenRect;
+ } else {
+ // if the frame is not specified, use the anchor node passed to OpenPopup.
+ // If that wasn't specified either, use the root frame. Note that
+ // mAnchorContent might be a different document so its presshell must be
+ // used.
+ nsIFrame* anchorFrame = GetAnchorFrame();
+ if (!anchorFrame) {
+ anchorFrame = rootFrame;
+ if (!anchorFrame) {
+ return NS_OK;
+ }
+ }
+
+ anchorRect = ComputeAnchorRect(rootPresContext, anchorFrame);
+ }
+ }
+
+ // Set the popup's size to the preferred size. Below, this size will be
+ // adjusted to fit on the screen or within the content area. If the anchor
+ // is sized to the popup, use the anchor's width instead of the preferred
+ // width. The preferred size should already be set by the parent frame.
+ {
+ NS_ASSERTION(mPrefSize.width >= 0 || mPrefSize.height >= 0,
+ "preferred size of popup not set");
+ mRect.SizeTo(mPrefSize);
+ }
+
+ // the screen position in app units where the popup should appear
+ nsPoint screenPoint;
+
+ // indicators of whether the popup should be flipped or resized.
+ FlipStyle hFlip = FlipStyle_None, vFlip = FlipStyle_None;
+
+ const nsMargin margin = GetMargin();
+
+ // the screen rectangle of the root frame, in dev pixels.
+ nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits();
+
+ bool isNoAutoHide = IsNoAutoHide();
+ nsPopupLevel popupLevel = PopupLevel(isNoAutoHide);
+
+ if (anchored) {
+ // if we are anchored, there are certain things we don't want to do when
+ // repositioning the popup to fit on the screen, such as end up positioned
+ // over the anchor, for instance a popup appearing over the menu label.
+ // When doing this reposition, we want to move the popup to the side with
+ // the most room. The combination of anchor and alignment dictate if we
+ // readjust above/below or to the left/right.
+ if (mAnchorContent || mAnchorType == MenuPopupAnchorType_Rect) {
+ // move the popup according to the anchor and alignment. This will also
+ // tell us which axis the popup is flush against in case we have to move
+ // it around later. The AdjustPositionForAnchorAlign method accounts for
+ // the popup's margin.
+ if (!mPositionedByMoveToRect) {
+ mUntransformedAnchorRect = anchorRect;
+ }
+ screenPoint = AdjustPositionForAnchorAlign(anchorRect, hFlip, vFlip);
+ } else {
+ // with no anchor, the popup is positioned relative to the root frame
+ anchorRect = rootScreenRect;
+ if (!mPositionedByMoveToRect) {
+ mUntransformedAnchorRect = anchorRect;
+ }
+ screenPoint = anchorRect.TopLeft() + nsPoint(margin.left, margin.top);
+ }
+
+ // mXPos and mYPos specify an additional offset passed to OpenPopup that
+ // should be added to the position. We also add the offset to the anchor
+ // pos so a later flip/resize takes the offset into account.
+ // FIXME(emilio): Wayland doesn't seem to be accounting for this offset
+ // anywhere, and it probably should.
+ nscoord anchorXOffset = CSSPixel::ToAppUnits(mXPos);
+ if (IsDirectionRTL()) {
+ screenPoint.x -= anchorXOffset;
+ anchorRect.x -= anchorXOffset;
+ } else {
+ screenPoint.x += anchorXOffset;
+ anchorRect.x += anchorXOffset;
+ }
+ nscoord anchorYOffset = CSSPixel::ToAppUnits(mYPos);
+ screenPoint.y += anchorYOffset;
+ anchorRect.y += anchorYOffset;
+
+ // If this is a noautohide popup, set the screen coordinates of the popup.
+ // This way, the popup stays at the location where it was opened even when
+ // the window is moved. Popups at the parent level follow the parent
+ // window as it is moved and remained anchored, so we want to maintain the
+ // anchoring instead.
+ if (isNoAutoHide && (popupLevel != ePopupLevelParent ||
+ mAnchorType == MenuPopupAnchorType_Rect)) {
+ // Account for the margin that will end up being added to the screen
+ // coordinate the next time SetPopupPosition is called.
+ mAnchorType = MenuPopupAnchorType_Point;
+ mScreenRect.x = screenPoint.x - margin.left;
+ mScreenRect.y = screenPoint.y - margin.top;
+ }
+ } else {
+ screenPoint = mScreenRect.TopLeft();
+ anchorRect = nsRect(screenPoint, nsSize());
+ if (!mPositionedByMoveToRect) {
+ mUntransformedAnchorRect = anchorRect;
+ }
+
+ // Right-align RTL context menus, and apply margin and offsets as per the
+ // platform conventions.
+ if (mIsContextMenu && IsDirectionRTL()) {
+ screenPoint.x -= mRect.Width();
+ screenPoint.MoveBy(-margin.right, margin.top);
+ } else {
+ screenPoint.MoveBy(margin.left, margin.top);
+ }
+
+#ifdef XP_MACOSX
+ // OSX tooltips follow standard flip rule but other popups flip horizontally
+ // not vertically
+ if (mPopupType == ePopupTypeTooltip) {
+ vFlip = FlipStyle_Outside;
+ } else {
+ hFlip = FlipStyle_Outside;
+ }
+#else
+ // Other OS screen positioned popups can be flipped vertically but never
+ // horizontally
+ vFlip = FlipStyle_Outside;
+#endif // #ifdef XP_MACOSX
+ }
+
+ nscoord oldAlignmentOffset = mAlignmentOffset;
+
+ // If a panel is being moved or has flip="none", don't constrain or flip it,
+ // in order to avoid visual noise when moving windows between screens.
+ // However, if a panel is already constrained or flipped (mIsOffset), then we
+ // want to continue to calculate this. Also, always do this for content
+ // shells, so that the popup doesn't extend outside the containing frame.
+ if (!IS_WAYLAND_DISPLAY() &&
+ (mInContentShell ||
+ (mFlip != FlipType_None &&
+ (!aIsMove || mIsOffset || mPopupType != ePopupTypePanel)))) {
+ const nsRect screenRect = [&] {
+ int32_t appPerDev = presContext->AppUnitsPerDevPixel();
+ auto anchorRectDevPix =
+ LayoutDeviceIntRect::FromAppUnitsToNearest(anchorRect, appPerDev);
+ auto rootScreenRectDevPix =
+ LayoutDeviceIntRect::FromAppUnitsToNearest(rootScreenRect, appPerDev);
+ auto screenRectDevPix =
+ GetConstraintRect(anchorRectDevPix, rootScreenRectDevPix, popupLevel);
+ nsRect sr = LayoutDeviceIntRect::ToAppUnits(screenRectDevPix, appPerDev);
+
+ // Expand the allowable screen rect by the input margin (which can't be
+ // interacted with).
+ const nscoord inputMargin =
+ StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits();
+ sr.Inflate(inputMargin);
+ return sr;
+ }();
+
+ // Ensure that anchorRect is on screen.
+ anchorRect = anchorRect.Intersect(screenRect);
+
+ // Shrink the the popup down if it is larger than the screen size
+ if (mRect.width > screenRect.width) {
+ mRect.width = screenRect.width;
+ }
+ if (mRect.height > screenRect.height) {
+ mRect.height = screenRect.height;
+ }
+
+ // At this point the anchor (anchorRect) is within the available screen
+ // area (screenRect) and the popup is known to be no larger than the
+ // screen.
+
+ // We might want to "slide" an arrow if the panel is of the correct type -
+ // but we can only slide on one axis - the other axis must be "flipped or
+ // resized" as normal.
+ bool slideHorizontal = false, slideVertical = false;
+ if (mFlip == FlipType_Slide) {
+ int8_t position = GetAlignmentPosition();
+ slideHorizontal = position >= POPUPPOSITION_BEFORESTART &&
+ position <= POPUPPOSITION_AFTEREND;
+ slideVertical = position >= POPUPPOSITION_STARTBEFORE &&
+ position <= POPUPPOSITION_ENDAFTER;
+ }
+
+ // Next, check if there is enough space to show the popup at full size
+ // when positioned at screenPoint. If not, flip the popups to the opposite
+ // side of their anchor point, or resize them as necessary.
+ const nsPoint preOffsetScreenPoint = screenPoint;
+ if (slideHorizontal) {
+ mRect.width = SlideOrResize(screenPoint.x, mRect.width, screenRect.x,
+ screenRect.XMost(), &mAlignmentOffset);
+ } else {
+ bool endAligned = IsDirectionRTL()
+ ? mPopupAlignment == POPUPALIGNMENT_TOPLEFT ||
+ mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT
+ : mPopupAlignment == POPUPALIGNMENT_TOPRIGHT ||
+ mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT;
+ mRect.width =
+ FlipOrResize(screenPoint.x, mRect.width, screenRect.x,
+ screenRect.XMost(), anchorRect.x, anchorRect.XMost(),
+ margin.left, margin.right, hFlip, endAligned, &mHFlip);
+ }
+ if (slideVertical) {
+ mRect.height = SlideOrResize(screenPoint.y, mRect.height, screenRect.y,
+ screenRect.YMost(), &mAlignmentOffset);
+ } else {
+ bool endAligned = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT ||
+ mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT;
+ mRect.height =
+ FlipOrResize(screenPoint.y, mRect.height, screenRect.y,
+ screenRect.YMost(), anchorRect.y, anchorRect.YMost(),
+ margin.top, margin.bottom, vFlip, endAligned, &mVFlip);
+ }
+ mIsOffset = preOffsetScreenPoint != screenPoint;
+
+ NS_ASSERTION(screenPoint.x >= screenRect.x, "Popup is offscreen (x start)");
+ NS_ASSERTION(screenPoint.y >= screenRect.y, "Popup is offscreen (y start)");
+ NS_ASSERTION(screenPoint.x + mRect.width <= screenRect.XMost(),
+ "Popup is offscreen (x end)");
+ NS_ASSERTION(screenPoint.y + mRect.height <= screenRect.YMost(),
+ "Popup is offscreen (y end)");
+ }
+
+ // snap the popup's position in screen coordinates to device pixels,
+ // see bug 622507, bug 961431
+ screenPoint.x = presContext->RoundAppUnitsToNearestDevPixels(screenPoint.x);
+ screenPoint.y = presContext->RoundAppUnitsToNearestDevPixels(screenPoint.y);
+
+ // determine the x and y position of the view by subtracting the desired
+ // screen position from the screen position of the root frame.
+ nsPoint viewPoint = screenPoint - rootScreenRect.TopLeft();
+
+ nsView* view = GetView();
+ NS_ASSERTION(view, "popup with no view");
+
+ // Offset the position by the width and height of the borders and titlebar.
+ // Even though GetClientOffset should return (0, 0) when there is no
+ // titlebar or borders, we skip these calculations anyway for non-panels
+ // to save time since they will never have a titlebar.
+ nsIWidget* widget = view->GetWidget();
+ if (mPopupType == ePopupTypePanel && widget) {
+ mLastClientOffset = widget->GetClientOffset();
+ viewPoint.x += presContext->DevPixelsToAppUnits(mLastClientOffset.x);
+ viewPoint.y += presContext->DevPixelsToAppUnits(mLastClientOffset.y);
+ }
+
+ presContext->GetPresShell()->GetViewManager()->MoveViewTo(view, viewPoint.x,
+ viewPoint.y);
+
+ // Now that we've positioned the view, sync up the frame's origin.
+ nsBoxFrame::SetPosition(viewPoint - GetParent()->GetOffsetTo(rootFrame));
+
+ // If the popup is in the positioned state or if it is shown and the position
+ // or size changed, dispatch a popuppositioned event if the popup wants it.
+ nsIntRect newRect(screenPoint.x, screenPoint.y, mRect.width, mRect.height);
+ if (mPopupState == ePopupPositioning ||
+ (mPopupState == ePopupShown && !newRect.IsEqualEdges(mUsedScreenRect)) ||
+ (mPopupState == ePopupShown && oldAlignmentOffset != mAlignmentOffset)) {
+ mUsedScreenRect = newRect;
+ if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && !mPendingPositionedEvent) {
+ mPendingPositionedEvent =
+ nsXULPopupPositionedEvent::DispatchIfNeeded(mContent);
+ }
+ }
+
+ // NOTE(emilio): This call below is kind of a workaround, but we need to do
+ // this here because some position changes don't go through the
+ // view system -> popup manager, like:
+ //
+ // https://searchfox.org/mozilla-central/rev/477950cf9ca9c9bb5ff6f34e0d0f6ca4718ea798/widget/gtk/nsWindow.cpp#3847
+ //
+ // So this might be the last chance we have to set the remote browser's
+ // position.
+ //
+ // Ultimately this probably wants to get fixed in the widget size of things,
+ // but given this is worst-case a redundant DOM traversal, and that popups
+ // usually don't have all that much content, this is probably an ok
+ // workaround.
+ WidgetPositionOrSizeDidChange();
+
+ return NS_OK;
+}
+
+void nsMenuPopupFrame::WidgetPositionOrSizeDidChange() {
+ // In the case this popup has remote contents having OOP iframes, it's
+ // possible that OOP iframe's nsSubDocumentFrame has been already reflowed
+ // thus, we will never have a chance to tell this parent browser's position
+ // update to the OOP documents without notifying it explicitly.
+ if (!HasRemoteContent()) {
+ return;
+ }
+ for (nsIContent* content = mContent->GetFirstChild(); content;
+ content = content->GetNextNode(mContent)) {
+ if (content->IsXULElement(nsGkAtoms::browser) &&
+ content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::remote,
+ nsGkAtoms::_true, eIgnoreCase)) {
+ if (auto* browserParent = dom::BrowserParent::GetFrom(content)) {
+ browserParent->NotifyPositionUpdatedForContentsInPopup();
+ }
+ }
+ }
+}
+
+LayoutDeviceIntRect nsMenuPopupFrame::GetConstraintRect(
+ const LayoutDeviceIntRect& aAnchorRect,
+ const LayoutDeviceIntRect& aRootScreenRect, nsPopupLevel aPopupLevel) {
+ LayoutDeviceIntRect screenRectPixels;
+
+ // GetConstraintRect() does not work on Wayland as we can't get absolute
+ // window position there.
+ MOZ_ASSERT(!IS_WAYLAND_DISPLAY(),
+ "GetConstraintRect does not work on Wayland");
+
+ // determine the available screen space. It will be reduced by the OS chrome
+ // such as menubars. It addition, for content shells, it will be the area of
+ // the content rather than the screen.
+ nsCOMPtr<nsIScreen> screen;
+ nsCOMPtr<nsIScreenManager> sm(
+ do_GetService("@mozilla.org/gfx/screenmanager;1"));
+ if (sm) {
+ // for content shells, get the screen where the root frame is located.
+ // This is because we need to constrain the content to this content area,
+ // so we should use the same screen. Otherwise, use the screen where the
+ // anchor is located.
+ DesktopToLayoutDeviceScale scale =
+ PresContext()->DeviceContext()->GetDesktopToDeviceScale();
+ DesktopRect rect =
+ (mInContentShell ? aRootScreenRect : aAnchorRect) / scale;
+ int32_t width = std::max(1, NSToIntRound(rect.width));
+ int32_t height = std::max(1, NSToIntRound(rect.height));
+ sm->ScreenForRect(rect.x, rect.y, width, height, getter_AddRefs(screen));
+ if (screen) {
+ // Non-top-level popups (which will always be panels)
+ // should never overlap the OS bar:
+ bool dontOverlapOSBar = aPopupLevel != ePopupLevelTop;
+ // get the total screen area if the popup is allowed to overlap it.
+ if (!dontOverlapOSBar && mMenuCanOverlapOSBar && !mInContentShell)
+ screen->GetRect(&screenRectPixels.x, &screenRectPixels.y,
+ &screenRectPixels.width, &screenRectPixels.height);
+ else
+ screen->GetAvailRect(&screenRectPixels.x, &screenRectPixels.y,
+ &screenRectPixels.width, &screenRectPixels.height);
+ }
+ }
+
+ if (mInContentShell) {
+ // for content shells, clip to the client area rather than the screen area
+ screenRectPixels.IntersectRect(screenRectPixels, aRootScreenRect);
+ } else if (!mOverrideConstraintRect.IsEmpty()) {
+ LayoutDeviceIntRect overrideConstrainRect =
+ LayoutDeviceIntRect::FromAppUnitsToNearest(
+ mOverrideConstraintRect, PresContext()->AppUnitsPerDevPixel());
+ // This is currently only used for <select> elements where we want to
+ // constrain vertically to the screen but not horizontally, so do the
+ // intersection and then reset the horizontal values.
+ screenRectPixels.IntersectRect(screenRectPixels, overrideConstrainRect);
+ screenRectPixels.x = overrideConstrainRect.x;
+ screenRectPixels.width = overrideConstrainRect.width;
+ }
+
+ return screenRectPixels;
+}
+
+void nsMenuPopupFrame::CanAdjustEdges(Side aHorizontalSide, Side aVerticalSide,
+ LayoutDeviceIntPoint& aChange) {
+ int8_t popupAlign(mPopupAlignment);
+ if (IsDirectionRTL()) {
+ popupAlign = -popupAlign;
+ }
+
+ if (aHorizontalSide == (mHFlip ? eSideRight : eSideLeft)) {
+ if (popupAlign == POPUPALIGNMENT_TOPLEFT ||
+ popupAlign == POPUPALIGNMENT_BOTTOMLEFT) {
+ aChange.x = 0;
+ }
+ } else if (aHorizontalSide == (mHFlip ? eSideLeft : eSideRight)) {
+ if (popupAlign == POPUPALIGNMENT_TOPRIGHT ||
+ popupAlign == POPUPALIGNMENT_BOTTOMRIGHT) {
+ aChange.x = 0;
+ }
+ }
+
+ if (aVerticalSide == (mVFlip ? eSideBottom : eSideTop)) {
+ if (popupAlign == POPUPALIGNMENT_TOPLEFT ||
+ popupAlign == POPUPALIGNMENT_TOPRIGHT) {
+ aChange.y = 0;
+ }
+ } else if (aVerticalSide == (mVFlip ? eSideTop : eSideBottom)) {
+ if (popupAlign == POPUPALIGNMENT_BOTTOMLEFT ||
+ popupAlign == POPUPALIGNMENT_BOTTOMRIGHT) {
+ aChange.y = 0;
+ }
+ }
+}
+
+ConsumeOutsideClicksResult nsMenuPopupFrame::ConsumeOutsideClicks() {
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::consumeoutsideclicks,
+ nsGkAtoms::_true, eCaseMatters)) {
+ return ConsumeOutsideClicks_True;
+ }
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::consumeoutsideclicks,
+ nsGkAtoms::_false, eCaseMatters)) {
+ return ConsumeOutsideClicks_ParentOnly;
+ }
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::consumeoutsideclicks,
+ nsGkAtoms::never, eCaseMatters)) {
+ return ConsumeOutsideClicks_Never;
+ }
+
+ nsCOMPtr<nsIContent> parentContent = mContent->GetParent();
+ if (parentContent) {
+ dom::NodeInfo* ni = parentContent->NodeInfo();
+ if (ni->Equals(nsGkAtoms::menulist, kNameSpaceID_XUL)) {
+ return ConsumeOutsideClicks_True; // Consume outside clicks for combo
+ // boxes on all platforms
+ }
+#if defined(XP_WIN)
+ // Don't consume outside clicks for menus in Windows
+ if (ni->Equals(nsGkAtoms::menu, kNameSpaceID_XUL) ||
+ ni->Equals(nsGkAtoms::popupset, kNameSpaceID_XUL) ||
+ ((ni->Equals(nsGkAtoms::button, kNameSpaceID_XUL) ||
+ ni->Equals(nsGkAtoms::toolbarbutton, kNameSpaceID_XUL)) &&
+ parentContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::menu,
+ eCaseMatters))) {
+ return ConsumeOutsideClicks_Never;
+ }
+#endif
+ }
+
+ return ConsumeOutsideClicks_True;
+}
+
+// XXXroc this is megalame. Fossicking around for a frame of the right
+// type is a recipe for disaster in the long term.
+nsIScrollableFrame* nsMenuPopupFrame::GetScrollFrame(nsIFrame* aStart) {
+ if (!aStart) return nullptr;
+
+ // try start frame and siblings
+ nsIFrame* currFrame = aStart;
+ do {
+ nsIScrollableFrame* sf = do_QueryFrame(currFrame);
+ if (sf) return sf;
+ currFrame = currFrame->GetNextSibling();
+ } while (currFrame);
+
+ // try children
+ currFrame = aStart;
+ do {
+ nsIFrame* childFrame = currFrame->PrincipalChildList().FirstChild();
+ nsIScrollableFrame* sf = GetScrollFrame(childFrame);
+ if (sf) return sf;
+ currFrame = currFrame->GetNextSibling();
+ } while (currFrame);
+
+ return nullptr;
+}
+
+void nsMenuPopupFrame::ChangeByPage(bool aIsUp) {
+ // Only scroll by page within menulists.
+ if (!IsMenuList()) {
+ return;
+ }
+
+ nsIScrollableFrame* scrollframe = GetScrollFrame(this);
+
+ RefPtr popup = &PopupElement();
+ XULButtonElement* currentMenu = popup->GetActiveMenuChild();
+ XULButtonElement* newMenu = nullptr;
+ if (!currentMenu) {
+ // If there is no current menu item, get the first item. When moving up,
+ // just use this as the newMenu and leave currentMenu null so that no check
+ // for a later element is performed. When moving down, set currentMenu so
+ // that we look for one page down from the first item.
+ newMenu = popup->GetFirstMenuItem();
+ if (!aIsUp) {
+ currentMenu = newMenu;
+ }
+ }
+
+ if (currentMenu && currentMenu->GetPrimaryFrame()) {
+ const nscoord scrollHeight =
+ scrollframe ? scrollframe->GetScrollPortRect().height : mRect.height;
+ const nsRect currentRect = currentMenu->GetPrimaryFrame()->GetRect();
+ const XULButtonElement* startMenu = currentMenu;
+
+ // Get the position of the current item and add or subtract one popup's
+ // height to or from it.
+ const nscoord targetPos = aIsUp ? currentRect.YMost() - scrollHeight
+ : currentRect.y + scrollHeight;
+ // Look for the next child which is just past the target position. This
+ // child will need to be selected.
+ for (; currentMenu;
+ currentMenu = aIsUp ? popup->GetPrevMenuItemFrom(*currentMenu)
+ : popup->GetNextMenuItemFrom(*currentMenu)) {
+ if (!currentMenu->GetPrimaryFrame()) {
+ continue;
+ }
+ const nsRect curRect = currentMenu->GetPrimaryFrame()->GetRect();
+ const nscoord curPos = aIsUp ? curRect.y : curRect.YMost();
+ // If the right position was found, break out. Otherwise, look for another
+ // item.
+ if (aIsUp ? (curPos < targetPos) : (curPos > targetPos)) {
+ if (!newMenu || newMenu == startMenu) {
+ newMenu = currentMenu;
+ }
+ break;
+ }
+
+ // Assign this item to newMenu. This item will be selected in case we
+ // don't find any more.
+ newMenu = currentMenu;
+ }
+ }
+
+ // Select the new menuitem.
+ if (RefPtr newMenuRef = newMenu) {
+ popup->SetActiveMenuChild(newMenuRef);
+ }
+}
+
+dom::XULPopupElement& nsMenuPopupFrame::PopupElement() const {
+ auto* popup = dom::XULPopupElement::FromNode(GetContent());
+ MOZ_DIAGNOSTIC_ASSERT(popup);
+ return *popup;
+}
+
+XULButtonElement* nsMenuPopupFrame::GetCurrentMenuItem() const {
+ return PopupElement().GetActiveMenuChild();
+}
+
+nsIFrame* nsMenuPopupFrame::GetCurrentMenuItemFrame() const {
+ auto* child = GetCurrentMenuItem();
+ return child ? child->GetPrimaryFrame() : nullptr;
+}
+
+void nsMenuPopupFrame::HandleEnterKeyPress(WidgetEvent& aEvent) {
+ mIncrementalString.Truncate();
+ if (RefPtr menu = GetCurrentMenuItem()) {
+ // Give it to the child.
+ menu->HandleEnterKeyPress(aEvent);
+ }
+}
+
+XULButtonElement* nsMenuPopupFrame::FindMenuWithShortcut(
+ mozilla::dom::KeyboardEvent& aKeyEvent, bool& aDoAction) {
+ uint32_t charCode = aKeyEvent.CharCode();
+ uint32_t keyCode = aKeyEvent.KeyCode();
+
+ aDoAction = false;
+
+ // Enumerate over our list of frames.
+ const bool isMenu = !IsMenuList();
+ TimeStamp keyTime = aKeyEvent.WidgetEventPtr()->mTimeStamp;
+ if (charCode == 0) {
+ if (keyCode == dom::KeyboardEvent_Binding::DOM_VK_BACK_SPACE) {
+ if (!isMenu && !mIncrementalString.IsEmpty()) {
+ mIncrementalString.SetLength(mIncrementalString.Length() - 1);
+ return nullptr;
+ }
+#ifdef XP_WIN
+ if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
+ sound->Beep();
+ }
+#endif // #ifdef XP_WIN
+ }
+ return nullptr;
+ }
+ char16_t uniChar = ToLowerCase(static_cast<char16_t>(charCode));
+ if (isMenu) {
+ // Menu supports only first-letter navigation
+ mIncrementalString = uniChar;
+ } else if (IsWithinIncrementalTime(keyTime)) {
+ mIncrementalString.Append(uniChar);
+ } else {
+ // Interval too long, treat as new typing
+ mIncrementalString = uniChar;
+ }
+
+ // See bug 188199 & 192346, if all letters in incremental string are same,
+ // just try to match the first one
+ nsAutoString incrementalString(mIncrementalString);
+ uint32_t charIndex = 1, stringLength = incrementalString.Length();
+ while (charIndex < stringLength &&
+ incrementalString[charIndex] == incrementalString[charIndex - 1]) {
+ charIndex++;
+ }
+ if (charIndex == stringLength) {
+ incrementalString.Truncate(1);
+ stringLength = 1;
+ }
+
+ sLastKeyTime = keyTime;
+
+ auto* item =
+ PopupElement().FindMenuWithShortcut(incrementalString, aDoAction);
+ if (item) {
+ return item;
+ }
+
+ // If we don't match anything, rollback the last typing
+ mIncrementalString.SetLength(mIncrementalString.Length() - 1);
+
+ // didn't find a matching menu item
+#ifdef XP_WIN
+ // behavior on Windows - this item is in a menu popup off of the
+ // menu bar, so beep and do nothing else
+ if (isMenu) {
+ if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
+ sound->Beep();
+ }
+ }
+#endif // #ifdef XP_WIN
+
+ return nullptr;
+}
+
+nsIWidget* nsMenuPopupFrame::GetWidget() const {
+ return mView ? mView->GetWidget() : nullptr;
+}
+
+// helpers /////////////////////////////////////////////////////////////
+
+nsresult nsMenuPopupFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType)
+
+{
+ nsresult rv =
+ nsBoxFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+
+ if (aAttribute == nsGkAtoms::left || aAttribute == nsGkAtoms::top) {
+ MoveToAttributePosition();
+ }
+
+ if (aAttribute == nsGkAtoms::remote) {
+ // When the remote attribute changes, we need to create a new widget to
+ // ensure that it has the correct compositor and transparency settings to
+ // match the new value.
+ PrepareWidget(true);
+ }
+
+ if (aAttribute == nsGkAtoms::followanchor) {
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (pm) {
+ pm->UpdateFollowAnchor(this);
+ }
+ }
+
+ if (aAttribute == nsGkAtoms::label) {
+ // set the label for the titlebar
+ nsView* view = GetView();
+ if (view) {
+ nsIWidget* widget = view->GetWidget();
+ if (widget) {
+ nsAutoString title;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label,
+ title);
+ if (!title.IsEmpty()) {
+ widget->SetTitle(title);
+ }
+ }
+ }
+ } else if (aAttribute == nsGkAtoms::ignorekeys) {
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (pm) {
+ nsAutoString ignorekeys;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::ignorekeys,
+ ignorekeys);
+ pm->UpdateIgnoreKeys(ignorekeys.EqualsLiteral("true"));
+ }
+ }
+
+ return rv;
+}
+
+void nsMenuPopupFrame::MoveToAttributePosition() {
+ // Move the widget around when the user sets the |left| and |top| attributes.
+ // Note that this is not the best way to move the widget, as it results in
+ // lots of FE notifications and is likely to be slow as molasses. Use |moveTo|
+ // on the element if possible.
+ nsAutoString left, top;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::left, left);
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::top, top);
+ nsresult err1, err2;
+ mozilla::CSSIntPoint pos(left.ToInteger(&err1), top.ToInteger(&err2));
+
+ if (NS_SUCCEEDED(err1) && NS_SUCCEEDED(err2)) MoveTo(pos, false);
+
+ PresShell()->FrameNeedsReflow(
+ this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
+}
+
+void nsMenuPopupFrame::DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) {
+ if (mReflowCallbackData.mPosted) {
+ PresShell()->CancelReflowCallback(this);
+ mReflowCallbackData.Clear();
+ }
+
+ // XXX: Currently we don't fire popuphidden for these popups, that seems wrong
+ // but alas, also pre-existing.
+ HidePopup(/* aDeselectMenu = */ false, ePopupClosed,
+ /* aFromFrameDestruction = */ true);
+
+ if (RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance()) {
+ pm->PopupDestroyed(this);
+ }
+
+ nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
+}
+
+nsMargin nsMenuPopupFrame::GetMargin() const {
+ nsMargin margin;
+ StyleMargin()->GetMargin(margin);
+ if (mIsTopLevelContextMenu) {
+ const CSSIntPoint offset(
+ LookAndFeel::GetInt(LookAndFeel::IntID::ContextMenuOffsetHorizontal),
+ LookAndFeel::GetInt(LookAndFeel::IntID::ContextMenuOffsetVertical));
+ auto auOffset = CSSIntPoint::ToAppUnits(offset);
+ margin.top += auOffset.y;
+ margin.bottom += auOffset.y;
+ margin.left += auOffset.x;
+ margin.right += auOffset.x;
+ }
+ return margin;
+}
+
+void nsMenuPopupFrame::MoveTo(const CSSPoint& aPos, bool aUpdateAttrs,
+ bool aByMoveToRect) {
+ nsIWidget* widget = GetWidget();
+ nsPoint appUnitsPos = CSSPixel::ToAppUnits(aPos);
+
+ // reposition the popup at the specified coordinates. Don't clear the anchor
+ // and position, because the popup can be reset to its anchor position by
+ // using (-1, -1) as coordinates.
+ //
+ // Subtract off the margin as it will be added to the position when
+ // SetPopupPosition is called.
+ {
+ nsMargin margin = GetMargin();
+ if (mIsContextMenu && IsDirectionRTL()) {
+ appUnitsPos.x += margin.right + mRect.Width();
+ } else {
+ appUnitsPos.x -= margin.left;
+ }
+ appUnitsPos.y -= margin.top;
+ }
+
+ if ((mScreenRect.x == appUnitsPos.x && mScreenRect.y == appUnitsPos.y) &&
+ (!widget || widget->GetClientOffset() == mLastClientOffset)) {
+ return;
+ }
+
+ mPositionedByMoveToRect = aByMoveToRect;
+ mScreenRect.MoveTo(appUnitsPos);
+ if (mAnchorType == MenuPopupAnchorType_Rect) {
+ // This ensures that the anchor width is still honored, to prevent it from
+ // changing spuriously.
+ mScreenRect.height = 0;
+ } else {
+ mAnchorType = MenuPopupAnchorType_Point;
+ }
+
+ SetPopupPosition(true);
+
+ RefPtr<Element> popup = mContent->AsElement();
+ if (aUpdateAttrs &&
+ (popup->HasAttr(nsGkAtoms::left) || popup->HasAttr(nsGkAtoms::top))) {
+ nsAutoString left, top;
+ left.AppendInt(RoundedToInt(aPos).x);
+ top.AppendInt(RoundedToInt(aPos).y);
+ popup->SetAttr(kNameSpaceID_None, nsGkAtoms::left, left, false);
+ popup->SetAttr(kNameSpaceID_None, nsGkAtoms::top, top, false);
+ }
+}
+
+void nsMenuPopupFrame::MoveToAnchor(nsIContent* aAnchorContent,
+ const nsAString& aPosition, int32_t aXPos,
+ int32_t aYPos, bool aAttributesOverride) {
+ NS_ASSERTION(IsVisible(), "popup must be visible to move it");
+
+ nsPopupState oldstate = mPopupState;
+ InitializePopup(aAnchorContent, mTriggerContent, aPosition, aXPos, aYPos,
+ MenuPopupAnchorType_Node, aAttributesOverride);
+ // InitializePopup changed the state so reset it.
+ mPopupState = oldstate;
+
+ // Pass false here so that flipping and adjusting to fit on the screen happen.
+ SetPopupPosition(false);
+}
+
+int8_t nsMenuPopupFrame::GetAlignmentPosition() const {
+ // The code below handles most cases of alignment, anchor and position values.
+ // Those that are not handled just return POPUPPOSITION_UNKNOWN.
+
+ if (mPosition == POPUPPOSITION_OVERLAP ||
+ mPosition == POPUPPOSITION_AFTERPOINTER ||
+ mPosition == POPUPPOSITION_SELECTION) {
+ return mPosition;
+ }
+
+ int8_t position = mPosition;
+
+ if (position == POPUPPOSITION_UNKNOWN) {
+ switch (mPopupAnchor) {
+ case POPUPALIGNMENT_BOTTOMRIGHT:
+ case POPUPALIGNMENT_BOTTOMLEFT:
+ case POPUPALIGNMENT_BOTTOMCENTER:
+ position = mPopupAlignment == POPUPALIGNMENT_TOPRIGHT
+ ? POPUPPOSITION_AFTEREND
+ : POPUPPOSITION_AFTERSTART;
+ break;
+ case POPUPALIGNMENT_TOPRIGHT:
+ case POPUPALIGNMENT_TOPLEFT:
+ case POPUPALIGNMENT_TOPCENTER:
+ position = mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT
+ ? POPUPPOSITION_BEFOREEND
+ : POPUPPOSITION_BEFORESTART;
+ break;
+ case POPUPALIGNMENT_LEFTCENTER:
+ position = mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT
+ ? POPUPPOSITION_STARTAFTER
+ : POPUPPOSITION_STARTBEFORE;
+ break;
+ case POPUPALIGNMENT_RIGHTCENTER:
+ position = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT
+ ? POPUPPOSITION_ENDAFTER
+ : POPUPPOSITION_ENDBEFORE;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (mHFlip) {
+ position = POPUPPOSITION_HFLIP(position);
+ }
+
+ if (mVFlip) {
+ position = POPUPPOSITION_VFLIP(position);
+ }
+
+ return position;
+}
+
+/**
+ * KEEP THIS IN SYNC WITH nsIFrame::CreateView
+ * as much as possible. Until we get rid of views finally...
+ */
+void nsMenuPopupFrame::CreatePopupView() {
+ if (HasView()) {
+ return;
+ }
+
+ nsViewManager* viewManager = PresContext()->GetPresShell()->GetViewManager();
+ NS_ASSERTION(nullptr != viewManager, "null view manager");
+
+ // Create a view
+ nsView* parentView = viewManager->GetRootView();
+ nsViewVisibility visibility = nsViewVisibility_kHide;
+
+ NS_ASSERTION(parentView, "no parent view");
+
+ // Create a view
+ nsView* view = viewManager->CreateView(GetRect(), parentView, visibility);
+ auto zIndex = ZIndex();
+ viewManager->SetViewZIndex(view, zIndex.isNothing(), zIndex.valueOr(0));
+ // XXX put view last in document order until we can do better
+ viewManager->InsertChild(parentView, view, nullptr, true);
+
+ // Remember our view
+ SetView(view);
+
+ NS_FRAME_LOG(
+ NS_FRAME_TRACE_CALLS,
+ ("nsMenuPopupFrame::CreatePopupView: frame=%p view=%p", this, view));
+}
+
+bool nsMenuPopupFrame::ShouldFollowAnchor() {
+ if (mAnchorType != MenuPopupAnchorType_Node || !mAnchorContent) {
+ return false;
+ }
+
+ // Follow anchor mode is used when followanchor="true" is set or for arrow
+ // panels.
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::followanchor,
+ nsGkAtoms::_true, eCaseMatters)) {
+ return true;
+ }
+
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::followanchor,
+ nsGkAtoms::_false, eCaseMatters)) {
+ return false;
+ }
+
+ return (mPopupType == ePopupTypePanel &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::arrow, eCaseMatters));
+}
+
+bool nsMenuPopupFrame::ShouldFollowAnchor(nsRect& aRect) {
+ if (!ShouldFollowAnchor()) {
+ return false;
+ }
+
+ if (nsIFrame* anchorFrame = GetAnchorFrame()) {
+ if (nsPresContext* rootPresContext = PresContext()->GetRootPresContext()) {
+ aRect = ComputeAnchorRect(rootPresContext, anchorFrame);
+ }
+ }
+
+ return true;
+}
+
+bool nsMenuPopupFrame::IsDirectionRTL() const {
+ const nsIFrame* anchor = GetAnchorFrame();
+ const nsIFrame* f = anchor ? anchor : this;
+ return f->StyleVisibility()->mDirection == StyleDirection::Rtl;
+}
+
+nsIFrame* nsMenuPopupFrame::GetAnchorFrame() const {
+ nsIContent* anchor = mAnchorContent;
+ if (!anchor) {
+ return nullptr;
+ }
+ return MaybeDelegatedAnchorFrame(anchor->GetPrimaryFrame());
+}
+
+void nsMenuPopupFrame::CheckForAnchorChange(nsRect& aRect) {
+ // Don't update if the popup isn't visible or we shouldn't be following the
+ // anchor.
+ if (!IsVisible() || !ShouldFollowAnchor()) {
+ return;
+ }
+
+ bool shouldHide = false;
+
+ nsPresContext* rootPresContext = PresContext()->GetRootPresContext();
+
+ // If the frame for the anchor has gone away, hide the popup.
+ nsIFrame* anchor = GetAnchorFrame();
+ if (!anchor || !rootPresContext) {
+ shouldHide = true;
+ } else if (!anchor->IsVisibleConsideringAncestors(
+ VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) {
+ // If the anchor is now inside something that is invisible, hide the popup.
+ shouldHide = true;
+ } else {
+ // If the anchor is now inside a hidden parent popup, hide the popup.
+ nsIFrame* frame = anchor;
+ while (frame) {
+ nsMenuPopupFrame* popup = do_QueryFrame(frame);
+ if (popup && popup->PopupState() != ePopupShown) {
+ shouldHide = true;
+ break;
+ }
+
+ frame = frame->GetParent();
+ }
+ }
+
+ if (shouldHide) {
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (pm) {
+ // As the caller will be iterating over the open popups, hide
+ // asyncronously.
+ pm->HidePopup(mContent, false, true, true, false);
+ }
+
+ return;
+ }
+
+ nsRect anchorRect = ComputeAnchorRect(rootPresContext, anchor);
+
+ // If the rectangles are different, move the popup.
+ if (!anchorRect.IsEqualEdges(aRect)) {
+ aRect = anchorRect;
+ SetPopupPosition(true);
+ }
+}
diff --git a/layout/xul/nsMenuPopupFrame.h b/layout/xul/nsMenuPopupFrame.h
new file mode 100644
index 0000000000..8f30dbe83b
--- /dev/null
+++ b/layout/xul/nsMenuPopupFrame.h
@@ -0,0 +1,638 @@
+/* -*- 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/. */
+
+//
+// nsMenuPopupFrame
+//
+
+#ifndef nsMenuPopupFrame_h__
+#define nsMenuPopupFrame_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/TimeStamp.h"
+#include "nsAtom.h"
+#include "nsGkAtoms.h"
+#include "nsCOMPtr.h"
+#include "nsIDOMEventListener.h"
+#include "nsIReflowCallback.h"
+#include "nsXULPopupManager.h"
+
+#include "nsBoxFrame.h"
+
+#include "Units.h"
+
+class nsIWidget;
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class KeyboardEvent;
+class XULButtonElement;
+class XULPopupElement;
+} // namespace dom
+} // namespace mozilla
+
+enum ConsumeOutsideClicksResult {
+ ConsumeOutsideClicks_ParentOnly =
+ 0, // Only consume clicks on the parent anchor
+ ConsumeOutsideClicks_True = 1, // Always consume clicks
+ ConsumeOutsideClicks_Never = 2 // Never consume clicks
+};
+
+// How a popup may be flipped. Flipping to the outside edge is like how
+// a submenu would work. The entire popup is flipped to the opposite side
+// of the anchor.
+enum FlipStyle {
+ FlipStyle_None = 0,
+ FlipStyle_Outside = 1,
+ FlipStyle_Inside = 2
+};
+
+// Values for the flip attribute
+enum FlipType {
+ FlipType_Default = 0,
+ FlipType_None = 1, // don't try to flip or translate to stay onscreen
+ FlipType_Both = 2, // flip in both directions
+ FlipType_Slide = 3 // allow the arrow to "slide" instead of resizing
+};
+
+enum MenuPopupAnchorType {
+ MenuPopupAnchorType_Node = 0, // anchored to a node
+ MenuPopupAnchorType_Point = 1, // unanchored and positioned at a screen point
+ MenuPopupAnchorType_Rect = 2, // anchored at a screen rectangle
+};
+
+// values are selected so that the direction can be flipped just by
+// changing the sign
+#define POPUPALIGNMENT_NONE 0
+#define POPUPALIGNMENT_TOPLEFT 1
+#define POPUPALIGNMENT_TOPRIGHT -1
+#define POPUPALIGNMENT_BOTTOMLEFT 2
+#define POPUPALIGNMENT_BOTTOMRIGHT -2
+
+#define POPUPALIGNMENT_LEFTCENTER 16
+#define POPUPALIGNMENT_RIGHTCENTER -16
+#define POPUPALIGNMENT_TOPCENTER 17
+#define POPUPALIGNMENT_BOTTOMCENTER 18
+
+// The constants here are selected so that horizontally and vertically flipping
+// can be easily handled using the two flip macros below.
+#define POPUPPOSITION_UNKNOWN -1
+#define POPUPPOSITION_BEFORESTART 0
+#define POPUPPOSITION_BEFOREEND 1
+#define POPUPPOSITION_AFTERSTART 2
+#define POPUPPOSITION_AFTEREND 3
+#define POPUPPOSITION_STARTBEFORE 4
+#define POPUPPOSITION_ENDBEFORE 5
+#define POPUPPOSITION_STARTAFTER 6
+#define POPUPPOSITION_ENDAFTER 7
+#define POPUPPOSITION_OVERLAP 8
+#define POPUPPOSITION_AFTERPOINTER 9
+#define POPUPPOSITION_SELECTION 10
+
+#define POPUPPOSITION_HFLIP(v) (v ^ 1)
+#define POPUPPOSITION_VFLIP(v) (v ^ 2)
+
+nsIFrame* NS_NewMenuPopupFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle);
+
+class nsView;
+class nsMenuPopupFrame;
+
+// this class is used for dispatching popupshown events asynchronously.
+class nsXULPopupShownEvent final : public mozilla::Runnable,
+ public nsIDOMEventListener {
+ public:
+ nsXULPopupShownEvent(nsIContent* aPopup, nsPresContext* aPresContext)
+ : mozilla::Runnable("nsXULPopupShownEvent"),
+ mPopup(aPopup),
+ mPresContext(aPresContext) {}
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIRUNNABLE
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ void CancelListener();
+
+ protected:
+ virtual ~nsXULPopupShownEvent() = default;
+
+ private:
+ const nsCOMPtr<nsIContent> mPopup;
+ const RefPtr<nsPresContext> mPresContext;
+};
+
+class nsMenuPopupFrame final : public nsBoxFrame, public nsIReflowCallback {
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsMenuPopupFrame)
+
+ explicit nsMenuPopupFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+ ~nsMenuPopupFrame();
+
+ // as popups are opened asynchronously, the popup pending state is used to
+ // prevent multiple requests from attempting to open the same popup twice
+ nsPopupState PopupState() { return mPopupState; }
+ void SetPopupState(nsPopupState);
+
+ /*
+ * When this popup is open, should clicks outside of it be consumed?
+ * Return true if the popup should rollup on an outside click,
+ * but consume that click so it can't be used for anything else.
+ * Return false to allow clicks outside the popup to activate content
+ * even when the popup is open.
+ * ---------------------------------------------------------------------
+ *
+ * Should clicks outside of a popup be eaten?
+ *
+ * Menus Autocomplete Comboboxes
+ * Mac Eat No Eat
+ * Win No No Eat
+ * Unix Eat No Eat
+ *
+ */
+ ConsumeOutsideClicksResult ConsumeOutsideClicks();
+
+ mozilla::dom::XULPopupElement& PopupElement() const;
+
+ void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ nsIWidget* GetWidget() const;
+
+ // Overridden methods
+ virtual void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ // FIXME: This shouldn't run script (this can end up calling HidePopup).
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void DestroyFrom(
+ nsIFrame* aDestructRoot, PostDestroyData& aPostDestroyData) override;
+
+ bool HasRemoteContent() const;
+
+ // Whether we should create a widget on Init().
+ bool ShouldCreateWidgetUpfront() const;
+
+ // Whether we should expand the menu to take the size of the parent menulist.
+ bool ShouldExpandToInflowParentOrAnchor() const;
+
+ // Returns true if the popup is a panel with the noautohide attribute set to
+ // true. These panels do not roll up automatically.
+ bool IsNoAutoHide() const;
+
+ nsPopupLevel PopupLevel() const { return PopupLevel(IsNoAutoHide()); }
+
+ // Ensure that a widget has already been created for this view, and create
+ // one if it hasn't. If aRecreate is true, destroys any existing widget and
+ // creates a new one, regardless of whether one has already been created.
+ void PrepareWidget(bool aRecreate = false);
+
+ MOZ_CAN_RUN_SCRIPT void EnsureActiveMenuListItemIsVisible();
+
+ nsresult CreateWidgetForView(nsView* aView);
+ mozilla::StyleWindowShadow GetShadowStyle();
+
+ void DidSetComputedStyle(ComputedStyle* aOldStyle) override;
+
+ // layout, position and display the popup as needed
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void LayoutPopup(nsBoxLayoutState& aState);
+
+ // Set the position of the popup relative to the anchor content, anchored at a
+ // rectangle, or at a specific point if a screen position is set. The popup
+ // will be adjusted so that it is on screen. If aIsMove is true, then the
+ // popup is being moved, and should not be flipped.
+ nsresult SetPopupPosition(bool aIsMove);
+
+ // Called when the Enter key is pressed while the popup is open. This will
+ // just pass the call down to the current menu, if any.
+ // Also, calling Enter will reset the current incremental search string,
+ // calculated in FindMenuWithShortcut.
+ MOZ_CAN_RUN_SCRIPT void HandleEnterKeyPress(mozilla::WidgetEvent&);
+
+ // Locate and return the menu frame that should be activated for the supplied
+ // key event. If aDoAction is set to true by this method, then the menu's
+ // action should be carried out, as if the user had pressed the Enter key. If
+ // aDoAction is false, the menu should just be highlighted.
+ // This method also handles incremental searching in menus so the user can
+ // type the first few letters of an item/s name to select it.
+ mozilla::dom::XULButtonElement* FindMenuWithShortcut(
+ mozilla::dom::KeyboardEvent& aKeyEvent, bool& aDoAction);
+
+ mozilla::dom::XULButtonElement* GetCurrentMenuItem() const;
+ nsIFrame* GetCurrentMenuItemFrame() const;
+
+ nsPopupType PopupType() const { return mPopupType; }
+ bool IsContextMenu() const { return mIsContextMenu; }
+
+ bool IsOpen() const {
+ return mPopupState == ePopupOpening || mPopupState == ePopupVisible ||
+ mPopupState == ePopupShown;
+ }
+ bool IsVisible() {
+ return mPopupState == ePopupVisible || mPopupState == ePopupShown;
+ }
+ bool IsVisibleOrShowing() {
+ return IsOpen() || mPopupState == ePopupPositioning ||
+ mPopupState == ePopupShowing;
+ }
+ bool IsNativeMenu() const { return mIsNativeMenu; }
+ bool IsMouseTransparent() const;
+
+ // Return true if the popup is for a menulist.
+ bool IsMenuList() const;
+
+ bool IsDragSource() const { return mIsDragSource; }
+ void SetIsDragSource(bool aIsDragSource) { mIsDragSource = aIsDragSource; }
+
+ static nsIContent* GetTriggerContent(nsMenuPopupFrame* aMenuPopupFrame);
+ void ClearTriggerContent() { mTriggerContent = nullptr; }
+ void ClearTriggerContentIncludingDocument();
+
+ // returns true if the popup is in a content shell, or false for a popup in
+ // a chrome shell
+ bool IsInContentShell() { return mInContentShell; }
+
+ // the Initialize methods are used to set the anchor position for
+ // each way of opening a popup.
+ void InitializePopup(nsIContent* aAnchorContent, nsIContent* aTriggerContent,
+ const nsAString& aPosition, int32_t aXPos, int32_t aYPos,
+ MenuPopupAnchorType aAnchorType,
+ bool aAttributesOverride);
+
+ void InitializePopupAtRect(nsIContent* aTriggerContent,
+ const nsAString& aPosition, const nsIntRect& aRect,
+ bool aAttributesOverride);
+
+ /**
+ * @param aIsContextMenu if true, then the popup is
+ * positioned at a slight offset from aXPos/aYPos to ensure the
+ * (presumed) mouse position is not over the menu.
+ */
+ void InitializePopupAtScreen(nsIContent* aTriggerContent, int32_t aXPos,
+ int32_t aYPos, bool aIsContextMenu);
+
+ // Called if this popup should be displayed as an OS-native context menu.
+ void InitializePopupAsNativeContextMenu(nsIContent* aTriggerContent,
+ int32_t aXPos, int32_t aYPos);
+
+ // indicate that the popup should be opened
+ void ShowPopup(bool aIsContextMenu);
+ // indicate that the popup should be hidden. The new state should either be
+ // ePopupClosed or ePopupInvisible.
+ MOZ_CAN_RUN_SCRIPT void HidePopup(bool aDeselectMenu, nsPopupState aNewState,
+ bool aFromFrameDestruction = false);
+
+ void ClearIncrementalString() { mIncrementalString.Truncate(); }
+ static bool IsWithinIncrementalTime(mozilla::TimeStamp time) {
+ return !sLastKeyTime.IsNull() &&
+ ((time - sLastKeyTime).ToMilliseconds() <=
+ mozilla::StaticPrefs::ui_menu_incremental_search_timeout());
+ }
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"MenuPopup"_ns, aResult);
+ }
+#endif
+
+ MOZ_CAN_RUN_SCRIPT void ChangeByPage(bool aIsUp);
+
+ // Move the popup to the screen coordinate |aPos| in CSS pixels.
+ // If aUpdateAttrs is true, and the popup already has left or top attributes,
+ // then those attributes are updated to the new location.
+ // The frame may be destroyed by this method.
+ void MoveTo(const mozilla::CSSPoint& aPos, bool aUpdateAttrs,
+ bool aByMoveToRect = false);
+
+ void MoveToAnchor(nsIContent* aAnchorContent, const nsAString& aPosition,
+ int32_t aXPos, int32_t aYPos, bool aAttributesOverride);
+
+ nsIScrollableFrame* GetScrollFrame(nsIFrame* aStart);
+
+ void SetOverrideConstraintRect(mozilla::LayoutDeviceIntRect aRect) {
+ mOverrideConstraintRect = ToAppUnits(aRect, mozilla::AppUnitsPerCSSPixel());
+ }
+
+ // For a popup that should appear anchored at the given rect, determine
+ // the screen area that it is constrained by. This will be the available
+ // area of the screen the popup should be displayed on. Content popups,
+ // however, will also be constrained by the content area, given by
+ // aRootScreenRect. All coordinates are in app units.
+ // For non-toplevel popups (which will always be panels), we will also
+ // constrain them to the available screen rect, ie they will not fall
+ // underneath the taskbar, dock or other fixed OS elements.
+ // This operates in device pixels.
+ mozilla::LayoutDeviceIntRect GetConstraintRect(
+ const mozilla::LayoutDeviceIntRect& aAnchorRect,
+ const mozilla::LayoutDeviceIntRect& aRootScreenRect,
+ nsPopupLevel aPopupLevel);
+
+ // Determines whether the given edges of the popup may be moved, where
+ // aHorizontalSide and aVerticalSide are one of the enum Side constants.
+ // aChange is the distance to move on those sides. If will be reset to 0
+ // if the side cannot be adjusted at all in that direction. For example, a
+ // popup cannot be moved if it is anchored on a particular side.
+ //
+ // Later, when bug 357725 is implemented, we can make this adjust aChange by
+ // the amount that the side can be resized, so that minimums and maximums
+ // can be taken into account.
+ void CanAdjustEdges(mozilla::Side aHorizontalSide,
+ mozilla::Side aVerticalSide,
+ mozilla::LayoutDeviceIntPoint& aChange);
+
+ // Return true if the popup is positioned relative to an anchor.
+ bool IsAnchored() const { return mAnchorType != MenuPopupAnchorType_Point; }
+
+ // Return the anchor if there is one.
+ nsIContent* GetAnchor() const { return mAnchorContent; }
+
+ // Return the screen coordinates in CSS pixels of the popup,
+ // or (-1, -1, 0, 0) if anchored.
+ mozilla::CSSIntRect GetScreenAnchorRect() const {
+ return mozilla::CSSRect::FromAppUnitsRounded(mScreenRect);
+ }
+
+ mozilla::LayoutDeviceIntPoint GetLastClientOffset() const {
+ return mLastClientOffset;
+ }
+
+ // Return the alignment of the popup
+ int8_t GetAlignmentPosition() const;
+
+ // Return the offset applied to the alignment of the popup
+ nscoord GetAlignmentOffset() const { return mAlignmentOffset; }
+
+ // Clear the mPopupShownDispatcher, remove the listener and return true if
+ // mPopupShownDispatcher was non-null.
+ bool ClearPopupShownDispatcher() {
+ if (mPopupShownDispatcher) {
+ mPopupShownDispatcher->CancelListener();
+ mPopupShownDispatcher = nullptr;
+ return true;
+ }
+
+ return false;
+ }
+
+ void ShowWithPositionedEvent() { mPopupState = ePopupPositioning; }
+
+ // Checks for the anchor to change and either moves or hides the popup
+ // accordingly. The original position of the anchor should be supplied as
+ // the argument. If the popup needs to be hidden, HidePopup will be called by
+ // CheckForAnchorChange. If the popup needs to be moved, aRect will be updated
+ // with the new rectangle.
+ void CheckForAnchorChange(nsRect& aRect);
+
+ void WillDispatchPopupPositioned() { mPendingPositionedEvent = false; }
+
+ // nsIReflowCallback
+ virtual bool ReflowFinished() override;
+ virtual void ReflowCallbackCanceled() override;
+
+ protected:
+ // returns the popup's level.
+ nsPopupLevel PopupLevel(bool aIsNoAutoHide) const;
+
+ void ConstrainSizeForWayland(nsSize&) const;
+
+ // redefine to tell the box system not to move the views.
+ ReflowChildFlags GetXULLayoutFlags() override;
+
+ void InitPositionFromAnchorAlign(const nsAString& aAnchor,
+ const nsAString& aAlign);
+
+ // return the position where the popup should be, when it should be
+ // anchored at anchorRect. aHFlip and aVFlip will be set if the popup may be
+ // flipped in that direction if there is not enough space available.
+ nsPoint AdjustPositionForAnchorAlign(nsRect& anchorRect, FlipStyle& aHFlip,
+ FlipStyle& aVFlip);
+
+ // For popups that are going to align to their selected item, get the frame of
+ // the selected item.
+ nsIFrame* GetSelectedItemForAlignment();
+
+ // check if the popup will fit into the available space and resize it. This
+ // method handles only one axis at a time so is called twice, once for
+ // horizontal and once for vertical. All arguments are specified for this
+ // one axis. All coordinates are in app units relative to the screen.
+ // aScreenPoint - the point where the popup should appear
+ // aSize - the size of the popup
+ // aScreenBegin - the left or top edge of the screen
+ // aScreenEnd - the right or bottom edge of the screen
+ // aAnchorBegin - the left or top edge of the anchor rectangle
+ // aAnchorEnd - the right or bottom edge of the anchor rectangle
+ // aMarginBegin - the left or top margin of the popup
+ // aMarginEnd - the right or bottom margin of the popup
+ // aFlip - how to flip or resize the popup when there isn't space
+ // aFlipSide - pointer to where current flip mode is stored
+ nscoord FlipOrResize(nscoord& aScreenPoint, nscoord aSize,
+ nscoord aScreenBegin, nscoord aScreenEnd,
+ nscoord aAnchorBegin, nscoord aAnchorEnd,
+ nscoord aMarginBegin, nscoord aMarginEnd,
+ FlipStyle aFlip, bool aIsOnEnd, bool* aFlipSide);
+
+ // check if the popup can fit into the available space by "sliding" (i.e.,
+ // by having the anchor arrow slide along one axis and only resizing if that
+ // can't provide the requested size). Only one axis can be slid - the other
+ // axis is "flipped" as normal. This method can handle either axis, but is
+ // only called for the sliding axis. All coordinates are in app units
+ // relative to the screen.
+ // aScreenPoint - the point where the popup should appear
+ // aSize - the size of the popup
+ // aScreenBegin - the left or top edge of the screen
+ // aScreenEnd - the right or bottom edge of the screen
+ // aOffset - the amount by which the arrow must be slid such that it is
+ // still aligned with the anchor.
+ // Result is the new size of the popup, which will typically be the same
+ // as aSize, unless aSize is greater than the screen width/height.
+ nscoord SlideOrResize(nscoord& aScreenPoint, nscoord aSize,
+ nscoord aScreenBegin, nscoord aScreenEnd,
+ nscoord* aOffset);
+
+ // Given an anchor frame, compute the anchor rectangle relative to the screen,
+ // using the popup frame's app units, and taking into account transforms.
+ nsRect ComputeAnchorRect(nsPresContext* aRootPresContext,
+ nsIFrame* aAnchorFrame);
+
+ // Move the popup to the position specified in its |left| and |top|
+ // attributes.
+ void MoveToAttributePosition();
+
+ // Create a popup view for this frame. The view is added a child of the root
+ // view, and is initially hidden.
+ void CreatePopupView();
+
+ nsView* GetViewInternal() const override { return mView; }
+ void SetViewInternal(nsView* aView) override { mView = aView; }
+
+ // Returns true if the popup should try to remain at the same relative
+ // location as the anchor while it is open. If the anchor becomes hidden
+ // either directly or indirectly because a parent popup or other element
+ // is no longer visible, or a parent deck page is changed, the popup hides
+ // as well. The second variation also sets the anchor rectangle, relative to
+ // the popup frame.
+ bool ShouldFollowAnchor();
+
+ nsIFrame* GetAnchorFrame() const;
+
+ public:
+ /**
+ * Return whether the popup direction should be RTL.
+ * If the popup has an anchor, its direction is the anchor direction.
+ * Otherwise, its the general direction of the UI.
+ *
+ * Return whether the popup direction should be RTL.
+ */
+ bool IsDirectionRTL() const;
+
+ bool ShouldFollowAnchor(nsRect& aRect);
+
+ // Returns parent menu widget for submenus that are in the same
+ // frame hierarchy, it's needed for Linux/Wayland which demands
+ // strict popup windows hierarchy.
+ nsIWidget* GetParentMenuWidget();
+
+ // Returns the effective margin for this popup. This is the CSS margin plus
+ // the context-menu shift, if needed.
+ nsMargin GetMargin() const;
+
+ // These are used by Wayland backend.
+ const nsRect& GetUntransformedAnchorRect() const {
+ return mUntransformedAnchorRect;
+ }
+ int GetPopupAlignment() const { return mPopupAlignment; }
+ int GetPopupAnchor() const { return mPopupAnchor; }
+ FlipType GetFlipType() const { return mFlip; }
+
+ void WidgetPositionOrSizeDidChange();
+
+ protected:
+ nsString mIncrementalString; // for incremental typing navigation
+
+ // the content that the popup is anchored to, if any, which may be in a
+ // different document than the popup.
+ nsCOMPtr<nsIContent> mAnchorContent;
+
+ // the content that triggered the popup, typically the node where the mouse
+ // was clicked. It will be cleared when the popup is hidden.
+ nsCOMPtr<nsIContent> mTriggerContent;
+
+ nsView* mView;
+
+ RefPtr<nsXULPopupShownEvent> mPopupShownDispatcher;
+
+ // The popup's screen rectangle in app units.
+ nsIntRect mUsedScreenRect;
+
+ // A popup's preferred size may be different than its actual size stored in
+ // mRect in the case where the popup was resized because it was too large
+ // for the screen. The preferred size mPrefSize holds the full size the popup
+ // would be before resizing. Computations are performed using this size.
+ nsSize mPrefSize;
+
+ // The position of the popup, in CSS pixels.
+ // The screen coordinates, if set to values other than -1,
+ // override mXPos and mYPos.
+ int32_t mXPos;
+ int32_t mYPos;
+ nsRect mScreenRect;
+ // Used for store rectangle which the popup is going to be anchored to, we
+ // need that for Wayland. It's important that this rect is unflipped, and
+ // without margins applied, as GTK is what takes care of determining how to
+ // flip etc. on Wayland.
+ nsRect mUntransformedAnchorRect;
+
+ // Whether we were moved by the move-to-rect Wayland callback. In that case,
+ // we stop updating the anchor so that we can end up with a stable position.
+ bool mPositionedByMoveToRect = false;
+
+ // If the panel prefers to "slide" rather than resize, then the arrow gets
+ // positioned at this offset (along either the x or y axis, depending on
+ // mPosition)
+ nscoord mAlignmentOffset;
+
+ // The value of the client offset of our widget the last time we positioned
+ // ourselves. We store this so that we can detect when it changes but the
+ // position of our widget didn't change.
+ mozilla::LayoutDeviceIntPoint mLastClientOffset;
+
+ nsPopupType mPopupType; // type of popup
+ nsPopupState mPopupState; // open state of the popup
+
+ // popup alignment relative to the anchor node
+ int8_t mPopupAlignment;
+ int8_t mPopupAnchor;
+ int8_t mPosition;
+
+ FlipType mFlip; // Whether to flip
+
+ struct ReflowCallbackData {
+ ReflowCallbackData() = default;
+ void MarkPosted(bool aIsOpenChanged) {
+ mPosted = true;
+ mIsOpenChanged = aIsOpenChanged;
+ }
+ void Clear() {
+ mPosted = false;
+ mIsOpenChanged = false;
+ }
+ bool mPosted = false;
+ bool mIsOpenChanged = false;
+ };
+ ReflowCallbackData mReflowCallbackData;
+
+ bool mIsOpenChanged; // true if the open state changed since the last layout
+ bool mIsContextMenu = false; // true for context menus and their submenus.
+ bool mIsTopLevelContextMenu = false; // true for the topmost context menu.
+
+ bool mMenuCanOverlapOSBar; // can we appear over the taskbar/menubar?
+ bool mInContentShell; // True if the popup is in a content shell
+
+ // True if this popup has been offset due to moving off / near the edge of the
+ // screen. (This is useful for ensuring that a move, which can't offset the
+ // popup, doesn't undo a previously set offset.)
+ bool mIsOffset;
+
+ // the flip modes that were used when the popup was opened
+ bool mHFlip;
+ bool mVFlip;
+
+ // Whether the most recent initialization of this menupopup happened via
+ // InitializePopupAsNativeContextMenu.
+ bool mIsNativeMenu = false;
+
+ // Whether we have a pending `popuppositioned` event.
+ bool mPendingPositionedEvent = false;
+
+ // Whether this popup is source of D&D operation. We can't close such
+ // popup on Wayland as it cancel whole D&D operation.
+ bool mIsDragSource = false;
+
+ // When POPUPPOSITION_SELECTION is used, this indicates the vertical offset
+ // that the original selected item was. This needs to be used in case the
+ // popup gets changed so that we can keep the popup at the same vertical
+ // offset.
+ nscoord mPositionedOffset;
+
+ // How the popup is anchored.
+ MenuPopupAnchorType mAnchorType;
+
+ nsRect mOverrideConstraintRect;
+
+ static int8_t sDefaultLevelIsTop;
+
+ static mozilla::TimeStamp sLastKeyTime;
+
+}; // class nsMenuPopupFrame
+
+#endif
diff --git a/layout/xul/nsRepeatService.cpp b/layout/xul/nsRepeatService.cpp
new file mode 100644
index 0000000000..b23c17396e
--- /dev/null
+++ b/layout/xul/nsRepeatService.cpp
@@ -0,0 +1,93 @@
+/* -*- 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/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "nsRepeatService.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/dom/Document.h"
+
+using namespace mozilla;
+
+static StaticAutoPtr<nsRepeatService> gRepeatService;
+
+nsRepeatService::nsRepeatService()
+ : mCallback(nullptr), mCallbackData(nullptr) {}
+
+nsRepeatService::~nsRepeatService() {
+ NS_ASSERTION(!mCallback && !mCallbackData,
+ "Callback was not removed before shutdown");
+}
+
+/* static */
+nsRepeatService* nsRepeatService::GetInstance() {
+ if (!gRepeatService) {
+ gRepeatService = new nsRepeatService();
+ }
+ return gRepeatService;
+}
+
+/*static*/
+void nsRepeatService::Shutdown() { gRepeatService = nullptr; }
+
+void nsRepeatService::Start(Callback aCallback, void* aCallbackData,
+ dom::Document* aDocument,
+ const nsACString& aCallbackName,
+ uint32_t aInitialDelay) {
+ MOZ_ASSERT(aCallback != nullptr, "null ptr");
+
+ mCallback = aCallback;
+ mCallbackData = aCallbackData;
+ mCallbackName = aCallbackName;
+
+ mRepeatTimer = NS_NewTimer(aDocument->EventTargetFor(TaskCategory::Other));
+
+ if (mRepeatTimer) {
+ InitTimerCallback(aInitialDelay);
+ }
+}
+
+void nsRepeatService::Stop(Callback aCallback, void* aCallbackData) {
+ if (mCallback != aCallback || mCallbackData != aCallbackData) return;
+
+ // printf("Stopping repeat timer\n");
+ if (mRepeatTimer) {
+ mRepeatTimer->Cancel();
+ mRepeatTimer = nullptr;
+ }
+ mCallback = nullptr;
+ mCallbackData = nullptr;
+}
+
+void nsRepeatService::InitTimerCallback(uint32_t aInitialDelay) {
+ if (!mRepeatTimer) {
+ return;
+ }
+
+ mRepeatTimer->InitWithNamedFuncCallback(
+ [](nsITimer* aTimer, void* aClosure) {
+ // Use gRepeatService instead of nsRepeatService::GetInstance() (because
+ // we don't want nsRepeatService::GetInstance() to re-create a new
+ // instance for us, if we happen to get invoked after
+ // nsRepeatService::Shutdown() has nulled out gRepeatService).
+ nsRepeatService* rs = gRepeatService;
+ if (!rs) {
+ return;
+ }
+
+ if (rs->mCallback) {
+ rs->mCallback(rs->mCallbackData);
+ }
+
+ rs->InitTimerCallback(REPEAT_DELAY);
+ },
+ nullptr, aInitialDelay, nsITimer::TYPE_ONE_SHOT, mCallbackName.Data());
+}
diff --git a/layout/xul/nsRepeatService.h b/layout/xul/nsRepeatService.h
new file mode 100644
index 0000000000..158e7ffd31
--- /dev/null
+++ b/layout/xul/nsRepeatService.h
@@ -0,0 +1,73 @@
+/* -*- 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/. */
+
+//
+// nsRepeatService
+//
+#ifndef nsRepeatService_h__
+#define nsRepeatService_h__
+
+#include "nsCOMPtr.h"
+#include "nsITimer.h"
+#include "nsString.h"
+
+#define INITAL_REPEAT_DELAY 250
+
+#ifdef XP_MACOSX
+# define REPEAT_DELAY 25
+#else
+# define REPEAT_DELAY 50
+#endif
+
+class nsITimer;
+
+namespace mozilla {
+namespace dom {
+class Document;
+}
+} // namespace mozilla
+
+class nsRepeatService final {
+ public:
+ typedef void (*Callback)(void* aData);
+
+ ~nsRepeatService();
+
+ // Start dispatching timer events to the callback. There is no memory
+ // management of aData here; it is the caller's responsibility to call
+ // Stop() before aData's memory is released.
+ //
+ // aCallbackName is the label of the callback, used to pass to
+ // InitWithNamedCallbackFunc.
+ //
+ // aDocument is used to get the event target in Start(). We need an event
+ // target to init mRepeatTimer.
+ void Start(Callback aCallback, void* aCallbackData,
+ mozilla::dom::Document* aDocument, const nsACString& aCallbackName,
+ uint32_t aInitialDelay = INITAL_REPEAT_DELAY);
+ // Stop dispatching timer events to the callback. If the repeat service
+ // is not currently configured with the given callback and data, this
+ // is just ignored.
+ void Stop(Callback aCallback, void* aData);
+
+ static nsRepeatService* GetInstance();
+ static void Shutdown();
+
+ protected:
+ nsRepeatService();
+
+ private:
+ // helper function to initialize callback function to mRepeatTimer
+ void InitTimerCallback(uint32_t aInitialDelay);
+
+ Callback mCallback;
+ void* mCallbackData;
+ nsCString mCallbackName;
+ nsCOMPtr<nsITimer> mRepeatTimer;
+
+}; // class nsRepeatService
+
+#endif
diff --git a/layout/xul/nsScrollbarButtonFrame.cpp b/layout/xul/nsScrollbarButtonFrame.cpp
new file mode 100644
index 0000000000..176c0ec09d
--- /dev/null
+++ b/layout/xul/nsScrollbarButtonFrame.cpp
@@ -0,0 +1,274 @@
+/* -*- 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/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "nsScrollbarButtonFrame.h"
+#include "nsPresContext.h"
+#include "nsIContent.h"
+#include "nsCOMPtr.h"
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsLayoutUtils.h"
+#include "nsSliderFrame.h"
+#include "nsScrollbarFrame.h"
+#include "nsIScrollbarMediator.h"
+#include "nsRepeatService.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/Telemetry.h"
+
+using namespace mozilla;
+
+//
+// NS_NewToolbarFrame
+//
+// Creates a new Toolbar frame and returns it
+//
+nsIFrame* NS_NewScrollbarButtonFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsScrollbarButtonFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsScrollbarButtonFrame)
+
+nsresult nsScrollbarButtonFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ NS_ENSURE_ARG_POINTER(aEventStatus);
+
+ // If a web page calls event.preventDefault() we still want to
+ // scroll when scroll arrow is clicked. See bug 511075.
+ if (!mContent->IsInNativeAnonymousSubtree() &&
+ nsEventStatus_eConsumeNoDefault == *aEventStatus) {
+ return NS_OK;
+ }
+
+ switch (aEvent->mMessage) {
+ case eMouseDown:
+ mCursorOnThis = true;
+ // if we didn't handle the press ourselves, pass it on to the superclass
+ if (HandleButtonPress(aPresContext, aEvent, aEventStatus)) {
+ return NS_OK;
+ }
+ break;
+ case eMouseUp:
+ HandleRelease(aPresContext, aEvent, aEventStatus);
+ break;
+ case eMouseOut:
+ mCursorOnThis = false;
+ break;
+ case eMouseMove: {
+ nsPoint cursor = nsLayoutUtils::GetEventCoordinatesRelativeTo(
+ aEvent, RelativeTo{this});
+ nsRect frameRect(nsPoint(0, 0), GetSize());
+ mCursorOnThis = frameRect.Contains(cursor);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return nsBoxFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+}
+
+bool nsScrollbarButtonFrame::HandleButtonPress(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ // Get the desired action for the scrollbar button.
+ LookAndFeel::IntID tmpAction;
+ uint16_t button = aEvent->AsMouseEvent()->mButton;
+ if (button == MouseButton::ePrimary) {
+ tmpAction = LookAndFeel::IntID::ScrollButtonLeftMouseButtonAction;
+ } else if (button == MouseButton::eMiddle) {
+ tmpAction = LookAndFeel::IntID::ScrollButtonMiddleMouseButtonAction;
+ } else if (button == MouseButton::eSecondary) {
+ tmpAction = LookAndFeel::IntID::ScrollButtonRightMouseButtonAction;
+ } else {
+ return false;
+ }
+
+ // Get the button action metric from the pres. shell.
+ int32_t pressedButtonAction;
+ if (NS_FAILED(LookAndFeel::GetInt(tmpAction, &pressedButtonAction))) {
+ return false;
+ }
+
+ // get the scrollbar control
+ nsIFrame* scrollbar;
+ GetParentWithTag(nsGkAtoms::scrollbar, this, scrollbar);
+
+ if (scrollbar == nullptr) return false;
+
+ static dom::Element::AttrValuesArray strings[] = {
+ nsGkAtoms::increment, nsGkAtoms::decrement, nullptr};
+ int32_t index = mContent->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::type, strings, eCaseMatters);
+ int32_t direction;
+ if (index == 0)
+ direction = 1;
+ else if (index == 1)
+ direction = -1;
+ else
+ return false;
+
+ bool repeat = pressedButtonAction != 2;
+ // set this attribute so we can style it later
+ AutoWeakFrame weakFrame(this);
+ mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::active,
+ u"true"_ns, true);
+
+ PresShell::SetCapturingContent(mContent, CaptureFlags::IgnoreAllowedState);
+
+ if (!weakFrame.IsAlive()) {
+ return false;
+ }
+
+ if (nsScrollbarFrame* sb = do_QueryFrame(scrollbar)) {
+ nsIScrollbarMediator* m = sb->GetScrollbarMediator();
+ switch (pressedButtonAction) {
+ case 0:
+ sb->SetIncrementToLine(direction);
+ if (m) {
+ m->ScrollByLine(sb, direction, ScrollSnapFlags::IntendedDirection);
+ }
+ break;
+ case 1:
+ sb->SetIncrementToPage(direction);
+ if (m) {
+ m->ScrollByPage(sb, direction,
+ ScrollSnapFlags::IntendedDirection |
+ ScrollSnapFlags::IntendedEndPosition);
+ }
+ break;
+ case 2:
+ sb->SetIncrementToWhole(direction);
+ if (m) {
+ m->ScrollByWhole(sb, direction, ScrollSnapFlags::IntendedEndPosition);
+ }
+ break;
+ case 3:
+ default:
+ // We were told to ignore this click, or someone assigned a non-standard
+ // value to the button's action.
+ return false;
+ }
+ if (!weakFrame.IsAlive()) {
+ return false;
+ }
+
+ if (!m) {
+ sb->MoveToNewPosition(nsScrollbarFrame::ImplementsScrollByUnit::No);
+ if (!weakFrame.IsAlive()) {
+ return false;
+ }
+ }
+ }
+ if (repeat) {
+ StartRepeat();
+ }
+ return true;
+}
+
+NS_IMETHODIMP
+nsScrollbarButtonFrame::HandleRelease(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ PresShell::ReleaseCapturingContent();
+ // we're not active anymore
+ mContent->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::active, true);
+ StopRepeat();
+ nsIFrame* scrollbar;
+ GetParentWithTag(nsGkAtoms::scrollbar, this, scrollbar);
+ nsScrollbarFrame* sb = do_QueryFrame(scrollbar);
+ if (sb) {
+ nsIScrollbarMediator* m = sb->GetScrollbarMediator();
+ if (m) {
+ m->ScrollbarReleased(sb);
+ }
+ }
+ return NS_OK;
+}
+
+void nsScrollbarButtonFrame::Notify() {
+ if (mCursorOnThis ||
+ LookAndFeel::GetInt(LookAndFeel::IntID::ScrollbarButtonAutoRepeatBehavior,
+ 0)) {
+ // get the scrollbar control
+ nsIFrame* scrollbar;
+ GetParentWithTag(nsGkAtoms::scrollbar, this, scrollbar);
+ nsScrollbarFrame* sb = do_QueryFrame(scrollbar);
+ if (sb) {
+ nsIScrollbarMediator* m = sb->GetScrollbarMediator();
+ if (m) {
+ m->RepeatButtonScroll(sb);
+ } else {
+ sb->MoveToNewPosition(nsScrollbarFrame::ImplementsScrollByUnit::No);
+ }
+ }
+ }
+}
+
+nsresult nsScrollbarButtonFrame::GetChildWithTag(nsAtom* atom, nsIFrame* start,
+ nsIFrame*& result) {
+ // recursively search our children
+ for (nsIFrame* childFrame : start->PrincipalChildList()) {
+ // get the content node
+ nsIContent* child = childFrame->GetContent();
+
+ if (child) {
+ // see if it is the child
+ if (child->IsXULElement(atom)) {
+ result = childFrame;
+
+ return NS_OK;
+ }
+ }
+
+ // recursive search the child
+ GetChildWithTag(atom, childFrame, result);
+ if (result != nullptr) return NS_OK;
+ }
+
+ result = nullptr;
+ return NS_OK;
+}
+
+nsresult nsScrollbarButtonFrame::GetParentWithTag(nsAtom* toFind,
+ nsIFrame* start,
+ nsIFrame*& result) {
+ while (start) {
+ start = start->GetParent();
+
+ if (start) {
+ // get the content node
+ nsIContent* child = start->GetContent();
+
+ if (child && child->IsXULElement(toFind)) {
+ result = start;
+ return NS_OK;
+ }
+ }
+ }
+
+ result = nullptr;
+ return NS_OK;
+}
+
+void nsScrollbarButtonFrame::DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) {
+ // Ensure our repeat service isn't going... it's possible that a scrollbar can
+ // disappear out from under you while you're in the process of scrolling.
+ StopRepeat();
+ nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
+}
diff --git a/layout/xul/nsScrollbarButtonFrame.h b/layout/xul/nsScrollbarButtonFrame.h
new file mode 100644
index 0000000000..2499c8c134
--- /dev/null
+++ b/layout/xul/nsScrollbarButtonFrame.h
@@ -0,0 +1,85 @@
+/* -*- 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/. */
+
+/**
+
+ Eric D Vaughan
+ This class lays out its children either vertically or horizontally
+
+**/
+
+#ifndef nsScrollbarButtonFrame_h___
+#define nsScrollbarButtonFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "nsBoxFrame.h"
+#include "nsRepeatService.h"
+
+namespace mozilla {
+class PresShell;
+} // namespace mozilla
+
+class nsScrollbarButtonFrame final : public nsBoxFrame {
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsScrollbarButtonFrame)
+
+ explicit nsScrollbarButtonFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsBoxFrame(aStyle, aPresContext, kClassID), mCursorOnThis(false) {}
+
+ // Overrides
+ virtual void DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) override;
+
+ friend nsIFrame* NS_NewScrollbarButtonFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+
+ nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ static nsresult GetChildWithTag(nsAtom* atom, nsIFrame* start,
+ nsIFrame*& result);
+ static nsresult GetParentWithTag(nsAtom* atom, nsIFrame* start,
+ nsIFrame*& result);
+
+ bool HandleButtonPress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus);
+
+ NS_IMETHOD HandleMultiplePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus,
+ bool aControlHeld) override {
+ return NS_OK;
+ }
+
+ MOZ_CAN_RUN_SCRIPT
+ NS_IMETHOD HandleDrag(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override {
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleRelease(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ protected:
+ void StartRepeat() {
+ nsRepeatService::GetInstance()->Start(Notify, this, mContent->OwnerDoc(),
+ "nsScrollbarButtonFrame"_ns);
+ }
+ void StopRepeat() { nsRepeatService::GetInstance()->Stop(Notify, this); }
+ void Notify();
+ static void Notify(void* aData) {
+ static_cast<nsScrollbarButtonFrame*>(aData)->Notify();
+ }
+
+ bool mCursorOnThis;
+};
+
+#endif
diff --git a/layout/xul/nsScrollbarFrame.cpp b/layout/xul/nsScrollbarFrame.cpp
new file mode 100644
index 0000000000..bc6db8a1af
--- /dev/null
+++ b/layout/xul/nsScrollbarFrame.cpp
@@ -0,0 +1,546 @@
+/* -*- 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/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "nsScrollbarFrame.h"
+#include "nsSliderFrame.h"
+#include "nsScrollbarButtonFrame.h"
+#include "nsContentCreatorFunctions.h"
+#include "nsGkAtoms.h"
+#include "nsIScrollableFrame.h"
+#include "nsIScrollbarMediator.h"
+#include "nsStyleConsts.h"
+#include "nsIContent.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/MutationEventBinding.h"
+#include "mozilla/StaticPrefs_apz.h"
+
+using namespace mozilla;
+using mozilla::dom::Element;
+
+//
+// NS_NewScrollbarFrame
+//
+// Creates a new scrollbar frame and returns it
+//
+nsIFrame* NS_NewScrollbarFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell)
+ nsScrollbarFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsScrollbarFrame)
+
+NS_QUERYFRAME_HEAD(nsScrollbarFrame)
+ NS_QUERYFRAME_ENTRY(nsScrollbarFrame)
+ NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
+NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame)
+
+void nsScrollbarFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsBoxFrame::Init(aContent, aParent, aPrevInFlow);
+
+ // We want to be a reflow root since we use reflows to move the
+ // slider. Any reflow inside the scrollbar frame will be a reflow to
+ // move the slider and will thus not change anything outside of the
+ // scrollbar or change the size of the scrollbar frame.
+ AddStateBits(NS_FRAME_REFLOW_ROOT);
+}
+
+void nsScrollbarFrame::DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) {
+ aPostDestroyData.AddAnonymousContent(mUpTopButton.forget());
+ aPostDestroyData.AddAnonymousContent(mDownTopButton.forget());
+ aPostDestroyData.AddAnonymousContent(mSlider.forget());
+ aPostDestroyData.AddAnonymousContent(mUpBottomButton.forget());
+ aPostDestroyData.AddAnonymousContent(mDownBottomButton.forget());
+ nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
+}
+
+void nsScrollbarFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ nsBoxFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus);
+
+ // nsGfxScrollFrame may have told us to shrink to nothing. If so, make sure
+ // our desired size agrees.
+ if (aReflowInput.AvailableWidth() == 0) {
+ aDesiredSize.Width() = 0;
+ }
+ if (aReflowInput.AvailableHeight() == 0) {
+ aDesiredSize.Height() = 0;
+ }
+}
+
+nsresult nsScrollbarFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ nsresult rv =
+ nsBoxFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+
+ // Update value in our children
+ UpdateChildrenAttributeValue(aAttribute, true);
+
+ // if the current position changes, notify any nsGfxScrollFrame
+ // parent we may have
+ if (aAttribute != nsGkAtoms::curpos) return rv;
+
+ nsIScrollableFrame* scrollable = do_QueryFrame(GetParent());
+ if (!scrollable) return rv;
+
+ nsCOMPtr<nsIContent> content(mContent);
+ scrollable->CurPosAttributeChanged(content);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsScrollbarFrame::HandlePress(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsScrollbarFrame::HandleMultiplePress(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus,
+ bool aControlHeld) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsScrollbarFrame::HandleDrag(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsScrollbarFrame::HandleRelease(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ return NS_OK;
+}
+
+void nsScrollbarFrame::SetScrollbarMediatorContent(nsIContent* aMediator) {
+ mScrollbarMediator = aMediator;
+}
+
+nsIScrollbarMediator* nsScrollbarFrame::GetScrollbarMediator() {
+ if (!mScrollbarMediator) {
+ return nullptr;
+ }
+ nsIFrame* f = mScrollbarMediator->GetPrimaryFrame();
+ nsIScrollableFrame* scrollFrame = do_QueryFrame(f);
+ nsIScrollbarMediator* sbm;
+
+ if (scrollFrame) {
+ nsIFrame* scrolledFrame = scrollFrame->GetScrolledFrame();
+ sbm = do_QueryFrame(scrolledFrame);
+ if (sbm) {
+ return sbm;
+ }
+ }
+ sbm = do_QueryFrame(f);
+ if (f && !sbm) {
+ f = f->PresShell()->GetRootScrollFrame();
+ if (f && f->GetContent() == mScrollbarMediator) {
+ return do_QueryFrame(f);
+ }
+ }
+ return sbm;
+}
+
+nsresult nsScrollbarFrame::GetXULMargin(nsMargin& aMargin) {
+ aMargin.SizeTo(0, 0, 0, 0);
+
+ const bool overlayScrollbars = PresContext()->UseOverlayScrollbars();
+
+ const bool horizontal = IsXULHorizontal();
+ bool didSetMargin = false;
+
+ if (overlayScrollbars) {
+ nsSize minSize;
+ bool widthSet = false;
+ bool heightSet = false;
+ AddXULMinSize(this, minSize, widthSet, heightSet);
+ if (horizontal) {
+ if (heightSet) {
+ aMargin.top = -minSize.height;
+ didSetMargin = true;
+ }
+ } else {
+ if (widthSet) {
+ aMargin.left = -minSize.width;
+ didSetMargin = true;
+ }
+ }
+ }
+
+ if (!didSetMargin) {
+ DebugOnly<nsresult> rv = nsIFrame::GetXULMargin(aMargin);
+ // TODO(emilio): Should probably not be fallible, it's not like anybody
+ // cares about the return value anyway.
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "nsIFrame::GetXULMargin can't really fail");
+ }
+
+ if (!horizontal) {
+ nsIScrollbarMediator* scrollFrame = GetScrollbarMediator();
+ if (scrollFrame && !scrollFrame->IsScrollbarOnRight()) {
+ std::swap(aMargin.left, aMargin.right);
+ }
+ }
+
+ return NS_OK;
+}
+
+void nsScrollbarFrame::SetIncrementToLine(int32_t aDirection) {
+ mSmoothScroll = true;
+ mDirection = aDirection;
+ mScrollUnit = ScrollUnit::LINES;
+
+ // get the scrollbar's content node
+ nsIContent* content = GetContent();
+ mIncrement = aDirection * nsSliderFrame::GetIncrement(content);
+}
+
+void nsScrollbarFrame::SetIncrementToPage(int32_t aDirection) {
+ mSmoothScroll = true;
+ mDirection = aDirection;
+ mScrollUnit = ScrollUnit::PAGES;
+
+ // get the scrollbar's content node
+ nsIContent* content = GetContent();
+ mIncrement = aDirection * nsSliderFrame::GetPageIncrement(content);
+}
+
+void nsScrollbarFrame::SetIncrementToWhole(int32_t aDirection) {
+ // Don't repeat or use smooth scrolling if scrolling to beginning or end
+ // of a page.
+ mSmoothScroll = false;
+ mDirection = aDirection;
+ mScrollUnit = ScrollUnit::WHOLE;
+
+ // get the scrollbar's content node
+ nsIContent* content = GetContent();
+ if (aDirection == -1)
+ mIncrement = -nsSliderFrame::GetCurrentPosition(content);
+ else
+ mIncrement = nsSliderFrame::GetMaxPosition(content) -
+ nsSliderFrame::GetCurrentPosition(content);
+}
+
+int32_t nsScrollbarFrame::MoveToNewPosition(
+ ImplementsScrollByUnit aImplementsScrollByUnit) {
+ if (aImplementsScrollByUnit == ImplementsScrollByUnit::Yes &&
+ StaticPrefs::apz_scrollbarbuttonrepeat_enabled()) {
+ nsIScrollbarMediator* m = GetScrollbarMediator();
+ MOZ_ASSERT(m);
+ // aImplementsScrollByUnit being Yes indicates the caller doesn't care
+ // about the return value.
+ // Note that this `MoveToNewPosition` is used for scrolling triggered by
+ // repeating scrollbar button press, so we'd use an intended-direction
+ // scroll snap flag.
+ m->ScrollByUnit(
+ this, mSmoothScroll ? ScrollMode::Smooth : ScrollMode::Instant,
+ mDirection, mScrollUnit, ScrollSnapFlags::IntendedDirection);
+ return 0;
+ }
+
+ // get the scrollbar's content node
+ RefPtr<Element> content = GetContent()->AsElement();
+
+ // get the current pos
+ int32_t curpos = nsSliderFrame::GetCurrentPosition(content);
+
+ // get the max pos
+ int32_t maxpos = nsSliderFrame::GetMaxPosition(content);
+
+ // increment the given amount
+ if (mIncrement) {
+ curpos += mIncrement;
+ }
+
+ // make sure the current position is between the current and max positions
+ if (curpos < 0) {
+ curpos = 0;
+ } else if (curpos > maxpos) {
+ curpos = maxpos;
+ }
+
+ // set the current position of the slider.
+ nsAutoString curposStr;
+ curposStr.AppendInt(curpos);
+
+ AutoWeakFrame weakFrame(this);
+ if (mSmoothScroll) {
+ content->SetAttr(kNameSpaceID_None, nsGkAtoms::smooth, u"true"_ns, false);
+ }
+ content->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos, curposStr, false);
+ // notify the nsScrollbarFrame of the change
+ AttributeChanged(kNameSpaceID_None, nsGkAtoms::curpos,
+ dom::MutationEvent_Binding::MODIFICATION);
+ if (!weakFrame.IsAlive()) {
+ return curpos;
+ }
+ // notify all nsSliderFrames of the change
+ for (const auto& childList : ChildLists()) {
+ for (nsIFrame* f : childList.mList) {
+ nsSliderFrame* sliderFrame = do_QueryFrame(f);
+ if (sliderFrame) {
+ sliderFrame->AttributeChanged(kNameSpaceID_None, nsGkAtoms::curpos,
+ dom::MutationEvent_Binding::MODIFICATION);
+ if (!weakFrame.IsAlive()) {
+ return curpos;
+ }
+ }
+ }
+ }
+ content->UnsetAttr(kNameSpaceID_None, nsGkAtoms::smooth, false);
+ return curpos;
+}
+
+static already_AddRefed<Element> MakeScrollbarButton(
+ dom::NodeInfo* aNodeInfo, bool aVertical, bool aBottom, bool aDown,
+ AnonymousContentKey& aKey) {
+ MOZ_ASSERT(aNodeInfo);
+ MOZ_ASSERT(
+ aNodeInfo->Equals(nsGkAtoms::scrollbarbutton, nullptr, kNameSpaceID_XUL));
+
+ static constexpr nsLiteralString kSbattrValues[2][2] = {
+ {
+ u"scrollbar-up-top"_ns,
+ u"scrollbar-up-bottom"_ns,
+ },
+ {
+ u"scrollbar-down-top"_ns,
+ u"scrollbar-down-bottom"_ns,
+ },
+ };
+
+ static constexpr nsLiteralString kTypeValues[2] = {
+ u"decrement"_ns,
+ u"increment"_ns,
+ };
+
+ aKey = AnonymousContentKey::Type_ScrollbarButton;
+ if (aVertical) {
+ aKey |= AnonymousContentKey::Flag_Vertical;
+ }
+ if (aBottom) {
+ aKey |= AnonymousContentKey::Flag_ScrollbarButton_Bottom;
+ }
+ if (aDown) {
+ aKey |= AnonymousContentKey::Flag_ScrollbarButton_Down;
+ }
+
+ RefPtr<Element> e;
+ NS_TrustedNewXULElement(getter_AddRefs(e), do_AddRef(aNodeInfo));
+ e->SetAttr(kNameSpaceID_None, nsGkAtoms::sbattr,
+ kSbattrValues[aDown][aBottom], false);
+ e->SetAttr(kNameSpaceID_None, nsGkAtoms::type, kTypeValues[aDown], false);
+ return e.forget();
+}
+
+nsresult nsScrollbarFrame::CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) {
+ nsNodeInfoManager* nodeInfoManager = mContent->NodeInfo()->NodeInfoManager();
+
+ Element* el(GetContent()->AsElement());
+
+ // If there are children already in the node, don't create any anonymous
+ // content (this only apply to crashtests/369038-1.xhtml)
+ if (el->HasChildren()) {
+ return NS_OK;
+ }
+
+ nsAutoString orient;
+ el->GetAttr(kNameSpaceID_None, nsGkAtoms::orient, orient);
+ bool vertical = orient.EqualsLiteral("vertical");
+
+ RefPtr<dom::NodeInfo> sbbNodeInfo =
+ nodeInfoManager->GetNodeInfo(nsGkAtoms::scrollbarbutton, nullptr,
+ kNameSpaceID_XUL, nsINode::ELEMENT_NODE);
+
+ bool createButtons = PresContext()->Theme()->ThemeSupportsScrollbarButtons();
+
+ if (createButtons) {
+ AnonymousContentKey key;
+ mUpTopButton =
+ MakeScrollbarButton(sbbNodeInfo, vertical, /* aBottom */ false,
+ /* aDown */ false, key);
+ aElements.AppendElement(ContentInfo(mUpTopButton, key));
+ }
+
+ if (createButtons) {
+ AnonymousContentKey key;
+ mDownTopButton =
+ MakeScrollbarButton(sbbNodeInfo, vertical, /* aBottom */ false,
+ /* aDown */ true, key);
+ aElements.AppendElement(ContentInfo(mDownTopButton, key));
+ }
+
+ {
+ AnonymousContentKey key = AnonymousContentKey::Type_Slider;
+ if (vertical) {
+ key |= AnonymousContentKey::Flag_Vertical;
+ }
+
+ NS_TrustedNewXULElement(
+ getter_AddRefs(mSlider),
+ nodeInfoManager->GetNodeInfo(nsGkAtoms::slider, nullptr,
+ kNameSpaceID_XUL, nsINode::ELEMENT_NODE));
+ mSlider->SetAttr(kNameSpaceID_None, nsGkAtoms::orient, orient, false);
+
+ aElements.AppendElement(ContentInfo(mSlider, key));
+
+ NS_TrustedNewXULElement(
+ getter_AddRefs(mThumb),
+ nodeInfoManager->GetNodeInfo(nsGkAtoms::thumb, nullptr,
+ kNameSpaceID_XUL, nsINode::ELEMENT_NODE));
+ mThumb->SetAttr(kNameSpaceID_None, nsGkAtoms::orient, orient, false);
+ mSlider->AppendChildTo(mThumb, false, IgnoreErrors());
+ }
+
+ if (createButtons) {
+ AnonymousContentKey key;
+ mUpBottomButton =
+ MakeScrollbarButton(sbbNodeInfo, vertical, /* aBottom */ true,
+ /* aDown */ false, key);
+ aElements.AppendElement(ContentInfo(mUpBottomButton, key));
+ }
+
+ if (createButtons) {
+ AnonymousContentKey key;
+ mDownBottomButton =
+ MakeScrollbarButton(sbbNodeInfo, vertical, /* aBottom */ true,
+ /* aDown */ true, key);
+ aElements.AppendElement(ContentInfo(mDownBottomButton, key));
+ }
+
+ // Don't cache styles if we are inside a <select> element, since we have
+ // some UA style sheet rules that depend on the <select>'s attributes.
+ if (GetContent()->GetParent() &&
+ GetContent()->GetParent()->IsHTMLElement(nsGkAtoms::select)) {
+ for (auto& info : aElements) {
+ info.mKey = AnonymousContentKey::None;
+ }
+ }
+
+ UpdateChildrenAttributeValue(nsGkAtoms::curpos, false);
+ UpdateChildrenAttributeValue(nsGkAtoms::maxpos, false);
+ UpdateChildrenAttributeValue(nsGkAtoms::disabled, false);
+ UpdateChildrenAttributeValue(nsGkAtoms::pageincrement, false);
+ UpdateChildrenAttributeValue(nsGkAtoms::increment, false);
+
+ return NS_OK;
+}
+
+void nsScrollbarFrame::UpdateChildrenAttributeValue(nsAtom* aAttribute,
+ bool aNotify) {
+ Element* el(GetContent()->AsElement());
+
+ nsAutoString value;
+ el->GetAttr(kNameSpaceID_None, aAttribute, value);
+
+ if (!el->HasAttr(kNameSpaceID_None, aAttribute)) {
+ if (mUpTopButton) {
+ mUpTopButton->UnsetAttr(kNameSpaceID_None, aAttribute, aNotify);
+ }
+ if (mDownTopButton) {
+ mDownTopButton->UnsetAttr(kNameSpaceID_None, aAttribute, aNotify);
+ }
+ if (mSlider) {
+ mSlider->UnsetAttr(kNameSpaceID_None, aAttribute, aNotify);
+ }
+ if (mThumb && aAttribute == nsGkAtoms::disabled) {
+ mThumb->UnsetAttr(kNameSpaceID_None, nsGkAtoms::collapsed, aNotify);
+ }
+ if (mUpBottomButton) {
+ mUpBottomButton->UnsetAttr(kNameSpaceID_None, aAttribute, aNotify);
+ }
+ if (mDownBottomButton) {
+ mDownBottomButton->UnsetAttr(kNameSpaceID_None, aAttribute, aNotify);
+ }
+ return;
+ }
+
+ if (aAttribute == nsGkAtoms::curpos || aAttribute == nsGkAtoms::maxpos) {
+ if (mUpTopButton) {
+ mUpTopButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mDownTopButton) {
+ mDownTopButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mSlider) {
+ mSlider->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mUpBottomButton) {
+ mUpBottomButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mDownBottomButton) {
+ mDownBottomButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ } else if (aAttribute == nsGkAtoms::disabled) {
+ if (mUpTopButton) {
+ mUpTopButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mDownTopButton) {
+ mDownTopButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mSlider) {
+ mSlider->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ // Set the value on "collapsed" attribute.
+ if (mThumb) {
+ mThumb->SetAttr(kNameSpaceID_None, nsGkAtoms::collapsed, value, aNotify);
+ }
+ if (mUpBottomButton) {
+ mUpBottomButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ if (mDownBottomButton) {
+ mDownBottomButton->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ } else if (aAttribute == nsGkAtoms::pageincrement ||
+ aAttribute == nsGkAtoms::increment) {
+ if (mSlider) {
+ mSlider->SetAttr(kNameSpaceID_None, aAttribute, value, aNotify);
+ }
+ }
+}
+
+void nsScrollbarFrame::AppendAnonymousContentTo(
+ nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
+ if (mUpTopButton) {
+ aElements.AppendElement(mUpTopButton);
+ }
+
+ if (mDownTopButton) {
+ aElements.AppendElement(mDownTopButton);
+ }
+
+ if (mSlider) {
+ aElements.AppendElement(mSlider);
+ }
+
+ if (mUpBottomButton) {
+ aElements.AppendElement(mUpBottomButton);
+ }
+
+ if (mDownBottomButton) {
+ aElements.AppendElement(mDownBottomButton);
+ }
+}
diff --git a/layout/xul/nsScrollbarFrame.h b/layout/xul/nsScrollbarFrame.h
new file mode 100644
index 0000000000..2539686f10
--- /dev/null
+++ b/layout/xul/nsScrollbarFrame.h
@@ -0,0 +1,159 @@
+/* -*- 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/. */
+
+//
+// nsScrollbarFrame
+//
+
+#ifndef nsScrollbarFrame_h__
+#define nsScrollbarFrame_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/ScrollTypes.h"
+#include "nsIAnonymousContentCreator.h"
+#include "nsBoxFrame.h"
+
+class nsIScrollbarMediator;
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class Element;
+}
+} // namespace mozilla
+
+nsIFrame* NS_NewScrollbarFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle);
+
+class nsScrollbarFrame final : public nsBoxFrame,
+ public nsIAnonymousContentCreator {
+ using Element = mozilla::dom::Element;
+
+ public:
+ explicit nsScrollbarFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
+ : nsBoxFrame(aStyle, aPresContext, kClassID),
+ mSmoothScroll(false),
+ mScrollUnit(mozilla::ScrollUnit::DEVICE_PIXELS),
+ mDirection(0),
+ mIncrement(0),
+ mScrollbarMediator(nullptr),
+ mUpTopButton(nullptr),
+ mDownTopButton(nullptr),
+ mSlider(nullptr),
+ mThumb(nullptr),
+ mUpBottomButton(nullptr),
+ mDownBottomButton(nullptr) {}
+
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsScrollbarFrame)
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"ScrollbarFrame"_ns, aResult);
+ }
+#endif
+
+ // nsIFrame overrides
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ NS_IMETHOD HandlePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ NS_IMETHOD HandleMultiplePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus,
+ bool aControlHeld) override;
+
+ MOZ_CAN_RUN_SCRIPT
+ NS_IMETHOD HandleDrag(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ NS_IMETHOD HandleRelease(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ virtual void DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) override;
+
+ virtual void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ virtual void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) override;
+
+ void SetScrollbarMediatorContent(nsIContent* aMediator);
+ nsIScrollbarMediator* GetScrollbarMediator();
+
+ // nsBox methods
+
+ /**
+ * Treat scrollbars as clipping their children; overflowing children
+ * will not be allowed to set an overflow rect on this
+ * frame. This means that when the scroll code decides to hide a
+ * scrollframe by setting its height or width to zero, that will
+ * hide the children too.
+ */
+ virtual bool DoesClipChildrenInBothAxes() override { return true; }
+
+ virtual nsresult GetXULMargin(nsMargin& aMargin) override;
+
+ /**
+ * The following three methods set the value of mIncrement when a
+ * scrollbar button is pressed.
+ */
+ void SetIncrementToLine(int32_t aDirection);
+ void SetIncrementToPage(int32_t aDirection);
+ void SetIncrementToWhole(int32_t aDirection);
+ /**
+ * If aImplementsScrollByUnit is Yes then this uses mSmoothScroll,
+ * mScrollUnit, and mDirection and calls ScrollByUnit on the
+ * nsIScrollbarMediator. The return value is 0. This is better because it is
+ * more modern and the scroll frame can perform the scroll via apz for
+ * example. The old way below is still supported for xul trees. If
+ * aImplementsScrollByUnit is No this adds mIncrement to the current
+ * position and updates the curpos attribute obeying mSmoothScroll.
+ * @returns The new position after clamping, in CSS Pixels
+ * @note This method might destroy the frame, pres shell, and other objects.
+ */
+ enum class ImplementsScrollByUnit { Yes, No };
+ int32_t MoveToNewPosition(ImplementsScrollByUnit aImplementsScrollByUnit);
+ int32_t GetIncrement() { return mIncrement; }
+
+ // nsIAnonymousContentCreator
+ virtual nsresult CreateAnonymousContent(
+ nsTArray<ContentInfo>& aElements) override;
+ virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
+ uint32_t aFilter) override;
+
+ void UpdateChildrenAttributeValue(nsAtom* aAttribute, bool aNotify);
+
+ protected:
+ bool mSmoothScroll;
+ mozilla::ScrollUnit mScrollUnit;
+ // Direction and multiple to scroll
+ int32_t mDirection;
+
+ // Amount to scroll, in CSSPixels
+ // Ignored in favour of mScrollUnit/mDirection for regular scroll frames.
+ // Trees use this though.
+ int32_t mIncrement;
+
+ private:
+ nsCOMPtr<nsIContent> mScrollbarMediator;
+
+ nsCOMPtr<Element> mUpTopButton;
+ nsCOMPtr<Element> mDownTopButton;
+ nsCOMPtr<Element> mSlider;
+ nsCOMPtr<Element> mThumb;
+ nsCOMPtr<Element> mUpBottomButton;
+ nsCOMPtr<Element> mDownBottomButton;
+}; // class nsScrollbarFrame
+
+#endif
diff --git a/layout/xul/nsSliderFrame.cpp b/layout/xul/nsSliderFrame.cpp
new file mode 100644
index 0000000000..42df839fb6
--- /dev/null
+++ b/layout/xul/nsSliderFrame.cpp
@@ -0,0 +1,1626 @@
+/* -*- 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/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "nsSliderFrame.h"
+
+#include "mozilla/ComputedStyle.h"
+#include "nsPresContext.h"
+#include "nsIContent.h"
+#include "nsCOMPtr.h"
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsHTMLParts.h"
+#include "nsCSSRendering.h"
+#include "nsScrollbarButtonFrame.h"
+#include "nsIScrollableFrame.h"
+#include "nsIScrollbarMediator.h"
+#include "nsISupportsImpl.h"
+#include "nsScrollbarFrame.h"
+#include "nsRepeatService.h"
+#include "nsBoxLayoutState.h"
+#include "nsSprocketLayout.h"
+#include "nsContentUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsDisplayList.h"
+#include "nsDeviceContext.h"
+#include "nsRefreshDriver.h" // for nsAPostRefreshObserver
+#include "mozilla/Assertions.h" // for MOZ_ASSERT
+#include "mozilla/DisplayPortUtils.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/SVGIntegrationUtils.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/layers/APZCCallbackHelper.h"
+#include "mozilla/layers/AsyncDragMetrics.h"
+#include "mozilla/layers/InputAPZContext.h"
+#include "mozilla/layers/WebRenderLayerManager.h"
+#include <algorithm>
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using mozilla::dom::Document;
+using mozilla::dom::Event;
+using mozilla::layers::AsyncDragMetrics;
+using mozilla::layers::InputAPZContext;
+using mozilla::layers::ScrollbarData;
+using mozilla::layers::ScrollDirection;
+
+bool nsSliderFrame::gMiddlePref = false;
+int32_t nsSliderFrame::gSnapMultiplier;
+
+// Turn this on if you want to debug slider frames.
+#undef DEBUG_SLIDER
+
+static already_AddRefed<nsIContent> GetContentOfBox(nsIFrame* aBox) {
+ nsCOMPtr<nsIContent> content = aBox->GetContent();
+ return content.forget();
+}
+
+nsIFrame* NS_NewSliderFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsSliderFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsSliderFrame)
+
+NS_QUERYFRAME_HEAD(nsSliderFrame)
+ NS_QUERYFRAME_ENTRY(nsSliderFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsBoxFrame)
+
+nsSliderFrame::nsSliderFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
+ : nsBoxFrame(aStyle, aPresContext, kClassID),
+ mRatio(0.0f),
+ mDragStart(0),
+ mThumbStart(0),
+ mCurPos(0),
+ mChange(0),
+ mDragFinished(true),
+ mUserChanged(false),
+ mScrollingWithAPZ(false),
+ mSuppressionActive(false) {}
+
+// stop timer
+nsSliderFrame::~nsSliderFrame() {
+ if (mSuppressionActive) {
+ if (mozilla::PresShell* presShell = PresShell()) {
+ presShell->SuppressDisplayport(false);
+ }
+ }
+}
+
+void nsSliderFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsBoxFrame::Init(aContent, aParent, aPrevInFlow);
+
+ static bool gotPrefs = false;
+ if (!gotPrefs) {
+ gotPrefs = true;
+
+ gMiddlePref = Preferences::GetBool("middlemouse.scrollbarPosition");
+ gSnapMultiplier = Preferences::GetInt("slider.snapMultiplier");
+ }
+
+ mCurPos = GetCurrentPosition(aContent);
+}
+
+void nsSliderFrame::RemoveFrame(ChildListID aListID, nsIFrame* aOldFrame) {
+ nsBoxFrame::RemoveFrame(aListID, aOldFrame);
+ if (mFrames.IsEmpty()) RemoveListener();
+}
+
+void nsSliderFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame,
+ const nsLineList::iterator* aPrevFrameLine,
+ nsFrameList&& aFrameList) {
+ bool wasEmpty = mFrames.IsEmpty();
+ nsBoxFrame::InsertFrames(aListID, aPrevFrame, aPrevFrameLine,
+ std::move(aFrameList));
+ if (wasEmpty) AddListener();
+}
+
+void nsSliderFrame::AppendFrames(ChildListID aListID,
+ nsFrameList&& aFrameList) {
+ // if we have no children and on was added then make sure we add the
+ // listener
+ bool wasEmpty = mFrames.IsEmpty();
+ nsBoxFrame::AppendFrames(aListID, std::move(aFrameList));
+ if (wasEmpty) AddListener();
+}
+
+int32_t nsSliderFrame::GetCurrentPosition(nsIContent* content) {
+ return GetIntegerAttribute(content, nsGkAtoms::curpos, 0);
+}
+
+int32_t nsSliderFrame::GetMinPosition(nsIContent* content) {
+ return GetIntegerAttribute(content, nsGkAtoms::minpos, 0);
+}
+
+int32_t nsSliderFrame::GetMaxPosition(nsIContent* content) {
+ return GetIntegerAttribute(content, nsGkAtoms::maxpos, 100);
+}
+
+int32_t nsSliderFrame::GetIncrement(nsIContent* content) {
+ return GetIntegerAttribute(content, nsGkAtoms::increment, 1);
+}
+
+int32_t nsSliderFrame::GetPageIncrement(nsIContent* content) {
+ return GetIntegerAttribute(content, nsGkAtoms::pageincrement, 10);
+}
+
+int32_t nsSliderFrame::GetIntegerAttribute(nsIContent* content, nsAtom* atom,
+ int32_t defaultValue) {
+ nsAutoString value;
+ if (content->IsElement()) {
+ content->AsElement()->GetAttr(kNameSpaceID_None, atom, value);
+ }
+ if (!value.IsEmpty()) {
+ nsresult error;
+
+ // convert it to an integer
+ defaultValue = value.ToInteger(&error);
+ }
+
+ return defaultValue;
+}
+
+nsresult nsSliderFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType) {
+ nsresult rv =
+ nsBoxFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+ // if the current position changes
+ if (aAttribute == nsGkAtoms::curpos) {
+ CurrentPositionChanged();
+ } else if (aAttribute == nsGkAtoms::minpos ||
+ aAttribute == nsGkAtoms::maxpos) {
+ // bounds check it.
+
+ nsIFrame* scrollbarBox = GetScrollbar();
+ nsCOMPtr<nsIContent> scrollbar = GetContentOfBox(scrollbarBox);
+ int32_t current = GetCurrentPosition(scrollbar);
+ int32_t min = GetMinPosition(scrollbar);
+ int32_t max = GetMaxPosition(scrollbar);
+
+ if (current < min || current > max) {
+ int32_t direction = 0;
+ if (current < min || max < min) {
+ current = min;
+ direction = -1;
+ } else if (current > max) {
+ current = max;
+ direction = 1;
+ }
+
+ // set the new position and notify observers
+ nsScrollbarFrame* scrollbarFrame = do_QueryFrame(scrollbarBox);
+ if (scrollbarFrame) {
+ nsIScrollbarMediator* mediator = scrollbarFrame->GetScrollbarMediator();
+ scrollbarFrame->SetIncrementToWhole(direction);
+ if (mediator) {
+ mediator->ScrollByWhole(scrollbarFrame, direction,
+ ScrollSnapFlags::IntendedEndPosition);
+ }
+ }
+ // 'this' might be destroyed here
+
+ nsContentUtils::AddScriptRunner(new nsSetAttrRunnable(
+ scrollbar->AsElement(), nsGkAtoms::curpos, current));
+ }
+ }
+
+ if (aAttribute == nsGkAtoms::minpos || aAttribute == nsGkAtoms::maxpos ||
+ aAttribute == nsGkAtoms::pageincrement ||
+ aAttribute == nsGkAtoms::increment) {
+ PresShell()->FrameNeedsReflow(
+ this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
+ }
+
+ return rv;
+}
+
+namespace mozilla {
+
+// Draw any tick marks that show the position of find in page results.
+class nsDisplaySliderMarks final : public nsPaintedDisplayItem {
+ public:
+ nsDisplaySliderMarks(nsDisplayListBuilder* aBuilder, nsSliderFrame* aFrame)
+ : nsPaintedDisplayItem(aBuilder, aFrame) {
+ MOZ_COUNT_CTOR(nsDisplaySliderMarks);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplaySliderMarks)
+
+ NS_DISPLAY_DECL_NAME("SliderMarks", TYPE_SLIDER_MARKS)
+
+ void PaintMarks(nsDisplayListBuilder* aDisplayListBuilder,
+ wr::DisplayListBuilder* aBuilder, gfxContext* aCtx);
+
+ virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder,
+ bool* aSnap) const override {
+ *aSnap = false;
+ return mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame();
+ }
+
+ bool CreateWebRenderCommands(
+ wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) override;
+
+ void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
+};
+
+// This is shared between the webrender and Paint() paths. For the former,
+// aBuilder should be assigned and aCtx will be null. For the latter, aBuilder
+// should be null and aCtx should be the gfxContext for painting.
+void nsDisplaySliderMarks::PaintMarks(nsDisplayListBuilder* aDisplayListBuilder,
+ wr::DisplayListBuilder* aBuilder,
+ gfxContext* aCtx) {
+ DrawTarget* drawTarget = nullptr;
+ if (aCtx) {
+ drawTarget = aCtx->GetDrawTarget();
+ } else {
+ MOZ_ASSERT(aBuilder);
+ }
+
+ Document* doc = mFrame->GetContent()->GetUncomposedDoc();
+ if (!doc) {
+ return;
+ }
+
+ nsGlobalWindowInner* window =
+ nsGlobalWindowInner::Cast(doc->GetInnerWindow());
+ if (!window) {
+ return;
+ }
+
+ nsSliderFrame* sliderFrame = static_cast<nsSliderFrame*>(mFrame);
+
+ nsIFrame* scrollbarBox = sliderFrame->GetScrollbar();
+ nsCOMPtr<nsIContent> scrollbar = GetContentOfBox(scrollbarBox);
+
+ int32_t minPos = sliderFrame->GetMinPosition(scrollbar);
+ int32_t maxPos = sliderFrame->GetMaxPosition(scrollbar);
+
+ // Use the text highlight color for the tick marks.
+ nscolor highlightColor =
+ LookAndFeel::Color(LookAndFeel::ColorID::TextHighlightBackground, mFrame);
+ DeviceColor fillColor = ToDeviceColor(highlightColor);
+ fillColor.a = 0.3; // make the mark mostly transparent
+
+ int32_t appUnitsPerDevPixel =
+ sliderFrame->PresContext()->AppUnitsPerDevPixel();
+ nsRect sliderRect = sliderFrame->GetRect();
+
+ nsPoint refPoint = aDisplayListBuilder->ToReferenceFrame(mFrame);
+
+ // Increase the height of the tick mark rectangle by one pixel. If the
+ // desktop scale is greater than 1, it should be increased more.
+ // The tick marks should be drawn ignoring any page zoom that is applied.
+ float increasePixels = sliderFrame->PresContext()
+ ->DeviceContext()
+ ->GetDesktopToDeviceScale()
+ .scale;
+ bool isHorizontal = sliderFrame->IsXULHorizontal();
+ float increasePixelsX = isHorizontal ? increasePixels : 0;
+ float increasePixelsY = isHorizontal ? 0 : increasePixels;
+ nsSize initialSize =
+ isHorizontal ? nsSize(0, sliderRect.height) : nsSize(sliderRect.width, 0);
+
+ nsTArray<uint32_t>& marks = window->GetScrollMarks();
+ for (uint32_t m = 0; m < marks.Length(); m++) {
+ uint32_t markValue = marks[m];
+ if (markValue > (uint32_t)maxPos) {
+ markValue = maxPos;
+ }
+ if (markValue < (uint32_t)minPos) {
+ markValue = minPos;
+ }
+
+ // The values in the marks array range up to the window's
+ // scrollMax{X,Y} - scrollMin{X,Y} (the same as the slider's maxpos).
+ // Scale the values to fit within the slider's width or height.
+ nsRect markRect(refPoint, initialSize);
+ if (isHorizontal) {
+ markRect.x +=
+ (nscoord)((double)markValue / (maxPos - minPos) * sliderRect.width);
+ } else {
+ markRect.y +=
+ (nscoord)((double)markValue / (maxPos - minPos) * sliderRect.height);
+ }
+
+ if (drawTarget) {
+ Rect devPixelRect =
+ NSRectToSnappedRect(markRect, appUnitsPerDevPixel, *drawTarget);
+ devPixelRect.Inflate(increasePixelsX, increasePixelsY);
+ drawTarget->FillRect(devPixelRect, ColorPattern(fillColor));
+ } else {
+ LayoutDeviceIntRect dRect = LayoutDeviceIntRect::FromAppUnitsToNearest(
+ markRect, appUnitsPerDevPixel);
+ dRect.Inflate(increasePixelsX, increasePixelsY);
+ wr::LayoutRect layoutRect = wr::ToLayoutRect(dRect);
+ aBuilder->PushRect(layoutRect, layoutRect, BackfaceIsHidden(), false,
+ false, wr::ToColorF(fillColor));
+ }
+ }
+}
+
+bool nsDisplaySliderMarks::CreateWebRenderCommands(
+ wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) {
+ PaintMarks(aDisplayListBuilder, &aBuilder, nullptr);
+ return true;
+}
+
+void nsDisplaySliderMarks::Paint(nsDisplayListBuilder* aBuilder,
+ gfxContext* aCtx) {
+ PaintMarks(aBuilder, nullptr, aCtx);
+}
+
+} // namespace mozilla
+
+void nsSliderFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ if (aBuilder->IsForEventDelivery() && isDraggingThumb()) {
+ // This is EVIL, we shouldn't be messing with event delivery just to get
+ // thumb mouse drag events to arrive at the slider!
+ aLists.Outlines()->AppendNewToTop<nsDisplayEventReceiver>(aBuilder, this);
+ return;
+ }
+
+ nsBoxFrame::BuildDisplayList(aBuilder, aLists);
+
+ // If this is a vertical scrollbar for the root frame, draw any markers.
+ // Markers are not drawn for other scrollbars.
+ if (!aBuilder->IsForEventDelivery()) {
+ nsIFrame* scrollbarBox = GetScrollbar();
+ if (scrollbarBox) {
+ if (nsIScrollableFrame* scrollFrame =
+ do_QueryFrame(scrollbarBox->GetParent())) {
+ if (scrollFrame->IsRootScrollFrameOfDocument()) {
+ Document* doc = mContent->GetUncomposedDoc();
+ if (doc) {
+ nsGlobalWindowInner* window =
+ nsGlobalWindowInner::Cast(doc->GetInnerWindow());
+ if (window &&
+ window->GetScrollMarksOnHScrollbar() == IsXULHorizontal() &&
+ window->GetScrollMarks().Length() > 0) {
+ aLists.Content()->AppendNewToTop<nsDisplaySliderMarks>(aBuilder,
+ this);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+static bool UsesCustomScrollbarMediator(nsIFrame* scrollbarBox) {
+ if (nsScrollbarFrame* scrollbarFrame = do_QueryFrame(scrollbarBox)) {
+ if (nsIScrollbarMediator* mediator =
+ scrollbarFrame->GetScrollbarMediator()) {
+ nsIScrollableFrame* scrollFrame = do_QueryFrame(mediator);
+ // The scrollbar mediator is not the scroll frame.
+ // That means this scroll frame has a custom scrollbar mediator.
+ if (!scrollFrame) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+void nsSliderFrame::BuildDisplayListForChildren(
+ nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) {
+ // if we are too small to have a thumb don't paint it.
+ nsIFrame* thumb = nsIFrame::GetChildXULBox(this);
+
+ if (thumb) {
+ nsRect thumbRect(thumb->GetRect());
+ nsMargin m;
+ thumb->GetXULMargin(m);
+ thumbRect.Inflate(m);
+
+ nsRect sliderTrack;
+ GetXULClientRect(sliderTrack);
+
+ if (sliderTrack.width < thumbRect.width ||
+ sliderTrack.height < thumbRect.height)
+ return;
+
+ // If this scrollbar is the scrollbar of an actively scrolled scroll frame,
+ // layerize the scrollbar thumb, wrap it in its own ContainerLayer and
+ // attach scrolling information to it.
+ // We do this here and not in the thumb's nsBoxFrame::BuildDisplayList so
+ // that the event region that gets created for the thumb is included in
+ // the nsDisplayOwnLayer contents.
+
+ const mozilla::layers::ScrollableLayerGuid::ViewID scrollTargetId =
+ aBuilder->GetCurrentScrollbarTarget();
+ const bool thumbGetsLayer =
+ (scrollTargetId != layers::ScrollableLayerGuid::NULL_SCROLL_ID);
+
+ if (thumbGetsLayer) {
+ const Maybe<ScrollDirection> scrollDirection =
+ aBuilder->GetCurrentScrollbarDirection();
+ MOZ_ASSERT(scrollDirection.isSome());
+ const bool isHorizontal =
+ *scrollDirection == ScrollDirection::eHorizontal;
+ const float appUnitsPerCss = float(AppUnitsPerCSSPixel());
+ const CSSCoord thumbLength = NSAppUnitsToFloatPixels(
+ isHorizontal ? thumbRect.width : thumbRect.height, appUnitsPerCss);
+
+ nsIFrame* scrollbarBox = GetScrollbar();
+ bool isAsyncDraggable = !UsesCustomScrollbarMediator(scrollbarBox);
+
+ nsPoint scrollPortOrigin;
+ if (nsIScrollableFrame* scrollFrame =
+ do_QueryFrame(scrollbarBox->GetParent())) {
+ scrollPortOrigin = scrollFrame->GetScrollPortRect().TopLeft();
+ } else {
+ isAsyncDraggable = false;
+ }
+
+ // This rect is the range in which the scroll thumb can slide in.
+ sliderTrack = sliderTrack + GetRect().TopLeft() +
+ scrollbarBox->GetPosition() - scrollPortOrigin;
+ const CSSCoord sliderTrackStart = NSAppUnitsToFloatPixels(
+ isHorizontal ? sliderTrack.x : sliderTrack.y, appUnitsPerCss);
+ const CSSCoord sliderTrackLength = NSAppUnitsToFloatPixels(
+ isHorizontal ? sliderTrack.width : sliderTrack.height,
+ appUnitsPerCss);
+ const CSSCoord thumbStart = NSAppUnitsToFloatPixels(
+ isHorizontal ? thumbRect.x : thumbRect.y, appUnitsPerCss);
+
+ const nsRect overflow = thumb->InkOverflowRectRelativeToParent();
+ nsSize refSize = aBuilder->RootReferenceFrame()->GetSize();
+ nsRect dirty = aBuilder->GetVisibleRect().Intersect(thumbRect);
+ dirty = nsLayoutUtils::ComputePartialPrerenderArea(
+ thumb, aBuilder->GetVisibleRect(), overflow, refSize);
+
+ nsDisplayListBuilder::AutoBuildingDisplayList buildingDisplayList(
+ aBuilder, this, dirty, dirty);
+
+ // Clip the thumb layer to the slider track. This is necessary to ensure
+ // FrameLayerBuilder is able to merge content before and after the
+ // scrollframe into the same layer (otherwise it thinks the thumb could
+ // potentially move anywhere within the existing clip).
+ DisplayListClipState::AutoSaveRestore thumbClipState(aBuilder);
+ thumbClipState.ClipContainingBlockDescendants(
+ GetRectRelativeToSelf() + aBuilder->ToReferenceFrame(this));
+
+ // Have the thumb's container layer capture the current clip, so
+ // it doesn't apply to the thumb's contents. This allows the contents
+ // to be fully rendered even if they're partially or fully offscreen,
+ // so async scrolling can still bring it into view.
+ DisplayListClipState::AutoSaveRestore thumbContentsClipState(aBuilder);
+ thumbContentsClipState.Clear();
+
+ nsDisplayListBuilder::AutoContainerASRTracker contASRTracker(aBuilder);
+ nsDisplayListCollection tempLists(aBuilder);
+ nsBoxFrame::BuildDisplayListForChildren(aBuilder, tempLists);
+
+ // This is a bit of a hack. Collect up all descendant display items
+ // and merge them into a single Content() list.
+ nsDisplayList masterList(aBuilder);
+ masterList.AppendToTop(tempLists.BorderBackground());
+ masterList.AppendToTop(tempLists.BlockBorderBackgrounds());
+ masterList.AppendToTop(tempLists.Floats());
+ masterList.AppendToTop(tempLists.Content());
+ masterList.AppendToTop(tempLists.PositionedDescendants());
+ masterList.AppendToTop(tempLists.Outlines());
+
+ // Restore the saved clip so it applies to the thumb container layer.
+ thumbContentsClipState.Restore();
+
+ // Wrap the list to make it its own layer.
+ const ActiveScrolledRoot* ownLayerASR = contASRTracker.GetContainerASR();
+ aLists.Content()->AppendNewToTopWithIndex<nsDisplayOwnLayer>(
+ aBuilder, this,
+ /* aIndex = */ nsDisplayOwnLayer::OwnLayerForScrollThumb, &masterList,
+ ownLayerASR, nsDisplayOwnLayerFlags::None,
+ ScrollbarData::CreateForThumb(*scrollDirection, GetThumbRatio(),
+ thumbStart, thumbLength,
+ isAsyncDraggable, sliderTrackStart,
+ sliderTrackLength, scrollTargetId),
+ true, false);
+
+ return;
+ }
+ }
+
+ nsBoxFrame::BuildDisplayListForChildren(aBuilder, aLists);
+}
+
+NS_IMETHODIMP
+nsSliderFrame::DoXULLayout(nsBoxLayoutState& aState) {
+ // get the thumb should be our only child
+ nsIFrame* thumbBox = nsIFrame::GetChildXULBox(this);
+
+ if (!thumbBox) {
+ SyncXULLayout(aState);
+ return NS_OK;
+ }
+
+ EnsureOrient();
+
+ // get the content area inside our borders
+ nsRect clientRect;
+ GetXULClientRect(clientRect);
+
+ // get the scrollbar
+ nsIFrame* scrollbarBox = GetScrollbar();
+ nsCOMPtr<nsIContent> scrollbar = GetContentOfBox(scrollbarBox);
+
+ // get the thumb's pref size
+ nsSize thumbSize = thumbBox->GetXULPrefSize(aState);
+
+ if (IsXULHorizontal())
+ thumbSize.height = clientRect.height;
+ else
+ thumbSize.width = clientRect.width;
+
+ int32_t curPos = GetCurrentPosition(scrollbar);
+ int32_t minPos = GetMinPosition(scrollbar);
+ int32_t maxPos = GetMaxPosition(scrollbar);
+ int32_t pageIncrement = GetPageIncrement(scrollbar);
+
+ maxPos = std::max(minPos, maxPos);
+ curPos = clamped(curPos, minPos, maxPos);
+
+ nscoord& availableLength =
+ IsXULHorizontal() ? clientRect.width : clientRect.height;
+ nscoord& thumbLength = IsXULHorizontal() ? thumbSize.width : thumbSize.height;
+
+ if ((pageIncrement + maxPos - minPos) > 0 && thumbBox->GetXULFlex() > 0) {
+ float ratio = float(pageIncrement) / float(maxPos - minPos + pageIncrement);
+ thumbLength =
+ std::max(thumbLength, NSToCoordRound(availableLength * ratio));
+ }
+
+ // Round the thumb's length to device pixels.
+ nsPresContext* presContext = PresContext();
+ thumbLength = presContext->DevPixelsToAppUnits(
+ presContext->AppUnitsToDevPixels(thumbLength));
+
+ // mRatio translates the thumb position in app units to the value.
+ mRatio = (minPos != maxPos)
+ ? float(availableLength - thumbLength) / float(maxPos - minPos)
+ : 1;
+
+ // in reverse mode, curpos is reversed such that lower values are to the
+ // right or bottom and increase leftwards or upwards. In this case, use the
+ // offset from the end instead of the beginning.
+ bool reverse = mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::dir, nsGkAtoms::reverse, eCaseMatters);
+ nscoord pos = reverse ? (maxPos - curPos) : (curPos - minPos);
+
+ // set the thumb's coord to be the current pos * the ratio.
+ nsRect thumbRect(clientRect.x, clientRect.y, thumbSize.width,
+ thumbSize.height);
+ int32_t& thumbPos = (IsXULHorizontal() ? thumbRect.x : thumbRect.y);
+ thumbPos += NSToCoordRound(pos * mRatio);
+
+ nsRect oldThumbRect(thumbBox->GetRect());
+ LayoutChildAt(aState, thumbBox, thumbRect);
+
+ SyncXULLayout(aState);
+
+ // Redraw only if thumb changed size.
+ if (!oldThumbRect.IsEqualInterior(thumbRect)) XULRedraw(aState);
+
+ return NS_OK;
+}
+
+nsresult nsSliderFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ NS_ENSURE_ARG_POINTER(aEventStatus);
+
+ if (mAPZDragInitiated &&
+ *mAPZDragInitiated == InputAPZContext::GetInputBlockId() &&
+ aEvent->mMessage == eMouseDown) {
+ // If we get the mousedown after the APZ notification, then immediately
+ // switch into the state corresponding to an APZ thumb-drag. Note that
+ // we can't just do this in AsyncScrollbarDragInitiated() directly because
+ // the handling for this mousedown event in the presShell will reset the
+ // capturing content which makes isDraggingThumb() return false. We check
+ // the input block here to make sure that we correctly handle any ordering
+ // of {eMouseDown arriving, AsyncScrollbarDragInitiated() being called}.
+ mAPZDragInitiated = Nothing();
+ DragThumb(true);
+ mScrollingWithAPZ = true;
+ return NS_OK;
+ }
+
+ // If a web page calls event.preventDefault() we still want to
+ // scroll when scroll arrow is clicked. See bug 511075.
+ if (!mContent->IsInNativeAnonymousSubtree() &&
+ nsEventStatus_eConsumeNoDefault == *aEventStatus) {
+ return NS_OK;
+ }
+
+ if (!mDragFinished && !isDraggingThumb()) {
+ StopDrag();
+ return NS_OK;
+ }
+
+ nsIFrame* scrollbarBox = GetScrollbar();
+ nsCOMPtr<nsIContent> scrollbar;
+ scrollbar = GetContentOfBox(scrollbarBox);
+ bool isHorizontal = IsXULHorizontal();
+
+ if (isDraggingThumb()) {
+ switch (aEvent->mMessage) {
+ case eTouchMove:
+ case eMouseMove: {
+ if (mScrollingWithAPZ) {
+ break;
+ }
+ nsPoint eventPoint;
+ if (!GetEventPoint(aEvent, eventPoint)) {
+ break;
+ }
+ if (mChange) {
+ // On Linux the destination point is determined by the initial click
+ // on the scrollbar track and doesn't change until the mouse button
+ // is released.
+#ifndef MOZ_WIDGET_GTK
+ // On the other platforms we need to update the destination point now.
+ mDestinationPoint = eventPoint;
+ StopRepeat();
+ StartRepeat();
+#endif
+ break;
+ }
+
+ nscoord pos = isHorizontal ? eventPoint.x : eventPoint.y;
+
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return NS_OK;
+ }
+
+ // take our current position and subtract the start location
+ pos -= mDragStart;
+ bool isMouseOutsideThumb = false;
+ if (gSnapMultiplier) {
+ nsSize thumbSize = thumbFrame->GetSize();
+ if (isHorizontal) {
+ // horizontal scrollbar - check if mouse is above or below thumb
+ // XXXbz what about looking at the .y of the thumb's rect? Is that
+ // always zero here?
+ if (eventPoint.y < -gSnapMultiplier * thumbSize.height ||
+ eventPoint.y >
+ thumbSize.height + gSnapMultiplier * thumbSize.height)
+ isMouseOutsideThumb = true;
+ } else {
+ // vertical scrollbar - check if mouse is left or right of thumb
+ if (eventPoint.x < -gSnapMultiplier * thumbSize.width ||
+ eventPoint.x >
+ thumbSize.width + gSnapMultiplier * thumbSize.width)
+ isMouseOutsideThumb = true;
+ }
+ }
+ if (aEvent->mClass == eTouchEventClass) {
+ *aEventStatus = nsEventStatus_eConsumeNoDefault;
+ }
+ if (isMouseOutsideThumb) {
+ SetCurrentThumbPosition(scrollbar, mThumbStart, false, false);
+ return NS_OK;
+ }
+
+ // set it
+ SetCurrentThumbPosition(scrollbar, pos, false, true); // with snapping
+ } break;
+
+ case eTouchEnd:
+ case eMouseUp:
+ if (ShouldScrollForEvent(aEvent)) {
+ StopDrag();
+ // we MUST call nsFrame HandleEvent for mouse ups to maintain the
+ // selection state and capture state.
+ return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ // return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+ return NS_OK;
+ }
+
+ if (ShouldScrollToClickForEvent(aEvent)) {
+ nsPoint eventPoint;
+ if (!GetEventPoint(aEvent, eventPoint)) {
+ return NS_OK;
+ }
+ nscoord pos = isHorizontal ? eventPoint.x : eventPoint.y;
+
+ // adjust so that the middle of the thumb is placed under the click
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return NS_OK;
+ }
+ nsSize thumbSize = thumbFrame->GetSize();
+ nscoord thumbLength = isHorizontal ? thumbSize.width : thumbSize.height;
+
+ // set it
+ AutoWeakFrame weakFrame(this);
+ // should aMaySnap be true here?
+ SetCurrentThumbPosition(scrollbar, pos - thumbLength / 2, false, false);
+ NS_ENSURE_TRUE(weakFrame.IsAlive(), NS_OK);
+
+ DragThumb(true);
+
+#ifdef MOZ_WIDGET_GTK
+ RefPtr<dom::Element> thumb = thumbFrame->GetContent()->AsElement();
+ thumb->SetAttr(kNameSpaceID_None, nsGkAtoms::active, u"true"_ns, true);
+#endif
+
+ if (aEvent->mClass == eTouchEventClass) {
+ *aEventStatus = nsEventStatus_eConsumeNoDefault;
+ }
+
+ if (isHorizontal)
+ mThumbStart = thumbFrame->GetPosition().x;
+ else
+ mThumbStart = thumbFrame->GetPosition().y;
+
+ mDragStart = pos - mThumbStart;
+ }
+#ifdef MOZ_WIDGET_GTK
+ else if (ShouldScrollForEvent(aEvent) && aEvent->mClass == eMouseEventClass &&
+ aEvent->AsMouseEvent()->mButton == MouseButton::eSecondary) {
+ // HandlePress and HandleRelease are usually called via
+ // nsIFrame::HandleEvent, but only for the left mouse button.
+ if (aEvent->mMessage == eMouseDown) {
+ HandlePress(aPresContext, aEvent, aEventStatus);
+ } else if (aEvent->mMessage == eMouseUp) {
+ HandleRelease(aPresContext, aEvent, aEventStatus);
+ }
+
+ return NS_OK;
+ }
+#endif
+
+ // XXX hack until handle release is actually called in nsframe.
+ // if (aEvent->mMessage == eMouseOut ||
+ // aEvent->mMessage == NS_MOUSE_RIGHT_BUTTON_UP ||
+ // aEvent->mMessage == NS_MOUSE_LEFT_BUTTON_UP) {
+ // HandleRelease(aPresContext, aEvent, aEventStatus);
+ // }
+
+ if (aEvent->mMessage == eMouseOut && mChange)
+ HandleRelease(aPresContext, aEvent, aEventStatus);
+
+ return nsIFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+}
+
+// Helper function to collect the "scroll to click" metric. Beware of
+// caching this, users expect to be able to change the system preference
+// and see the browser change its behavior immediately.
+bool nsSliderFrame::GetScrollToClick() {
+ if (GetScrollbar() != this) {
+ return LookAndFeel::GetInt(LookAndFeel::IntID::ScrollToClick, false);
+ }
+
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::movetoclick,
+ nsGkAtoms::_true, eCaseMatters)) {
+ return true;
+ }
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::movetoclick,
+ nsGkAtoms::_false, eCaseMatters)) {
+ return false;
+ }
+
+#ifdef XP_MACOSX
+ return true;
+#else
+ return false;
+#endif
+}
+
+nsIFrame* nsSliderFrame::GetScrollbar() {
+ // if we are in a scrollbar then return the scrollbar's content node
+ // if we are not then return ours.
+ nsIFrame* scrollbar;
+ nsScrollbarButtonFrame::GetParentWithTag(nsGkAtoms::scrollbar, this,
+ scrollbar);
+
+ if (scrollbar == nullptr) return this;
+
+ return scrollbar->IsXULBoxFrame() ? scrollbar : this;
+}
+
+void nsSliderFrame::PageUpDown(nscoord change) {
+ // on a page up or down get our page increment. We get this by getting the
+ // scrollbar we are in and asking it for the current position and the page
+ // increment. If we are not in a scrollbar we will get the values from our own
+ // node.
+ nsIFrame* scrollbarBox = GetScrollbar();
+ nsCOMPtr<nsIContent> scrollbar;
+ scrollbar = GetContentOfBox(scrollbarBox);
+
+ nscoord pageIncrement = GetPageIncrement(scrollbar);
+ int32_t curpos = GetCurrentPosition(scrollbar);
+ int32_t minpos = GetMinPosition(scrollbar);
+ int32_t maxpos = GetMaxPosition(scrollbar);
+
+ // get the new position and make sure it is in bounds
+ int32_t newpos = curpos + change * pageIncrement;
+ if (newpos < minpos || maxpos < minpos)
+ newpos = minpos;
+ else if (newpos > maxpos)
+ newpos = maxpos;
+
+ SetCurrentPositionInternal(scrollbar, newpos, true);
+}
+
+// called when the current position changed and we need to update the thumb's
+// location
+void nsSliderFrame::CurrentPositionChanged() {
+ nsIFrame* scrollbarBox = GetScrollbar();
+ nsCOMPtr<nsIContent> scrollbar = GetContentOfBox(scrollbarBox);
+
+ // get the current position
+ int32_t curPos = GetCurrentPosition(scrollbar);
+
+ // do nothing if the position did not change
+ if (mCurPos == curPos) return;
+
+ // get our current min and max position from our content node
+ int32_t minPos = GetMinPosition(scrollbar);
+ int32_t maxPos = GetMaxPosition(scrollbar);
+
+ maxPos = std::max(minPos, maxPos);
+ curPos = clamped(curPos, minPos, maxPos);
+
+ // get the thumb's rect
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) return; // The thumb may stream in asynchronously via XBL.
+
+ nsRect thumbRect = thumbFrame->GetRect();
+
+ nsRect clientRect;
+ GetXULClientRect(clientRect);
+
+ // figure out the new rect
+ nsRect newThumbRect(thumbRect);
+
+ bool reverse = mContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::dir, nsGkAtoms::reverse, eCaseMatters);
+ nscoord pos = reverse ? (maxPos - curPos) : (curPos - minPos);
+
+ if (IsXULHorizontal())
+ newThumbRect.x = clientRect.x + NSToCoordRound(pos * mRatio);
+ else
+ newThumbRect.y = clientRect.y + NSToCoordRound(pos * mRatio);
+
+ // avoid putting the scroll thumb at subpixel positions which cause needless
+ // invalidations
+ nscoord appUnitsPerPixel = PresContext()->AppUnitsPerDevPixel();
+ nsPoint snappedThumbLocation =
+ ToAppUnits(newThumbRect.TopLeft().ToNearestPixels(appUnitsPerPixel),
+ appUnitsPerPixel);
+ if (IsXULHorizontal()) {
+ newThumbRect.x = snappedThumbLocation.x;
+ } else {
+ newThumbRect.y = snappedThumbLocation.y;
+ }
+
+ // set the rect
+ thumbFrame->SetRect(newThumbRect);
+
+ // Request a repaint of the scrollbar
+ nsScrollbarFrame* scrollbarFrame = do_QueryFrame(scrollbarBox);
+ nsIScrollbarMediator* mediator =
+ scrollbarFrame ? scrollbarFrame->GetScrollbarMediator() : nullptr;
+ if (!mediator || !mediator->ShouldSuppressScrollbarRepaints()) {
+ SchedulePaint();
+ }
+
+ mCurPos = curPos;
+}
+
+static void UpdateAttribute(dom::Element* aScrollbar, nscoord aNewPos,
+ bool aNotify, bool aIsSmooth) {
+ nsAutoString str;
+ str.AppendInt(aNewPos);
+
+ if (aIsSmooth) {
+ aScrollbar->SetAttr(kNameSpaceID_None, nsGkAtoms::smooth, u"true"_ns,
+ false);
+ }
+ aScrollbar->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos, str, aNotify);
+ if (aIsSmooth) {
+ aScrollbar->UnsetAttr(kNameSpaceID_None, nsGkAtoms::smooth, false);
+ }
+}
+
+// Use this function when you want to set the scroll position via the position
+// of the scrollbar thumb, e.g. when dragging the slider. This function scrolls
+// the content in such a way that thumbRect.x/.y becomes aNewThumbPos.
+void nsSliderFrame::SetCurrentThumbPosition(nsIContent* aScrollbar,
+ nscoord aNewThumbPos,
+ bool aIsSmooth, bool aMaySnap) {
+ nsRect crect;
+ GetXULClientRect(crect);
+ nscoord offset = IsXULHorizontal() ? crect.x : crect.y;
+ int32_t newPos = NSToIntRound((aNewThumbPos - offset) / mRatio);
+
+ if (aMaySnap &&
+ mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::snap,
+ nsGkAtoms::_true, eCaseMatters)) {
+ // If snap="true", then the slider may only be set to min + (increment * x).
+ // Otherwise, the slider may be set to any positive integer.
+ int32_t increment = GetIncrement(aScrollbar);
+ newPos = NSToIntRound(newPos / float(increment)) * increment;
+ }
+
+ SetCurrentPosition(aScrollbar, newPos, aIsSmooth);
+}
+
+// Use this function when you know the target scroll position of the scrolled
+// content. aNewPos should be passed to this function as a position as if the
+// minpos is 0. That is, the minpos will be added to the position by this
+// function. In a reverse direction slider, the newpos should be the distance
+// from the end.
+void nsSliderFrame::SetCurrentPosition(nsIContent* aScrollbar, int32_t aNewPos,
+ bool aIsSmooth) {
+ // get min and max position from our content node
+ int32_t minpos = GetMinPosition(aScrollbar);
+ int32_t maxpos = GetMaxPosition(aScrollbar);
+
+ // in reverse direction sliders, flip the value so that it goes from
+ // right to left, or bottom to top.
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::dir,
+ nsGkAtoms::reverse, eCaseMatters))
+ aNewPos = maxpos - aNewPos;
+ else
+ aNewPos += minpos;
+
+ // get the new position and make sure it is in bounds
+ if (aNewPos < minpos || maxpos < minpos)
+ aNewPos = minpos;
+ else if (aNewPos > maxpos)
+ aNewPos = maxpos;
+
+ SetCurrentPositionInternal(aScrollbar, aNewPos, aIsSmooth);
+}
+
+void nsSliderFrame::SetCurrentPositionInternal(nsIContent* aScrollbar,
+ int32_t aNewPos,
+ bool aIsSmooth) {
+ nsCOMPtr<nsIContent> scrollbar = aScrollbar;
+ nsIFrame* scrollbarBox = GetScrollbar();
+ AutoWeakFrame weakFrame(this);
+
+ mUserChanged = true;
+
+ nsScrollbarFrame* scrollbarFrame = do_QueryFrame(scrollbarBox);
+ if (scrollbarFrame) {
+ // See if we have a mediator.
+ nsIScrollbarMediator* mediator = scrollbarFrame->GetScrollbarMediator();
+ if (mediator) {
+ nscoord oldPos =
+ nsPresContext::CSSPixelsToAppUnits(GetCurrentPosition(scrollbar));
+ nscoord newPos = nsPresContext::CSSPixelsToAppUnits(aNewPos);
+ mediator->ThumbMoved(scrollbarFrame, oldPos, newPos);
+ if (!weakFrame.IsAlive()) {
+ return;
+ }
+ UpdateAttribute(scrollbar->AsElement(), aNewPos, /* aNotify */ false,
+ aIsSmooth);
+ CurrentPositionChanged();
+ mUserChanged = false;
+ return;
+ }
+ }
+
+ UpdateAttribute(scrollbar->AsElement(), aNewPos, true, aIsSmooth);
+ if (!weakFrame.IsAlive()) {
+ return;
+ }
+ mUserChanged = false;
+
+#ifdef DEBUG_SLIDER
+ printf("Current Pos=%d\n", aNewPos);
+#endif
+}
+
+void nsSliderFrame::SetInitialChildList(ChildListID aListID,
+ nsFrameList&& aChildList) {
+ nsBoxFrame::SetInitialChildList(aListID, std::move(aChildList));
+ if (aListID == FrameChildListID::Principal) {
+ AddListener();
+ }
+}
+
+nsresult nsSliderMediator::HandleEvent(dom::Event* aEvent) {
+ // Only process the event if the thumb is not being dragged.
+ if (mSlider && !mSlider->isDraggingThumb()) return mSlider->StartDrag(aEvent);
+
+ return NS_OK;
+}
+
+static bool ScrollFrameWillBuildScrollInfoLayer(nsIFrame* aScrollFrame) {
+ /*
+ * Note: if changing the conditions in this function, make a corresponding
+ * change to nsDisplayListBuilder::ShouldBuildScrollInfoItemsForHoisting()
+ * in nsDisplayList.cpp.
+ */
+ nsIFrame* current = aScrollFrame;
+ while (current) {
+ if (SVGIntegrationUtils::UsesSVGEffectsNotSupportedInCompositor(current)) {
+ return true;
+ }
+ current = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(current);
+ }
+ return false;
+}
+
+nsIScrollableFrame* nsSliderFrame::GetScrollFrame() {
+ nsIFrame* scrollbarBox = GetScrollbar();
+ if (!scrollbarBox) {
+ return nullptr;
+ }
+
+ nsContainerFrame* scrollFrame = scrollbarBox->GetParent();
+ if (!scrollFrame) {
+ return nullptr;
+ }
+
+ nsIScrollableFrame* scrollFrameAsScrollable = do_QueryFrame(scrollFrame);
+ return scrollFrameAsScrollable;
+}
+
+void nsSliderFrame::StartAPZDrag(WidgetGUIEvent* aEvent) {
+ if (!aEvent->mFlags.mHandledByAPZ) {
+ return;
+ }
+
+ if (!gfxPlatform::GetPlatform()->SupportsApzDragInput()) {
+ return;
+ }
+
+ nsIFrame* scrollbarBox = GetScrollbar();
+ nsContainerFrame* scrollFrame = scrollbarBox->GetParent();
+ if (!scrollFrame) {
+ return;
+ }
+
+ nsIContent* scrollableContent = scrollFrame->GetContent();
+ if (!scrollableContent) {
+ return;
+ }
+
+ // APZ dragging requires the scrollbar to be layerized, which doesn't
+ // happen for scroll info layers.
+ if (ScrollFrameWillBuildScrollInfoLayer(scrollFrame)) {
+ return;
+ }
+
+ // Custom scrollbar mediators are not supported in the APZ codepath.
+ if (UsesCustomScrollbarMediator(scrollbarBox)) {
+ return;
+ }
+
+ bool isHorizontal = IsXULHorizontal();
+
+ mozilla::layers::ScrollableLayerGuid::ViewID scrollTargetId;
+ bool hasID = nsLayoutUtils::FindIDFor(scrollableContent, &scrollTargetId);
+ bool hasAPZView =
+ hasID && (scrollTargetId != layers::ScrollableLayerGuid::NULL_SCROLL_ID);
+
+ if (!hasAPZView) {
+ return;
+ }
+
+ if (!DisplayPortUtils::HasNonMinimalDisplayPort(scrollableContent)) {
+ return;
+ }
+
+ mozilla::PresShell* presShell = PresShell();
+ uint64_t inputblockId = InputAPZContext::GetInputBlockId();
+ uint32_t presShellId = presShell->GetPresShellId();
+ AsyncDragMetrics dragMetrics(
+ scrollTargetId, presShellId, inputblockId,
+ NSAppUnitsToFloatPixels(mDragStart, float(AppUnitsPerCSSPixel())),
+ isHorizontal ? ScrollDirection::eHorizontal : ScrollDirection::eVertical);
+
+ // It's important to set this before calling
+ // nsIWidget::StartAsyncScrollbarDrag(), because in some configurations, that
+ // can call AsyncScrollbarDragRejected() synchronously, which clears the flag
+ // (and we want it to stay cleared in that case).
+ mScrollingWithAPZ = true;
+
+ // When we start an APZ drag, we wont get mouse events for the drag.
+ // APZ will consume them all and only notify us of the new scroll position.
+ bool waitForRefresh = InputAPZContext::HavePendingLayerization();
+ nsIWidget* widget = this->GetNearestWidget();
+ if (waitForRefresh) {
+ waitForRefresh = false;
+ if (nsPresContext* presContext = presShell->GetPresContext()) {
+ presContext->RegisterManagedPostRefreshObserver(
+ new ManagedPostRefreshObserver(
+ presContext, [widget = RefPtr<nsIWidget>(widget),
+ dragMetrics](bool aWasCanceled) {
+ if (!aWasCanceled) {
+ widget->StartAsyncScrollbarDrag(dragMetrics);
+ }
+ return ManagedPostRefreshObserver::Unregister::Yes;
+ }));
+ waitForRefresh = true;
+ }
+ }
+ if (!waitForRefresh) {
+ widget->StartAsyncScrollbarDrag(dragMetrics);
+ }
+}
+
+nsresult nsSliderFrame::StartDrag(Event* aEvent) {
+#ifdef DEBUG_SLIDER
+ printf("Begin dragging\n");
+#endif
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters))
+ return NS_OK;
+
+ WidgetGUIEvent* event = aEvent->WidgetEventPtr()->AsGUIEvent();
+
+ if (!ShouldScrollForEvent(event)) {
+ return NS_OK;
+ }
+
+ nsPoint pt;
+ if (!GetEventPoint(event, pt)) {
+ return NS_OK;
+ }
+ bool isHorizontal = IsXULHorizontal();
+ nscoord pos = isHorizontal ? pt.x : pt.y;
+
+ // If we should scroll-to-click, first place the middle of the slider thumb
+ // under the mouse.
+ nsCOMPtr<nsIContent> scrollbar;
+ nscoord newpos = pos;
+ bool scrollToClick = ShouldScrollToClickForEvent(event);
+ if (scrollToClick) {
+ // adjust so that the middle of the thumb is placed under the click
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return NS_OK;
+ }
+ nsSize thumbSize = thumbFrame->GetSize();
+ nscoord thumbLength = isHorizontal ? thumbSize.width : thumbSize.height;
+
+ newpos -= (thumbLength / 2);
+
+ nsIFrame* scrollbarBox = GetScrollbar();
+ scrollbar = GetContentOfBox(scrollbarBox);
+ }
+
+ DragThumb(true);
+
+ if (scrollToClick) {
+ // should aMaySnap be true here?
+ SetCurrentThumbPosition(scrollbar, newpos, false, false);
+ }
+
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return NS_OK;
+ }
+
+#ifdef MOZ_WIDGET_GTK
+ RefPtr<dom::Element> thumb = thumbFrame->GetContent()->AsElement();
+ thumb->SetAttr(kNameSpaceID_None, nsGkAtoms::active, u"true"_ns, true);
+#endif
+
+ if (isHorizontal)
+ mThumbStart = thumbFrame->GetPosition().x;
+ else
+ mThumbStart = thumbFrame->GetPosition().y;
+
+ mDragStart = pos - mThumbStart;
+
+ mScrollingWithAPZ = false;
+ StartAPZDrag(event); // sets mScrollingWithAPZ=true if appropriate
+
+#ifdef DEBUG_SLIDER
+ printf("Pressed mDragStart=%d\n", mDragStart);
+#endif
+
+ if (!mScrollingWithAPZ) {
+ SuppressDisplayport();
+ }
+
+ return NS_OK;
+}
+
+nsresult nsSliderFrame::StopDrag() {
+ AddListener();
+ DragThumb(false);
+
+ mScrollingWithAPZ = false;
+
+ UnsuppressDisplayport();
+
+#ifdef MOZ_WIDGET_GTK
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (thumbFrame) {
+ RefPtr<dom::Element> thumb = thumbFrame->GetContent()->AsElement();
+ thumb->UnsetAttr(kNameSpaceID_None, nsGkAtoms::active, true);
+ }
+#endif
+
+ if (mChange) {
+ StopRepeat();
+ mChange = 0;
+ }
+ return NS_OK;
+}
+
+void nsSliderFrame::DragThumb(bool aGrabMouseEvents) {
+ mDragFinished = !aGrabMouseEvents;
+
+ if (aGrabMouseEvents) {
+ PresShell::SetCapturingContent(
+ GetContent(),
+ CaptureFlags::IgnoreAllowedState | CaptureFlags::PreventDragStart);
+ } else {
+ PresShell::ReleaseCapturingContent();
+ }
+}
+
+bool nsSliderFrame::isDraggingThumb() const {
+ return PresShell::GetCapturingContent() == GetContent();
+}
+
+void nsSliderFrame::AddListener() {
+ if (!mMediator) {
+ mMediator = new nsSliderMediator(this);
+ }
+
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return;
+ }
+ thumbFrame->GetContent()->AddSystemEventListener(u"mousedown"_ns, mMediator,
+ false, false);
+ thumbFrame->GetContent()->AddSystemEventListener(u"touchstart"_ns, mMediator,
+ false, false);
+}
+
+void nsSliderFrame::RemoveListener() {
+ NS_ASSERTION(mMediator, "No listener was ever added!!");
+
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) return;
+
+ thumbFrame->GetContent()->RemoveSystemEventListener(u"mousedown"_ns,
+ mMediator, false);
+ thumbFrame->GetContent()->RemoveSystemEventListener(u"touchstart"_ns,
+ mMediator, false);
+}
+
+bool nsSliderFrame::ShouldScrollForEvent(WidgetGUIEvent* aEvent) {
+ switch (aEvent->mMessage) {
+ case eTouchStart:
+ case eTouchEnd:
+ return true;
+ case eMouseDown:
+ case eMouseUp: {
+ uint16_t button = aEvent->AsMouseEvent()->mButton;
+#ifdef MOZ_WIDGET_GTK
+ return (button == MouseButton::ePrimary) ||
+ (button == MouseButton::eSecondary && GetScrollToClick()) ||
+ (button == MouseButton::eMiddle && gMiddlePref &&
+ !GetScrollToClick());
+#else
+ return (button == MouseButton::ePrimary) ||
+ (button == MouseButton::eMiddle && gMiddlePref);
+#endif
+ }
+ default:
+ return false;
+ }
+}
+
+bool nsSliderFrame::ShouldScrollToClickForEvent(WidgetGUIEvent* aEvent) {
+ if (!ShouldScrollForEvent(aEvent)) {
+ return false;
+ }
+
+ if (aEvent->mMessage != eMouseDown && aEvent->mMessage != eTouchStart) {
+ return false;
+ }
+
+#if defined(XP_MACOSX) || defined(MOZ_WIDGET_GTK)
+ // On Mac and Linux, clicking the scrollbar thumb should never scroll to
+ // click.
+ if (IsEventOverThumb(aEvent)) {
+ return false;
+ }
+#endif
+
+ if (aEvent->mMessage == eTouchStart) {
+ return GetScrollToClick();
+ }
+
+ WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent();
+ if (mouseEvent->mButton == MouseButton::ePrimary) {
+#ifdef XP_MACOSX
+ bool invertPref = mouseEvent->IsAlt();
+#else
+ bool invertPref = mouseEvent->IsShift();
+#endif
+ return GetScrollToClick() != invertPref;
+ }
+
+#ifdef MOZ_WIDGET_GTK
+ if (mouseEvent->mButton == MouseButton::eSecondary) {
+ return !GetScrollToClick();
+ }
+#endif
+
+ return true;
+}
+
+bool nsSliderFrame::IsEventOverThumb(WidgetGUIEvent* aEvent) {
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ return false;
+ }
+
+ nsPoint eventPoint;
+ if (!GetEventPoint(aEvent, eventPoint)) {
+ return false;
+ }
+
+ nsRect thumbRect = thumbFrame->GetRect();
+#if defined(MOZ_WIDGET_GTK)
+ /* Scrollbar track can have padding, so it's better to check that eventPoint
+ * is inside of actual thumb, not just its one axis. The part of the scrollbar
+ * track adjacent to thumb can actually receive events in GTK3 */
+ return eventPoint.x >= thumbRect.x && eventPoint.x < thumbRect.XMost() &&
+ eventPoint.y >= thumbRect.y && eventPoint.y < thumbRect.YMost();
+#else
+ bool isHorizontal = IsXULHorizontal();
+ nscoord eventPos = isHorizontal ? eventPoint.x : eventPoint.y;
+ nscoord thumbStart = isHorizontal ? thumbRect.x : thumbRect.y;
+ nscoord thumbEnd = isHorizontal ? thumbRect.XMost() : thumbRect.YMost();
+
+ return eventPos >= thumbStart && eventPos < thumbEnd;
+#endif
+}
+
+NS_IMETHODIMP
+nsSliderFrame::HandlePress(nsPresContext* aPresContext, WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ if (!ShouldScrollForEvent(aEvent) || ShouldScrollToClickForEvent(aEvent)) {
+ return NS_OK;
+ }
+
+ if (IsEventOverThumb(aEvent)) {
+ return NS_OK;
+ }
+
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) // display:none?
+ return NS_OK;
+
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters))
+ return NS_OK;
+
+ nsRect thumbRect = thumbFrame->GetRect();
+
+ nscoord change = 1;
+ nsPoint eventPoint;
+ if (!GetEventPoint(aEvent, eventPoint)) {
+ return NS_OK;
+ }
+
+ if (IsXULHorizontal() ? eventPoint.x < thumbRect.x
+ : eventPoint.y < thumbRect.y)
+ change = -1;
+
+ mChange = change;
+ DragThumb(true);
+ // On Linux we want to keep scrolling in the direction indicated by |change|
+ // until the mouse is released. On the other platforms we want to stop
+ // scrolling as soon as the scrollbar thumb has reached the current mouse
+ // position.
+#ifdef MOZ_WIDGET_GTK
+ nsRect clientRect;
+ GetXULClientRect(clientRect);
+
+ // Set the destination point to the very end of the scrollbar so that
+ // scrolling doesn't stop halfway through.
+ if (change > 0) {
+ mDestinationPoint = nsPoint(clientRect.width, clientRect.height);
+ } else {
+ mDestinationPoint = nsPoint(0, 0);
+ }
+#else
+ mDestinationPoint = eventPoint;
+#endif
+ StartRepeat();
+ PageScroll(change);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSliderFrame::HandleRelease(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ StopRepeat();
+
+ nsIFrame* scrollbar = GetScrollbar();
+ nsScrollbarFrame* sb = do_QueryFrame(scrollbar);
+ if (sb) {
+ nsIScrollbarMediator* m = sb->GetScrollbarMediator();
+ if (m) {
+ m->ScrollbarReleased(sb);
+ }
+ }
+ return NS_OK;
+}
+
+void nsSliderFrame::DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) {
+ // tell our mediator if we have one we are gone.
+ if (mMediator) {
+ mMediator->SetSlider(nullptr);
+ mMediator = nullptr;
+ }
+ StopRepeat();
+
+ // call base class Destroy()
+ nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
+}
+
+nsSize nsSliderFrame::GetXULPrefSize(nsBoxLayoutState& aState) {
+ EnsureOrient();
+ return nsBoxFrame::GetXULPrefSize(aState);
+}
+
+nsSize nsSliderFrame::GetXULMinSize(nsBoxLayoutState& aState) {
+ EnsureOrient();
+
+ // our min size is just our borders and padding
+ return nsIFrame::GetUncachedXULMinSize(aState);
+}
+
+nsSize nsSliderFrame::GetXULMaxSize(nsBoxLayoutState& aState) {
+ EnsureOrient();
+ return nsBoxFrame::GetXULMaxSize(aState);
+}
+
+void nsSliderFrame::EnsureOrient() {
+ nsIFrame* scrollbarBox = GetScrollbar();
+
+ bool isHorizontal = scrollbarBox->HasAnyStateBits(NS_STATE_IS_HORIZONTAL);
+ if (isHorizontal)
+ AddStateBits(NS_STATE_IS_HORIZONTAL);
+ else
+ RemoveStateBits(NS_STATE_IS_HORIZONTAL);
+}
+
+void nsSliderFrame::Notify(void) {
+ bool stop = false;
+
+ nsIFrame* thumbFrame = mFrames.FirstChild();
+ if (!thumbFrame) {
+ StopRepeat();
+ return;
+ }
+ nsRect thumbRect = thumbFrame->GetRect();
+
+ bool isHorizontal = IsXULHorizontal();
+
+ // See if the thumb has moved past our destination point.
+ // if it has we want to stop.
+ if (isHorizontal) {
+ if (mChange < 0) {
+ if (thumbRect.x < mDestinationPoint.x) stop = true;
+ } else {
+ if (thumbRect.x + thumbRect.width > mDestinationPoint.x) stop = true;
+ }
+ } else {
+ if (mChange < 0) {
+ if (thumbRect.y < mDestinationPoint.y) stop = true;
+ } else {
+ if (thumbRect.y + thumbRect.height > mDestinationPoint.y) stop = true;
+ }
+ }
+
+ if (stop) {
+ StopRepeat();
+ } else {
+ PageScroll(mChange);
+ }
+}
+
+void nsSliderFrame::PageScroll(nscoord aChange) {
+ if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::dir,
+ nsGkAtoms::reverse, eCaseMatters)) {
+ aChange = -aChange;
+ }
+ nsIFrame* scrollbar = GetScrollbar();
+ nsScrollbarFrame* sb = do_QueryFrame(scrollbar);
+ if (sb) {
+ nsIScrollbarMediator* m = sb->GetScrollbarMediator();
+ sb->SetIncrementToPage(aChange);
+ if (m) {
+ m->ScrollByPage(sb, aChange,
+ ScrollSnapFlags::IntendedDirection |
+ ScrollSnapFlags::IntendedEndPosition);
+ return;
+ }
+ }
+ PageUpDown(aChange);
+}
+
+float nsSliderFrame::GetThumbRatio() const {
+ // mRatio is in thumb app units per scrolled css pixels. Convert it to a
+ // ratio of the thumb's CSS pixels per scrolled CSS pixels. (Note the thumb
+ // is in the scrollframe's parent's space whereas the scrolled CSS pixels
+ // are in the scrollframe's space).
+ return mRatio / mozilla::AppUnitsPerCSSPixel();
+}
+
+void nsSliderFrame::AsyncScrollbarDragInitiated(uint64_t aDragBlockId) {
+ mAPZDragInitiated = Some(aDragBlockId);
+}
+
+void nsSliderFrame::AsyncScrollbarDragRejected() {
+ mScrollingWithAPZ = false;
+ // Only suppress the displayport if we're still dragging the thumb.
+ // Otherwise, no one will unsuppress it.
+ if (isDraggingThumb()) {
+ SuppressDisplayport();
+ }
+}
+
+void nsSliderFrame::SuppressDisplayport() {
+ if (!mSuppressionActive) {
+ PresShell()->SuppressDisplayport(true);
+ mSuppressionActive = true;
+ }
+}
+
+void nsSliderFrame::UnsuppressDisplayport() {
+ if (mSuppressionActive) {
+ PresShell()->SuppressDisplayport(false);
+ mSuppressionActive = false;
+ }
+}
+
+bool nsSliderFrame::OnlySystemGroupDispatch(EventMessage aMessage) const {
+ // If we are in a native anonymous subtree, do not dispatch mouse-move or
+ // pointer-move events targeted at this slider frame to web content. This
+ // matches the behaviour of other browsers.
+ return (aMessage == eMouseMove || aMessage == ePointerMove) &&
+ isDraggingThumb() && GetContent()->IsInNativeAnonymousSubtree();
+}
+
+NS_IMPL_ISUPPORTS(nsSliderMediator, nsIDOMEventListener)
diff --git a/layout/xul/nsSliderFrame.h b/layout/xul/nsSliderFrame.h
new file mode 100644
index 0000000000..f3323d6e46
--- /dev/null
+++ b/layout/xul/nsSliderFrame.h
@@ -0,0 +1,226 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsSliderFrame_h__
+#define nsSliderFrame_h__
+
+#include "mozilla/Attributes.h"
+#include "nsRepeatService.h"
+#include "nsBoxFrame.h"
+#include "nsAtom.h"
+#include "nsCOMPtr.h"
+#include "nsITimer.h"
+#include "nsIDOMEventListener.h"
+
+class nsITimer;
+class nsSliderFrame;
+
+namespace mozilla {
+class nsDisplaySliderMarks;
+class PresShell;
+} // namespace mozilla
+
+nsIFrame* NS_NewSliderFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle);
+
+class nsSliderMediator final : public nsIDOMEventListener {
+ public:
+ NS_DECL_ISUPPORTS
+
+ nsSliderFrame* mSlider;
+
+ explicit nsSliderMediator(nsSliderFrame* aSlider) { mSlider = aSlider; }
+
+ virtual void SetSlider(nsSliderFrame* aSlider) { mSlider = aSlider; }
+
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ protected:
+ virtual ~nsSliderMediator() = default;
+};
+
+class nsSliderFrame final : public nsBoxFrame {
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsSliderFrame)
+ NS_DECL_QUERYFRAME
+
+ friend class nsSliderMediator;
+ friend class mozilla::nsDisplaySliderMarks;
+
+ explicit nsSliderFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+ virtual ~nsSliderFrame();
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"SliderFrame"_ns, aResult);
+ }
+#endif
+
+ virtual nsSize GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual nsSize GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual nsSize GetXULMaxSize(nsBoxLayoutState& aBoxLayoutState) override;
+ NS_IMETHOD DoXULLayout(nsBoxLayoutState& aBoxLayoutState) override;
+
+ // nsIFrame overrides
+ virtual void DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) override;
+
+ virtual void BuildDisplayListForChildren(
+ nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) override;
+
+ virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ virtual void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* asPrevInFlow) override;
+
+ virtual nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ // nsContainerFrame overrides
+ 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;
+ virtual void RemoveFrame(ChildListID aListID, nsIFrame* aOldFrame) override;
+
+ nsresult StartDrag(mozilla::dom::Event* aEvent);
+ nsresult StopDrag();
+
+ void StartAPZDrag(mozilla::WidgetGUIEvent* aEvent);
+
+ static int32_t GetCurrentPosition(nsIContent* content);
+ static int32_t GetMinPosition(nsIContent* content);
+ static int32_t GetMaxPosition(nsIContent* content);
+ static int32_t GetIncrement(nsIContent* content);
+ static int32_t GetPageIncrement(nsIContent* content);
+ static int32_t GetIntegerAttribute(nsIContent* content, nsAtom* atom,
+ int32_t defaultValue);
+ void EnsureOrient();
+
+ NS_IMETHOD HandlePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ NS_IMETHOD HandleMultiplePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus,
+ bool aControlHeld) override {
+ return NS_OK;
+ }
+
+ MOZ_CAN_RUN_SCRIPT
+ NS_IMETHOD HandleDrag(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override {
+ return NS_OK;
+ }
+
+ NS_IMETHOD HandleRelease(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ // Return the ratio the scrollbar thumb should move in proportion to the
+ // scrolled frame.
+ float GetThumbRatio() const;
+
+ // Notify the slider frame that an async scrollbar drag was started on the
+ // APZ side without consulting the main thread. The block id is the APZ
+ // input block id of the mousedown that started the drag.
+ void AsyncScrollbarDragInitiated(uint64_t aDragBlockId);
+
+ // Notify the slider frame that an async scrollbar drag requested in
+ // StartAPZDrag() was rejected by APZ, and the slider frame should
+ // fall back to main-thread dragging.
+ void AsyncScrollbarDragRejected();
+
+ bool OnlySystemGroupDispatch(mozilla::EventMessage aMessage) const override;
+
+ // Returns the associated scrollframe that contains this slider if any.
+ nsIScrollableFrame* GetScrollFrame();
+
+ private:
+ bool GetScrollToClick();
+ nsIFrame* GetScrollbar();
+ bool ShouldScrollForEvent(mozilla::WidgetGUIEvent* aEvent);
+ bool ShouldScrollToClickForEvent(mozilla::WidgetGUIEvent* aEvent);
+ bool IsEventOverThumb(mozilla::WidgetGUIEvent* aEvent);
+
+ void PageUpDown(nscoord change);
+ void SetCurrentThumbPosition(nsIContent* aScrollbar, nscoord aNewPos,
+ bool aIsSmooth, bool aMaySnap);
+ void SetCurrentPosition(nsIContent* aScrollbar, int32_t aNewPos,
+ bool aIsSmooth);
+ void SetCurrentPositionInternal(nsIContent* aScrollbar, int32_t pos,
+ bool aIsSmooth);
+ void CurrentPositionChanged();
+
+ void DragThumb(bool aGrabMouseEvents);
+ void AddListener();
+ void RemoveListener();
+ bool isDraggingThumb() const;
+
+ void SuppressDisplayport();
+ void UnsuppressDisplayport();
+
+ void StartRepeat() {
+ nsRepeatService::GetInstance()->Start(Notify, this, mContent->OwnerDoc(),
+ "nsSliderFrame"_ns);
+ }
+ void StopRepeat() { nsRepeatService::GetInstance()->Stop(Notify, this); }
+ void Notify();
+ static void Notify(void* aData) {
+ (static_cast<nsSliderFrame*>(aData))->Notify();
+ }
+ void PageScroll(nscoord aChange);
+
+ nsPoint mDestinationPoint;
+ RefPtr<nsSliderMediator> mMediator;
+
+ float mRatio;
+
+ nscoord mDragStart;
+ nscoord mThumbStart;
+
+ int32_t mCurPos;
+
+ nscoord mChange;
+
+ bool mDragFinished;
+
+ // true if an attribute change has been caused by the user manipulating the
+ // slider. This allows notifications to tell how a slider's current position
+ // was changed.
+ bool mUserChanged;
+
+ // true if we've handed off the scrolling to APZ. This means that we should
+ // ignore scrolling events as the position will be updated by APZ. If we were
+ // to process these events then the scroll position update would conflict
+ // causing the scroll position to jump.
+ bool mScrollingWithAPZ;
+
+ // true if displayport suppression is active, for more performant
+ // scrollbar-dragging behaviour.
+ bool mSuppressionActive;
+
+ // If APZ initiated a scrollbar drag without main-thread involvement, it
+ // notifies us and this variable stores the input block id of the APZ input
+ // block that started the drag. This lets us handle the corresponding
+ // mousedown event properly, if it arrives after the scroll position has
+ // been shifted due to async scrollbar drag.
+ Maybe<uint64_t> mAPZDragInitiated;
+
+ static bool gMiddlePref;
+ static int32_t gSnapMultiplier;
+}; // class nsSliderFrame
+
+#endif
diff --git a/layout/xul/nsSplitterFrame.cpp b/layout/xul/nsSplitterFrame.cpp
new file mode 100644
index 0000000000..9874b8a800
--- /dev/null
+++ b/layout/xul/nsSplitterFrame.cpp
@@ -0,0 +1,956 @@
+/* -*- 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/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "gfxContext.h"
+#include "nsSplitterFrame.h"
+#include "nsGkAtoms.h"
+#include "nsXULElement.h"
+#include "nsPresContext.h"
+#include "mozilla/dom/Document.h"
+#include "nsNameSpaceManager.h"
+#include "nsScrollbarButtonFrame.h"
+#include "nsIDOMEventListener.h"
+#include "nsICSSDeclaration.h"
+#include "nsFrameList.h"
+#include "nsHTMLParts.h"
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/CSSOrderAwareFrameIterator.h"
+#include "nsBoxLayoutState.h"
+#include "nsContainerFrame.h"
+#include "nsContentCID.h"
+#include "nsLayoutUtils.h"
+#include "nsDisplayList.h"
+#include "nsContentUtils.h"
+#include "nsFlexContainerFrame.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/UniquePtr.h"
+#include "nsStyledElement.h"
+
+using namespace mozilla;
+
+using mozilla::dom::Element;
+using mozilla::dom::Event;
+
+class nsSplitterInfo {
+ public:
+ nscoord min;
+ nscoord max;
+ nscoord current;
+ nscoord pref;
+ nscoord changed;
+ nsCOMPtr<nsIContent> childElem;
+};
+
+enum class ResizeType {
+ // Resize the closest sibling in a given direction.
+ Closest,
+ // Resize the farthest sibling in a given direction.
+ Farthest,
+ // Resize only flexible siblings in a given direction.
+ Flex,
+ // No space should be taken out of any children in that direction.
+ // FIXME(emilio): This is a rather odd name...
+ Grow,
+ // Only resize adjacent siblings.
+ Sibling,
+ // Don't resize anything in a given direction.
+ None,
+};
+static ResizeType ResizeTypeFromAttribute(const Element& aElement,
+ nsAtom* aAttribute) {
+ static Element::AttrValuesArray strings[] = {
+ nsGkAtoms::farthest, nsGkAtoms::flex, nsGkAtoms::grow,
+ nsGkAtoms::sibling, nsGkAtoms::none, nullptr};
+ switch (aElement.FindAttrValueIn(kNameSpaceID_None, aAttribute, strings,
+ eCaseMatters)) {
+ case 0:
+ return ResizeType::Farthest;
+ case 1:
+ return ResizeType::Flex;
+ case 2:
+ // Grow only applies to resizeAfter.
+ if (aAttribute == nsGkAtoms::resizeafter) {
+ return ResizeType::Grow;
+ }
+ break;
+ case 3:
+ return ResizeType::Sibling;
+ case 4:
+ return ResizeType::None;
+ default:
+ break;
+ }
+ return ResizeType::Closest;
+}
+
+class nsSplitterFrameInner final : public nsIDOMEventListener {
+ protected:
+ virtual ~nsSplitterFrameInner();
+
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ explicit nsSplitterFrameInner(nsSplitterFrame* aSplitter)
+ : mOuter(aSplitter) {}
+
+ void Disconnect() { mOuter = nullptr; }
+
+ nsresult MouseDown(Event* aMouseEvent);
+ nsresult MouseUp(Event* aMouseEvent);
+ nsresult MouseMove(Event* aMouseEvent);
+
+ void MouseDrag(nsPresContext* aPresContext, WidgetGUIEvent* aEvent);
+ void MouseUp(nsPresContext* aPresContext, WidgetGUIEvent* aEvent);
+
+ void AdjustChildren(nsPresContext* aPresContext);
+ void AdjustChildren(nsPresContext* aPresContext,
+ nsTArray<nsSplitterInfo>& aChildInfos,
+ bool aIsHorizontal);
+
+ void AddRemoveSpace(nscoord aDiff, nsTArray<nsSplitterInfo>& aChildInfos,
+ int32_t& aSpaceLeft);
+
+ void ResizeChildTo(nscoord& aDiff);
+
+ void UpdateState();
+
+ void AddListener();
+ void RemoveListener();
+
+ enum class State { Open, CollapsedBefore, CollapsedAfter, Dragging };
+ enum CollapseDirection { Before, After };
+
+ ResizeType GetResizeBefore();
+ ResizeType GetResizeAfter();
+ State GetState();
+
+ bool SupportsCollapseDirection(CollapseDirection aDirection);
+
+ void EnsureOrient();
+ void SetPreferredSize(nsBoxLayoutState& aState, nsIFrame* aChildBox,
+ bool aIsHorizontal, nscoord aSize);
+
+ nsSplitterFrame* mOuter;
+ bool mDidDrag = false;
+ nscoord mDragStart = 0;
+ nsIFrame* mParentBox = nullptr;
+ bool mPressed = false;
+ nsTArray<nsSplitterInfo> mChildInfosBefore;
+ nsTArray<nsSplitterInfo> mChildInfosAfter;
+ State mState = State::Open;
+ nscoord mSplitterPos = 0;
+ bool mDragging = false;
+
+ const Element* SplitterElement() const {
+ return mOuter->GetContent()->AsElement();
+ }
+};
+
+NS_IMPL_ISUPPORTS(nsSplitterFrameInner, nsIDOMEventListener)
+
+ResizeType nsSplitterFrameInner::GetResizeBefore() {
+ return ResizeTypeFromAttribute(*SplitterElement(), nsGkAtoms::resizebefore);
+}
+
+ResizeType nsSplitterFrameInner::GetResizeAfter() {
+ return ResizeTypeFromAttribute(*SplitterElement(), nsGkAtoms::resizeafter);
+}
+
+nsSplitterFrameInner::~nsSplitterFrameInner() = default;
+
+nsSplitterFrameInner::State nsSplitterFrameInner::GetState() {
+ static Element::AttrValuesArray strings[] = {nsGkAtoms::dragging,
+ nsGkAtoms::collapsed, nullptr};
+ static Element::AttrValuesArray strings_substate[] = {
+ nsGkAtoms::before, nsGkAtoms::after, nullptr};
+ switch (SplitterElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::state, strings, eCaseMatters)) {
+ case 0:
+ return State::Dragging;
+ case 1:
+ switch (SplitterElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::substate, strings_substate,
+ eCaseMatters)) {
+ case 0:
+ return State::CollapsedBefore;
+ case 1:
+ return State::CollapsedAfter;
+ default:
+ if (SupportsCollapseDirection(After)) {
+ return State::CollapsedAfter;
+ }
+ return State::CollapsedBefore;
+ }
+ }
+ return State::Open;
+}
+
+//
+// NS_NewSplitterFrame
+//
+// Creates a new Toolbar frame and returns it
+//
+nsIFrame* NS_NewSplitterFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsSplitterFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsSplitterFrame)
+
+nsSplitterFrame::nsSplitterFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsBoxFrame(aStyle, aPresContext, kClassID) {}
+
+void nsSplitterFrame::DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) {
+ if (mInner) {
+ mInner->RemoveListener();
+ mInner->Disconnect();
+ mInner = nullptr;
+ }
+ nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
+}
+
+nsresult nsSplitterFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ nsresult rv =
+ nsBoxFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+ if (aAttribute == nsGkAtoms::state) {
+ mInner->UpdateState();
+ }
+
+ return rv;
+}
+
+/**
+ * Initialize us. If we are in a box get our alignment so we know what direction
+ * we are
+ */
+void nsSplitterFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ MOZ_ASSERT(!mInner);
+ mInner = new nsSplitterFrameInner(this);
+
+ nsBoxFrame::Init(aContent, aParent, aPrevInFlow);
+
+ mInner->AddListener();
+ mInner->mParentBox = nullptr;
+}
+
+static bool IsValidParentBox(nsIFrame* aFrame) {
+ return aFrame->IsXULBoxFrame() || aFrame->IsFlexContainerFrame();
+}
+
+static nsIFrame* GetValidParentBox(nsIFrame* aChild) {
+ return aChild->GetParent() && IsValidParentBox(aChild->GetParent())
+ ? aChild->GetParent()
+ : nullptr;
+}
+
+NS_IMETHODIMP
+nsSplitterFrame::DoXULLayout(nsBoxLayoutState& aState) {
+ if (HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) {
+ mInner->mParentBox = GetValidParentBox(this);
+ mInner->UpdateState();
+ }
+
+ return nsBoxFrame::DoXULLayout(aState);
+}
+
+static bool SplitterIsHorizontal(const nsIFrame* aParentBox) {
+ // If our parent is horizontal, the splitter is vertical and vice-versa.
+ if (aParentBox->IsXULBoxFrame()) {
+ return !aParentBox->HasAnyStateBits(NS_STATE_IS_HORIZONTAL);
+ }
+ MOZ_ASSERT(aParentBox->IsFlexContainerFrame());
+ const FlexboxAxisInfo info(aParentBox);
+ return !info.mIsRowOriented;
+}
+
+void nsSplitterFrame::GetInitialOrientation(bool& aIsHorizontal) {
+ if (nsIFrame* parent = GetValidParentBox(this)) {
+ aIsHorizontal = SplitterIsHorizontal(parent);
+ } else {
+ nsBoxFrame::GetInitialOrientation(aIsHorizontal);
+ }
+}
+
+NS_IMETHODIMP
+nsSplitterFrame::HandlePress(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSplitterFrame::HandleMultiplePress(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus,
+ bool aControlHeld) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSplitterFrame::HandleDrag(nsPresContext* aPresContext, WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSplitterFrame::HandleRelease(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ return NS_OK;
+}
+
+void nsSplitterFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ nsBoxFrame::BuildDisplayList(aBuilder, aLists);
+
+ // if the mouse is captured always return us as the frame.
+ if (mInner->mDragging && aBuilder->IsForEventDelivery()) {
+ // XXX It's probably better not to check visibility here, right?
+ aLists.Outlines()->AppendNewToTop<nsDisplayEventReceiver>(aBuilder, this);
+ return;
+ }
+}
+
+nsresult nsSplitterFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ NS_ENSURE_ARG_POINTER(aEventStatus);
+ if (nsEventStatus_eConsumeNoDefault == *aEventStatus) {
+ return NS_OK;
+ }
+
+ AutoWeakFrame weakFrame(this);
+ RefPtr<nsSplitterFrameInner> inner(mInner);
+ switch (aEvent->mMessage) {
+ case eMouseMove:
+ inner->MouseDrag(aPresContext, aEvent);
+ break;
+
+ case eMouseUp:
+ if (aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary) {
+ inner->MouseUp(aPresContext, aEvent);
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ NS_ENSURE_STATE(weakFrame.IsAlive());
+ return nsBoxFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
+}
+
+void nsSplitterFrameInner::MouseUp(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent) {
+ if (mDragging && mOuter) {
+ AdjustChildren(aPresContext);
+ AddListener();
+ PresShell::ReleaseCapturingContent(); // XXXndeakin is this needed?
+ mDragging = false;
+ State newState = GetState();
+ // if the state is dragging then make it Open.
+ if (newState == State::Dragging) {
+ mOuter->mContent->AsElement()->SetAttr(kNameSpaceID_None,
+ nsGkAtoms::state, u""_ns, true);
+ }
+
+ mPressed = false;
+
+ // if we dragged then fire a command event.
+ if (mDidDrag) {
+ RefPtr<nsXULElement> element =
+ nsXULElement::FromNode(mOuter->GetContent());
+ element->DoCommand();
+ }
+
+ // printf("MouseUp\n");
+ }
+
+ mChildInfosBefore.Clear();
+ mChildInfosAfter.Clear();
+}
+
+void nsSplitterFrameInner::MouseDrag(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent) {
+ if (!mDragging || !mOuter) {
+ return;
+ }
+
+ const bool isHorizontal = !mOuter->IsXULHorizontal();
+ nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo(
+ aEvent, RelativeTo{mParentBox});
+ nscoord pos = isHorizontal ? pt.x : pt.y;
+
+ // take our current position and subtract the start location,
+ // mDragStart is in parent-box relative coordinates already.
+ pos -= mDragStart;
+
+ for (auto& info : mChildInfosBefore) {
+ info.changed = info.current;
+ }
+
+ for (auto& info : mChildInfosAfter) {
+ info.changed = info.current;
+ }
+ nscoord oldPos = pos;
+
+ ResizeChildTo(pos);
+
+ State currentState = GetState();
+ bool supportsBefore = SupportsCollapseDirection(Before);
+ bool supportsAfter = SupportsCollapseDirection(After);
+
+ const bool isRTL =
+ mOuter->StyleVisibility()->mDirection == StyleDirection::Rtl;
+ bool pastEnd = oldPos > 0 && oldPos > pos;
+ bool pastBegin = oldPos < 0 && oldPos < pos;
+ if (isRTL) {
+ // Swap the boundary checks in RTL mode
+ std::swap(pastEnd, pastBegin);
+ }
+ const bool isCollapsedBefore = pastBegin && supportsBefore;
+ const bool isCollapsedAfter = pastEnd && supportsAfter;
+
+ // if we are in a collapsed position
+ if (isCollapsedBefore || isCollapsedAfter) {
+ // and we are not collapsed then collapse
+ if (currentState == State::Dragging) {
+ if (pastEnd) {
+ // printf("Collapse right\n");
+ if (supportsAfter) {
+ RefPtr<Element> outer = mOuter->mContent->AsElement();
+ outer->SetAttr(kNameSpaceID_None, nsGkAtoms::substate, u"after"_ns,
+ true);
+ outer->SetAttr(kNameSpaceID_None, nsGkAtoms::state, u"collapsed"_ns,
+ true);
+ }
+
+ } else if (pastBegin) {
+ // printf("Collapse left\n");
+ if (supportsBefore) {
+ RefPtr<Element> outer = mOuter->mContent->AsElement();
+ outer->SetAttr(kNameSpaceID_None, nsGkAtoms::substate, u"before"_ns,
+ true);
+ outer->SetAttr(kNameSpaceID_None, nsGkAtoms::state, u"collapsed"_ns,
+ true);
+ }
+ }
+ }
+ } else {
+ // if we are not in a collapsed position and we are not dragging make sure
+ // we are dragging.
+ if (currentState != State::Dragging) {
+ mOuter->mContent->AsElement()->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::state, u"dragging"_ns, true);
+ }
+ AdjustChildren(aPresContext);
+ }
+
+ mDidDrag = true;
+}
+
+void nsSplitterFrameInner::AddListener() {
+ mOuter->GetContent()->AddEventListener(u"mouseup"_ns, this, false, false);
+ mOuter->GetContent()->AddEventListener(u"mousedown"_ns, this, false, false);
+ mOuter->GetContent()->AddEventListener(u"mousemove"_ns, this, false, false);
+ mOuter->GetContent()->AddEventListener(u"mouseout"_ns, this, false, false);
+}
+
+void nsSplitterFrameInner::RemoveListener() {
+ NS_ENSURE_TRUE_VOID(mOuter);
+ mOuter->GetContent()->RemoveEventListener(u"mouseup"_ns, this, false);
+ mOuter->GetContent()->RemoveEventListener(u"mousedown"_ns, this, false);
+ mOuter->GetContent()->RemoveEventListener(u"mousemove"_ns, this, false);
+ mOuter->GetContent()->RemoveEventListener(u"mouseout"_ns, this, false);
+}
+
+nsresult nsSplitterFrameInner::HandleEvent(dom::Event* aEvent) {
+ nsAutoString eventType;
+ aEvent->GetType(eventType);
+ if (eventType.EqualsLiteral("mouseup")) return MouseUp(aEvent);
+ if (eventType.EqualsLiteral("mousedown")) return MouseDown(aEvent);
+ if (eventType.EqualsLiteral("mousemove") ||
+ eventType.EqualsLiteral("mouseout"))
+ return MouseMove(aEvent);
+
+ MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
+ return NS_OK;
+}
+
+nsresult nsSplitterFrameInner::MouseUp(Event* aMouseEvent) {
+ NS_ENSURE_TRUE(mOuter, NS_OK);
+ mPressed = false;
+
+ PresShell::ReleaseCapturingContent();
+
+ return NS_OK;
+}
+
+template <typename LengthLike>
+static nscoord ToLengthWithFallback(const LengthLike& aLengthLike,
+ nscoord aFallback) {
+ if (aLengthLike.ConvertsToLength()) {
+ return aLengthLike.ToLength();
+ }
+ return aFallback;
+}
+
+template <typename LengthLike>
+static nsSize ToLengthWithFallback(const LengthLike& aWidth,
+ const LengthLike& aHeight,
+ nscoord aFallback = 0) {
+ return {ToLengthWithFallback(aWidth, aFallback),
+ ToLengthWithFallback(aHeight, aFallback)};
+}
+
+nsresult nsSplitterFrameInner::MouseDown(Event* aMouseEvent) {
+ NS_ENSURE_TRUE(mOuter, NS_OK);
+ dom::MouseEvent* mouseEvent = aMouseEvent->AsMouseEvent();
+ if (!mouseEvent) {
+ return NS_OK;
+ }
+
+ // only if left button
+ if (mouseEvent->Button() != 0) {
+ return NS_OK;
+ }
+
+ if (SplitterElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled,
+ nsGkAtoms::_true, eCaseMatters))
+ return NS_OK;
+
+ mParentBox = GetValidParentBox(mOuter);
+ if (!mParentBox) {
+ return NS_OK;
+ }
+
+ // get our index
+ nsPresContext* outerPresContext = mOuter->PresContext();
+
+ RefPtr<gfxContext> rc =
+ outerPresContext->PresShell()->CreateReferenceRenderingContext();
+ nsBoxLayoutState state(outerPresContext, rc);
+
+ mDidDrag = false;
+
+ EnsureOrient();
+ const bool isHorizontal = !mOuter->IsXULHorizontal();
+
+ const nsIContent* outerContent = mOuter->GetContent();
+
+ const ResizeType resizeBefore = GetResizeBefore();
+ const ResizeType resizeAfter = GetResizeAfter();
+ const int32_t childCount = mParentBox->PrincipalChildList().GetLength();
+
+ mChildInfosBefore.Clear();
+ mChildInfosAfter.Clear();
+ int32_t count = 0;
+
+ bool foundOuter = false;
+ CSSOrderAwareFrameIterator iter(
+ mParentBox, FrameChildListID::Principal,
+ CSSOrderAwareFrameIterator::ChildFilter::IncludeAll,
+ CSSOrderAwareFrameIterator::OrderState::Unknown,
+ CSSOrderAwareFrameIterator::OrderingProperty::BoxOrdinalGroup);
+ for (; !iter.AtEnd(); iter.Next()) {
+ nsIFrame* childBox = iter.get();
+ if (childBox == mOuter) {
+ foundOuter = true;
+ if (!count) {
+ // We're at the beginning, nothing to do.
+ return NS_OK;
+ }
+ if (count == childCount - 1 && resizeAfter != ResizeType::Grow) {
+ // If it's the last index then we need to allow for resizeafter="grow"
+ return NS_OK;
+ }
+ }
+ count++;
+
+ nsIContent* content = childBox->GetContent();
+ const nscoord flex = childBox->GetXULFlex();
+ const bool isBefore = !foundOuter;
+ const bool isResizable = [&] {
+ if (auto* element = nsXULElement::FromNode(content)) {
+ if (element->NodeInfo()->NameAtom() == nsGkAtoms::splitter) {
+ // skip over any splitters
+ return false;
+ }
+
+ // We need to check for hidden attribute too, since treecols with
+ // the hidden="true" attribute are not really hidden, just collapsed
+ if (element->GetXULBoolAttr(nsGkAtoms::fixed) ||
+ element->GetXULBoolAttr(nsGkAtoms::hidden)) {
+ return false;
+ }
+ }
+
+ // We need to check this here rather than in the switch before because we
+ // want `sibling` to work in the DOM order, not frame tree order.
+ if (resizeBefore == ResizeType::Sibling &&
+ content->GetNextElementSibling() == outerContent) {
+ return true;
+ }
+ if (resizeAfter == ResizeType::Sibling &&
+ content->GetPreviousElementSibling() == outerContent) {
+ return true;
+ }
+
+ const ResizeType resizeType = isBefore ? resizeBefore : resizeAfter;
+ switch (resizeType) {
+ case ResizeType::Grow:
+ case ResizeType::None:
+ case ResizeType::Sibling:
+ return false;
+ case ResizeType::Flex:
+ return flex > 0;
+ case ResizeType::Closest:
+ case ResizeType::Farthest:
+ break;
+ }
+ return true;
+ }();
+
+ if (!isResizable) {
+ continue;
+ }
+
+ nsSize minSize;
+ nsSize prefSize;
+ nsSize maxSize(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE);
+ nsSize curSize = childBox->GetSize();
+ if (childBox->IsXULBoxFrame()) {
+ minSize = childBox->GetXULMinSize(state);
+ maxSize = childBox->GetXULMaxSize(state);
+ prefSize = childBox->GetXULPrefSize(state);
+ } else {
+ const auto& pos = *childBox->StylePosition();
+ minSize = ToLengthWithFallback(pos.mMinWidth, pos.mMinHeight);
+ maxSize = ToLengthWithFallback(pos.mMaxWidth, pos.mMaxHeight,
+ NS_UNCONSTRAINEDSIZE);
+ prefSize.width = ToLengthWithFallback(pos.mWidth, curSize.width);
+ prefSize.height = ToLengthWithFallback(pos.mHeight, curSize.height);
+ }
+
+ maxSize = nsIFrame::XULBoundsCheckMinMax(minSize, maxSize);
+ prefSize = nsIFrame::XULBoundsCheck(minSize, prefSize, maxSize);
+
+ nsSplitterFrame::AddXULMargin(childBox, minSize);
+ nsSplitterFrame::AddXULMargin(childBox, maxSize);
+ nsSplitterFrame::AddXULMargin(childBox, prefSize);
+ nsSplitterFrame::AddXULMargin(childBox, curSize);
+
+ auto& list = isBefore ? mChildInfosBefore : mChildInfosAfter;
+ nsSplitterInfo& info = *list.AppendElement();
+ info.childElem = content;
+ info.min = isHorizontal ? minSize.width : minSize.height;
+ info.max = isHorizontal ? maxSize.width : maxSize.height;
+ info.pref = isHorizontal ? prefSize.width : prefSize.height;
+ info.current = info.changed = isHorizontal ? curSize.width : curSize.height;
+ }
+
+ if (!foundOuter) {
+ return NS_OK;
+ }
+
+ mPressed = true;
+
+ const bool reverseDirection = [&] {
+ if (mParentBox->IsXULBoxFrame()) {
+ return !mParentBox->IsXULNormalDirection();
+ }
+ MOZ_ASSERT(mParentBox->IsFlexContainerFrame());
+ const FlexboxAxisInfo info(mParentBox);
+ if (!info.mIsRowOriented) {
+ return info.mIsMainAxisReversed;
+ }
+ const bool rtl =
+ mParentBox->StyleVisibility()->mDirection == StyleDirection::Rtl;
+ return info.mIsMainAxisReversed != rtl;
+ }();
+
+ if (reverseDirection) {
+ // The before array is really the after array, and the order needs to be
+ // reversed. First reverse both arrays.
+ mChildInfosBefore.Reverse();
+ mChildInfosAfter.Reverse();
+
+ // Now swap the two arrays.
+ std::swap(mChildInfosBefore, mChildInfosAfter);
+ }
+
+ // if resizebefore is not Farthest, reverse the list because the first child
+ // in the list is the farthest, and we want the first child to be the closest.
+ if (resizeBefore != ResizeType::Farthest) {
+ mChildInfosBefore.Reverse();
+ }
+
+ // if the resizeafter is the Farthest we must reverse the list because the
+ // first child in the list is the closest we want the first child to be the
+ // Farthest.
+ if (resizeAfter == ResizeType::Farthest) {
+ mChildInfosAfter.Reverse();
+ }
+
+ int32_t c;
+ nsPoint pt =
+ nsLayoutUtils::GetDOMEventCoordinatesRelativeTo(mouseEvent, mParentBox);
+ if (isHorizontal) {
+ c = pt.x;
+ mSplitterPos = mOuter->mRect.x;
+ } else {
+ c = pt.y;
+ mSplitterPos = mOuter->mRect.y;
+ }
+
+ mDragStart = c;
+
+ // printf("Pressed mDragStart=%d\n",mDragStart);
+
+ PresShell::SetCapturingContent(mOuter->GetContent(),
+ CaptureFlags::IgnoreAllowedState);
+
+ return NS_OK;
+}
+
+nsresult nsSplitterFrameInner::MouseMove(Event* aMouseEvent) {
+ NS_ENSURE_TRUE(mOuter, NS_OK);
+ if (!mPressed) {
+ return NS_OK;
+ }
+
+ if (mDragging) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDOMEventListener> kungfuDeathGrip(this);
+ mOuter->mContent->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::state,
+ u"dragging"_ns, true);
+
+ RemoveListener();
+ mDragging = true;
+
+ return NS_OK;
+}
+
+bool nsSplitterFrameInner::SupportsCollapseDirection(
+ nsSplitterFrameInner::CollapseDirection aDirection) {
+ static Element::AttrValuesArray strings[] = {
+ nsGkAtoms::before, nsGkAtoms::after, nsGkAtoms::both, nullptr};
+
+ switch (SplitterElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::collapse, strings, eCaseMatters)) {
+ case 0:
+ return (aDirection == Before);
+ case 1:
+ return (aDirection == After);
+ case 2:
+ return true;
+ }
+
+ return false;
+}
+
+void nsSplitterFrameInner::UpdateState() {
+ // State Transitions:
+ // Open -> Dragging
+ // Open -> CollapsedBefore
+ // Open -> CollapsedAfter
+ // CollapsedBefore -> Open
+ // CollapsedBefore -> Dragging
+ // CollapsedAfter -> Open
+ // CollapsedAfter -> Dragging
+ // Dragging -> Open
+ // Dragging -> CollapsedBefore (auto collapse)
+ // Dragging -> CollapsedAfter (auto collapse)
+
+ State newState = GetState();
+
+ if (newState == mState) {
+ // No change.
+ return;
+ }
+
+ if ((SupportsCollapseDirection(Before) || SupportsCollapseDirection(After)) &&
+ IsValidParentBox(mOuter->GetParent())) {
+ // Find the splitter's immediate sibling.
+ const bool prev =
+ newState == State::CollapsedBefore || mState == State::CollapsedBefore;
+ nsIFrame* splitterSibling =
+ nsBoxFrame::SlowOrdinalGroupAwareSibling(mOuter, !prev);
+ if (splitterSibling) {
+ nsCOMPtr<nsIContent> sibling = splitterSibling->GetContent();
+ if (sibling && sibling->IsElement()) {
+ if (mState == State::CollapsedBefore ||
+ mState == State::CollapsedAfter) {
+ // CollapsedBefore -> Open
+ // CollapsedBefore -> Dragging
+ // CollapsedAfter -> Open
+ // CollapsedAfter -> Dragging
+ nsContentUtils::AddScriptRunner(new nsUnsetAttrRunnable(
+ sibling->AsElement(), nsGkAtoms::collapsed));
+ } else if ((mState == State::Open || mState == State::Dragging) &&
+ (newState == State::CollapsedBefore ||
+ newState == State::CollapsedAfter)) {
+ // Open -> CollapsedBefore / CollapsedAfter
+ // Dragging -> CollapsedBefore / CollapsedAfter
+ nsContentUtils::AddScriptRunner(new nsSetAttrRunnable(
+ sibling->AsElement(), nsGkAtoms::collapsed, u"true"_ns));
+ }
+ }
+ }
+ }
+ mState = newState;
+}
+
+void nsSplitterFrameInner::EnsureOrient() {
+ if (SplitterIsHorizontal(mParentBox))
+ mOuter->AddStateBits(NS_STATE_IS_HORIZONTAL);
+ else
+ mOuter->RemoveStateBits(NS_STATE_IS_HORIZONTAL);
+}
+
+void nsSplitterFrameInner::AdjustChildren(nsPresContext* aPresContext) {
+ EnsureOrient();
+ const bool isHorizontal = !mOuter->IsXULHorizontal();
+
+ AdjustChildren(aPresContext, mChildInfosBefore, isHorizontal);
+ AdjustChildren(aPresContext, mChildInfosAfter, isHorizontal);
+}
+
+static nsIFrame* GetChildBoxForContent(nsIFrame* aParentBox,
+ nsIContent* aContent) {
+ // XXX Can this use GetPrimaryFrame?
+ for (nsIFrame* f : aParentBox->PrincipalChildList()) {
+ if (f->GetContent() == aContent) {
+ return f;
+ }
+ }
+ return nullptr;
+}
+
+void nsSplitterFrameInner::AdjustChildren(nsPresContext* aPresContext,
+ nsTArray<nsSplitterInfo>& aChildInfos,
+ bool aIsHorizontal) {
+ /// printf("------- AdjustChildren------\n");
+
+ nsBoxLayoutState state(aPresContext);
+
+ for (auto& info : aChildInfos) {
+ nscoord newPref = info.pref + (info.changed - info.current);
+ if (nsIFrame* childBox =
+ GetChildBoxForContent(mParentBox, info.childElem)) {
+ SetPreferredSize(state, childBox, aIsHorizontal, newPref);
+ }
+ }
+}
+
+void nsSplitterFrameInner::SetPreferredSize(nsBoxLayoutState& aState,
+ nsIFrame* aChildBox,
+ bool aIsHorizontal, nscoord aSize) {
+ nsMargin margin(0, 0, 0, 0);
+ aChildBox->GetXULMargin(margin);
+
+ if (aIsHorizontal) {
+ aSize -= (margin.left + margin.right);
+ } else {
+ aSize -= (margin.top + margin.bottom);
+ }
+
+ RefPtr element = nsStyledElement::FromNode(aChildBox->GetContent());
+ if (!element) {
+ return;
+ }
+
+ // We set both the attribute and the CSS value, so that XUL persist="" keeps
+ // working, see bug 1790712.
+
+ int32_t pixels = aSize / AppUnitsPerCSSPixel();
+ nsAutoString attrValue;
+ attrValue.AppendInt(pixels);
+ element->SetAttr(aIsHorizontal ? nsGkAtoms::width : nsGkAtoms::height,
+ attrValue, IgnoreErrors());
+
+ nsCOMPtr<nsICSSDeclaration> decl = element->Style();
+
+ nsAutoCString cssValue;
+ cssValue.AppendInt(pixels);
+ cssValue.AppendLiteral("px");
+ decl->SetProperty(aIsHorizontal ? "width"_ns : "height"_ns, cssValue, ""_ns,
+ IgnoreErrors());
+}
+
+void nsSplitterFrameInner::AddRemoveSpace(nscoord aDiff,
+ nsTArray<nsSplitterInfo>& aChildInfos,
+ int32_t& aSpaceLeft) {
+ aSpaceLeft = 0;
+
+ for (auto& info : aChildInfos) {
+ nscoord min = info.min;
+ nscoord max = info.max;
+ nscoord& c = info.changed;
+
+ // figure our how much space to add or remove
+ if (c + aDiff < min) {
+ aDiff += (c - min);
+ c = min;
+ } else if (c + aDiff > max) {
+ aDiff -= (max - c);
+ c = max;
+ } else {
+ c += aDiff;
+ aDiff = 0;
+ }
+
+ // there is not space left? We are done
+ if (aDiff == 0) {
+ break;
+ }
+ }
+
+ aSpaceLeft = aDiff;
+}
+
+/**
+ * Ok if we want to resize a child we will know the actual size in pixels we
+ * want it to be. This is not the preferred size. But the only way we can change
+ * a child is by manipulating its preferred size. So give the actual pixel size
+ * this method will figure out the preferred size and set it.
+ */
+
+void nsSplitterFrameInner::ResizeChildTo(nscoord& aDiff) {
+ nscoord spaceLeft = 0;
+
+ if (!mChildInfosBefore.IsEmpty()) {
+ AddRemoveSpace(aDiff, mChildInfosBefore, spaceLeft);
+ // If there is any space left over remove it from the diff we were
+ // originally given.
+ aDiff -= spaceLeft;
+ }
+
+ AddRemoveSpace(-aDiff, mChildInfosAfter, spaceLeft);
+
+ if (spaceLeft != 0 && !mChildInfosAfter.IsEmpty()) {
+ aDiff += spaceLeft;
+ AddRemoveSpace(spaceLeft, mChildInfosBefore, spaceLeft);
+ }
+}
diff --git a/layout/xul/nsSplitterFrame.h b/layout/xul/nsSplitterFrame.h
new file mode 100644
index 0000000000..75af7cd83c
--- /dev/null
+++ b/layout/xul/nsSplitterFrame.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/. */
+
+//
+// nsSplitterFrame
+//
+
+#ifndef nsSplitterFrame_h__
+#define nsSplitterFrame_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/RefPtr.h"
+#include "nsBoxFrame.h"
+
+class nsSplitterFrameInner;
+
+namespace mozilla {
+class PresShell;
+} // namespace mozilla
+
+nsIFrame* NS_NewSplitterFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle);
+
+class nsSplitterFrame final : public nsBoxFrame {
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsSplitterFrame)
+
+ explicit nsSplitterFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+ virtual void DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) override;
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override {
+ return MakeFrameName(u"SplitterFrame"_ns, aResult);
+ }
+#endif
+
+ // nsIFrame overrides
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ virtual void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ NS_IMETHOD DoXULLayout(nsBoxLayoutState& aBoxLayoutState) override;
+
+ NS_IMETHOD HandlePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ NS_IMETHOD HandleMultiplePress(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus,
+ bool aControlHeld) override;
+
+ MOZ_CAN_RUN_SCRIPT
+ NS_IMETHOD HandleDrag(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ NS_IMETHOD HandleRelease(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ virtual nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ virtual void GetInitialOrientation(bool& aIsHorizontal) override;
+
+ private:
+ friend class nsSplitterFrameInner;
+ RefPtr<nsSplitterFrameInner> mInner;
+
+}; // class nsSplitterFrame
+
+#endif
diff --git a/layout/xul/nsSprocketLayout.cpp b/layout/xul/nsSprocketLayout.cpp
new file mode 100644
index 0000000000..e0f5e21d7e
--- /dev/null
+++ b/layout/xul/nsSprocketLayout.cpp
@@ -0,0 +1,1372 @@
+/* -*- 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/. */
+
+//
+// Eric Vaughan
+// Netscape Communications
+//
+// See documentation in associated header file
+//
+
+#include "nsBoxLayoutState.h"
+#include "nsSprocketLayout.h"
+#include "nsPresContext.h"
+#include "nsCOMPtr.h"
+#include "nsIContent.h"
+#include "nsContainerFrame.h"
+#include "nsBoxFrame.h"
+#include "StackArena.h"
+#include "mozilla/Likely.h"
+#include "mozilla/CSSOrderAwareFrameIterator.h"
+#include <algorithm>
+
+using mozilla::StyleDirection;
+using namespace mozilla;
+
+nsBoxLayout* nsSprocketLayout::gInstance = nullptr;
+
+static Maybe<CSSOrderAwareFrameIterator> IterFor(nsIFrame* aBoxFrame) {
+ Maybe<CSSOrderAwareFrameIterator> ret;
+ if (aBoxFrame->IsXULBoxFrame()) {
+ ret.emplace(aBoxFrame, FrameChildListID::Principal,
+ CSSOrderAwareFrameIterator::ChildFilter::IncludeAll,
+ CSSOrderAwareFrameIterator::OrderState::Unknown,
+ CSSOrderAwareFrameIterator::OrderingProperty::BoxOrdinalGroup);
+ }
+ return ret;
+}
+
+nsresult NS_NewSprocketLayout(nsCOMPtr<nsBoxLayout>& aNewLayout) {
+ if (!nsSprocketLayout::gInstance) {
+ nsSprocketLayout::gInstance = new nsSprocketLayout();
+ NS_IF_ADDREF(nsSprocketLayout::gInstance);
+ }
+ // we have not instance variables so just return our static one.
+ aNewLayout = nsSprocketLayout::gInstance;
+ return NS_OK;
+}
+
+/*static*/
+void nsSprocketLayout::Shutdown() { NS_IF_RELEASE(gInstance); }
+
+nsSprocketLayout::nsSprocketLayout() = default;
+
+bool nsSprocketLayout::IsXULHorizontal(nsIFrame* aBox) {
+ return aBox->HasAnyStateBits(NS_STATE_IS_HORIZONTAL);
+}
+
+void nsSprocketLayout::GetFrameState(nsIFrame* aBox, nsFrameState& aState) {
+ aState = aBox->GetStateBits();
+}
+
+static StyleDirection GetFrameDirection(nsIFrame* aBox) {
+ return aBox->StyleVisibility()->mDirection;
+}
+
+static void HandleBoxPack(nsIFrame* aBox, const nsFrameState& aFrameState,
+ nscoord& aX, nscoord& aY, const nsRect& aOriginalRect,
+ const nsRect& aClientRect) {
+ // In the normal direction we lay out our kids in the positive direction
+ // (e.g., |x| will get bigger for a horizontal box, and |y| will get bigger
+ // for a vertical box). In the reverse direction, the opposite is true. We'll
+ // be laying out each child at a smaller |x| or |y|.
+ StyleDirection frameDirection = GetFrameDirection(aBox);
+
+ if (aFrameState & NS_STATE_IS_HORIZONTAL) {
+ if (aFrameState & NS_STATE_IS_DIRECTION_NORMAL) {
+ // The normal direction. |x| increases as we move through our children.
+ aX = aClientRect.x;
+ } else {
+ // The reverse direction. |x| decreases as we move through our children.
+ aX = aClientRect.x + aOriginalRect.width;
+ }
+ // |y| is always in the normal direction in horizontal boxes
+ aY = aClientRect.y;
+ } else {
+ // take direction property into account for |x| in vertical boxes
+ if (frameDirection == StyleDirection::Ltr) {
+ // The normal direction. |x| increases as we move through our children.
+ aX = aClientRect.x;
+ } else {
+ // The reverse direction. |x| decreases as we move through our children.
+ aX = aClientRect.x + aOriginalRect.width;
+ }
+ if (aFrameState & NS_STATE_IS_DIRECTION_NORMAL) {
+ // The normal direction. |y| increases as we move through our children.
+ aY = aClientRect.y;
+ } else {
+ // The reverse direction. |y| decreases as we move through our children.
+ aY = aClientRect.y + aOriginalRect.height;
+ }
+ }
+
+ // Get our pack/alignment information.
+ nsIFrame::Halignment halign = aBox->GetXULHAlign();
+ nsIFrame::Valignment valign = aBox->GetXULVAlign();
+
+ // The following code handles box PACKING. Packing comes into play in the
+ // case where the computed size for all of our children (now stored in our
+ // client rect) is smaller than the size available for the box (stored in
+ // |aOriginalRect|).
+ //
+ // Here we adjust our |x| and |y| variables accordingly so that we start at
+ // the beginning, middle, or end of the box.
+ //
+ // XXXdwh JUSTIFY needs to be implemented!
+ if (aFrameState & NS_STATE_IS_HORIZONTAL) {
+ switch (halign) {
+ case nsBoxFrame::hAlign_Left:
+ break; // Nothing to do. The default initialized us properly.
+
+ case nsBoxFrame::hAlign_Center:
+ if (aFrameState & NS_STATE_IS_DIRECTION_NORMAL)
+ aX += (aOriginalRect.width - aClientRect.width) / 2;
+ else
+ aX -= (aOriginalRect.width - aClientRect.width) / 2;
+ break;
+
+ case nsBoxFrame::hAlign_Right:
+ if (aFrameState & NS_STATE_IS_DIRECTION_NORMAL)
+ aX += (aOriginalRect.width - aClientRect.width);
+ else
+ aX -= (aOriginalRect.width - aClientRect.width);
+ break; // Nothing to do for the reverse dir. The default initialized
+ // us properly.
+ }
+ } else {
+ switch (valign) {
+ case nsBoxFrame::vAlign_Top:
+ case nsBoxFrame::vAlign_BaseLine: // This value is technically impossible
+ // to specify for pack.
+ break; // Don't do anything. We were initialized correctly.
+
+ case nsBoxFrame::vAlign_Middle:
+ if (aFrameState & NS_STATE_IS_DIRECTION_NORMAL)
+ aY += (aOriginalRect.height - aClientRect.height) / 2;
+ else
+ aY -= (aOriginalRect.height - aClientRect.height) / 2;
+ break;
+
+ case nsBoxFrame::vAlign_Bottom:
+ if (aFrameState & NS_STATE_IS_DIRECTION_NORMAL)
+ aY += (aOriginalRect.height - aClientRect.height);
+ else
+ aY -= (aOriginalRect.height - aClientRect.height);
+ break;
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsSprocketLayout::XULLayout(nsIFrame* aBox, nsBoxLayoutState& aState) {
+ // See if we are collapsed. If we are, then simply iterate over all our
+ // children and give them a rect of 0 width and height.
+ if (aBox->IsXULCollapsed()) {
+ for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) {
+ nsBoxFrame::LayoutChildAt(aState, iter->get(), nsRect(0, 0, 0, 0));
+ }
+ return NS_OK;
+ }
+
+ nsBoxLayoutState::AutoReflowDepth depth(aState);
+ mozilla::AutoStackArena arena;
+
+ // ----- figure out our size ----------
+ const nsSize originalSize = aBox->GetSize();
+
+ // -- make sure we remove our border and padding ----
+ nsRect clientRect;
+ aBox->GetXULClientRect(clientRect);
+
+ // |originalClientRect| represents the rect of the entire box (excluding
+ // borders and padding). We store it here because we're going to use
+ // |clientRect| to hold the required size for all our kids. As an example,
+ // consider an hbox with a specified width of 300. If the kids total only 150
+ // pixels of width, then we have 150 pixels left over. |clientRect| is going
+ // to hold a width of 150 and is going to be adjusted based off the value of
+ // the PACK property. If flexible objects are in the box, then the two rects
+ // will match.
+ nsRect originalClientRect(clientRect);
+
+ // The frame state contains cached knowledge about our box, such as our
+ // orientation and direction.
+ nsFrameState frameState = nsFrameState(0);
+ GetFrameState(aBox, frameState);
+
+ // Build a list of our children's desired sizes and computed sizes
+ nsBoxSize* boxSizes = nullptr;
+ nsComputedBoxSize* computedBoxSizes = nullptr;
+
+ nscoord min = 0;
+ nscoord max = 0;
+ int32_t flexes = 0;
+ PopulateBoxSizes(aBox, aState, boxSizes, min, max, flexes);
+
+ // The |size| variable will hold the total size of children along the axis of
+ // the box. Continuing with the example begun in the comment above, size
+ // would be 150 pixels.
+ nscoord size = clientRect.width;
+ if (!IsXULHorizontal(aBox)) size = clientRect.height;
+ ComputeChildSizes(aBox, aState, size, boxSizes, computedBoxSizes);
+
+ // After the call to ComputeChildSizes, the |size| variable contains the
+ // total required size of all the children. We adjust our clientRect in the
+ // appropriate dimension to match this size. In our example, we now assign
+ // 150 pixels into the clientRect.width.
+ //
+ // The variables |min| and |max| hold the minimum required size box must be
+ // in the OPPOSITE orientation, e.g., for a horizontal box, |min| is the
+ // minimum height we require to enclose our children, and |max| is the maximum
+ // height required to enclose our children.
+ if (IsXULHorizontal(aBox)) {
+ clientRect.width = size;
+ if (clientRect.height < min) clientRect.height = min;
+
+ if (frameState & NS_STATE_AUTO_STRETCH) {
+ if (clientRect.height > max) clientRect.height = max;
+ }
+ } else {
+ clientRect.height = size;
+ if (clientRect.width < min) clientRect.width = min;
+
+ if (frameState & NS_STATE_AUTO_STRETCH) {
+ if (clientRect.width > max) clientRect.width = max;
+ }
+ }
+
+ // With the sizes computed, now it's time to lay out our children.
+ bool finished;
+ nscoord passes = 0;
+
+ // We flow children at their preferred locations (along with the appropriate
+ // computed flex). After we flow a child, it is possible that the child will
+ // change its size. If/when this happens, we have to do another pass.
+ // Typically only 2 passes are required, but the code is prepared to do as
+ // many passes as are necessary to achieve equilibrium.
+ nscoord x = 0;
+ nscoord y = 0;
+ nscoord origX = 0;
+ nscoord origY = 0;
+
+ // |childResized| lets us know if a child changed its size after we attempted
+ // to lay it out at the specified size. If this happens, we usually have to
+ // do another pass.
+ bool childResized = false;
+
+ // |passes| stores our number of passes. If for any reason we end up doing
+ // more than, say, 10 passes, we assert to indicate that something is
+ // seriously screwed up.
+ passes = 0;
+ do {
+ // Always assume that we're done. This will change if, for example,
+ // children don't stay the same size after being flowed.
+ finished = true;
+
+ // Handle box packing.
+ HandleBoxPack(aBox, frameState, x, y, originalClientRect, clientRect);
+
+ // Now that packing is taken care of we set up a few additional
+ // tracking variables.
+ origX = x;
+ origY = y;
+
+ // Now we iterate over our box children and our box size lists in
+ // parallel. For each child, we look at its sizes and figure out
+ // where to place it.
+ nsComputedBoxSize* childComputedBoxSize = computedBoxSizes;
+ nsBoxSize* childBoxSize = boxSizes;
+
+ auto iter = IterFor(aBox);
+ int32_t count = 0;
+ while ((iter && !iter->AtEnd()) || (childBoxSize && childBoxSize->bogus)) {
+ // If for some reason, our lists are not the same length, we guard
+ // by bailing out of the loop.
+ if (childBoxSize == nullptr) {
+ MOZ_ASSERT_UNREACHABLE("Lists not the same length.");
+ break;
+ }
+
+ nscoord width = clientRect.width;
+ nscoord height = clientRect.height;
+
+ if (!childBoxSize->bogus) {
+ nsIFrame* child = iter->get();
+
+ // We have a valid box size entry. This entry already contains
+ // information about our sizes along the axis of the box (e.g., widths
+ // in a horizontal box). If our default ALIGN is not stretch, however,
+ // then we also need to know the child's size along the opposite axis.
+ if (!(frameState & NS_STATE_AUTO_STRETCH)) {
+ nsSize prefSize = child->GetXULPrefSize(aState);
+ nsSize minSize = child->GetXULMinSize(aState);
+ nsSize maxSize = child->GetXULMaxSize(aState);
+ prefSize = nsIFrame::XULBoundsCheck(minSize, prefSize, maxSize);
+
+ AddXULMargin(child, prefSize);
+ width = std::min(prefSize.width, originalClientRect.width);
+ height = std::min(prefSize.height, originalClientRect.height);
+ }
+ }
+
+ // Obtain the computed size along the axis of the box for this child from
+ // the computedBoxSize entry. We store the result in |width| for
+ // horizontal boxes and |height| for vertical boxes.
+ if (frameState & NS_STATE_IS_HORIZONTAL)
+ width = childComputedBoxSize->size;
+ else
+ height = childComputedBoxSize->size;
+
+ // Adjust our x/y for the left/right spacing.
+ if (frameState & NS_STATE_IS_HORIZONTAL) {
+ if (frameState & NS_STATE_IS_DIRECTION_NORMAL)
+ x += (childBoxSize->left);
+ else
+ x -= (childBoxSize->right);
+ } else {
+ if (frameState & NS_STATE_IS_DIRECTION_NORMAL)
+ y += (childBoxSize->left);
+ else
+ y -= (childBoxSize->right);
+ }
+
+ // Now we build a child rect.
+ nscoord rectX = x;
+ nscoord rectY = y;
+ if (!(frameState & NS_STATE_IS_DIRECTION_NORMAL)) {
+ if (frameState & NS_STATE_IS_HORIZONTAL)
+ rectX -= width;
+ else
+ rectY -= height;
+ }
+
+ // We now create an accurate child rect based off our computed size
+ // information.
+ nsRect childRect(rectX, rectY, width, height);
+
+ // Sanity check against our clientRect. It is possible that a child
+ // specified a size that is too large to fit. If that happens, then we
+ // have to grow our client rect. Remember, clientRect is not the total
+ // rect of the enclosing box. It currently holds our perception of how
+ // big the children needed to be.
+ if (childRect.width > clientRect.width)
+ clientRect.width = childRect.width;
+
+ if (childRect.height > clientRect.height)
+ clientRect.height = childRect.height;
+
+ // Either |nextX| or |nextY| is updated by this function call, according
+ // to our axis.
+ nscoord nextX = x;
+ nscoord nextY = y;
+
+ ComputeChildsNextPosition(aBox, x, y, nextX, nextY, childRect);
+
+ // Now we further update our nextX/Y along our axis.
+ // We also set childRect.y/x along the opposite axis appropriately for a
+ // stretch alignment. (Non-stretch alignment is handled below.)
+ if (frameState & NS_STATE_IS_HORIZONTAL) {
+ if (frameState & NS_STATE_IS_DIRECTION_NORMAL)
+ nextX += (childBoxSize->right);
+ else
+ nextX -= (childBoxSize->left);
+ childRect.y = originalClientRect.y;
+ } else {
+ if (frameState & NS_STATE_IS_DIRECTION_NORMAL)
+ nextY += (childBoxSize->right);
+ else
+ nextY -= (childBoxSize->left);
+ if (GetFrameDirection(aBox) == StyleDirection::Ltr) {
+ childRect.x = originalClientRect.x;
+ } else {
+ // keep the right edge of the box the same
+ childRect.x =
+ clientRect.x + originalClientRect.width - childRect.width;
+ }
+ }
+
+ // If we encounter a completely bogus box size, we just leave this child
+ // completely alone and continue through the loop to the next child.
+ if (childBoxSize->bogus) {
+ childComputedBoxSize = childComputedBoxSize->next;
+ childBoxSize = childBoxSize->next;
+ count++;
+ x = nextX;
+ y = nextY;
+ // FIXME(emilio): shouldn't this update `child` / `iter`? This looks
+ // broken.
+ continue;
+ }
+
+ nsIFrame* child = iter->get();
+ nsMargin margin(0, 0, 0, 0);
+
+ bool layout = true;
+
+ // Deflate the rect of our child by its margin.
+ child->GetXULMargin(margin);
+ childRect.Deflate(margin);
+ if (childRect.width < 0) childRect.width = 0;
+ if (childRect.height < 0) childRect.height = 0;
+
+ // Now we're trying to figure out if we have to lay out this child, i.e.,
+ // to call the child's XULLayout method.
+ if (passes > 0) {
+ layout = false;
+ } else {
+ // Always perform layout if we are dirty or have dirty children
+ if (!child->IsSubtreeDirty()) {
+ layout = false;
+ }
+ }
+
+ nsRect oldRect(child->GetRect());
+
+ // Non-stretch alignment will be handled in AlignChildren(), so don't
+ // change child out-of-axis positions yet.
+ if (!(frameState & NS_STATE_AUTO_STRETCH)) {
+ if (frameState & NS_STATE_IS_HORIZONTAL) {
+ childRect.y = oldRect.y;
+ } else {
+ childRect.x = oldRect.x;
+ }
+ }
+
+ // We computed a childRect. Now we want to set the bounds of the child to
+ // be that rect. If our old rect is different, then we know our size
+ // changed and we cache that fact in the |sizeChanged| variable.
+
+ child->SetXULBounds(aState, childRect);
+ bool sizeChanged = (childRect.width != oldRect.width ||
+ childRect.height != oldRect.height);
+
+ if (sizeChanged) {
+ // Our size is different. Sanity check against our maximum allowed size
+ // to ensure we didn't exceed it.
+ nsSize minSize = child->GetXULMinSize(aState);
+ nsSize maxSize = child->GetXULMaxSize(aState);
+ maxSize = nsIFrame::XULBoundsCheckMinMax(minSize, maxSize);
+
+ // make sure the size is in our max size.
+ if (childRect.width > maxSize.width) childRect.width = maxSize.width;
+
+ if (childRect.height > maxSize.height)
+ childRect.height = maxSize.height;
+
+ // set it again
+ child->SetXULBounds(aState, childRect);
+ }
+
+ // If we already determined that layout was required or if our size has
+ // changed, then we make sure to call layout on the child, since its
+ // children may need to be shifted around as a result of the size change.
+ if (layout || sizeChanged) child->XULLayout(aState);
+
+ // If the child was a block or inline (e.g., HTML) it may have changed its
+ // rect *during* layout. We have to check for this.
+ nsRect newChildRect(child->GetRect());
+
+ if (!newChildRect.IsEqualInterior(childRect)) {
+#ifdef DEBUG_GROW
+ printf(" GREW from (%d,%d) -> (%d,%d)\n", childRect.width,
+ childRect.height, newChildRect.width, newChildRect.height);
+#endif
+ newChildRect.Inflate(margin);
+ childRect.Inflate(margin);
+
+ // The child changed size during layout. The ChildResized method
+ // handles this scenario.
+ ChildResized(aBox, aState, child, childBoxSize, childComputedBoxSize,
+ boxSizes, computedBoxSizes, childRect, newChildRect,
+ clientRect, flexes, finished);
+
+ // We note that a child changed size, which means that another pass will
+ // be required.
+ childResized = true;
+
+ // Now that a child resized, it's entirely possible that OUR rect is too
+ // small. Now we ensure that |originalClientRect| is grown to
+ // accommodate the size of |clientRect|.
+ if (clientRect.width > originalClientRect.width)
+ originalClientRect.width = clientRect.width;
+
+ if (clientRect.height > originalClientRect.height)
+ originalClientRect.height = clientRect.height;
+
+ if (!(frameState & NS_STATE_IS_DIRECTION_NORMAL)) {
+ // Our childRect had its XMost() or YMost() (depending on our layout
+ // direction), positioned at a certain point. Ensure that the
+ // newChildRect satisfies the same constraint. Note that this is
+ // just equivalent to adjusting the x/y by the difference in
+ // width/height between childRect and newChildRect. So we don't need
+ // to reaccount for the left and right of the box layout state again.
+ if (frameState & NS_STATE_IS_HORIZONTAL)
+ newChildRect.x = childRect.XMost() - newChildRect.width;
+ else
+ newChildRect.y = childRect.YMost() - newChildRect.height;
+ }
+
+ if (!(frameState & NS_STATE_IS_HORIZONTAL)) {
+ if (GetFrameDirection(aBox) != StyleDirection::Ltr) {
+ // keep the right edge the same
+ newChildRect.x = childRect.XMost() - newChildRect.width;
+ }
+ }
+
+ // If the child resized then recompute its position.
+ ComputeChildsNextPosition(aBox, x, y, nextX, nextY, newChildRect);
+
+ if (newChildRect.width >= margin.left + margin.right &&
+ newChildRect.height >= margin.top + margin.bottom)
+ newChildRect.Deflate(margin);
+
+ if (childRect.width >= margin.left + margin.right &&
+ childRect.height >= margin.top + margin.bottom)
+ childRect.Deflate(margin);
+
+ child->SetXULBounds(aState, newChildRect);
+
+ // If we are the first box that changed size, then we don't need to do a
+ // second pass
+ if (count == 0) finished = true;
+ }
+
+ // Now update our x/y finally.
+ x = nextX;
+ y = nextY;
+
+ // Move to the next child.
+ childComputedBoxSize = childComputedBoxSize->next;
+ childBoxSize = childBoxSize->next;
+
+ iter->Next();
+ count++;
+ }
+
+ // Sanity-checking code to ensure we don't do an infinite # of passes.
+ passes++;
+ NS_ASSERTION(passes < 10, "A Box's child is constantly growing!!!!!");
+ if (passes >= 10) break;
+ } while (false == finished);
+
+ // Get rid of our size lists.
+ while (boxSizes) {
+ nsBoxSize* toDelete = boxSizes;
+ boxSizes = boxSizes->next;
+ delete toDelete;
+ }
+
+ while (computedBoxSizes) {
+ nsComputedBoxSize* toDelete = computedBoxSizes;
+ computedBoxSizes = computedBoxSizes->next;
+ delete toDelete;
+ }
+
+ if (childResized) {
+ // See if one of our children forced us to get bigger
+ nsRect tmpClientRect(originalClientRect);
+ nsMargin bp(0, 0, 0, 0);
+ aBox->GetXULBorderAndPadding(bp);
+ tmpClientRect.Inflate(bp);
+
+ if (tmpClientRect.width > originalSize.width ||
+ tmpClientRect.height > originalSize.height) {
+ // if it did reset our bounds.
+ nsRect bounds(aBox->GetRect());
+ if (tmpClientRect.width > originalSize.width)
+ bounds.width = tmpClientRect.width;
+
+ if (tmpClientRect.height > originalSize.height)
+ bounds.height = tmpClientRect.height;
+
+ aBox->SetXULBounds(aState, bounds);
+ }
+ }
+
+ // Because our size grew, we now have to readjust because of box packing.
+ // Repack in order to update our x and y to the correct values.
+ HandleBoxPack(aBox, frameState, x, y, originalClientRect, clientRect);
+
+ // Compare against our original x and y and only worry about adjusting the
+ // children if we really did have to change the positions because of packing
+ // (typically for 'center' or 'end' pack values).
+ if (x != origX || y != origY) {
+ // reposition all our children
+ for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) {
+ nsIFrame* child = iter->get();
+ nsRect childRect(child->GetRect());
+ childRect.x += (x - origX);
+ childRect.y += (y - origY);
+ child->SetXULBounds(aState, childRect);
+ }
+ }
+
+ // Perform out-of-axis alignment for non-stretch alignments
+ if (!(frameState & NS_STATE_AUTO_STRETCH)) {
+ AlignChildren(aBox, aState);
+ }
+
+ // That's it! If you made it this far without having a nervous breakdown,
+ // congratulations! Go get yourself a beer.
+ return NS_OK;
+}
+
+void nsSprocketLayout::PopulateBoxSizes(nsIFrame* aBox,
+ nsBoxLayoutState& aState,
+ nsBoxSize*& aBoxSizes,
+ nscoord& aMinSize, nscoord& aMaxSize,
+ int32_t& aFlexes) {
+ aMinSize = 0;
+ aMaxSize = NS_UNCONSTRAINEDSIZE;
+
+ bool isHorizontal;
+
+ if (IsXULHorizontal(aBox))
+ isHorizontal = true;
+ else
+ isHorizontal = false;
+
+ // this is a nice little optimization
+ // it turns out that if we only have 1 flexable child
+ // then it does not matter what its preferred size is
+ // there is nothing to flex it relative. This is great
+ // because we can avoid asking for a preferred size in this
+ // case. Why is this good? Well you might have html inside it
+ // and asking html for its preferred size is rather expensive.
+ // so we can just optimize it out this way.
+
+ // set flexes
+ aFlexes = 0;
+ nsBoxSize* currentBox = aBoxSizes;
+ nsBoxSize* last = nullptr;
+
+ nscoord maxFlex = 0;
+ int32_t childCount = 0;
+
+ for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) {
+ nsIFrame* child = iter->get();
+ while (currentBox && currentBox->bogus) {
+ last = currentBox;
+ currentBox = currentBox->next;
+ }
+ ++childCount;
+ nsSize pref(0, 0);
+ nsSize minSize(0, 0);
+ nsSize maxSize(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE);
+ bool collapsed = child->IsXULCollapsed();
+
+ if (!collapsed) {
+ // only one flexible child? Cool we will just make its preferred size
+ // 0 then and not even have to ask for it.
+ // if (flexes != 1) {
+
+ pref = child->GetXULPrefSize(aState);
+ minSize = child->GetXULMinSize(aState);
+ maxSize =
+ nsIFrame::XULBoundsCheckMinMax(minSize, child->GetXULMaxSize(aState));
+ child->GetXULBoxAscent(aState);
+ //}
+
+ pref = nsIFrame::XULBoundsCheck(minSize, pref, maxSize);
+
+ AddXULMargin(child, pref);
+ AddXULMargin(child, minSize);
+ AddXULMargin(child, maxSize);
+ }
+
+ if (!currentBox) {
+ // create one.
+ currentBox = new (aState) nsBoxSize();
+ if (!aBoxSizes) {
+ aBoxSizes = currentBox;
+ last = aBoxSizes;
+ } else {
+ last->next = currentBox;
+ last = currentBox;
+ }
+
+ nscoord minWidth;
+ nscoord maxWidth;
+ nscoord prefWidth;
+
+ // get sizes from child
+ if (isHorizontal) {
+ minWidth = minSize.width;
+ maxWidth = maxSize.width;
+ prefWidth = pref.width;
+ } else {
+ minWidth = minSize.height;
+ maxWidth = maxSize.height;
+ prefWidth = pref.height;
+ }
+
+ nscoord flex = child->GetXULFlex();
+
+ // set them if you collapsed you are not flexible.
+ if (collapsed) {
+ currentBox->flex = 0;
+ } else {
+ if (flex > maxFlex) {
+ maxFlex = flex;
+ }
+ currentBox->flex = flex;
+ }
+
+ currentBox->pref = prefWidth;
+ currentBox->min = minWidth;
+ currentBox->max = maxWidth;
+
+ NS_ASSERTION(minWidth <= prefWidth && prefWidth <= maxWidth,
+ "Bad min, pref, max widths!");
+ }
+
+ if (!isHorizontal) {
+ if (minSize.width > aMinSize) aMinSize = minSize.width;
+
+ if (maxSize.width < aMaxSize) aMaxSize = maxSize.width;
+
+ } else {
+ if (minSize.height > aMinSize) aMinSize = minSize.height;
+
+ if (maxSize.height < aMaxSize) aMaxSize = maxSize.height;
+ }
+
+ currentBox->collapsed = collapsed;
+ aFlexes += currentBox->flex;
+
+ last = currentBox;
+ currentBox = currentBox->next;
+ }
+
+ if (childCount > 0) {
+ nscoord maxAllowedFlex = nscoord_MAX / childCount;
+
+ if (MOZ_UNLIKELY(maxFlex > maxAllowedFlex)) {
+ // clamp all the flexes
+ currentBox = aBoxSizes;
+ while (currentBox) {
+ currentBox->flex = std::min(currentBox->flex, maxAllowedFlex);
+ currentBox = currentBox->next;
+ }
+ }
+ }
+#ifdef DEBUG
+ else {
+ NS_ASSERTION(maxFlex == 0, "How did that happen?");
+ }
+#endif
+}
+
+void nsSprocketLayout::ComputeChildsNextPosition(
+ nsIFrame* aBox, const nscoord& aCurX, const nscoord& aCurY, nscoord& aNextX,
+ nscoord& aNextY, const nsRect& aCurrentChildSize) {
+ // Get the position along the box axis for the child.
+ // The out-of-axis position is not set.
+ nsFrameState frameState = nsFrameState(0);
+ GetFrameState(aBox, frameState);
+
+ if (IsXULHorizontal(aBox)) {
+ // horizontal box's children.
+ if (frameState & NS_STATE_IS_DIRECTION_NORMAL)
+ aNextX = aCurX + aCurrentChildSize.width;
+ else
+ aNextX = aCurX - aCurrentChildSize.width;
+
+ } else {
+ // vertical box's children.
+ if (frameState & NS_STATE_IS_DIRECTION_NORMAL)
+ aNextY = aCurY + aCurrentChildSize.height;
+ else
+ aNextY = aCurY - aCurrentChildSize.height;
+ }
+}
+
+void nsSprocketLayout::AlignChildren(nsIFrame* aBox, nsBoxLayoutState& aState) {
+ nsFrameState frameState = nsFrameState(0);
+ GetFrameState(aBox, frameState);
+ bool isHorizontal = (frameState & NS_STATE_IS_HORIZONTAL) != 0;
+ nsRect clientRect;
+ aBox->GetXULClientRect(clientRect);
+
+ MOZ_ASSERT(!(frameState & NS_STATE_AUTO_STRETCH),
+ "Only AlignChildren() with non-stretch alignment");
+
+ // These are only calculated if needed
+ nsIFrame::Halignment halign;
+ nsIFrame::Valignment valign;
+ nscoord maxAscent = 0;
+ bool isLTR;
+
+ if (isHorizontal) {
+ valign = aBox->GetXULVAlign();
+ if (valign == nsBoxFrame::vAlign_BaseLine) {
+ maxAscent = aBox->GetXULBoxAscent(aState);
+ }
+ } else {
+ isLTR = GetFrameDirection(aBox) == StyleDirection::Ltr;
+ halign = aBox->GetXULHAlign();
+ }
+
+ for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) {
+ nsIFrame* child = iter->get();
+ nsMargin margin;
+ child->GetXULMargin(margin);
+ nsRect childRect = child->GetRect();
+
+ if (isHorizontal) {
+ const nscoord startAlign = clientRect.y + margin.top;
+ const nscoord endAlign =
+ clientRect.YMost() - margin.bottom - childRect.height;
+
+ nscoord y = 0;
+ switch (valign) {
+ case nsBoxFrame::vAlign_Top:
+ y = startAlign;
+ break;
+ case nsBoxFrame::vAlign_Middle:
+ // Should this center the border box?
+ // This centers the margin box, the historical behavior.
+ y = (startAlign + endAlign) / 2;
+ break;
+ case nsBoxFrame::vAlign_Bottom:
+ y = endAlign;
+ break;
+ case nsBoxFrame::vAlign_BaseLine:
+ // Alignments don't force the box to grow (only sizes do),
+ // so keep the children within the box.
+ y = maxAscent - child->GetXULBoxAscent(aState);
+ y = std::max(startAlign, y);
+ y = std::min(y, endAlign);
+ break;
+ }
+
+ childRect.y = y;
+
+ } else { // vertical box
+ const nscoord leftAlign = clientRect.x + margin.left;
+ const nscoord rightAlign =
+ clientRect.XMost() - margin.right - childRect.width;
+
+ nscoord x = 0;
+ switch (halign) {
+ case nsBoxFrame::hAlign_Left: // start
+ x = isLTR ? leftAlign : rightAlign;
+ break;
+ case nsBoxFrame::hAlign_Center:
+ x = (leftAlign + rightAlign) / 2;
+ break;
+ case nsBoxFrame::hAlign_Right: // end
+ x = isLTR ? rightAlign : leftAlign;
+ break;
+ }
+
+ childRect.x = x;
+ }
+
+ if (childRect.TopLeft() != child->GetPosition()) {
+ child->SetXULBounds(aState, childRect);
+ }
+ }
+}
+
+void nsSprocketLayout::ChildResized(
+ nsIFrame* aBox, nsBoxLayoutState& aState, nsIFrame* aChild,
+ nsBoxSize* aChildBoxSize, nsComputedBoxSize* aChildComputedSize,
+ nsBoxSize* aBoxSizes, nsComputedBoxSize* aComputedBoxSizes,
+ const nsRect& aChildLayoutRect, nsRect& aChildActualRect,
+ nsRect& aContainingRect, int32_t aFlexes, bool& aFinished)
+
+{
+ nsRect childCurrentRect(aChildLayoutRect);
+
+ bool isHorizontal = IsXULHorizontal(aBox);
+ nscoord childLayoutWidth = GET_WIDTH(aChildLayoutRect, isHorizontal);
+ nscoord& childActualWidth = GET_WIDTH(aChildActualRect, isHorizontal);
+ nscoord& containingWidth = GET_WIDTH(aContainingRect, isHorizontal);
+
+ // nscoord childLayoutHeight = GET_HEIGHT(aChildLayoutRect,isHorizontal);
+ nscoord& childActualHeight = GET_HEIGHT(aChildActualRect, isHorizontal);
+ nscoord& containingHeight = GET_HEIGHT(aContainingRect, isHorizontal);
+
+ bool recompute = false;
+
+ // if we are a horizontal box see if the child will fit inside us.
+ if (childActualHeight > containingHeight) {
+ // if we are a horizontal box and the child is bigger than our height
+
+ // ok if the height changed then we need to reflow everyone but us at the
+ // new height so we will set the changed index to be us. And signal that we
+ // need a new pass.
+
+ nsSize min = aChild->GetXULMinSize(aState);
+ nsSize max =
+ nsIFrame::XULBoundsCheckMinMax(min, aChild->GetXULMaxSize(aState));
+ AddXULMargin(aChild, max);
+
+ if (isHorizontal)
+ childActualHeight =
+ max.height < childActualHeight ? max.height : childActualHeight;
+ else
+ childActualHeight =
+ max.width < childActualHeight ? max.width : childActualHeight;
+
+ // only set if it changes
+ if (childActualHeight > containingHeight) {
+ containingHeight = childActualHeight;
+
+ // remember we do not need to clear the resized list because changing the
+ // height of a horizontal box will not affect the width of any of its
+ // children because block flow left to right, top to bottom. Just trust me
+ // on this one.
+ aFinished = false;
+
+ // only recompute if there are flexes.
+ if (aFlexes > 0) {
+ // relayout everything
+ recompute = true;
+ InvalidateComputedSizes(aComputedBoxSizes);
+ nsComputedBoxSize* node = aComputedBoxSizes;
+
+ while (node) {
+ node->resized = false;
+ node = node->next;
+ }
+ }
+ }
+ }
+
+ if (childActualWidth > childLayoutWidth) {
+ nsSize min = aChild->GetXULMinSize(aState);
+ nsSize max =
+ nsIFrame::XULBoundsCheckMinMax(min, aChild->GetXULMaxSize(aState));
+
+ AddXULMargin(aChild, max);
+
+ // our width now becomes the new size
+
+ if (isHorizontal)
+ childActualWidth =
+ max.width < childActualWidth ? max.width : childActualWidth;
+ else
+ childActualWidth =
+ max.height < childActualWidth ? max.height : childActualWidth;
+
+ if (childActualWidth > childLayoutWidth) {
+ aChildComputedSize->size = childActualWidth;
+ aChildBoxSize->min = childActualWidth;
+ if (aChildBoxSize->pref < childActualWidth)
+ aChildBoxSize->pref = childActualWidth;
+ if (aChildBoxSize->max < childActualWidth)
+ aChildBoxSize->max = childActualWidth;
+
+ // if we have flexible elements with us then reflex things. Otherwise we
+ // can skip doing it.
+ if (aFlexes > 0) {
+ InvalidateComputedSizes(aComputedBoxSizes);
+
+ nsComputedBoxSize* node = aComputedBoxSizes;
+ aChildComputedSize->resized = true;
+
+ while (node) {
+ if (node->resized) node->valid = true;
+
+ node = node->next;
+ }
+
+ recompute = true;
+ aFinished = false;
+ } else {
+ containingWidth += aChildComputedSize->size - childLayoutWidth;
+ }
+ }
+ }
+
+ if (recompute)
+ ComputeChildSizes(aBox, aState, containingWidth, aBoxSizes,
+ aComputedBoxSizes);
+
+ if (!childCurrentRect.IsEqualInterior(aChildActualRect)) {
+ // the childRect includes the margin
+ // make sure we remove it before setting
+ // the bounds.
+ nsMargin margin(0, 0, 0, 0);
+ aChild->GetXULMargin(margin);
+ nsRect rect(aChildActualRect);
+ if (rect.width >= margin.left + margin.right &&
+ rect.height >= margin.top + margin.bottom)
+ rect.Deflate(margin);
+
+ aChild->SetXULBounds(aState, rect);
+ aChild->XULLayout(aState);
+ }
+}
+
+void nsSprocketLayout::InvalidateComputedSizes(
+ nsComputedBoxSize* aComputedBoxSizes) {
+ while (aComputedBoxSizes) {
+ aComputedBoxSizes->valid = false;
+ aComputedBoxSizes = aComputedBoxSizes->next;
+ }
+}
+
+void nsSprocketLayout::ComputeChildSizes(
+ nsIFrame* aBox, nsBoxLayoutState& aState, nscoord& aGivenSize,
+ nsBoxSize* aBoxSizes, nsComputedBoxSize*& aComputedBoxSizes) {
+ // nscoord onePixel = aState.PresContext()->IntScaledPixelsToTwips(1);
+
+ int32_t sizeRemaining = aGivenSize;
+ int32_t spacerConstantsRemaining = 0;
+
+ // ----- calculate the spacers constants and the size remaining -----
+
+ if (!aComputedBoxSizes) aComputedBoxSizes = new (aState) nsComputedBoxSize();
+
+ nsBoxSize* boxSizes = aBoxSizes;
+ nsComputedBoxSize* computedBoxSizes = aComputedBoxSizes;
+ int32_t count = 0;
+ int32_t validCount = 0;
+
+ while (boxSizes) {
+ NS_ASSERTION(
+ (boxSizes->min <= boxSizes->pref && boxSizes->pref <= boxSizes->max),
+ "bad pref, min, max size");
+
+ // ignore collapsed children
+ // if (boxSizes->collapsed)
+ // {
+ // computedBoxSizes->valid = true;
+ // computedBoxSizes->size = boxSizes->pref;
+ // validCount++;
+ // boxSizes->flex = 0;
+ // }// else {
+
+ if (computedBoxSizes->valid) {
+ sizeRemaining -= computedBoxSizes->size;
+ validCount++;
+ } else {
+ if (boxSizes->flex == 0) {
+ computedBoxSizes->valid = true;
+ computedBoxSizes->size = boxSizes->pref;
+ validCount++;
+ }
+
+ spacerConstantsRemaining += boxSizes->flex;
+ sizeRemaining -= boxSizes->pref;
+ }
+
+ sizeRemaining -= (boxSizes->left + boxSizes->right);
+
+ //}
+
+ boxSizes = boxSizes->next;
+
+ if (boxSizes && !computedBoxSizes->next)
+ computedBoxSizes->next = new (aState) nsComputedBoxSize();
+
+ computedBoxSizes = computedBoxSizes->next;
+ count++;
+ }
+
+ // everything accounted for?
+ if (validCount < count) {
+ // ----- Ok we are give a size to fit into so stretch or squeeze to fit
+ // ----- Make sure we look at our min and max size
+ bool limit = true;
+ while (limit) {
+ limit = false;
+ boxSizes = aBoxSizes;
+ computedBoxSizes = aComputedBoxSizes;
+
+ while (boxSizes) {
+ // ignore collapsed spacers
+
+ // if (!boxSizes->collapsed) {
+
+ nscoord pref = 0;
+ nscoord max = NS_UNCONSTRAINEDSIZE;
+ nscoord min = 0;
+ nscoord flex = 0;
+
+ pref = boxSizes->pref;
+ min = boxSizes->min;
+ max = boxSizes->max;
+ flex = boxSizes->flex;
+
+ // ----- look at our min and max limits make sure we aren't too small or
+ // too big -----
+ if (!computedBoxSizes->valid) {
+ int32_t newSize = pref + int32_t(int64_t(sizeRemaining) * flex /
+ spacerConstantsRemaining);
+
+ if (newSize <= min) {
+ computedBoxSizes->size = min;
+ computedBoxSizes->valid = true;
+ spacerConstantsRemaining -= flex;
+ sizeRemaining += pref;
+ sizeRemaining -= min;
+ limit = true;
+ } else if (newSize >= max) {
+ computedBoxSizes->size = max;
+ computedBoxSizes->valid = true;
+ spacerConstantsRemaining -= flex;
+ sizeRemaining += pref;
+ sizeRemaining -= max;
+ limit = true;
+ }
+ }
+ // }
+ boxSizes = boxSizes->next;
+ computedBoxSizes = computedBoxSizes->next;
+ }
+ }
+ }
+
+ // ---- once we have removed and min and max issues just stretch us out in the
+ // remaining space
+ // ---- or shrink us. Depends on the size remaining and the spacer constants
+ aGivenSize = 0;
+ boxSizes = aBoxSizes;
+ computedBoxSizes = aComputedBoxSizes;
+
+ while (boxSizes) {
+ // ignore collapsed spacers
+ // if (!(boxSizes && boxSizes->collapsed)) {
+
+ nscoord pref = 0;
+ nscoord flex = 0;
+ pref = boxSizes->pref;
+ flex = boxSizes->flex;
+
+ if (!computedBoxSizes->valid) {
+ computedBoxSizes->size = pref + int32_t(int64_t(sizeRemaining) * flex /
+ spacerConstantsRemaining);
+ computedBoxSizes->valid = true;
+ }
+
+ aGivenSize += (boxSizes->left + boxSizes->right);
+ aGivenSize += computedBoxSizes->size;
+
+ // }
+
+ boxSizes = boxSizes->next;
+ computedBoxSizes = computedBoxSizes->next;
+ }
+}
+
+nsSize nsSprocketLayout::GetXULPrefSize(nsIFrame* aBox,
+ nsBoxLayoutState& aState) {
+ nsSize vpref(0, 0);
+ bool isHorizontal = IsXULHorizontal(aBox);
+
+ // run through all the children and get their min, max, and preferred sizes
+ // return us the size of the box
+
+ for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) {
+ nsIFrame* child = iter->get();
+ // ignore collapsed children
+ if (child->IsXULCollapsed()) {
+ continue;
+ }
+ nsSize pref = child->GetXULPrefSize(aState);
+ AddXULMargin(child, pref);
+ AddLargestSize(vpref, pref, isHorizontal);
+ }
+
+ // now add our border and padding
+ AddXULBorderAndPadding(aBox, vpref);
+
+ return vpref;
+}
+
+nsSize nsSprocketLayout::GetXULMinSize(nsIFrame* aBox,
+ nsBoxLayoutState& aState) {
+ nsSize minSize(0, 0);
+ bool isHorizontal = IsXULHorizontal(aBox);
+
+ // run through all the children and get their min, max, and preferred sizes
+ // return us the size of the box
+
+ for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) {
+ nsIFrame* child = iter->get();
+
+ // ignore collapsed children
+ if (child->IsXULCollapsed()) {
+ continue;
+ }
+
+ nsSize min = child->GetXULMinSize(aState);
+ nsSize pref(0, 0);
+
+ // if the child is not flexible then
+ // its min size is its pref size.
+ if (child->GetXULFlex() == 0) {
+ pref = child->GetXULPrefSize(aState);
+ if (isHorizontal)
+ min.width = pref.width;
+ else
+ min.height = pref.height;
+ }
+
+ AddXULMargin(child, min);
+ AddLargestSize(minSize, min, isHorizontal);
+ }
+
+ // now add our border and padding
+ AddXULBorderAndPadding(aBox, minSize);
+
+ return minSize;
+}
+
+nsSize nsSprocketLayout::GetXULMaxSize(nsIFrame* aBox,
+ nsBoxLayoutState& aState) {
+ bool isHorizontal = IsXULHorizontal(aBox);
+
+ nsSize maxSize(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE);
+
+ // run through all the children and get their min, max, and preferred sizes
+ // return us the size of the box
+
+ for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) {
+ nsIFrame* child = iter->get();
+
+ // ignore collapsed children
+ if (child->IsXULCollapsed()) {
+ continue;
+ }
+ // if completely redefined don't even ask our child for its size.
+ nsSize min = child->GetXULMinSize(aState);
+ nsSize max =
+ nsIFrame::XULBoundsCheckMinMax(min, child->GetXULMaxSize(aState));
+
+ AddXULMargin(child, max);
+ AddSmallestSize(maxSize, max, isHorizontal);
+ }
+
+ // now add our border and padding
+ AddXULBorderAndPadding(aBox, maxSize);
+
+ return maxSize;
+}
+
+nscoord nsSprocketLayout::GetAscent(nsIFrame* aBox, nsBoxLayoutState& aState) {
+ nscoord vAscent = 0;
+
+ bool isHorizontal = IsXULHorizontal(aBox);
+
+ // run through all the children and get their min, max, and preferred sizes
+ // return us the size of the box
+
+ for (auto iter = IterFor(aBox); iter && !iter->AtEnd(); iter->Next()) {
+ nsIFrame* child = iter->get();
+
+ // ignore collapsed children
+ // if (!child->IsXULCollapsed())
+ //{
+ // if completely redefined don't even ask our child for its size.
+ nscoord ascent = child->GetXULBoxAscent(aState);
+
+ nsMargin margin;
+ child->GetXULMargin(margin);
+ ascent += margin.top;
+
+ if (isHorizontal) {
+ if (ascent > vAscent) vAscent = ascent;
+ } else {
+ if (vAscent == 0) vAscent = ascent;
+ }
+ //}
+
+ child = nsIFrame::GetNextXULBox(child);
+ }
+
+ nsMargin borderPadding;
+ aBox->GetXULBorderAndPadding(borderPadding);
+
+ return vAscent + borderPadding.top;
+}
+
+void nsSprocketLayout::SetLargestSize(nsSize& aSize1, const nsSize& aSize2,
+ bool aIsHorizontal) {
+ if (aIsHorizontal) {
+ if (aSize1.height < aSize2.height) aSize1.height = aSize2.height;
+ } else {
+ if (aSize1.width < aSize2.width) aSize1.width = aSize2.width;
+ }
+}
+
+void nsSprocketLayout::SetSmallestSize(nsSize& aSize1, const nsSize& aSize2,
+ bool aIsHorizontal) {
+ if (aIsHorizontal) {
+ if (aSize1.height > aSize2.height) aSize1.height = aSize2.height;
+ } else {
+ if (aSize1.width > aSize2.width) aSize1.width = aSize2.width;
+ }
+}
+
+void nsSprocketLayout::AddLargestSize(nsSize& aSize, const nsSize& aSizeToAdd,
+ bool aIsHorizontal) {
+ if (aIsHorizontal)
+ AddCoord(aSize.width, aSizeToAdd.width);
+ else
+ AddCoord(aSize.height, aSizeToAdd.height);
+
+ SetLargestSize(aSize, aSizeToAdd, aIsHorizontal);
+}
+
+void nsSprocketLayout::AddCoord(nscoord& aCoord, nscoord aCoordToAdd) {
+ if (aCoord != NS_UNCONSTRAINEDSIZE) {
+ if (aCoordToAdd == NS_UNCONSTRAINEDSIZE)
+ aCoord = aCoordToAdd;
+ else
+ aCoord += aCoordToAdd;
+ }
+}
+void nsSprocketLayout::AddSmallestSize(nsSize& aSize, const nsSize& aSizeToAdd,
+ bool aIsHorizontal) {
+ if (aIsHorizontal)
+ AddCoord(aSize.width, aSizeToAdd.width);
+ else
+ AddCoord(aSize.height, aSizeToAdd.height);
+
+ SetSmallestSize(aSize, aSizeToAdd, aIsHorizontal);
+}
+
+bool nsSprocketLayout::GetDefaultFlex(int32_t& aFlex) {
+ aFlex = 0;
+ return true;
+}
+
+nsComputedBoxSize::nsComputedBoxSize() {
+ resized = false;
+ valid = false;
+ size = 0;
+ next = nullptr;
+}
+
+nsBoxSize::nsBoxSize() {
+ pref = 0;
+ min = 0;
+ max = NS_UNCONSTRAINEDSIZE;
+ collapsed = false;
+ left = 0;
+ right = 0;
+ flex = 0;
+ next = nullptr;
+ bogus = false;
+}
+
+void* nsBoxSize::operator new(size_t sz,
+ nsBoxLayoutState& aState) noexcept(true) {
+ return mozilla::AutoStackArena::Allocate(sz);
+}
+
+void nsBoxSize::operator delete(void* aPtr, size_t sz) {}
+
+void* nsComputedBoxSize::operator new(size_t sz,
+ nsBoxLayoutState& aState) noexcept(true) {
+ return mozilla::AutoStackArena::Allocate(sz);
+}
+
+void nsComputedBoxSize::operator delete(void* aPtr, size_t sz) {}
diff --git a/layout/xul/nsSprocketLayout.h b/layout/xul/nsSprocketLayout.h
new file mode 100644
index 0000000000..e1e7fe377e
--- /dev/null
+++ b/layout/xul/nsSprocketLayout.h
@@ -0,0 +1,156 @@
+/* -*- 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 nsSprocketLayout_h___
+#define nsSprocketLayout_h___
+
+#include "mozilla/Attributes.h"
+#include "nsBoxLayout.h"
+#include "nsCOMPtr.h"
+#include "nsFrameState.h"
+
+class nsIFrame;
+struct nsRect;
+
+class nsBoxSize {
+ public:
+ nsBoxSize();
+
+ nscoord pref;
+ nscoord min;
+ nscoord max;
+ nscoord flex;
+ nscoord left;
+ nscoord right;
+ bool collapsed;
+ bool bogus;
+
+ nsBoxSize* next;
+
+ void* operator new(size_t sz, nsBoxLayoutState& aState) noexcept(true);
+ void operator delete(void* aPtr, size_t sz);
+};
+
+class nsComputedBoxSize {
+ public:
+ nsComputedBoxSize();
+
+ nscoord size;
+ bool valid;
+ bool resized;
+ nsComputedBoxSize* next;
+
+ void* operator new(size_t sz, nsBoxLayoutState& aState) noexcept(true);
+ void operator delete(void* aPtr, size_t sz);
+};
+
+#define GET_WIDTH(size, isHorizontal) (isHorizontal ? size.width : size.height)
+#define GET_HEIGHT(size, isHorizontal) (isHorizontal ? size.height : size.width)
+#define GET_X(size, isHorizontal) (isHorizontal ? size.x : size.y)
+#define GET_Y(size, isHorizontal) (isHorizontal ? size.y : size.x)
+#define GET_COORD(aX, aY, isHorizontal) (isHorizontal ? aX : aY)
+
+#define SET_WIDTH(size, coord, isHorizontal) \
+ if (isHorizontal) { \
+ (size).width = (coord); \
+ } else { \
+ (size).height = (coord); \
+ }
+#define SET_HEIGHT(size, coord, isHorizontal) \
+ if (isHorizontal) { \
+ (size).height = (coord); \
+ } else { \
+ (size).width = (coord); \
+ }
+#define SET_X(size, coord, isHorizontal) \
+ if (isHorizontal) { \
+ (size).x = (coord); \
+ } else { \
+ (size).y = (coord); \
+ }
+#define SET_Y(size, coord, isHorizontal) \
+ if (isHorizontal) { \
+ (size).y = (coord); \
+ } else { \
+ (size).x = (coord); \
+ }
+
+#define SET_COORD(aX, aY, coord, isHorizontal) \
+ if (isHorizontal) { \
+ aX = (coord); \
+ } else { \
+ aY = (coord); \
+ }
+
+nsresult NS_NewSprocketLayout(nsCOMPtr<nsBoxLayout>& aNewLayout);
+
+class nsSprocketLayout : public nsBoxLayout {
+ public:
+ friend nsresult NS_NewSprocketLayout(nsCOMPtr<nsBoxLayout>& aNewLayout);
+ static void Shutdown();
+
+ NS_IMETHOD XULLayout(nsIFrame* aBox, nsBoxLayoutState& aState) override;
+
+ virtual nsSize GetXULPrefSize(nsIFrame* aBox,
+ nsBoxLayoutState& aBoxLayoutState) override;
+ virtual nsSize GetXULMinSize(nsIFrame* aBox,
+ nsBoxLayoutState& aBoxLayoutState) override;
+ virtual nsSize GetXULMaxSize(nsIFrame* aBox,
+ nsBoxLayoutState& aBoxLayoutState) override;
+ virtual nscoord GetAscent(nsIFrame* aBox,
+ nsBoxLayoutState& aBoxLayoutState) override;
+
+ nsSprocketLayout();
+
+ static bool IsXULHorizontal(nsIFrame* aBox);
+
+ static void SetLargestSize(nsSize& aSize1, const nsSize& aSize2,
+ bool aIsHorizontal);
+ static void SetSmallestSize(nsSize& aSize1, const nsSize& aSize2,
+ bool aIsHorizontal);
+
+ static void AddLargestSize(nsSize& aSize, const nsSize& aSizeToAdd,
+ bool aIsHorizontal);
+ static void AddSmallestSize(nsSize& aSize, const nsSize& aSizeToAdd,
+ bool aIsHorizontal);
+ static void AddCoord(nscoord& aCoord, nscoord aCoordToAdd);
+
+ protected:
+ void ComputeChildsNextPosition(nsIFrame* aBox, const nscoord& aCurX,
+ const nscoord& aCurY, nscoord& aNextX,
+ nscoord& aNextY, const nsRect& aChildSize);
+
+ void ChildResized(nsIFrame* aBox, nsBoxLayoutState& aState, nsIFrame* aChild,
+ nsBoxSize* aChildBoxSize,
+ nsComputedBoxSize* aChildComputedBoxSize,
+ nsBoxSize* aBoxSizes, nsComputedBoxSize* aComputedBoxSizes,
+ const nsRect& aChildLayoutRect, nsRect& aChildActualRect,
+ nsRect& aContainingRect, int32_t aFlexes, bool& aFinished);
+
+ void AlignChildren(nsIFrame* aBox, nsBoxLayoutState& aState);
+
+ virtual void ComputeChildSizes(nsIFrame* aBox, nsBoxLayoutState& aState,
+ nscoord& aGivenSize, nsBoxSize* aBoxSizes,
+ nsComputedBoxSize*& aComputedBoxSizes);
+
+ virtual void PopulateBoxSizes(nsIFrame* aBox,
+ nsBoxLayoutState& aBoxLayoutState,
+ nsBoxSize*& aBoxSizes, nscoord& aMinSize,
+ nscoord& aMaxSize, int32_t& aFlexes);
+
+ virtual void InvalidateComputedSizes(nsComputedBoxSize* aComputedBoxSizes);
+
+ virtual bool GetDefaultFlex(int32_t& aFlex);
+
+ virtual void GetFrameState(nsIFrame* aBox, nsFrameState& aState);
+
+ private:
+ // because the sprocket layout manager has no instance variables. We
+ // can make a static one and reuse it everywhere.
+ static nsBoxLayout* gInstance;
+};
+
+#endif
diff --git a/layout/xul/nsTextBoxFrame.cpp b/layout/xul/nsTextBoxFrame.cpp
new file mode 100644
index 0000000000..d31b8418da
--- /dev/null
+++ b/layout/xul/nsTextBoxFrame.cpp
@@ -0,0 +1,1055 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=4 sw=2 sts=2 et cindent: */
+/* 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 "nsTextBoxFrame.h"
+
+#include "gfx2DGlue.h"
+#include "gfxUtils.h"
+#include "mozilla/intl/BidiEmbeddingLevel.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/intl/Segmenter.h"
+#include "mozilla/layers/RenderRootStateManager.h"
+#include "mozilla/gfx/2D.h"
+#include "nsFontMetrics.h"
+#include "nsReadableUtils.h"
+#include "nsCOMPtr.h"
+#include "nsCRT.h"
+#include "nsGkAtoms.h"
+#include "nsPresContext.h"
+#include "gfxContext.h"
+#include "nsIContent.h"
+#include "nsNameSpaceManager.h"
+#include "nsBoxLayoutState.h"
+#include "nsMenuBarListener.h"
+#include "nsString.h"
+#include "nsITheme.h"
+#include "nsUnicharUtils.h"
+#include "nsContentUtils.h"
+#include "nsDisplayList.h"
+#include "nsCSSRendering.h"
+#include "nsIReflowCallback.h"
+#include "nsBoxFrame.h"
+#include "nsLayoutUtils.h"
+#include "TextDrawTarget.h"
+
+#ifdef ACCESSIBILITY
+# include "nsAccessibilityService.h"
+#endif
+
+#include "nsBidiUtils.h"
+#include "nsBidiPresUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+class nsAccessKeyInfo {
+ public:
+ int32_t mAccesskeyIndex;
+ nscoord mBeforeWidth, mAccessWidth, mAccessUnderlineSize, mAccessOffset;
+};
+
+bool nsTextBoxFrame::gAlwaysAppendAccessKey = false;
+bool nsTextBoxFrame::gAccessKeyPrefInitialized = false;
+bool nsTextBoxFrame::gInsertSeparatorBeforeAccessKey = false;
+bool nsTextBoxFrame::gInsertSeparatorPrefInitialized = false;
+
+nsIFrame* NS_NewTextBoxFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsTextBoxFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsTextBoxFrame)
+
+NS_QUERYFRAME_HEAD(nsTextBoxFrame)
+ NS_QUERYFRAME_ENTRY(nsTextBoxFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsLeafBoxFrame)
+
+nsresult nsTextBoxFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ bool aResize;
+ bool aRedraw;
+
+ UpdateAttributes(aAttribute, aResize, aRedraw);
+
+ if (aResize) {
+ PresShell()->FrameNeedsReflow(
+ this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
+ } else if (aRedraw) {
+ nsBoxLayoutState state(PresContext());
+ XULRedraw(state);
+ }
+
+ return NS_OK;
+}
+
+nsTextBoxFrame::nsTextBoxFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsLeafBoxFrame(aStyle, aPresContext, kClassID),
+ mAccessKeyInfo(nullptr),
+ mCropType(CropRight),
+ mAscent(0),
+ mNeedsReflowCallback(false) {
+ MarkIntrinsicISizesDirty();
+}
+
+nsTextBoxFrame::~nsTextBoxFrame() { delete mAccessKeyInfo; }
+
+void nsTextBoxFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsLeafBoxFrame::Init(aContent, aParent, aPrevInFlow);
+
+ bool aResize;
+ bool aRedraw;
+ UpdateAttributes(nullptr, aResize, aRedraw); /* update all */
+}
+
+bool nsTextBoxFrame::AlwaysAppendAccessKey() {
+ if (!gAccessKeyPrefInitialized) {
+ gAccessKeyPrefInitialized = true;
+
+ const char* prefName = "intl.menuitems.alwaysappendaccesskeys";
+ nsAutoString val;
+ Preferences::GetLocalizedString(prefName, val);
+ gAlwaysAppendAccessKey = val.EqualsLiteral("true");
+ }
+ return gAlwaysAppendAccessKey;
+}
+
+bool nsTextBoxFrame::InsertSeparatorBeforeAccessKey() {
+ if (!gInsertSeparatorPrefInitialized) {
+ gInsertSeparatorPrefInitialized = true;
+
+ const char* prefName = "intl.menuitems.insertseparatorbeforeaccesskeys";
+ nsAutoString val;
+ Preferences::GetLocalizedString(prefName, val);
+ gInsertSeparatorBeforeAccessKey = val.EqualsLiteral("true");
+ }
+ return gInsertSeparatorBeforeAccessKey;
+}
+
+class nsAsyncAccesskeyUpdate final : public nsIReflowCallback {
+ public:
+ explicit nsAsyncAccesskeyUpdate(nsIFrame* aFrame) : mWeakFrame(aFrame) {}
+
+ virtual bool ReflowFinished() override {
+ bool shouldFlush = false;
+ nsTextBoxFrame* frame = static_cast<nsTextBoxFrame*>(mWeakFrame.GetFrame());
+ if (frame) {
+ shouldFlush = frame->UpdateAccesskey(mWeakFrame);
+ }
+ delete this;
+ return shouldFlush;
+ }
+
+ virtual void ReflowCallbackCanceled() override { delete this; }
+
+ WeakFrame mWeakFrame;
+};
+
+bool nsTextBoxFrame::UpdateAccesskey(WeakFrame& aWeakThis) {
+ nsAutoString accesskey;
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
+ accesskey);
+
+ if (!accesskey.Equals(mAccessKey)) {
+ // Need to get clean mTitle.
+ RecomputeTitle();
+ mAccessKey = accesskey;
+ UpdateAccessTitle();
+ PresShell()->FrameNeedsReflow(
+ this, IntrinsicDirty::FrameAncestorsAndDescendants, NS_FRAME_IS_DIRTY);
+ return true;
+ }
+ return false;
+}
+
+void nsTextBoxFrame::UpdateAttributes(nsAtom* aAttribute, bool& aResize,
+ bool& aRedraw) {
+ bool doUpdateTitle = false;
+ aResize = false;
+ aRedraw = false;
+
+ if (aAttribute == nullptr || aAttribute == nsGkAtoms::crop) {
+ static dom::Element::AttrValuesArray strings[] = {
+ nsGkAtoms::left, nsGkAtoms::start, nsGkAtoms::center,
+ nsGkAtoms::right, nsGkAtoms::end, nsGkAtoms::none,
+ nullptr};
+ CroppingStyle cropType;
+ switch (mContent->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::crop, strings, eCaseMatters)) {
+ case 0:
+ case 1:
+ cropType = CropLeft;
+ break;
+ case 2:
+ cropType = CropCenter;
+ break;
+ case 3:
+ case 4:
+ cropType = CropRight;
+ break;
+ case 5:
+ cropType = CropNone;
+ break;
+ default:
+ cropType = CropAuto;
+ break;
+ }
+
+ if (cropType != mCropType) {
+ aResize = true;
+ mCropType = cropType;
+ }
+ }
+
+ if (aAttribute == nullptr || aAttribute == nsGkAtoms::value) {
+ RecomputeTitle();
+ doUpdateTitle = true;
+ }
+
+ if (aAttribute == nullptr || aAttribute == nsGkAtoms::accesskey) {
+ mNeedsReflowCallback = true;
+ // Ensure that layout is refreshed and reflow callback called.
+ aResize = true;
+ }
+
+ if (doUpdateTitle) {
+ UpdateAccessTitle();
+ aResize = true;
+ }
+}
+
+namespace mozilla {
+
+class nsDisplayXULTextBox final : public nsPaintedDisplayItem {
+ public:
+ nsDisplayXULTextBox(nsDisplayListBuilder* aBuilder, nsTextBoxFrame* aFrame)
+ : nsPaintedDisplayItem(aBuilder, aFrame) {
+ MOZ_COUNT_CTOR(nsDisplayXULTextBox);
+ }
+#ifdef NS_BUILD_REFCNT_LOGGING
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayXULTextBox)
+#endif
+
+ virtual void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override;
+ virtual nsRect GetBounds(nsDisplayListBuilder* aBuilder,
+ bool* aSnap) const override;
+ NS_DISPLAY_DECL_NAME("XULTextBox", TYPE_XUL_TEXT_BOX)
+
+ virtual nsRect GetComponentAlphaBounds(
+ nsDisplayListBuilder* aBuilder) const override;
+
+ void PaintTextToContext(gfxContext* aCtx, nsPoint aOffset,
+ const nscolor* aColor);
+
+ virtual bool CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) override;
+};
+
+static void PaintTextShadowCallback(gfxContext* aCtx, nsPoint aShadowOffset,
+ const nscolor& aShadowColor, void* aData) {
+ reinterpret_cast<nsDisplayXULTextBox*>(aData)->PaintTextToContext(
+ aCtx, aShadowOffset, &aShadowColor);
+}
+
+void nsDisplayXULTextBox::Paint(nsDisplayListBuilder* aBuilder,
+ gfxContext* aCtx) {
+ // Paint the text shadow before doing any foreground stuff
+ nsRect drawRect =
+ static_cast<nsTextBoxFrame*>(mFrame)->mTextDrawRect + ToReferenceFrame();
+ nsLayoutUtils::PaintTextShadow(mFrame, aCtx, drawRect,
+ GetPaintRect(aBuilder, aCtx),
+ mFrame->StyleText()->mColor.ToColor(),
+ PaintTextShadowCallback, (void*)this);
+
+ PaintTextToContext(aCtx, nsPoint(0, 0), nullptr);
+}
+
+void nsDisplayXULTextBox::PaintTextToContext(gfxContext* aCtx, nsPoint aOffset,
+ const nscolor* aColor) {
+ static_cast<nsTextBoxFrame*>(mFrame)->PaintTitle(
+ *aCtx, mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame(),
+ ToReferenceFrame() + aOffset, aColor);
+}
+
+bool nsDisplayXULTextBox::CreateWebRenderCommands(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager,
+ nsDisplayListBuilder* aDisplayListBuilder) {
+ bool snap = false;
+ auto bounds = GetBounds(aDisplayListBuilder, &snap);
+
+ if (bounds.IsEmpty()) {
+ return true;
+ }
+
+ auto appUnitsPerDevPixel = Frame()->PresContext()->AppUnitsPerDevPixel();
+ gfx::Point deviceOffset =
+ LayoutDevicePoint::FromAppUnits(bounds.TopLeft(), appUnitsPerDevPixel)
+ .ToUnknownPoint();
+
+ RefPtr<mozilla::layout::TextDrawTarget> textDrawer =
+ new mozilla::layout::TextDrawTarget(aBuilder, aResources, aSc, aManager,
+ this, bounds);
+ RefPtr<gfxContext> captureCtx =
+ gfxContext::CreateOrNull(textDrawer, deviceOffset);
+
+ Paint(aDisplayListBuilder, captureCtx);
+ textDrawer->TerminateShadows();
+
+ return textDrawer->Finish();
+}
+
+nsRect nsDisplayXULTextBox::GetBounds(nsDisplayListBuilder* aBuilder,
+ bool* aSnap) const {
+ *aSnap = false;
+ return mFrame->InkOverflowRectRelativeToSelf() + ToReferenceFrame();
+}
+
+nsRect nsDisplayXULTextBox::GetComponentAlphaBounds(
+ nsDisplayListBuilder* aBuilder) const {
+ return static_cast<nsTextBoxFrame*>(mFrame)->GetComponentAlphaBounds() +
+ ToReferenceFrame();
+}
+
+} // namespace mozilla
+
+void nsTextBoxFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ if (!IsVisibleForPainting()) return;
+
+ nsLeafBoxFrame::BuildDisplayList(aBuilder, aLists);
+
+ aLists.Content()->AppendNewToTop<nsDisplayXULTextBox>(aBuilder, this);
+}
+
+void nsTextBoxFrame::PaintTitle(gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nsPoint aPt,
+ const nscolor* aOverrideColor) {
+ if (mTitle.IsEmpty()) return;
+
+ DrawText(aRenderingContext, aDirtyRect, mTextDrawRect + aPt, aOverrideColor);
+}
+
+void nsTextBoxFrame::DrawText(gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, const nsRect& aTextRect,
+ const nscolor* aOverrideColor) {
+ nsPresContext* presContext = PresContext();
+ int32_t appUnitsPerDevPixel = presContext->AppUnitsPerDevPixel();
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+
+ // paint the title
+ nscolor overColor = 0;
+ nscolor underColor = 0;
+ nscolor strikeColor = 0;
+ auto overStyle = StyleTextDecorationStyle::None;
+ auto underStyle = StyleTextDecorationStyle::None;
+ auto strikeStyle = StyleTextDecorationStyle::None;
+
+ // Begin with no decorations
+ auto decorations = StyleTextDecorationLine::NONE;
+ // A mask of all possible line decorations.
+ auto decorMask = StyleTextDecorationLine::UNDERLINE |
+ StyleTextDecorationLine::OVERLINE |
+ StyleTextDecorationLine::LINE_THROUGH;
+
+ WritingMode wm = GetWritingMode();
+ bool vertical = wm.IsVertical();
+
+ nsIFrame* f = this;
+ do { // find decoration colors
+ ComputedStyle* context = f->Style();
+ if (!context->HasTextDecorationLines()) {
+ break;
+ }
+ const nsStyleTextReset* styleText = context->StyleTextReset();
+
+ // a decoration defined here
+ if (decorMask & styleText->mTextDecorationLine) {
+ nscolor color;
+ if (aOverrideColor) {
+ color = *aOverrideColor;
+ } else {
+ color = styleText->mTextDecorationColor.CalcColor(*context);
+ }
+ const auto style = styleText->mTextDecorationStyle;
+
+ if (StyleTextDecorationLine::UNDERLINE & decorMask &
+ styleText->mTextDecorationLine) {
+ underColor = color;
+ underStyle = style;
+ decorMask &= ~StyleTextDecorationLine::UNDERLINE;
+ decorations |= StyleTextDecorationLine::UNDERLINE;
+ }
+ if (StyleTextDecorationLine::OVERLINE & decorMask &
+ styleText->mTextDecorationLine) {
+ overColor = color;
+ overStyle = style;
+ decorMask &= ~StyleTextDecorationLine::OVERLINE;
+ decorations |= StyleTextDecorationLine::OVERLINE;
+ }
+ if (StyleTextDecorationLine::LINE_THROUGH & decorMask &
+ styleText->mTextDecorationLine) {
+ strikeColor = color;
+ strikeStyle = style;
+ decorMask &= ~StyleTextDecorationLine::LINE_THROUGH;
+ decorations |= StyleTextDecorationLine::LINE_THROUGH;
+ }
+ }
+ } while (decorMask && (f = nsLayoutUtils::GetParentOrPlaceholderFor(f)));
+
+ RefPtr<nsFontMetrics> fontMet =
+ nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
+ fontMet->SetVertical(wm.IsVertical());
+ fontMet->SetTextOrientation(StyleVisibility()->mTextOrientation);
+
+ nscoord offset;
+ nscoord size;
+ nscoord ascent = fontMet->MaxAscent();
+
+ nsPoint baselinePt;
+ if (wm.IsVertical()) {
+ baselinePt.x = presContext->RoundAppUnitsToNearestDevPixels(
+ aTextRect.x + (wm.IsVerticalRL() ? aTextRect.width - ascent : ascent));
+ baselinePt.y = aTextRect.y;
+ } else {
+ baselinePt.x = aTextRect.x;
+ baselinePt.y =
+ presContext->RoundAppUnitsToNearestDevPixels(aTextRect.y + ascent);
+ }
+
+ nsCSSRendering::PaintDecorationLineParams params;
+ params.dirtyRect = ToRect(presContext->AppUnitsToGfxUnits(aDirtyRect));
+ params.pt = Point(presContext->AppUnitsToGfxUnits(aTextRect.x),
+ presContext->AppUnitsToGfxUnits(aTextRect.y));
+ params.icoordInFrame =
+ Float(PresContext()->AppUnitsToGfxUnits(mTextDrawRect.x));
+ params.lineSize = Size(presContext->AppUnitsToGfxUnits(aTextRect.width), 0);
+ params.ascent = presContext->AppUnitsToGfxUnits(ascent);
+ params.vertical = vertical;
+
+ // XXX todo: vertical-mode support for decorations not tested yet,
+ // probably won't be positioned correctly
+
+ // Underlines are drawn before overlines, and both before the text
+ // itself, per http://www.w3.org/TR/CSS21/zindex.html point 7.2.1.4.1.1.
+ // (We don't apply this rule to the access-key underline because we only
+ // find out where that is as a side effect of drawing the text, in the
+ // general case -- see below.)
+ if (decorations & (StyleTextDecorationLine::OVERLINE |
+ StyleTextDecorationLine::UNDERLINE)) {
+ fontMet->GetUnderline(offset, size);
+ params.lineSize.height = presContext->AppUnitsToGfxUnits(size);
+ if ((decorations & StyleTextDecorationLine::UNDERLINE) &&
+ underStyle != StyleTextDecorationStyle::None) {
+ params.color = underColor;
+ params.offset = presContext->AppUnitsToGfxUnits(offset);
+ params.decoration = StyleTextDecorationLine::UNDERLINE;
+ params.style = underStyle;
+ nsCSSRendering::PaintDecorationLine(this, *drawTarget, params);
+ }
+ if ((decorations & StyleTextDecorationLine::OVERLINE) &&
+ overStyle != StyleTextDecorationStyle::None) {
+ params.color = overColor;
+ params.offset = params.ascent;
+ params.decoration = StyleTextDecorationLine::OVERLINE;
+ params.style = overStyle;
+ nsCSSRendering::PaintDecorationLine(this, *drawTarget, params);
+ }
+ }
+
+ RefPtr<gfxContext> refContext =
+ PresShell()->CreateReferenceRenderingContext();
+ DrawTarget* refDrawTarget = refContext->GetDrawTarget();
+
+ CalculateUnderline(refDrawTarget, *fontMet);
+
+ DeviceColor color = ToDeviceColor(
+ aOverrideColor ? *aOverrideColor : StyleText()->mColor.ToColor());
+ ColorPattern colorPattern(color);
+ aRenderingContext.SetDeviceColor(color);
+
+ nsresult rv = NS_ERROR_FAILURE;
+
+ if (mState & NS_FRAME_IS_BIDI) {
+ presContext->SetBidiEnabled();
+ mozilla::intl::BidiEmbeddingLevel level =
+ nsBidiPresUtils::BidiLevelFromStyle(Style());
+ if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) {
+ // We let the RenderText function calculate the mnemonic's
+ // underline position for us.
+ nsBidiPositionResolve posResolve;
+ posResolve.logicalIndex = mAccessKeyInfo->mAccesskeyIndex;
+ rv = nsBidiPresUtils::RenderText(
+ mCroppedTitle.get(), mCroppedTitle.Length(), level, presContext,
+ aRenderingContext, refDrawTarget, *fontMet, baselinePt.x,
+ baselinePt.y, &posResolve, 1);
+ mAccessKeyInfo->mBeforeWidth = posResolve.visualLeftTwips;
+ mAccessKeyInfo->mAccessWidth = posResolve.visualWidth;
+ } else {
+ rv = nsBidiPresUtils::RenderText(
+ mCroppedTitle.get(), mCroppedTitle.Length(), level, presContext,
+ aRenderingContext, refDrawTarget, *fontMet, baselinePt.x,
+ baselinePt.y);
+ }
+ }
+ if (NS_FAILED(rv)) {
+ fontMet->SetTextRunRTL(false);
+
+ if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) {
+ // In the simple (non-BiDi) case, we calculate the mnemonic's
+ // underline position by getting the text metric.
+ // XXX are attribute values always two byte?
+ if (mAccessKeyInfo->mAccesskeyIndex > 0)
+ mAccessKeyInfo->mBeforeWidth = nsLayoutUtils::AppUnitWidthOfString(
+ mCroppedTitle.get(), mAccessKeyInfo->mAccesskeyIndex, *fontMet,
+ refDrawTarget);
+ else
+ mAccessKeyInfo->mBeforeWidth = 0;
+ }
+
+ fontMet->DrawString(mCroppedTitle.get(), mCroppedTitle.Length(),
+ baselinePt.x, baselinePt.y, &aRenderingContext,
+ refDrawTarget);
+ }
+
+ if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) {
+ nsRect r(aTextRect.x + mAccessKeyInfo->mBeforeWidth,
+ aTextRect.y + mAccessKeyInfo->mAccessOffset,
+ mAccessKeyInfo->mAccessWidth,
+ mAccessKeyInfo->mAccessUnderlineSize);
+ Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget);
+ drawTarget->FillRect(devPxRect, colorPattern);
+ }
+
+ // Strikeout is drawn on top of the text, per
+ // http://www.w3.org/TR/CSS21/zindex.html point 7.2.1.4.1.1.
+ if ((decorations & StyleTextDecorationLine::LINE_THROUGH) &&
+ strikeStyle != StyleTextDecorationStyle::None) {
+ fontMet->GetStrikeout(offset, size);
+ params.color = strikeColor;
+ params.lineSize.height = presContext->AppUnitsToGfxUnits(size);
+ params.offset = presContext->AppUnitsToGfxUnits(offset);
+ params.decoration = StyleTextDecorationLine::LINE_THROUGH;
+ params.style = strikeStyle;
+ nsCSSRendering::PaintDecorationLine(this, *drawTarget, params);
+ }
+}
+
+void nsTextBoxFrame::CalculateUnderline(DrawTarget* aDrawTarget,
+ nsFontMetrics& aFontMetrics) {
+ if (mAccessKeyInfo && mAccessKeyInfo->mAccesskeyIndex != kNotFound) {
+ // Calculate all fields of mAccessKeyInfo which
+ // are the same for both BiDi and non-BiDi frames.
+ const char16_t* titleString = mCroppedTitle.get();
+ aFontMetrics.SetTextRunRTL(false);
+ mAccessKeyInfo->mAccessWidth = nsLayoutUtils::AppUnitWidthOfString(
+ titleString[mAccessKeyInfo->mAccesskeyIndex], aFontMetrics,
+ aDrawTarget);
+
+ nscoord offset, baseline;
+ aFontMetrics.GetUnderline(offset, mAccessKeyInfo->mAccessUnderlineSize);
+ baseline = aFontMetrics.MaxAscent();
+ mAccessKeyInfo->mAccessOffset = baseline - offset;
+ }
+}
+
+void nsTextBoxFrame::CropStringForWidth(nsAString& aText,
+ gfxContext& aRenderingContext,
+ nsFontMetrics& aFontMetrics,
+ nscoord aWidth,
+ CroppingStyle aCropType) {
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+
+ // See if the width is even smaller than the ellipsis
+ // If so, clear the text completely.
+ const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis();
+ aFontMetrics.SetTextRunRTL(false);
+ nscoord ellipsisWidth =
+ nsLayoutUtils::AppUnitWidthOfString(kEllipsis, aFontMetrics, drawTarget);
+
+ if (ellipsisWidth > aWidth) {
+ aText.Truncate(0);
+ return;
+ }
+ if (ellipsisWidth == aWidth) {
+ aText.Assign(kEllipsis);
+ return;
+ }
+
+ // We will be drawing an ellipsis, thank you very much.
+ // Subtract out the required width of the ellipsis.
+ // This is the total remaining width we have to play with.
+ aWidth -= ellipsisWidth;
+
+ using mozilla::intl::GraphemeClusterBreakIteratorUtf16;
+ using mozilla::intl::GraphemeClusterBreakReverseIteratorUtf16;
+
+ // Now we crop. This is quite basic: it will not be really accurate in the
+ // presence of complex scripts with contextual shaping, etc., as it measures
+ // each grapheme cluster in isolation, not in its proper context.
+ switch (aCropType) {
+ case CropAuto:
+ case CropNone:
+ case CropRight: {
+ const Span text(aText);
+ GraphemeClusterBreakIteratorUtf16 iter(text);
+ uint32_t pos = 0;
+ nscoord totalWidth = 0;
+
+ while (Maybe<uint32_t> nextPos = iter.Next()) {
+ const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString(
+ text.FromTo(pos, *nextPos), aFontMetrics, drawTarget);
+ if (totalWidth + charWidth > aWidth) {
+ break;
+ }
+ pos = *nextPos;
+ totalWidth += charWidth;
+ }
+
+ if (pos < aText.Length()) {
+ aText.Replace(pos, aText.Length() - pos, kEllipsis);
+ }
+ } break;
+
+ case CropLeft: {
+ const Span text(aText);
+ GraphemeClusterBreakReverseIteratorUtf16 iter(text);
+ uint32_t pos = text.Length();
+ nscoord totalWidth = 0;
+
+ // nextPos is decreasing since we use a reverse iterator.
+ while (Maybe<uint32_t> nextPos = iter.Next()) {
+ const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString(
+ text.FromTo(*nextPos, pos), aFontMetrics, drawTarget);
+ if (totalWidth + charWidth > aWidth) {
+ break;
+ }
+
+ pos = *nextPos;
+ totalWidth += charWidth;
+ }
+
+ if (pos > 0) {
+ aText.Replace(0, pos, kEllipsis);
+ }
+ } break;
+
+ case CropCenter: {
+ const Span text(aText);
+ nscoord totalWidth = 0;
+ GraphemeClusterBreakIteratorUtf16 leftIter(text);
+ GraphemeClusterBreakReverseIteratorUtf16 rightIter(text);
+ uint32_t leftPos = 0;
+ uint32_t rightPos = text.Length();
+
+ while (leftPos < rightPos) {
+ Maybe<uint32_t> nextPos = leftIter.Next();
+ nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString(
+ text.FromTo(leftPos, *nextPos), aFontMetrics, drawTarget);
+ if (totalWidth + charWidth > aWidth) {
+ break;
+ }
+
+ leftPos = *nextPos;
+ totalWidth += charWidth;
+
+ if (leftPos >= rightPos) {
+ break;
+ }
+
+ nextPos = rightIter.Next();
+ charWidth = nsLayoutUtils::AppUnitWidthOfString(
+ text.FromTo(*nextPos, rightPos), aFontMetrics, drawTarget);
+ if (totalWidth + charWidth > aWidth) {
+ break;
+ }
+
+ rightPos = *nextPos;
+ totalWidth += charWidth;
+ }
+
+ if (leftPos < rightPos) {
+ aText.Replace(leftPos, rightPos - leftPos, kEllipsis);
+ }
+ } break;
+ }
+}
+
+nscoord nsTextBoxFrame::CalculateTitleForWidth(gfxContext& aRenderingContext,
+ nscoord aMaxWidth) {
+ if (mTitle.IsEmpty()) {
+ mCroppedTitle.Truncate();
+ return 0;
+ }
+
+ RefPtr<nsFontMetrics> fm = nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
+
+ // See if the text needs to be cropped to fit in the width given.
+ mCroppedTitle = mTitle;
+ nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi(
+ mCroppedTitle, this, *fm, aRenderingContext);
+ if (width > aMaxWidth && mCropType != CropNone) {
+ CropStringForWidth(mCroppedTitle, aRenderingContext, *fm, aMaxWidth,
+ mCropType);
+ width = nsLayoutUtils::AppUnitWidthOfStringBidi(mCroppedTitle, this, *fm,
+ aRenderingContext);
+ }
+
+ if (StyleVisibility()->mDirection == StyleDirection::Rtl ||
+ HasRTLChars(mCroppedTitle)) {
+ AddStateBits(NS_FRAME_IS_BIDI);
+ }
+
+ return width;
+}
+
+#define OLD_ELLIPSIS u"..."_ns
+
+// the following block is to append the accesskey to mTitle if there is an
+// accesskey but the mTitle doesn't have the character
+void nsTextBoxFrame::UpdateAccessTitle() {
+ /*
+ * Note that if you change appending access key label spec,
+ * you need to maintain same logic in following methods. See bug 324159.
+ * toolkit/components/prompts/src/CommonDialog.jsm (setLabelForNode)
+ * toolkit/content/widgets/text.js (formatAccessKey)
+ */
+ int32_t menuAccessKey = nsMenuBarListener::GetMenuAccessKey();
+ if (!menuAccessKey || mAccessKey.IsEmpty()) return;
+
+ if (!AlwaysAppendAccessKey() &&
+ FindInReadable(mAccessKey, mTitle, nsCaseInsensitiveStringComparator))
+ return;
+
+ nsAutoString accessKeyLabel;
+ accessKeyLabel += '(';
+ accessKeyLabel += mAccessKey;
+ ToUpperCase(accessKeyLabel);
+ accessKeyLabel += ')';
+
+ if (mTitle.IsEmpty()) {
+ mTitle = accessKeyLabel;
+ return;
+ }
+
+ if (StringEndsWith(mTitle, accessKeyLabel)) {
+ // Never append another "(X)" if the title already ends with "(X)".
+ return;
+ }
+
+ const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis();
+ uint32_t offset = mTitle.Length();
+ if (StringEndsWith(mTitle, kEllipsis)) {
+ offset -= kEllipsis.Length();
+ } else if (StringEndsWith(mTitle, OLD_ELLIPSIS)) {
+ // Try to check with our old ellipsis (for old addons)
+ offset -= OLD_ELLIPSIS.Length();
+ } else {
+ // Try to check with
+ // our default ellipsis (for non-localized addons) or ':'
+ const char16_t kLastChar = mTitle.Last();
+ if (kLastChar == char16_t(0x2026) || kLastChar == char16_t(':')) offset--;
+ }
+
+ if (InsertSeparatorBeforeAccessKey() && offset > 0 &&
+ !NS_IS_SPACE(mTitle[offset - 1])) {
+ mTitle.Insert(' ', offset);
+ offset++;
+ }
+
+ mTitle.Insert(accessKeyLabel, offset);
+}
+
+void nsTextBoxFrame::UpdateAccessIndex() {
+ int32_t menuAccessKey = nsMenuBarListener::GetMenuAccessKey();
+ if (menuAccessKey) {
+ if (mAccessKey.IsEmpty()) {
+ if (mAccessKeyInfo) {
+ delete mAccessKeyInfo;
+ mAccessKeyInfo = nullptr;
+ }
+ } else {
+ if (!mAccessKeyInfo) {
+ mAccessKeyInfo = new nsAccessKeyInfo();
+ if (!mAccessKeyInfo) return;
+ }
+
+ nsAString::const_iterator start, end;
+
+ mCroppedTitle.BeginReading(start);
+ mCroppedTitle.EndReading(end);
+
+ // remember the beginning of the string
+ nsAString::const_iterator originalStart = start;
+
+ bool found;
+ if (!AlwaysAppendAccessKey()) {
+ // not appending access key - do case-sensitive search
+ // first
+ found = FindInReadable(mAccessKey, start, end);
+ if (!found) {
+ // didn't find it - perform a case-insensitive search
+ start = originalStart;
+ found = FindInReadable(mAccessKey, start, end,
+ nsCaseInsensitiveStringComparator);
+ }
+ } else {
+ found = RFindInReadable(mAccessKey, start, end,
+ nsCaseInsensitiveStringComparator);
+ }
+
+ if (found)
+ mAccessKeyInfo->mAccesskeyIndex = Distance(originalStart, start);
+ else
+ mAccessKeyInfo->mAccesskeyIndex = kNotFound;
+ }
+ }
+}
+
+void nsTextBoxFrame::RecomputeTitle() {
+ mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::value, mTitle);
+
+ // This doesn't handle language-specific uppercasing/lowercasing
+ // rules, unlike textruns.
+ StyleTextTransform textTransform = StyleText()->mTextTransform;
+ if (textTransform.case_ == StyleTextTransformCase::Uppercase) {
+ ToUpperCase(mTitle);
+ } else if (textTransform.case_ == StyleTextTransformCase::Lowercase) {
+ ToLowerCase(mTitle);
+ }
+ // We can't handle StyleTextTransformCase::Capitalize because we
+ // have no clue about word boundaries here. We also don't handle
+ // the full-width or full-size-kana transforms.
+}
+
+void nsTextBoxFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) {
+ nsLeafBoxFrame::DidSetComputedStyle(aOldComputedStyle);
+
+ if (!aOldComputedStyle) {
+ // We're just being initialized
+ return;
+ }
+
+ const nsStyleText* oldTextStyle = aOldComputedStyle->StyleText();
+ if (oldTextStyle->mTextTransform != StyleText()->mTextTransform) {
+ RecomputeTitle();
+ UpdateAccessTitle();
+ }
+}
+
+NS_IMETHODIMP
+nsTextBoxFrame::DoXULLayout(nsBoxLayoutState& aBoxLayoutState) {
+ if (mNeedsReflowCallback) {
+ nsIReflowCallback* cb = new nsAsyncAccesskeyUpdate(this);
+ if (cb) {
+ PresShell()->PostReflowCallback(cb);
+ }
+ mNeedsReflowCallback = false;
+ }
+
+ nsresult rv = nsLeafBoxFrame::DoXULLayout(aBoxLayoutState);
+
+ CalcDrawRect(*aBoxLayoutState.GetRenderingContext());
+
+ const nsStyleText* textStyle = StyleText();
+
+ nsRect scrollBounds(nsPoint(0, 0), GetSize());
+ nsRect textRect = mTextDrawRect;
+
+ RefPtr<nsFontMetrics> fontMet =
+ nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
+ nsBoundingMetrics metrics = fontMet->GetInkBoundsForInkOverflow(
+ mCroppedTitle.get(), mCroppedTitle.Length(),
+ aBoxLayoutState.GetRenderingContext()->GetDrawTarget());
+
+ WritingMode wm = GetWritingMode();
+ LogicalRect tr(wm, textRect, GetSize());
+
+ tr.IStart(wm) -= metrics.leftBearing;
+ tr.ISize(wm) = metrics.width;
+ // In DrawText() we always draw with the baseline at MaxAscent() (relative to
+ // mTextDrawRect),
+ tr.BStart(wm) += fontMet->MaxAscent() - metrics.ascent;
+ tr.BSize(wm) = metrics.ascent + metrics.descent;
+
+ textRect = tr.GetPhysicalRect(wm, GetSize());
+
+ // Our scrollable overflow is our bounds; our ink overflow may
+ // extend beyond that.
+ nsRect visualBounds;
+ visualBounds.UnionRect(scrollBounds, textRect);
+ OverflowAreas overflow(visualBounds, scrollBounds);
+
+ if (textStyle->HasTextShadow()) {
+ // text-shadow extends our visual but not scrollable bounds
+ nsRect& vis = overflow.InkOverflow();
+ vis.UnionRect(vis,
+ nsLayoutUtils::GetTextShadowRectsUnion(mTextDrawRect, this));
+ }
+ FinishAndStoreOverflow(overflow, GetSize());
+
+ return rv;
+}
+
+nsRect nsTextBoxFrame::GetComponentAlphaBounds() const {
+ if (StyleText()->HasTextShadow()) {
+ return InkOverflowRectRelativeToSelf();
+ }
+ return mTextDrawRect;
+}
+
+bool nsTextBoxFrame::XULComputesOwnOverflowArea() { return true; }
+
+/* virtual */
+void nsTextBoxFrame::MarkIntrinsicISizesDirty() {
+ mNeedsRecalc = true;
+ nsLeafBoxFrame::MarkIntrinsicISizesDirty();
+}
+
+void nsTextBoxFrame::GetTextSize(gfxContext& aRenderingContext,
+ const nsString& aString, nsSize& aSize,
+ nscoord& aAscent) {
+ RefPtr<nsFontMetrics> fontMet =
+ nsLayoutUtils::GetFontMetricsForFrame(this, 1.0f);
+ aSize.height = fontMet->MaxHeight();
+ aSize.width = nsLayoutUtils::AppUnitWidthOfStringBidi(aString, this, *fontMet,
+ aRenderingContext);
+ aAscent = fontMet->MaxAscent();
+}
+
+void nsTextBoxFrame::CalcTextSize(nsBoxLayoutState& aBoxLayoutState) {
+ if (mNeedsRecalc) {
+ nsSize size;
+ gfxContext* rendContext = aBoxLayoutState.GetRenderingContext();
+ if (rendContext) {
+ GetTextSize(*rendContext, mTitle, size, mAscent);
+ if (GetWritingMode().IsVertical()) {
+ std::swap(size.width, size.height);
+ }
+ mTextSize = size;
+ mNeedsRecalc = false;
+ }
+ }
+}
+
+void nsTextBoxFrame::CalcDrawRect(gfxContext& aRenderingContext) {
+ WritingMode wm = GetWritingMode();
+
+ LogicalRect textRect(wm, LogicalPoint(wm, 0, 0), GetLogicalSize(wm));
+ nsMargin borderPadding;
+ GetXULBorderAndPadding(borderPadding);
+ textRect.Deflate(wm, LogicalMargin(wm, borderPadding));
+
+ // determine (cropped) title and underline position
+ // determine (cropped) title which fits in aRect, and its width
+ // (where "width" is the text measure along its baseline, i.e. actually
+ // a physical height in vertical writing modes)
+ nscoord titleWidth =
+ CalculateTitleForWidth(aRenderingContext, textRect.ISize(wm));
+
+#ifdef ACCESSIBILITY
+ // Make sure to update the accessible tree in case when cropped title is
+ // changed.
+ nsAccessibilityService* accService = GetAccService();
+ if (accService) {
+ accService->UpdateLabelValue(PresShell(), mContent, mCroppedTitle);
+ }
+#endif
+
+ // determine if and at which position to put the underline
+ UpdateAccessIndex();
+
+ // make the rect as small as our (cropped) text.
+ nscoord outerISize = textRect.ISize(wm);
+ textRect.ISize(wm) = titleWidth;
+
+ // Align our text within the overall rect by checking our text-align property.
+ const nsStyleText* textStyle = StyleText();
+ if (textStyle->mTextAlign == StyleTextAlign::Center) {
+ textRect.IStart(wm) += (outerISize - textRect.ISize(wm)) / 2;
+ } else if (textStyle->mTextAlign == StyleTextAlign::End ||
+ (textStyle->mTextAlign == StyleTextAlign::Left &&
+ wm.IsBidiRTL()) ||
+ (textStyle->mTextAlign == StyleTextAlign::Right &&
+ wm.IsBidiLTR())) {
+ textRect.IStart(wm) += (outerISize - textRect.ISize(wm));
+ }
+
+ mTextDrawRect = textRect.GetPhysicalRect(wm, GetSize());
+}
+
+/**
+ * Ok return our dimensions
+ */
+nsSize nsTextBoxFrame::GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) {
+ CalcTextSize(aBoxLayoutState);
+
+ nsSize size = mTextSize;
+ DISPLAY_PREF_SIZE(this, size);
+
+ AddXULBorderAndPadding(size);
+ bool widthSet, heightSet;
+ nsIFrame::AddXULPrefSize(this, size, widthSet, heightSet);
+
+ return size;
+}
+
+/**
+ * Ok return our dimensions
+ */
+nsSize nsTextBoxFrame::GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) {
+ CalcTextSize(aBoxLayoutState);
+
+ nsSize size = mTextSize;
+ DISPLAY_MIN_SIZE(this, size);
+
+ // if there is cropping our min width becomes our border and padding
+ if (mCropType != CropNone && mCropType != CropAuto) {
+ if (GetWritingMode().IsVertical()) {
+ size.height = 0;
+ } else {
+ size.width = 0;
+ }
+ }
+
+ AddXULBorderAndPadding(size);
+ bool widthSet, heightSet;
+ nsIFrame::AddXULMinSize(this, size, widthSet, heightSet);
+
+ return size;
+}
+
+nscoord nsTextBoxFrame::GetXULBoxAscent(nsBoxLayoutState& aBoxLayoutState) {
+ CalcTextSize(aBoxLayoutState);
+
+ nscoord ascent = mAscent;
+
+ nsMargin m(0, 0, 0, 0);
+ GetXULBorderAndPadding(m);
+
+ WritingMode wm = GetWritingMode();
+ ascent += LogicalMargin(wm, m).BStart(wm);
+
+ return ascent;
+}
+
+#ifdef DEBUG_FRAME_DUMP
+nsresult nsTextBoxFrame::GetFrameName(nsAString& aResult) const {
+ MakeFrameName(u"TextBox"_ns, aResult);
+ aResult += u"[value="_ns + mTitle + u"]"_ns;
+ return NS_OK;
+}
+#endif
diff --git a/layout/xul/nsTextBoxFrame.h b/layout/xul/nsTextBoxFrame.h
new file mode 100644
index 0000000000..14e45bc9bd
--- /dev/null
+++ b/layout/xul/nsTextBoxFrame.h
@@ -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/. */
+#ifndef nsTextBoxFrame_h___
+#define nsTextBoxFrame_h___
+
+#include "mozilla/Attributes.h"
+#include "nsLeafBoxFrame.h"
+
+class nsAccessKeyInfo;
+class nsAsyncAccesskeyUpdate;
+class nsFontMetrics;
+
+namespace mozilla {
+class nsDisplayXULTextBox;
+class PresShell;
+} // namespace mozilla
+
+class nsTextBoxFrame final : public nsLeafBoxFrame {
+ public:
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsTextBoxFrame)
+
+ virtual nsSize GetXULPrefSize(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual nsSize GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual nscoord GetXULBoxAscent(nsBoxLayoutState& aBoxLayoutState) override;
+ NS_IMETHOD DoXULLayout(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual void MarkIntrinsicISizesDirty() override;
+
+ enum CroppingStyle { CropNone, CropLeft, CropRight, CropCenter, CropAuto };
+
+ friend nsIFrame* NS_NewTextBoxFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+
+ virtual void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* asPrevInFlow) override;
+
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+#ifdef DEBUG_FRAME_DUMP
+ virtual nsresult GetFrameName(nsAString& aResult) const override;
+#endif
+
+ void UpdateAttributes(nsAtom* aAttribute, bool& aResize, bool& aRedraw);
+
+ virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ virtual ~nsTextBoxFrame();
+
+ void PaintTitle(gfxContext& aRenderingContext, const nsRect& aDirtyRect,
+ nsPoint aPt, const nscolor* aOverrideColor);
+
+ nsRect GetComponentAlphaBounds() const;
+
+ virtual bool XULComputesOwnOverflowArea() override;
+
+ void GetCroppedTitle(nsString& aTitle) const { aTitle = mCroppedTitle; }
+
+ virtual void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override;
+
+ // Helper for GetCroppedTitle, factored out so that we can also use it from
+ // nsTreeBodyFrame::AdjustForCellText.
+ static void CropStringForWidth(nsAString& aText,
+ gfxContext& aRenderingContext,
+ nsFontMetrics& aFontMetrics, nscoord aWidth,
+ CroppingStyle aCropType);
+
+ static bool AlwaysAppendAccessKey();
+ static bool InsertSeparatorBeforeAccessKey();
+
+ protected:
+ friend class nsAsyncAccesskeyUpdate;
+ friend class mozilla::nsDisplayXULTextBox;
+ // Should be called only by nsAsyncAccesskeyUpdate.
+ // Returns true if accesskey was updated.
+ bool UpdateAccesskey(WeakFrame& aWeakThis);
+ void UpdateAccessTitle();
+ void UpdateAccessIndex();
+
+ // Recompute our title, ignoring the access key but taking into
+ // account text-transform.
+ void RecomputeTitle();
+
+ // REVIEW: SORRY! Couldn't resist devirtualizing these
+ void LayoutTitle(nsPresContext* aPresContext, gfxContext& aRenderingContext,
+ const nsRect& aRect);
+
+ void CalculateUnderline(DrawTarget* aDrawTarget, nsFontMetrics& aFontMetrics);
+
+ void CalcTextSize(nsBoxLayoutState& aBoxLayoutState);
+
+ void CalcDrawRect(gfxContext& aRenderingContext);
+
+ explicit nsTextBoxFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+
+ nscoord CalculateTitleForWidth(gfxContext& aRenderingContext, nscoord aWidth);
+
+ void GetTextSize(gfxContext& aRenderingContext, const nsString& aString,
+ nsSize& aSize, nscoord& aAscent);
+
+ private:
+ void DrawText(gfxContext& aRenderingContext, const nsRect& aDirtyRect,
+ const nsRect& aTextRect, const nscolor* aOverrideColor);
+
+ nsString mTitle;
+ nsString mCroppedTitle;
+ nsString mAccessKey;
+ nsSize mTextSize;
+ nsRect mTextDrawRect;
+ nsAccessKeyInfo* mAccessKeyInfo;
+
+ CroppingStyle mCropType;
+ nscoord mAscent;
+ bool mNeedsRecalc;
+ bool mNeedsReflowCallback;
+
+ static bool gAlwaysAppendAccessKey;
+ static bool gAccessKeyPrefInitialized;
+ static bool gInsertSeparatorBeforeAccessKey;
+ static bool gInsertSeparatorPrefInitialized;
+
+}; // class nsTextBoxFrame
+
+#endif /* nsTextBoxFrame_h___ */
diff --git a/layout/xul/nsXULPopupManager.cpp b/layout/xul/nsXULPopupManager.cpp
new file mode 100644
index 0000000000..27e4e18134
--- /dev/null
+++ b/layout/xul/nsXULPopupManager.cpp
@@ -0,0 +1,2800 @@
+/* -*- 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 "XULButtonElement.h"
+#include "XULMenuParentElement.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/FlushType.h"
+#include "mozilla/UniquePtr.h"
+#include "nsGkAtoms.h"
+#include "nsISound.h"
+#include "nsXULPopupManager.h"
+#include "nsMenuPopupFrame.h"
+#include "nsMenuBarFrame.h"
+#include "nsMenuBarListener.h"
+#include "nsContentUtils.h"
+#include "nsXULElement.h"
+#include "nsIDOMXULCommandDispatcher.h"
+#include "nsCSSFrameConstructor.h"
+#include "nsGlobalWindow.h"
+#include "nsIContentInlines.h"
+#include "nsLayoutUtils.h"
+#include "nsViewManager.h"
+#include "nsITimer.h"
+#include "nsFocusManager.h"
+#include "nsIDocShell.h"
+#include "nsPIDOMWindow.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIBaseWindow.h"
+#include "nsCaret.h"
+#include "mozilla/dom/Document.h"
+#include "nsPIWindowRoot.h"
+#include "nsFrameManager.h"
+#include "nsPresContextInlines.h"
+#include "nsIObserverService.h"
+#include "mozilla/AnimationUtils.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h" // for Event
+#include "mozilla/dom/HTMLSlotElement.h"
+#include "mozilla/dom/KeyboardEvent.h"
+#include "mozilla/dom/KeyboardEventBinding.h"
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/dom/UIEvent.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/PopupPositionedEvent.h"
+#include "mozilla/dom/PopupPositionedEventBinding.h"
+#include "mozilla/dom/XULCommandEvent.h"
+#include "mozilla/dom/XULMenuElement.h"
+#include "mozilla/dom/XULPopupElement.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/widget/nsAutoRollup.h"
+#include "mozilla/widget/NativeMenuSupport.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using mozilla::widget::NativeMenu;
+
+static_assert(KeyboardEvent_Binding::DOM_VK_HOME ==
+ KeyboardEvent_Binding::DOM_VK_END + 1 &&
+ KeyboardEvent_Binding::DOM_VK_LEFT ==
+ KeyboardEvent_Binding::DOM_VK_END + 2 &&
+ KeyboardEvent_Binding::DOM_VK_UP ==
+ KeyboardEvent_Binding::DOM_VK_END + 3 &&
+ KeyboardEvent_Binding::DOM_VK_RIGHT ==
+ KeyboardEvent_Binding::DOM_VK_END + 4 &&
+ KeyboardEvent_Binding::DOM_VK_DOWN ==
+ KeyboardEvent_Binding::DOM_VK_END + 5,
+ "nsXULPopupManager assumes some keyCode values are consecutive");
+
+const nsNavigationDirection DirectionFromKeyCodeTable[2][6] = {
+ {
+ eNavigationDirection_Last, // KeyboardEvent_Binding::DOM_VK_END
+ eNavigationDirection_First, // KeyboardEvent_Binding::DOM_VK_HOME
+ eNavigationDirection_Start, // KeyboardEvent_Binding::DOM_VK_LEFT
+ eNavigationDirection_Before, // KeyboardEvent_Binding::DOM_VK_UP
+ eNavigationDirection_End, // KeyboardEvent_Binding::DOM_VK_RIGHT
+ eNavigationDirection_After // KeyboardEvent_Binding::DOM_VK_DOWN
+ },
+ {
+ eNavigationDirection_Last, // KeyboardEvent_Binding::DOM_VK_END
+ eNavigationDirection_First, // KeyboardEvent_Binding::DOM_VK_HOME
+ eNavigationDirection_End, // KeyboardEvent_Binding::DOM_VK_LEFT
+ eNavigationDirection_Before, // KeyboardEvent_Binding::DOM_VK_UP
+ eNavigationDirection_Start, // KeyboardEvent_Binding::DOM_VK_RIGHT
+ eNavigationDirection_After // KeyboardEvent_Binding::DOM_VK_DOWN
+ }};
+
+nsXULPopupManager* nsXULPopupManager::sInstance = nullptr;
+
+PendingPopup::PendingPopup(nsIContent* aPopup, mozilla::dom::Event* aEvent)
+ : mPopup(aPopup), mEvent(aEvent), mModifiers(0) {
+ InitMousePoint();
+}
+
+void PendingPopup::InitMousePoint() {
+ // get the event coordinates relative to the root frame of the document
+ // containing the popup.
+ if (!mEvent) {
+ return;
+ }
+
+ WidgetEvent* event = mEvent->WidgetEventPtr();
+ WidgetInputEvent* inputEvent = event->AsInputEvent();
+ if (inputEvent) {
+ mModifiers = inputEvent->mModifiers;
+ }
+ Document* doc = mPopup->GetUncomposedDoc();
+ if (!doc) {
+ return;
+ }
+
+ PresShell* presShell = doc->GetPresShell();
+ nsPresContext* presContext;
+ if (presShell && (presContext = presShell->GetPresContext())) {
+ nsPresContext* rootDocPresContext = presContext->GetRootPresContext();
+ if (!rootDocPresContext) {
+ return;
+ }
+
+ nsIFrame* rootDocumentRootFrame =
+ rootDocPresContext->PresShell()->GetRootFrame();
+ if ((event->mClass == eMouseEventClass ||
+ event->mClass == eMouseScrollEventClass ||
+ event->mClass == eWheelEventClass) &&
+ !event->AsGUIEvent()->mWidget) {
+ // no widget, so just use the client point if available
+ MouseEvent* mouseEvent = mEvent->AsMouseEvent();
+ nsIntPoint clientPt(mouseEvent->ClientX(), mouseEvent->ClientY());
+
+ // XXX this doesn't handle IFRAMEs in transforms
+ nsPoint thisDocToRootDocOffset =
+ presShell->GetRootFrame()->GetOffsetToCrossDoc(rootDocumentRootFrame);
+ // convert to device pixels
+ mMousePoint.x = presContext->AppUnitsToDevPixels(
+ nsPresContext::CSSPixelsToAppUnits(clientPt.x) +
+ thisDocToRootDocOffset.x);
+ mMousePoint.y = presContext->AppUnitsToDevPixels(
+ nsPresContext::CSSPixelsToAppUnits(clientPt.y) +
+ thisDocToRootDocOffset.y);
+ } else if (rootDocumentRootFrame) {
+ nsPoint pnt = nsLayoutUtils::GetEventCoordinatesRelativeTo(
+ event, RelativeTo{rootDocumentRootFrame});
+ mMousePoint =
+ LayoutDeviceIntPoint(rootDocPresContext->AppUnitsToDevPixels(pnt.x),
+ rootDocPresContext->AppUnitsToDevPixels(pnt.y));
+ }
+ }
+}
+
+already_AddRefed<nsIContent> PendingPopup::GetTriggerContent() const {
+ nsCOMPtr<nsIContent> target =
+ do_QueryInterface(mEvent ? mEvent->GetTarget() : nullptr);
+ return target.forget();
+}
+
+uint16_t PendingPopup::MouseInputSource() const {
+ if (mEvent) {
+ mozilla::WidgetMouseEventBase* mouseEvent =
+ mEvent->WidgetEventPtr()->AsMouseEventBase();
+ if (mouseEvent) {
+ return mouseEvent->mInputSource;
+ }
+
+ RefPtr<XULCommandEvent> commandEvent = mEvent->AsXULCommandEvent();
+ if (commandEvent) {
+ return commandEvent->InputSource();
+ }
+ }
+
+ return MouseEvent_Binding::MOZ_SOURCE_UNKNOWN;
+}
+
+nsIContent* nsMenuChainItem::Content() { return mFrame->GetContent(); }
+
+void nsMenuChainItem::SetParent(UniquePtr<nsMenuChainItem> aParent) {
+ MOZ_ASSERT_IF(aParent, !aParent->mChild);
+ auto oldParent = Detach();
+ mParent = std::move(aParent);
+ if (mParent) {
+ mParent->mChild = this;
+ }
+}
+
+UniquePtr<nsMenuChainItem> nsMenuChainItem::Detach() {
+ if (mParent) {
+ MOZ_ASSERT(mParent->mChild == this,
+ "Unexpected - parent's child not set to this");
+ mParent->mChild = nullptr;
+ }
+ return std::move(mParent);
+}
+
+void nsXULPopupManager::RemoveMenuChainItem(nsMenuChainItem* aItem) {
+ auto parent = aItem->Detach();
+ if (auto* child = aItem->GetChild()) {
+ MOZ_ASSERT(aItem != mPopups,
+ "Unexpected - popup with child at end of chain");
+ // This will kill aItem by changing child's mParent pointer.
+ child->SetParent(std::move(parent));
+ } else {
+ // An item without a child should be the first item in the chain, so set
+ // the first item pointer, pointed to by aRoot, to the parent.
+ MOZ_ASSERT(aItem == mPopups,
+ "Unexpected - popup with no child not at end of chain");
+ mPopups = std::move(parent);
+ }
+}
+
+void nsMenuChainItem::UpdateFollowAnchor() {
+ mFollowAnchor = mFrame->ShouldFollowAnchor(mCurrentRect);
+}
+
+void nsMenuChainItem::CheckForAnchorChange() {
+ if (mFollowAnchor) {
+ mFrame->CheckForAnchorChange(mCurrentRect);
+ }
+}
+
+NS_IMPL_ISUPPORTS(nsXULPopupManager, nsIDOMEventListener, nsIObserver)
+
+nsXULPopupManager::nsXULPopupManager()
+ : mActiveMenuBar(nullptr), mPopups(nullptr), mPendingPopup(nullptr) {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(this, "xpcom-shutdown", false);
+ }
+}
+
+nsXULPopupManager::~nsXULPopupManager() {
+ NS_ASSERTION(!mPopups, "XUL popups still open");
+
+ if (mNativeMenu) {
+ mNativeMenu->RemoveObserver(this);
+ }
+}
+
+nsresult nsXULPopupManager::Init() {
+ sInstance = new nsXULPopupManager();
+ NS_ENSURE_TRUE(sInstance, NS_ERROR_OUT_OF_MEMORY);
+ NS_ADDREF(sInstance);
+ return NS_OK;
+}
+
+void nsXULPopupManager::Shutdown() { NS_IF_RELEASE(sInstance); }
+
+NS_IMETHODIMP
+nsXULPopupManager::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!nsCRT::strcmp(aTopic, "xpcom-shutdown")) {
+ if (mKeyListener) {
+ mKeyListener->RemoveEventListener(u"keypress"_ns, this, true);
+ mKeyListener->RemoveEventListener(u"keydown"_ns, this, true);
+ mKeyListener->RemoveEventListener(u"keyup"_ns, this, true);
+ mKeyListener = nullptr;
+ }
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, "xpcom-shutdown");
+ }
+ }
+
+ return NS_OK;
+}
+
+nsXULPopupManager* nsXULPopupManager::GetInstance() {
+ MOZ_ASSERT(sInstance);
+ return sInstance;
+}
+
+bool nsXULPopupManager::RollupNativeMenu() {
+ if (mNativeMenu) {
+ RefPtr<NativeMenu> menu = mNativeMenu;
+ return menu->Close();
+ }
+ return false;
+}
+
+bool nsXULPopupManager::Rollup(uint32_t aCount, bool aFlush,
+ const LayoutDeviceIntPoint* pos,
+ nsIContent** aLastRolledUp) {
+ if (aLastRolledUp) {
+ *aLastRolledUp = nullptr;
+ }
+
+ // We can disable the autohide behavior via a pref to ease debugging.
+ if (StaticPrefs::ui_popup_disable_autohide()) {
+ // Required on linux to allow events to work on other targets.
+ if (mWidget) {
+ mWidget->CaptureRollupEvents(false);
+ }
+ return false;
+ }
+
+ bool consume = false;
+
+ if (nsMenuChainItem* item = GetTopVisibleMenu()) {
+ if (aLastRolledUp) {
+ // We need to get the popup that will be closed last, so that widget can
+ // keep track of it so it doesn't reopen if a mousedown event is going to
+ // processed. Keep going up the menu chain to get the first level menu of
+ // the same type. If a different type is encountered it means we have,
+ // for example, a menulist or context menu inside a panel, and we want to
+ // treat these as distinct. It's possible that this menu doesn't end up
+ // closing because the popuphiding event was cancelled, but in that case
+ // we don't need to deal with the menu reopening as it will already still
+ // be open.
+ nsMenuChainItem* first = item;
+ while (first->GetParent()) {
+ nsMenuChainItem* parent = first->GetParent();
+ if (first->Frame()->PopupType() != parent->Frame()->PopupType() ||
+ first->IsContextMenu() != parent->IsContextMenu()) {
+ break;
+ }
+ first = parent;
+ }
+
+ *aLastRolledUp = first->Content();
+ }
+
+ ConsumeOutsideClicksResult consumeResult =
+ item->Frame()->ConsumeOutsideClicks();
+ consume = consumeResult == ConsumeOutsideClicks_True;
+
+ bool rollup = true;
+
+ // If norolluponanchor is true, then don't rollup when clicking the anchor.
+ // This would be used to allow adjusting the caret position in an
+ // autocomplete field without hiding the popup for example.
+ bool noRollupOnAnchor =
+ (!consume && pos &&
+ item->Frame()->GetContent()->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::norolluponanchor, nsGkAtoms::_true,
+ eCaseMatters));
+
+ // When ConsumeOutsideClicks_ParentOnly is used, always consume the click
+ // when the click was over the anchor. This way, clicking on a menu doesn't
+ // reopen the menu.
+ if ((consumeResult == ConsumeOutsideClicks_ParentOnly ||
+ noRollupOnAnchor) &&
+ pos) {
+ nsMenuPopupFrame* popupFrame = item->Frame();
+ CSSIntRect anchorRect = [&] {
+ if (popupFrame->IsAnchored()) {
+ // Check if the popup has a screen anchor rectangle. If not, get the
+ // rectangle from the anchor element.
+ auto r = popupFrame->GetScreenAnchorRect();
+ if (r.x != -1 && r.y != -1) {
+ return r;
+ }
+ }
+ auto* anchor = Element::FromNodeOrNull(popupFrame->GetAnchor());
+ if (!anchor) {
+ return CSSIntRect();
+ }
+
+ // Check if the anchor has indicated another node to use for checking
+ // for roll-up. That way, we can anchor a popup on anonymous content
+ // or an individual icon, while clicking elsewhere within a button or
+ // other container doesn't result in us re-opening the popup.
+ nsAutoString consumeAnchor;
+ anchor->GetAttr(nsGkAtoms::consumeanchor, consumeAnchor);
+ if (!consumeAnchor.IsEmpty()) {
+ if (Element* newAnchor =
+ anchor->OwnerDoc()->GetElementById(consumeAnchor)) {
+ anchor = newAnchor;
+ }
+ }
+
+ nsIFrame* f = anchor->GetPrimaryFrame();
+ if (!f) {
+ return CSSIntRect();
+ }
+ return f->GetScreenRect();
+ }();
+
+ // It's possible that some other element is above the anchor at the same
+ // position, but the only thing that would happen is that the mouse
+ // event will get consumed, so here only a quick coordinates check is
+ // done rather than a slower complete check of what is at that location.
+ nsPresContext* presContext = item->Frame()->PresContext();
+ CSSIntPoint posCSSPixels = presContext->DevPixelsToIntCSSPixels(*pos);
+ if (anchorRect.Contains(posCSSPixels)) {
+ if (consumeResult == ConsumeOutsideClicks_ParentOnly) {
+ consume = true;
+ }
+
+ if (noRollupOnAnchor) {
+ rollup = false;
+ }
+ }
+ }
+
+ if (rollup) {
+ // if a number of popups to close has been specified, determine the last
+ // popup to close
+ nsIContent* lastPopup = nullptr;
+ if (aCount != UINT32_MAX) {
+ nsMenuChainItem* last = item;
+ while (--aCount && last->GetParent()) {
+ last = last->GetParent();
+ }
+ if (last) {
+ lastPopup = last->Content();
+ }
+ }
+
+ nsPresContext* presContext = item->Frame()->PresContext();
+ RefPtr<nsViewManager> viewManager =
+ presContext->PresShell()->GetViewManager();
+
+ HidePopup(item->Content(), true, true, false, true, lastPopup);
+
+ if (aFlush) {
+ // The popup's visibility doesn't update until the minimize animation
+ // has finished, so call UpdateWidgetGeometry to update it right away.
+ viewManager->UpdateWidgetGeometry();
+ }
+ }
+ }
+
+ return consume;
+}
+
+////////////////////////////////////////////////////////////////////////
+bool nsXULPopupManager::ShouldRollupOnMouseWheelEvent() {
+ // should rollup only for autocomplete widgets
+ // XXXndeakin this should really be something the popup has more control over
+
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (!item) {
+ return false;
+ }
+
+ nsIContent* content = item->Frame()->GetContent();
+ if (!content || !content->IsElement()) return false;
+
+ Element* element = content->AsElement();
+ if (element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::rolluponmousewheel,
+ nsGkAtoms::_true, eCaseMatters))
+ return true;
+
+ if (element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::rolluponmousewheel,
+ nsGkAtoms::_false, eCaseMatters))
+ return false;
+
+ nsAutoString value;
+ element->GetAttr(kNameSpaceID_None, nsGkAtoms::type, value);
+ return StringBeginsWith(value, u"autocomplete"_ns);
+}
+
+bool nsXULPopupManager::ShouldConsumeOnMouseWheelEvent() {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (!item) {
+ return false;
+ }
+
+ nsMenuPopupFrame* frame = item->Frame();
+ if (frame->PopupType() != ePopupTypePanel) return true;
+
+ return !frame->GetContent()->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::arrow, eCaseMatters);
+}
+
+// a menu should not roll up if activated by a mouse activate message (eg.
+// X-mouse)
+bool nsXULPopupManager::ShouldRollupOnMouseActivate() { return false; }
+
+uint32_t nsXULPopupManager::GetSubmenuWidgetChain(
+ nsTArray<nsIWidget*>* aWidgetChain) {
+ // this method is used by the widget code to determine the list of popups
+ // that are open. If a mouse click occurs outside one of these popups, the
+ // panels will roll up. If the click is inside a popup, they will not roll up
+ uint32_t count = 0, sameTypeCount = 0;
+
+ NS_ASSERTION(aWidgetChain, "null parameter");
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ while (item) {
+ nsMenuChainItem* parent = item->GetParent();
+ if (!item->IsNoAutoHide()) {
+ nsCOMPtr<nsIWidget> widget = item->Frame()->GetWidget();
+ NS_ASSERTION(widget, "open popup has no widget");
+ if (widget) {
+ aWidgetChain->AppendElement(widget.get());
+ // In the case when a menulist inside a panel is open, clicking in the
+ // panel should still roll up the menu, so if a different type is found,
+ // stop scanning.
+ if (!sameTypeCount) {
+ count++;
+ if (!parent ||
+ item->Frame()->PopupType() != parent->Frame()->PopupType() ||
+ item->IsContextMenu() != parent->IsContextMenu()) {
+ sameTypeCount = count;
+ }
+ }
+ }
+ }
+ item = parent;
+ }
+
+ return sameTypeCount;
+}
+
+nsIWidget* nsXULPopupManager::GetRollupWidget() {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ return item ? item->Frame()->GetWidget() : nullptr;
+}
+
+void nsXULPopupManager::AdjustPopupsOnWindowChange(
+ nsPIDOMWindowOuter* aWindow) {
+ // When the parent window is moved, adjust any child popups. Dismissable
+ // menus and panels are expected to roll up when a window is moved, so there
+ // is no need to check these popups, only the noautohide popups.
+
+ // The items are added to a list so that they can be adjusted bottom to top.
+ nsTArray<nsMenuPopupFrame*> list;
+
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ // only move popups that are within the same window and where auto
+ // positioning has not been disabled
+ if (!item->IsNoAutoHide()) {
+ continue;
+ }
+ nsMenuPopupFrame* frame = item->Frame();
+ nsIContent* popup = frame->GetContent();
+ if (!popup) {
+ continue;
+ }
+ Document* document = popup->GetUncomposedDoc();
+ if (!document) {
+ continue;
+ }
+ nsPIDOMWindowOuter* window = document->GetWindow();
+ if (!window) {
+ continue;
+ }
+ window = window->GetPrivateRoot();
+ if (window == aWindow) {
+ list.AppendElement(frame);
+ }
+ }
+
+ for (int32_t l = list.Length() - 1; l >= 0; l--) {
+ list[l]->SetPopupPosition(true);
+ }
+}
+
+void nsXULPopupManager::AdjustPopupsOnWindowChange(PresShell* aPresShell) {
+ if (aPresShell->GetDocument()) {
+ AdjustPopupsOnWindowChange(aPresShell->GetDocument()->GetWindow());
+ }
+}
+
+static nsMenuPopupFrame* GetPopupToMoveOrResize(nsIFrame* aFrame) {
+ nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(aFrame);
+ if (!menuPopupFrame) return nullptr;
+
+ // no point moving or resizing hidden popups
+ if (!menuPopupFrame->IsVisible()) return nullptr;
+
+ nsIWidget* widget = menuPopupFrame->GetWidget();
+ if (widget && !widget->IsVisible()) return nullptr;
+
+ return menuPopupFrame;
+}
+
+void nsXULPopupManager::PopupMoved(nsIFrame* aFrame, nsIntPoint aPnt,
+ bool aByMoveToRect) {
+ nsMenuPopupFrame* menuPopupFrame = GetPopupToMoveOrResize(aFrame);
+ if (!menuPopupFrame) {
+ return;
+ }
+
+ nsView* view = menuPopupFrame->GetView();
+ if (!view) {
+ return;
+ }
+
+ menuPopupFrame->WidgetPositionOrSizeDidChange();
+
+ // Don't do anything if the popup is already at the specified location. This
+ // prevents recursive calls when a popup is positioned.
+ LayoutDeviceIntRect curDevSize = view->CalcWidgetBounds(eWindowType_popup);
+ nsIWidget* widget = menuPopupFrame->GetWidget();
+ if (curDevSize.x == aPnt.x && curDevSize.y == aPnt.y &&
+ (!widget ||
+ widget->GetClientOffset() == menuPopupFrame->GetLastClientOffset())) {
+ return;
+ }
+
+ // Update the popup's position using SetPopupPosition if the popup is
+ // anchored and at the parent level as these maintain their position
+ // relative to the parent window. Otherwise, just update the popup to
+ // the specified screen coordinates.
+ if (menuPopupFrame->IsAnchored() &&
+ menuPopupFrame->PopupLevel() == ePopupLevelParent) {
+ menuPopupFrame->SetPopupPosition(true);
+ } else {
+ CSSPoint cssPos = LayoutDeviceIntPoint::FromUnknownPoint(aPnt) /
+ menuPopupFrame->PresContext()->CSSToDevPixelScale();
+ menuPopupFrame->MoveTo(cssPos, false, aByMoveToRect);
+ }
+}
+
+void nsXULPopupManager::PopupResized(nsIFrame* aFrame,
+ LayoutDeviceIntSize aSize) {
+ nsMenuPopupFrame* menuPopupFrame = GetPopupToMoveOrResize(aFrame);
+ if (!menuPopupFrame) {
+ return;
+ }
+
+ menuPopupFrame->WidgetPositionOrSizeDidChange();
+
+ nsView* view = menuPopupFrame->GetView();
+ if (!view) {
+ return;
+ }
+
+ LayoutDeviceIntRect curDevSize = view->CalcWidgetBounds(eWindowType_popup);
+ // If the size is what we think it is, we have nothing to do.
+ if (curDevSize.width == aSize.width && curDevSize.height == aSize.height) {
+ return;
+ }
+
+ Element* popup = menuPopupFrame->GetContent()->AsElement();
+
+ // Only set the width and height if the popup already has these attributes.
+ if (!popup->HasAttr(kNameSpaceID_None, nsGkAtoms::width) ||
+ !popup->HasAttr(kNameSpaceID_None, nsGkAtoms::height)) {
+ return;
+ }
+
+ // The size is different. Convert the actual size to css pixels and store it
+ // as 'width' and 'height' attributes on the popup.
+ nsPresContext* presContext = menuPopupFrame->PresContext();
+
+ CSSIntSize newCSS(presContext->DevPixelsToIntCSSPixels(aSize.width),
+ presContext->DevPixelsToIntCSSPixels(aSize.height));
+
+ nsAutoString width, height;
+ width.AppendInt(newCSS.width);
+ height.AppendInt(newCSS.height);
+ popup->SetAttr(kNameSpaceID_None, nsGkAtoms::width, width, false);
+ popup->SetAttr(kNameSpaceID_None, nsGkAtoms::height, height, true);
+}
+
+nsMenuPopupFrame* nsXULPopupManager::GetPopupFrameForContent(
+ nsIContent* aContent, bool aShouldFlush) {
+ if (aShouldFlush) {
+ Document* document = aContent->GetUncomposedDoc();
+ if (document) {
+ if (RefPtr<PresShell> presShell = document->GetPresShell()) {
+ presShell->FlushPendingNotifications(FlushType::Layout);
+ }
+ }
+ }
+
+ return do_QueryFrame(aContent->GetPrimaryFrame());
+}
+
+nsMenuChainItem* nsXULPopupManager::GetTopVisibleMenu() {
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (!item->IsNoAutoHide() &&
+ item->Frame()->PopupState() != ePopupInvisible) {
+ return item;
+ }
+ }
+ return nullptr;
+}
+
+void nsXULPopupManager::SetActiveMenuBar(nsMenuBarFrame* aMenuBar,
+ bool aActivate) {
+ if (aActivate) {
+ mActiveMenuBar = aMenuBar;
+ } else if (mActiveMenuBar == aMenuBar) {
+ mActiveMenuBar = nullptr;
+ }
+ UpdateKeyboardListeners();
+}
+
+static CloseMenuMode GetCloseMenuMode(nsIContent* aMenu) {
+ if (!aMenu->IsElement()) {
+ return CloseMenuMode_Auto;
+ }
+
+ static Element::AttrValuesArray strings[] = {nsGkAtoms::none,
+ nsGkAtoms::single, nullptr};
+ switch (aMenu->AsElement()->FindAttrValueIn(
+ kNameSpaceID_None, nsGkAtoms::closemenu, strings, eCaseMatters)) {
+ case 0:
+ return CloseMenuMode_None;
+ case 1:
+ return CloseMenuMode_Single;
+ default:
+ return CloseMenuMode_Auto;
+ }
+}
+
+auto nsXULPopupManager::MayShowMenu(nsIContent* aMenu) -> MayShowMenuResult {
+ if (mNativeMenu && aMenu->IsElement() &&
+ mNativeMenu->Element()->Contains(aMenu)) {
+ return {true};
+ }
+
+ auto* menu = XULButtonElement::FromNode(aMenu);
+ if (!menu) {
+ return {};
+ }
+
+ nsMenuPopupFrame* popupFrame = menu->GetMenuPopup(FlushType::None);
+ if (!popupFrame || !MayShowPopup(popupFrame)) {
+ return {};
+ }
+ return {false, menu, popupFrame};
+}
+
+void nsXULPopupManager::ShowMenu(nsIContent* aMenu, bool aSelectFirstItem) {
+ auto mayShowResult = MayShowMenu(aMenu);
+ if (NS_WARN_IF(!mayShowResult)) {
+ return;
+ }
+
+ if (mayShowResult.mIsNative) {
+ mNativeMenu->OpenSubmenu(aMenu->AsElement());
+ return;
+ }
+
+ nsMenuPopupFrame* popupFrame = mayShowResult.mMenuPopupFrame;
+
+ // inherit whether or not we're a context menu from the parent
+ const bool onMenuBar = mayShowResult.mMenuButton->IsOnMenuBar();
+ const bool onmenu = mayShowResult.mMenuButton->IsOnMenu();
+ const bool parentIsContextMenu = mayShowResult.mMenuButton->IsOnContextMenu();
+
+ nsAutoString position;
+
+#ifdef XP_MACOSX
+ if (aMenu->IsXULElement(nsGkAtoms::menulist)) {
+ position.AssignLiteral("selection");
+ } else
+#endif
+
+ if (onMenuBar || !onmenu)
+ position.AssignLiteral("after_start");
+ else
+ position.AssignLiteral("end_before");
+
+ // there is no trigger event for menus
+ popupFrame->InitializePopup(aMenu, nullptr, position, 0, 0,
+ MenuPopupAnchorType_Node, true);
+ PendingPopup pendingPopup(popupFrame->GetContent(), nullptr);
+ BeginShowingPopup(pendingPopup, parentIsContextMenu, aSelectFirstItem);
+}
+
+void nsXULPopupManager::ShowPopup(nsIContent* aPopup,
+ nsIContent* aAnchorContent,
+ const nsAString& aPosition, int32_t aXPos,
+ int32_t aYPos, bool aIsContextMenu,
+ bool aAttributesOverride,
+ bool aSelectFirstItem, Event* aTriggerEvent) {
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
+ if (!popupFrame || !MayShowPopup(popupFrame)) {
+ return;
+ }
+
+ PendingPopup pendingPopup(aPopup, aTriggerEvent);
+ nsCOMPtr<nsIContent> triggerContent = pendingPopup.GetTriggerContent();
+
+ popupFrame->InitializePopup(aAnchorContent, triggerContent, aPosition, aXPos,
+ aYPos, MenuPopupAnchorType_Node,
+ aAttributesOverride);
+
+ BeginShowingPopup(pendingPopup, aIsContextMenu, aSelectFirstItem);
+}
+
+static bool ShouldUseNativeContextMenus() {
+#ifdef HAS_NATIVE_MENU_SUPPORT
+ return mozilla::widget::NativeMenuSupport::ShouldUseNativeContextMenus();
+#else
+ return false;
+#endif
+}
+
+void nsXULPopupManager::ShowPopupAtScreen(nsIContent* aPopup, int32_t aXPos,
+ int32_t aYPos, bool aIsContextMenu,
+ Event* aTriggerEvent) {
+ if (aIsContextMenu && ShouldUseNativeContextMenus() &&
+ ShowPopupAsNativeMenu(aPopup, aXPos, aYPos, aIsContextMenu,
+ aTriggerEvent)) {
+ return;
+ }
+
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
+ if (!popupFrame || !MayShowPopup(popupFrame)) return;
+
+ PendingPopup pendingPopup(aPopup, aTriggerEvent);
+ nsCOMPtr<nsIContent> triggerContent = pendingPopup.GetTriggerContent();
+
+ popupFrame->InitializePopupAtScreen(triggerContent, aXPos, aYPos,
+ aIsContextMenu);
+ BeginShowingPopup(pendingPopup, aIsContextMenu, false);
+}
+
+bool nsXULPopupManager::ShowPopupAsNativeMenu(nsIContent* aPopup, int32_t aXPos,
+ int32_t aYPos,
+ bool aIsContextMenu,
+ Event* aTriggerEvent) {
+ if (mNativeMenu) {
+ NS_WARNING("Native menu still open when trying to open another");
+ RefPtr<NativeMenu> menu = mNativeMenu;
+ (void)menu->Close();
+ menu->RemoveObserver(this);
+ mNativeMenu = nullptr;
+ }
+
+ RefPtr<NativeMenu> menu;
+#ifdef HAS_NATIVE_MENU_SUPPORT
+ if (aPopup->IsElement()) {
+ menu = mozilla::widget::NativeMenuSupport::CreateNativeContextMenu(
+ aPopup->AsElement());
+ }
+#endif
+
+ if (!menu) {
+ return false;
+ }
+
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
+ if (!popupFrame) {
+ return true;
+ }
+
+ // Hide the menu from our accessibility code so that we don't dispatch custom
+ // accessibility notifications which would conflict with the system ones.
+ aPopup->AsElement()->SetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden,
+ u"true"_ns, true);
+
+ PendingPopup pendingPopup(aPopup, aTriggerEvent);
+ nsCOMPtr<nsIContent> triggerContent = pendingPopup.GetTriggerContent();
+
+ popupFrame->InitializePopupAsNativeContextMenu(triggerContent, aXPos, aYPos);
+
+ RefPtr<nsPresContext> presContext = popupFrame->PresContext();
+ nsEventStatus status = FirePopupShowingEvent(pendingPopup, presContext);
+
+ // if the event was cancelled, don't open the popup, reset its state back
+ // to closed and clear its trigger content.
+ if (status == nsEventStatus_eConsumeNoDefault) {
+ if (nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true)) {
+ popupFrame->SetPopupState(ePopupClosed);
+ popupFrame->ClearTriggerContent();
+ }
+ return true;
+ }
+
+ mNativeMenu = menu;
+ mNativeMenu->AddObserver(this);
+ nsIFrame* frame = presContext->PresShell()->GetCurrentEventFrame();
+ if (!frame) {
+ frame = presContext->PresShell()->GetRootFrame();
+ }
+ mNativeMenu->ShowAsContextMenu(frame, CSSIntPoint(aXPos, aYPos));
+
+ // While the native menu is open, it consumes mouseup events.
+ // Clear any :active state, mouse capture state and drag tracking now.
+ EventStateManager* activeESM = static_cast<EventStateManager*>(
+ EventStateManager::GetActiveEventStateManager());
+ if (activeESM) {
+ EventStateManager::ClearGlobalActiveContent(activeESM);
+ activeESM->StopTrackingDragGesture(true);
+ }
+ PresShell::ReleaseCapturingContent();
+
+ return true;
+}
+
+void nsXULPopupManager::OnNativeMenuOpened() {
+ if (!mNativeMenu) {
+ return;
+ }
+
+ RefPtr<nsXULPopupManager> kungFuDeathGrip(this);
+
+ nsCOMPtr<nsIContent> popup = mNativeMenu->Element();
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(popup, true);
+ if (popupFrame) {
+ popupFrame->SetPopupState(ePopupShown);
+ }
+}
+
+void nsXULPopupManager::OnNativeMenuClosed() {
+ if (!mNativeMenu) {
+ return;
+ }
+
+ RefPtr<nsXULPopupManager> kungFuDeathGrip(this);
+
+ bool shouldHideChain =
+ mNativeMenuActivatedItemCloseMenuMode == Some(CloseMenuMode_Auto);
+
+ nsCOMPtr<nsIContent> popup = mNativeMenu->Element();
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(popup, true);
+ if (popupFrame) {
+ popupFrame->ClearTriggerContentIncludingDocument();
+ popupFrame->SetPopupState(ePopupClosed);
+ }
+ mNativeMenu->RemoveObserver(this);
+ mNativeMenu = nullptr;
+ mNativeMenuActivatedItemCloseMenuMode = Nothing();
+ mNativeMenuSubmenuStates.Clear();
+
+ // Stop hiding the menu from accessibility code, in case it gets opened as a
+ // non-native menu in the future.
+ popup->AsElement()->UnsetAttr(kNameSpaceID_None, nsGkAtoms::aria_hidden,
+ true);
+
+ if (shouldHideChain && mPopups && mPopups->PopupType() == ePopupTypeMenu) {
+ // A menu item was activated before this menu closed, and the item requested
+ // the entire popup chain to be closed, which includes any open non-native
+ // menus.
+ // Close the non-native menus now. This matches the HidePopup call in
+ // nsXULMenuCommandEvent::Run.
+ HidePopup(mPopups->Content(), true, false, false, false);
+ }
+}
+
+void nsXULPopupManager::OnNativeSubMenuWillOpen(
+ mozilla::dom::Element* aPopupElement) {
+ mNativeMenuSubmenuStates.InsertOrUpdate(aPopupElement, ePopupShowing);
+}
+
+void nsXULPopupManager::OnNativeSubMenuDidOpen(
+ mozilla::dom::Element* aPopupElement) {
+ mNativeMenuSubmenuStates.InsertOrUpdate(aPopupElement, ePopupShown);
+}
+
+void nsXULPopupManager::OnNativeSubMenuClosed(
+ mozilla::dom::Element* aPopupElement) {
+ mNativeMenuSubmenuStates.Remove(aPopupElement);
+}
+
+void nsXULPopupManager::OnNativeMenuWillActivateItem(
+ mozilla::dom::Element* aMenuItemElement) {
+ if (!mNativeMenu) {
+ return;
+ }
+
+ CloseMenuMode cmm = GetCloseMenuMode(aMenuItemElement);
+ mNativeMenuActivatedItemCloseMenuMode = Some(cmm);
+
+ if (cmm == CloseMenuMode_Auto) {
+ // If any non-native menus are visible (for example because the context menu
+ // was opened on a non-native menu item, e.g. in a bookmarks folder), hide
+ // the non-native menus before executing the item.
+ HideOpenMenusBeforeExecutingMenu(CloseMenuMode_Auto);
+ }
+}
+
+void nsXULPopupManager::ShowPopupAtScreenRect(
+ nsIContent* aPopup, const nsAString& aPosition, const nsIntRect& aRect,
+ bool aIsContextMenu, bool aAttributesOverride, Event* aTriggerEvent) {
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
+ if (!popupFrame || !MayShowPopup(popupFrame)) return;
+
+ PendingPopup pendingPopup(aPopup, aTriggerEvent);
+ nsCOMPtr<nsIContent> triggerContent = pendingPopup.GetTriggerContent();
+
+ popupFrame->InitializePopupAtRect(triggerContent, aPosition, aRect,
+ aAttributesOverride);
+
+ BeginShowingPopup(pendingPopup, aIsContextMenu, false);
+}
+
+void nsXULPopupManager::ShowTooltipAtScreen(
+ nsIContent* aPopup, nsIContent* aTriggerContent,
+ const LayoutDeviceIntPoint& aScreenPoint) {
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(aPopup, true);
+ if (!popupFrame || !MayShowPopup(popupFrame)) {
+ return;
+ }
+
+ PendingPopup pendingPopup(aPopup, nullptr);
+
+ nsPresContext* pc = popupFrame->PresContext();
+ pendingPopup.SetMousePoint([&] {
+ // Event coordinates are relative to the root widget
+ if (nsPresContext* rootPresContext = pc->GetRootPresContext()) {
+ if (nsCOMPtr<nsIWidget> rootWidget = rootPresContext->GetRootWidget()) {
+ return aScreenPoint - rootWidget->WidgetToScreenOffset();
+ }
+ }
+ return aScreenPoint;
+ }());
+
+ auto screenCSSPoint =
+ CSSIntPoint::Round(aScreenPoint / pc->CSSToDevPixelScale());
+ popupFrame->InitializePopupAtScreen(aTriggerContent, screenCSSPoint.x,
+ screenCSSPoint.y, false);
+
+ BeginShowingPopup(pendingPopup, false, false);
+}
+
+static void CheckCaretDrawingState() {
+ // There is 1 caret per document, we need to find the focused
+ // document and erase its caret.
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (fm) {
+ nsCOMPtr<mozIDOMWindowProxy> window;
+ fm->GetFocusedWindow(getter_AddRefs(window));
+ if (!window) return;
+
+ auto* piWindow = nsPIDOMWindowOuter::From(window);
+ MOZ_ASSERT(piWindow);
+
+ nsCOMPtr<Document> focusedDoc = piWindow->GetDoc();
+ if (!focusedDoc) return;
+
+ PresShell* presShell = focusedDoc->GetPresShell();
+ if (!presShell) {
+ return;
+ }
+
+ RefPtr<nsCaret> caret = presShell->GetCaret();
+ if (!caret) return;
+ caret->SchedulePaint();
+ }
+}
+
+void nsXULPopupManager::ShowPopupCallback(nsIContent* aPopup,
+ nsMenuPopupFrame* aPopupFrame,
+ bool aIsContextMenu,
+ bool aSelectFirstItem) {
+ nsPopupType popupType = aPopupFrame->PopupType();
+ const bool isMenu = popupType == ePopupTypeMenu;
+
+ // Popups normally hide when an outside click occurs. Panels may use
+ // the noautohide attribute to disable this behaviour. It is expected
+ // that the application will hide these popups manually. The tooltip
+ // listener will handle closing the tooltip also.
+ bool isNoAutoHide =
+ aPopupFrame->IsNoAutoHide() || popupType == ePopupTypeTooltip;
+
+ auto item = MakeUnique<nsMenuChainItem>(aPopupFrame, isNoAutoHide,
+ aIsContextMenu, popupType);
+
+ // install keyboard event listeners for navigating menus. For panels, the
+ // escape key may be used to close the panel. However, the ignorekeys
+ // attribute may be used to disable adding these event listeners for popups
+ // that want to handle their own keyboard events.
+ nsAutoString ignorekeys;
+ if (aPopup->IsElement()) {
+ aPopup->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::ignorekeys,
+ ignorekeys);
+ }
+ if (ignorekeys.EqualsLiteral("true")) {
+ item->SetIgnoreKeys(eIgnoreKeys_True);
+ } else if (ignorekeys.EqualsLiteral("shortcuts")) {
+ item->SetIgnoreKeys(eIgnoreKeys_Shortcuts);
+ }
+
+ if (isMenu) {
+ // if the menu is on a menubar, use the menubar's listener instead
+ if (auto* menu = aPopupFrame->PopupElement().GetContainingMenu()) {
+ item->SetOnMenuBar(menu->IsOnMenuBar());
+ }
+ }
+
+ // use a weak frame as the popup will set an open attribute if it is a menu
+ AutoWeakFrame weakFrame(aPopupFrame);
+ aPopupFrame->ShowPopup(aIsContextMenu);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ item->UpdateFollowAnchor();
+
+ // popups normally hide when an outside click occurs. Panels may use
+ // the noautohide attribute to disable this behaviour. It is expected
+ // that the application will hide these popups manually. The tooltip
+ // listener will handle closing the tooltip also.
+ nsIContent* oldmenu = nullptr;
+ if (mPopups) {
+ oldmenu = mPopups->Content();
+ }
+ item->SetParent(std::move(mPopups));
+ mPopups = std::move(item);
+ SetCaptureState(oldmenu);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ RefPtr popup = &aPopupFrame->PopupElement();
+ popup->PopupOpened(aSelectFirstItem);
+
+ if (isMenu) {
+ UpdateMenuItems(aPopup);
+ }
+
+ // Caret visibility may have been affected, ensure that
+ // the caret isn't now drawn when it shouldn't be.
+ CheckCaretDrawingState();
+}
+
+nsMenuChainItem* nsXULPopupManager::FindPopup(nsIContent* aPopup) const {
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (item->Frame()->GetContent() == aPopup) {
+ return item;
+ }
+ }
+ return nullptr;
+}
+
+void nsXULPopupManager::HidePopup(nsIContent* aPopup, bool aHideChain,
+ bool aDeselectMenu, bool aAsynchronous,
+ bool aIsCancel, nsIContent* aLastPopup) {
+ if (mNativeMenu && mNativeMenu->Element() == aPopup) {
+ RefPtr<NativeMenu> menu = mNativeMenu;
+ (void)menu->Close();
+ return;
+ }
+
+ nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
+ if (!popupFrame) {
+ return;
+ }
+
+ nsMenuChainItem* foundPopup = FindPopup(aPopup);
+
+ bool deselectMenu = false;
+ nsCOMPtr<nsIContent> popupToHide, nextPopup, lastPopup;
+
+ if (foundPopup) {
+ if (foundPopup->IsNoAutoHide()) {
+ // If this is a noautohide panel, remove it but don't close any other
+ // panels.
+ popupToHide = aPopup;
+ } else {
+ // At this point, foundPopup will be set to the found item in the list. If
+ // foundPopup is the topmost menu, the one to remove, then there are no
+ // other popups to hide. If foundPopup is not the topmost menu, then there
+ // may be open submenus below it. In this case, we need to make sure that
+ // those submenus are closed up first. To do this, we scan up the menu
+ // list to find the topmost popup with only menus between it and
+ // foundPopup and close that menu first. In synchronous mode, the
+ // FirePopupHidingEvent method will be called which in turn calls
+ // HidePopupCallback to close up the next popup in the chain. These two
+ // methods will be called in sequence recursively to close up all the
+ // necessary popups. In asynchronous mode, a similar process occurs except
+ // that the FirePopupHidingEvent method is called asynchronously. In
+ // either case, nextPopup is set to the content node of the next popup to
+ // close, and lastPopup is set to the last popup in the chain to close,
+ // which will be aPopup, or null to close up all menus.
+
+ nsMenuChainItem* topMenu = foundPopup;
+ // Use IsMenu to ensure that foundPopup is a menu and scan down the child
+ // list until a non-menu is found. If foundPopup isn't a menu at all,
+ // don't scan and just close up this menu.
+ if (foundPopup->IsMenu()) {
+ nsMenuChainItem* child = foundPopup->GetChild();
+ while (child && child->IsMenu()) {
+ topMenu = child;
+ child = child->GetChild();
+ }
+ }
+
+ deselectMenu = aDeselectMenu;
+ popupToHide = topMenu->Content();
+ popupFrame = topMenu->Frame();
+
+ // Close up another popup if there is one, and we are either hiding the
+ // entire chain or the item to hide isn't the topmost popup.
+ nsMenuChainItem* parent = topMenu->GetParent();
+ if (parent && (aHideChain || topMenu != foundPopup)) {
+ while (parent && parent->IsNoAutoHide()) {
+ parent = parent->GetParent();
+ }
+
+ if (parent) {
+ nextPopup = parent->Content();
+ }
+ }
+
+ lastPopup = aLastPopup ? aLastPopup : (aHideChain ? nullptr : aPopup);
+ }
+ } else if (popupFrame->PopupState() == ePopupPositioning) {
+ // When the popup is in the popuppositioning state, it will not be in the
+ // mPopups list. We need another way to find it and make sure it does not
+ // continue the popup showing process.
+ deselectMenu = aDeselectMenu;
+ popupToHide = aPopup;
+ }
+
+ if (popupToHide) {
+ nsPopupState state = popupFrame->PopupState();
+ // If the popup is already being hidden, don't attempt to hide it again
+ if (state == ePopupHiding) {
+ return;
+ }
+
+ // Change the popup state to hiding. Don't set the hiding state if the
+ // popup is invisible, otherwise nsMenuPopupFrame::HidePopup will
+ // run again. In the invisible state, we just want the events to fire.
+ if (state != ePopupInvisible) {
+ popupFrame->SetPopupState(ePopupHiding);
+ }
+
+ // For menus, popupToHide is always the frontmost item in the list to hide.
+ if (aAsynchronous) {
+ nsCOMPtr<nsIRunnable> event = new nsXULPopupHidingEvent(
+ popupToHide, nextPopup, lastPopup, popupFrame->PopupType(),
+ deselectMenu, aIsCancel);
+ aPopup->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
+ } else {
+ RefPtr<nsPresContext> presContext = popupFrame->PresContext();
+ FirePopupHidingEvent(popupToHide, nextPopup, lastPopup, presContext,
+ popupFrame->PopupType(), deselectMenu, aIsCancel);
+ }
+ }
+}
+
+void nsXULPopupManager::HideMenu(nsIContent* aMenu) {
+ if (mNativeMenu && aMenu->IsElement() &&
+ mNativeMenu->Element()->Contains(aMenu)) {
+ mNativeMenu->CloseSubmenu(aMenu->AsElement());
+ return;
+ }
+
+ auto* button = XULButtonElement::FromNode(aMenu);
+ if (!button || !button->IsMenu()) {
+ return;
+ }
+ auto* popup = button->GetMenuPopupContent();
+ if (!popup) {
+ return;
+ }
+ HidePopup(popup, false, true, false, false);
+}
+
+// This is used to hide the popup after a transition finishes.
+class TransitionEnder final : public nsIDOMEventListener {
+ private:
+ // Effectively const but is cycle collected
+ MOZ_KNOWN_LIVE RefPtr<nsIContent> mContent;
+
+ protected:
+ virtual ~TransitionEnder() = default;
+
+ public:
+ bool mDeselectMenu;
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(TransitionEnder)
+
+ TransitionEnder(nsIContent* aContent, bool aDeselectMenu)
+ : mContent(aContent), mDeselectMenu(aDeselectMenu) {}
+
+ MOZ_CAN_RUN_SCRIPT NS_IMETHOD HandleEvent(Event* aEvent) override {
+ mContent->RemoveSystemEventListener(u"transitionend"_ns, this, false);
+
+ nsMenuPopupFrame* popupFrame = do_QueryFrame(mContent->GetPrimaryFrame());
+ if (!popupFrame) {
+ return NS_OK;
+ }
+
+ // Now hide the popup. There could be other properties transitioning, but
+ // we'll assume they all end at the same time and just hide the popup upon
+ // the first one ending.
+ if (RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance()) {
+ pm->HidePopupCallback(mContent, popupFrame, nullptr, nullptr,
+ popupFrame->PopupType(), mDeselectMenu);
+ }
+
+ return NS_OK;
+ }
+};
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TransitionEnder)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TransitionEnder)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransitionEnder)
+ NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION(TransitionEnder, mContent);
+void nsXULPopupManager::HidePopupCallback(
+ nsIContent* aPopup, nsMenuPopupFrame* aPopupFrame, nsIContent* aNextPopup,
+ nsIContent* aLastPopup, nsPopupType aPopupType, bool aDeselectMenu) {
+ if (mCloseTimer && mTimerMenu == aPopupFrame) {
+ mCloseTimer->Cancel();
+ mCloseTimer = nullptr;
+ mTimerMenu = nullptr;
+ }
+
+ // The popup to hide is aPopup. Search the list again to find the item that
+ // corresponds to the popup to hide aPopup. This is done because it's
+ // possible someone added another item (attempted to open another popup)
+ // or removed a popup frame during the event processing so the item isn't at
+ // the front anymore.
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (item->Content() == aPopup) {
+ RemoveMenuChainItem(item);
+ SetCaptureState(aPopup);
+ break;
+ }
+ }
+
+ AutoWeakFrame weakFrame(aPopupFrame);
+ aPopupFrame->HidePopup(aDeselectMenu, ePopupClosed);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ // send the popuphidden event synchronously. This event has no default
+ // behaviour.
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent event(true, eXULPopupHidden, nullptr,
+ WidgetMouseEvent::eReal);
+ RefPtr<nsPresContext> presContext = aPopupFrame->PresContext();
+ EventDispatcher::Dispatch(aPopup, presContext, &event, nullptr, &status);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ // Force any popups that might be anchored on elements within this popup to
+ // update.
+ UpdatePopupPositions(presContext->RefreshDriver());
+
+ // if there are more popups to close, look for the next one
+ if (aNextPopup && aPopup != aLastPopup) {
+ nsMenuChainItem* foundMenu = FindPopup(aNextPopup);
+
+ // continue hiding the chain of popups until the last popup aLastPopup
+ // is reached, or until a popup of a different type is reached. This
+ // last check is needed so that a menulist inside a non-menu panel only
+ // closes the menu and not the panel as well.
+ if (foundMenu && (aLastPopup || aPopupType == foundMenu->PopupType())) {
+ nsCOMPtr<nsIContent> popupToHide = foundMenu->Content();
+ nsMenuChainItem* parent = foundMenu->GetParent();
+
+ nsCOMPtr<nsIContent> nextPopup;
+ if (parent && popupToHide != aLastPopup) nextPopup = parent->Content();
+
+ nsMenuPopupFrame* popupFrame = foundMenu->Frame();
+ nsPopupState state = popupFrame->PopupState();
+ if (state == ePopupHiding) return;
+ if (state != ePopupInvisible) popupFrame->SetPopupState(ePopupHiding);
+
+ RefPtr<nsPresContext> presContext = popupFrame->PresContext();
+ FirePopupHidingEvent(popupToHide, nextPopup, aLastPopup, presContext,
+ foundMenu->PopupType(), aDeselectMenu, false);
+ }
+ }
+}
+
+void nsXULPopupManager::HidePopupAfterDelay(nsMenuPopupFrame* aPopup,
+ int32_t aDelay) {
+ // Don't close up immediately.
+ // Kick off a close timer.
+ KillMenuTimer();
+
+ // Kick off the timer.
+ nsIEventTarget* target =
+ aPopup->PopupElement().OwnerDoc()->EventTargetFor(TaskCategory::Other);
+ NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mCloseTimer),
+ [](nsITimer* aTimer, void* aClosure) {
+ if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
+ pm->KillMenuTimer();
+ }
+ },
+ nullptr, aDelay, nsITimer::TYPE_ONE_SHOT, "KillMenuTimer", target);
+ // the popup will call PopupDestroyed if it is destroyed, which checks if it
+ // is set to mTimerMenu, so it should be safe to keep a reference to it
+ mTimerMenu = aPopup;
+}
+
+void nsXULPopupManager::HidePopupsInList(
+ const nsTArray<nsMenuPopupFrame*>& aFrames) {
+ // Create a weak frame list. This is done in a separate array with the
+ // right capacity predetermined to avoid multiple allocations.
+ nsTArray<WeakFrame> weakPopups(aFrames.Length());
+ uint32_t f;
+ for (f = 0; f < aFrames.Length(); f++) {
+ WeakFrame* wframe = weakPopups.AppendElement();
+ if (wframe) *wframe = aFrames[f];
+ }
+
+ for (f = 0; f < weakPopups.Length(); f++) {
+ // check to ensure that the frame is still alive before hiding it.
+ if (weakPopups[f].IsAlive()) {
+ auto* frame = static_cast<nsMenuPopupFrame*>(weakPopups[f].GetFrame());
+ frame->HidePopup(true, ePopupInvisible);
+ }
+ }
+
+ SetCaptureState(nullptr);
+}
+
+bool nsXULPopupManager::IsChildOfDocShell(Document* aDoc,
+ nsIDocShellTreeItem* aExpected) {
+ nsCOMPtr<nsIDocShellTreeItem> docShellItem(aDoc->GetDocShell());
+ while (docShellItem) {
+ if (docShellItem == aExpected) return true;
+
+ nsCOMPtr<nsIDocShellTreeItem> parent;
+ docShellItem->GetInProcessParent(getter_AddRefs(parent));
+ docShellItem = parent;
+ }
+
+ return false;
+}
+
+void nsXULPopupManager::HidePopupsInDocShell(
+ nsIDocShellTreeItem* aDocShellToHide) {
+ nsTArray<nsMenuPopupFrame*> popupsToHide;
+
+ // Iterate to get the set of popup frames to hide
+ nsMenuChainItem* item = mPopups.get();
+ while (item) {
+ // Get the parent before calling detach so that we can keep iterating.
+ nsMenuChainItem* parent = item->GetParent();
+ if (item->Frame()->PopupState() != ePopupInvisible &&
+ IsChildOfDocShell(item->Content()->OwnerDoc(), aDocShellToHide)) {
+ nsMenuPopupFrame* frame = item->Frame();
+ RemoveMenuChainItem(item);
+ popupsToHide.AppendElement(frame);
+ }
+ item = parent;
+ }
+
+ HidePopupsInList(popupsToHide);
+}
+
+void nsXULPopupManager::UpdatePopupPositions(nsRefreshDriver* aRefreshDriver) {
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (item->Frame()->PresContext()->RefreshDriver() == aRefreshDriver) {
+ item->CheckForAnchorChange();
+ }
+ }
+}
+
+void nsXULPopupManager::UpdateFollowAnchor(nsMenuPopupFrame* aPopup) {
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (item->Frame() == aPopup) {
+ item->UpdateFollowAnchor();
+ break;
+ }
+ }
+}
+
+void nsXULPopupManager::HideOpenMenusBeforeExecutingMenu(CloseMenuMode aMode) {
+ if (aMode == CloseMenuMode_None) {
+ return;
+ }
+
+ // When a menuitem is selected to be executed, first hide all the open
+ // popups, but don't remove them yet. This is needed when a menu command
+ // opens a modal dialog. The views associated with the popups needed to be
+ // hidden and the accesibility events fired before the command executes, but
+ // the popuphiding/popuphidden events are fired afterwards.
+ nsTArray<nsMenuPopupFrame*> popupsToHide;
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ while (item) {
+ // if it isn't a <menupopup>, don't close it automatically
+ if (!item->IsMenu()) {
+ break;
+ }
+
+ nsMenuChainItem* next = item->GetParent();
+ popupsToHide.AppendElement(item->Frame());
+ if (aMode == CloseMenuMode_Single) {
+ // only close one level of menu
+ break;
+ }
+ item = next;
+ }
+
+ // Now hide the popups. If the closemenu mode is auto, deselect the menu,
+ // otherwise only one popup is closing, so keep the parent menu selected.
+ HidePopupsInList(popupsToHide);
+}
+
+void nsXULPopupManager::ExecuteMenu(nsIContent* aMenu,
+ nsXULMenuCommandEvent* aEvent) {
+ CloseMenuMode cmm = GetCloseMenuMode(aMenu);
+ HideOpenMenusBeforeExecutingMenu(cmm);
+ aEvent->SetCloseMenuMode(cmm);
+ nsCOMPtr<nsIRunnable> event = aEvent;
+ aMenu->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
+}
+
+bool nsXULPopupManager::ActivateNativeMenuItem(nsIContent* aItem,
+ mozilla::Modifiers aModifiers,
+ int16_t aButton,
+ mozilla::ErrorResult& aRv) {
+ if (mNativeMenu && aItem->IsElement() &&
+ mNativeMenu->Element()->Contains(aItem)) {
+ mNativeMenu->ActivateItem(aItem->AsElement(), aModifiers, aButton, aRv);
+ return true;
+ }
+ return false;
+}
+
+nsEventStatus nsXULPopupManager::FirePopupShowingEvent(
+ const PendingPopup& aPendingPopup, nsPresContext* aPresContext) {
+ // Cache the pending popup so that the trigger node and other properties can
+ // be retrieved during the popupshowing event. It will be cleared below after
+ // the event has fired.
+ AutoRestore<const PendingPopup*> restorePendingPopup(mPendingPopup);
+ mPendingPopup = &aPendingPopup;
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent event(true, eXULPopupShowing, nullptr,
+ WidgetMouseEvent::eReal);
+
+ // coordinates are relative to the root widget
+ nsPresContext* rootPresContext = aPresContext->GetRootPresContext();
+ if (rootPresContext) {
+ event.mWidget =
+ rootPresContext->PresShell()->GetViewManager()->GetRootWidget();
+ } else {
+ event.mWidget = nullptr;
+ }
+
+ event.mInputSource = aPendingPopup.MouseInputSource();
+ event.mRefPoint = aPendingPopup.mMousePoint;
+ event.mModifiers = aPendingPopup.mModifiers;
+ RefPtr<nsIContent> popup = aPendingPopup.mPopup;
+ EventDispatcher::Dispatch(popup, aPresContext, &event, nullptr, &status);
+
+ return status;
+}
+
+void nsXULPopupManager::BeginShowingPopup(const PendingPopup& aPendingPopup,
+ bool aIsContextMenu,
+ bool aSelectFirstItem) {
+ RefPtr<nsIContent> popup = aPendingPopup.mPopup;
+
+ nsMenuPopupFrame* popupFrame = do_QueryFrame(popup->GetPrimaryFrame());
+ if (NS_WARN_IF(!popupFrame)) {
+ return;
+ }
+
+ RefPtr<nsPresContext> presContext = popupFrame->PresContext();
+ RefPtr<PresShell> presShell = presContext->PresShell();
+ presShell->FrameNeedsReflow(popupFrame, IntrinsicDirty::FrameAndAncestors,
+ NS_FRAME_IS_DIRTY);
+
+ nsPopupType popupType = popupFrame->PopupType();
+
+ nsEventStatus status = FirePopupShowingEvent(aPendingPopup, presContext);
+
+ // if a panel, blur whatever has focus so that the panel can take the focus.
+ // This is done after the popupshowing event in case that event is cancelled.
+ // Using noautofocus="true" will disable this behaviour, which is needed for
+ // the autocomplete widget as it manages focus itself.
+ if (popupType == ePopupTypePanel &&
+ !popup->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::noautofocus, nsGkAtoms::_true,
+ eCaseMatters)) {
+ if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
+ Document* doc = popup->GetUncomposedDoc();
+
+ // Only remove the focus if the currently focused item is ouside the
+ // popup. It isn't a big deal if the current focus is in a child popup
+ // inside the popup as that shouldn't be visible. This check ensures that
+ // a node inside the popup that is focused during a popupshowing event
+ // remains focused.
+ RefPtr<Element> currentFocus = fm->GetFocusedElement();
+ if (doc && currentFocus &&
+ !nsContentUtils::ContentIsCrossDocDescendantOf(currentFocus, popup)) {
+ nsCOMPtr<nsPIDOMWindowOuter> outerWindow = doc->GetWindow();
+ fm->ClearFocus(outerWindow);
+ }
+ }
+ }
+
+ popup->OwnerDoc()->FlushPendingNotifications(FlushType::Frames);
+
+ // get the frame again in case it went away
+ popupFrame = do_QueryFrame(popup->GetPrimaryFrame());
+ if (popupFrame) {
+ // if the event was cancelled or the popup was closed in the mean time,
+ // don't open the popup, reset its state back to closed and clear its
+ // trigger content.
+ if (popupFrame->PopupState() == ePopupClosed ||
+ status == nsEventStatus_eConsumeNoDefault) {
+ popupFrame->SetPopupState(ePopupClosed);
+ popupFrame->ClearTriggerContent();
+ } else {
+ // Now check if we need to fire the popuppositioned event. If not, call
+ // ShowPopupCallback directly.
+
+ // The popuppositioned event only fires on arrow panels for now.
+ if (popup->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::arrow, eCaseMatters)) {
+ popupFrame->ShowWithPositionedEvent();
+ presShell->FrameNeedsReflow(popupFrame,
+ IntrinsicDirty::FrameAndAncestors,
+ NS_FRAME_HAS_DIRTY_CHILDREN);
+ } else {
+ ShowPopupCallback(popup, popupFrame, aIsContextMenu, aSelectFirstItem);
+ }
+ }
+ }
+}
+
+void nsXULPopupManager::FirePopupHidingEvent(
+ nsIContent* aPopup, nsIContent* aNextPopup, nsIContent* aLastPopup,
+ nsPresContext* aPresContext, nsPopupType aPopupType, bool aDeselectMenu,
+ bool aIsCancel) {
+ nsCOMPtr<nsIContent> popup = aPopup;
+ RefPtr<PresShell> presShell = aPresContext->PresShell();
+ Unused << presShell; // This presShell may be keeping things alive
+ // on non GTK platforms
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetMouseEvent event(true, eXULPopupHiding, nullptr,
+ WidgetMouseEvent::eReal);
+ EventDispatcher::Dispatch(aPopup, aPresContext, &event, nullptr, &status);
+
+ // when a panel is closed, blur whatever has focus inside the popup
+ if (aPopupType == ePopupTypePanel &&
+ (!aPopup->IsElement() || !aPopup->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::noautofocus,
+ nsGkAtoms::_true, eCaseMatters))) {
+ if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) {
+ Document* doc = aPopup->GetUncomposedDoc();
+
+ // Remove the focus from the focused node only if it is inside the popup.
+ RefPtr<Element> currentFocus = fm->GetFocusedElement();
+ if (doc && currentFocus &&
+ nsContentUtils::ContentIsCrossDocDescendantOf(currentFocus, aPopup)) {
+ nsCOMPtr<nsPIDOMWindowOuter> outerWindow = doc->GetWindow();
+ fm->ClearFocus(outerWindow);
+ }
+ }
+ }
+
+ aPopup->OwnerDoc()->FlushPendingNotifications(FlushType::Frames);
+
+ // get frame again in case it went away
+ nsMenuPopupFrame* popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
+ if (popupFrame) {
+ // if the event was cancelled, don't hide the popup, and reset its
+ // state back to open. Only popups in chrome shells can prevent a popup
+ // from hiding.
+ if (status == nsEventStatus_eConsumeNoDefault &&
+ !popupFrame->IsInContentShell()) {
+ // XXXndeakin
+ // If an attempt was made to hide this popup before the popupshown event
+ // fired, then ePopupShown is set here even though it should be
+ // ePopupVisible. This probably isn't worth the hassle of handling.
+ popupFrame->SetPopupState(ePopupShown);
+ } else {
+ // If the popup has an animate attribute and it is not set to false, check
+ // if it has a closing transition and wait for it to finish. The
+ // transition may still occur either way, but the view will be hidden and
+ // you won't be able to see it. If there is a next popup, indicating that
+ // mutliple popups are rolling up, don't wait and hide the popup right
+ // away since the effect would likely be undesirable.
+ if (LookAndFeel::GetInt(LookAndFeel::IntID::PanelAnimations) &&
+ !aNextPopup && aPopup->IsElement() &&
+ aPopup->AsElement()->HasAttr(nsGkAtoms::animate)) {
+ // If animate="false" then don't transition at all. If animate="cancel",
+ // only show the transition if cancelling the popup or rolling up.
+ // Otherwise, always show the transition.
+ nsAutoString animate;
+ aPopup->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::animate,
+ animate);
+
+ if (!animate.EqualsLiteral("false") &&
+ (!animate.EqualsLiteral("cancel") || aIsCancel)) {
+ presShell->FlushPendingNotifications(FlushType::Layout);
+
+ // Get the frame again in case the flush caused it to go away
+ popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
+ if (!popupFrame) return;
+
+ if (AnimationUtils::HasCurrentTransitions(
+ aPopup->AsElement(), PseudoStyleType::NotPseudo)) {
+ RefPtr<TransitionEnder> ender =
+ new TransitionEnder(aPopup, aDeselectMenu);
+ aPopup->AddSystemEventListener(u"transitionend"_ns, ender, false,
+ false);
+ return;
+ }
+ }
+ }
+
+ HidePopupCallback(aPopup, popupFrame, aNextPopup, aLastPopup, aPopupType,
+ aDeselectMenu);
+ }
+ }
+}
+
+bool nsXULPopupManager::IsPopupOpen(nsIContent* aPopup) {
+ if (mNativeMenu && mNativeMenu->Element() == aPopup) {
+ return true;
+ }
+
+ // a popup is open if it is in the open list. The assertions ensure that the
+ // frame is in the correct state. If the popup is in the hiding or invisible
+ // state, it will still be in the open popup list until it is closed.
+ if (nsMenuChainItem* item = FindPopup(aPopup)) {
+ NS_ASSERTION(item->Frame()->IsOpen() ||
+ item->Frame()->PopupState() == ePopupHiding ||
+ item->Frame()->PopupState() == ePopupInvisible,
+ "popup in open list not actually open");
+ Unused << item;
+ return true;
+ }
+ return false;
+}
+
+nsIFrame* nsXULPopupManager::GetTopPopup(nsPopupType aType) {
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (item->Frame()->IsVisible() &&
+ (item->PopupType() == aType || aType == ePopupTypeAny)) {
+ return item->Frame();
+ }
+ }
+ return nullptr;
+}
+
+nsIContent* nsXULPopupManager::GetTopActiveMenuItemContent() {
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ if (!item->Frame()->IsVisible()) {
+ continue;
+ }
+ if (auto* content = item->Frame()->PopupElement().GetActiveMenuChild()) {
+ return content;
+ }
+ }
+ return nullptr;
+}
+
+void nsXULPopupManager::GetVisiblePopups(nsTArray<nsIFrame*>& aPopups) {
+ aPopups.Clear();
+ for (nsMenuChainItem* item = mPopups.get(); item; item = item->GetParent()) {
+ // Skip panels which are not visible as well as popups that are transparent
+ // to mouse events.
+ if (item->Frame()->IsVisible() && !item->Frame()->IsMouseTransparent()) {
+ aPopups.AppendElement(item->Frame());
+ }
+ }
+}
+
+already_AddRefed<nsINode> nsXULPopupManager::GetLastTriggerNode(
+ Document* aDocument, bool aIsTooltip) {
+ if (!aDocument) return nullptr;
+
+ RefPtr<nsINode> node;
+
+ // If a pending popup is set, it means that a popupshowing event is being
+ // fired. In this case, just use the cached node, as the popup is not yet in
+ // the list of open popups.
+ RefPtr<nsIContent> openingPopup =
+ mPendingPopup ? mPendingPopup->mPopup : nullptr;
+ if (openingPopup && openingPopup->GetUncomposedDoc() == aDocument &&
+ aIsTooltip == openingPopup->IsXULElement(nsGkAtoms::tooltip)) {
+ node = nsMenuPopupFrame::GetTriggerContent(
+ GetPopupFrameForContent(openingPopup, false));
+ } else if (mNativeMenu && !aIsTooltip) {
+ RefPtr<dom::Element> popup = mNativeMenu->Element();
+ if (popup->GetUncomposedDoc() == aDocument) {
+ nsMenuPopupFrame* popupFrame = GetPopupFrameForContent(popup, false);
+ node = nsMenuPopupFrame::GetTriggerContent(popupFrame);
+ }
+ } else {
+ for (nsMenuChainItem* item = mPopups.get(); item;
+ item = item->GetParent()) {
+ // look for a popup of the same type and document.
+ if ((item->PopupType() == ePopupTypeTooltip) == aIsTooltip &&
+ item->Content()->GetUncomposedDoc() == aDocument) {
+ node = nsMenuPopupFrame::GetTriggerContent(item->Frame());
+ if (node) {
+ break;
+ }
+ }
+ }
+ }
+
+ return node.forget();
+}
+
+bool nsXULPopupManager::MayShowPopup(nsMenuPopupFrame* aPopup) {
+ // if a popup's IsOpen method returns true, then the popup must always be in
+ // the popup chain scanned in IsPopupOpen.
+ NS_ASSERTION(!aPopup->IsOpen() || IsPopupOpen(aPopup->GetContent()),
+ "popup frame state doesn't match XULPopupManager open state");
+
+ nsPopupState state = aPopup->PopupState();
+
+ // if the popup is not in the open popup chain, then it must have a state that
+ // is either closed, in the process of being shown, or invisible.
+ NS_ASSERTION(IsPopupOpen(aPopup->GetContent()) || state == ePopupClosed ||
+ state == ePopupShowing || state == ePopupPositioning ||
+ state == ePopupInvisible,
+ "popup not in XULPopupManager open list is open");
+
+ // don't show popups unless they are closed or invisible
+ if (state != ePopupClosed && state != ePopupInvisible) return false;
+
+ // Don't show popups that we already have in our popup chain
+ if (IsPopupOpen(aPopup->GetContent())) {
+ NS_WARNING("Refusing to show duplicate popup");
+ return false;
+ }
+
+ // if the popup was just rolled up, don't reopen it
+ if (mozilla::widget::nsAutoRollup::GetLastRollup() == aPopup->GetContent()) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = aPopup->PresContext()->GetDocShell();
+
+ nsCOMPtr<nsIBaseWindow> baseWin = do_QueryInterface(docShell);
+ if (!baseWin) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDocShellTreeItem> root;
+ docShell->GetInProcessRootTreeItem(getter_AddRefs(root));
+ if (!root) {
+ return false;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> rootWin = root->GetWindow();
+
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess(),
+ "Cannot have XUL in content process showing popups.");
+
+ // chrome shells can always open popups, but other types of shells can only
+ // open popups when they are focused and visible
+ if (docShell->ItemType() != nsIDocShellTreeItem::typeChrome) {
+ // only allow popups in active windows
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (!fm || !rootWin) {
+ return false;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> activeWindow = fm->GetActiveWindow();
+ if (activeWindow != rootWin) {
+ return false;
+ }
+
+ // only allow popups in visible frames
+ // TODO: This visibility check should be replaced with a check of
+ // bc->IsActive(). It is okay for now since this is only called
+ // in the parent process. Bug 1698533.
+ bool visible;
+ baseWin->GetVisibility(&visible);
+ if (!visible) {
+ return false;
+ }
+ }
+
+ // platforms respond differently when an popup is opened in a minimized
+ // window, so this is always disabled.
+ nsCOMPtr<nsIWidget> mainWidget;
+ baseWin->GetMainWidget(getter_AddRefs(mainWidget));
+ if (mainWidget && mainWidget->SizeMode() == nsSizeMode_Minimized) {
+ return false;
+ }
+
+#ifdef XP_MACOSX
+ if (rootWin) {
+ auto globalWin = nsGlobalWindowOuter::Cast(rootWin.get());
+ if (globalWin->IsInModalState()) {
+ return false;
+ }
+ }
+#endif
+
+ // cannot open a popup that is a submenu of a menupopup that isn't open.
+ if (auto* menu = aPopup->PopupElement().GetContainingMenu()) {
+ if (auto* parent = XULPopupElement::FromNodeOrNull(menu->GetMenuParent())) {
+ nsMenuPopupFrame* f = do_QueryFrame(parent->GetPrimaryFrame());
+ if (f && !f->IsOpen()) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+void nsXULPopupManager::PopupDestroyed(nsMenuPopupFrame* aPopup) {
+ // when a popup frame is destroyed, just unhook it from the list of popups
+ CancelMenuTimer(aPopup);
+
+ nsMenuChainItem* item = FindPopup(aPopup->GetContent());
+ if (!item) {
+ return;
+ }
+
+ nsTArray<nsMenuPopupFrame*> popupsToHide;
+ // XXXndeakin shouldn't this only happen for menus?
+ if (!item->IsNoAutoHide() && item->Frame()->PopupState() != ePopupInvisible) {
+ // Iterate through any child menus and hide them as well, since the
+ // parent is going away. We won't remove them from the list yet, just
+ // hide them, as they will be removed from the list when this function
+ // gets called for that child frame.
+ for (auto* child = item->GetChild(); child; child = child->GetChild()) {
+ // If the popup is a child frame of the menu that was destroyed, add it
+ // to the list of popups to hide. Don't bother with the events since the
+ // frames are going away. If the child menu is not a child frame, for
+ // example, a context menu, use HidePopup instead, but call it
+ // asynchronously since we are in the middle of frame destruction.
+ if (nsLayoutUtils::IsProperAncestorFrame(item->Frame(), child->Frame())) {
+ popupsToHide.AppendElement(child->Frame());
+ } else {
+ // HidePopup will take care of hiding any of its children, so
+ // break out afterwards
+ HidePopup(child->Content(), false, false, true, false);
+ break;
+ }
+ }
+ }
+
+ RemoveMenuChainItem(item);
+ HidePopupsInList(popupsToHide);
+}
+
+bool nsXULPopupManager::HasContextMenu(nsMenuPopupFrame* aPopup) {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ while (item && item->Frame() != aPopup) {
+ if (item->IsContextMenu()) return true;
+ item = item->GetParent();
+ }
+
+ return false;
+}
+
+void nsXULPopupManager::SetCaptureState(nsIContent* aOldPopup) {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (item && aOldPopup == item->Content()) return;
+
+ if (mWidget) {
+ mWidget->CaptureRollupEvents(false);
+ mWidget = nullptr;
+ }
+
+ if (item) {
+ nsMenuPopupFrame* popup = item->Frame();
+ mWidget = popup->GetWidget();
+ if (mWidget) {
+ mWidget->CaptureRollupEvents(true);
+ }
+ }
+
+ UpdateKeyboardListeners();
+}
+
+void nsXULPopupManager::UpdateKeyboardListeners() {
+ nsCOMPtr<EventTarget> newTarget;
+ bool isForMenu = false;
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (item) {
+ if (item->IgnoreKeys() != eIgnoreKeys_True) {
+ newTarget = item->Content()->GetComposedDoc();
+ }
+ isForMenu = item->PopupType() == ePopupTypeMenu;
+ } else if (mActiveMenuBar) {
+ newTarget = mActiveMenuBar->GetContent()->GetComposedDoc();
+ isForMenu = true;
+ }
+
+ if (mKeyListener != newTarget) {
+ OwningNonNull<nsXULPopupManager> kungFuDeathGrip(*this);
+ if (mKeyListener) {
+ mKeyListener->RemoveEventListener(u"keypress"_ns, this, true);
+ mKeyListener->RemoveEventListener(u"keydown"_ns, this, true);
+ mKeyListener->RemoveEventListener(u"keyup"_ns, this, true);
+ mKeyListener = nullptr;
+ nsContentUtils::NotifyInstalledMenuKeyboardListener(false);
+ }
+
+ if (newTarget) {
+ newTarget->AddEventListener(u"keypress"_ns, this, true);
+ newTarget->AddEventListener(u"keydown"_ns, this, true);
+ newTarget->AddEventListener(u"keyup"_ns, this, true);
+ nsContentUtils::NotifyInstalledMenuKeyboardListener(isForMenu);
+ mKeyListener = newTarget;
+ }
+ }
+}
+
+void nsXULPopupManager::UpdateMenuItems(nsIContent* aPopup) {
+ // Walk all of the menu's children, checking to see if any of them has a
+ // command attribute. If so, then several attributes must potentially be
+ // updated.
+
+ nsCOMPtr<Document> document = aPopup->GetUncomposedDoc();
+ if (!document) {
+ return;
+ }
+
+ // When a menu is opened, make sure that command updating is unlocked first.
+ nsCOMPtr<nsIDOMXULCommandDispatcher> commandDispatcher =
+ document->GetCommandDispatcher();
+ if (commandDispatcher) {
+ commandDispatcher->Unlock();
+ }
+
+ for (nsCOMPtr<nsIContent> grandChild = aPopup->GetFirstChild(); grandChild;
+ grandChild = grandChild->GetNextSibling()) {
+ if (grandChild->IsXULElement(nsGkAtoms::menugroup)) {
+ if (grandChild->GetChildCount() == 0) {
+ continue;
+ }
+ grandChild = grandChild->GetFirstChild();
+ }
+ if (grandChild->IsXULElement(nsGkAtoms::menuitem)) {
+ // See if we have a command attribute.
+ Element* grandChildElement = grandChild->AsElement();
+ nsAutoString command;
+ grandChildElement->GetAttr(kNameSpaceID_None, nsGkAtoms::command,
+ command);
+ if (!command.IsEmpty()) {
+ // We do! Look it up in our document
+ RefPtr<dom::Element> commandElement = document->GetElementById(command);
+ if (commandElement) {
+ nsAutoString commandValue;
+ // The menu's disabled state needs to be updated to match the command.
+ if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
+ commandValue))
+ grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
+ commandValue, true);
+ else
+ grandChildElement->UnsetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
+ true);
+
+ // The menu's label, accesskey checked and hidden states need to be
+ // updated to match the command. Note that unlike the disabled state
+ // if the command has *no* value, we assume the menu is supplying its
+ // own.
+ if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::label,
+ commandValue))
+ grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::label,
+ commandValue, true);
+
+ if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
+ commandValue))
+ grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::accesskey,
+ commandValue, true);
+
+ if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::checked,
+ commandValue))
+ grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::checked,
+ commandValue, true);
+
+ if (commandElement->GetAttr(kNameSpaceID_None, nsGkAtoms::hidden,
+ commandValue))
+ grandChildElement->SetAttr(kNameSpaceID_None, nsGkAtoms::hidden,
+ commandValue, true);
+ }
+ }
+ }
+ if (!grandChild->GetNextSibling() &&
+ grandChild->GetParent()->IsXULElement(nsGkAtoms::menugroup)) {
+ grandChild = grandChild->GetParent();
+ }
+ }
+}
+
+// Notify
+//
+// The item selection timer has fired, we might have to readjust the
+// selected item. There are two cases here that we are trying to deal with:
+// (1) diagonal movement from a parent menu to a submenu passing briefly over
+// other items, and
+// (2) moving out from a submenu to a parent or grandparent menu.
+// In both cases, |mTimerMenu| is the menu item that might have an open submenu
+// and the first item in |mPopups| is the item the mouse is currently over,
+// which could be none of them.
+//
+// case (1):
+// As the mouse moves from the parent item of a submenu (we'll call 'A')
+// diagonally into the submenu, it probably passes through one or more
+// sibilings (B). As the mouse passes through B, it becomes the current menu
+// item and the timer is set and mTimerMenu is set to A. Before the timer
+// fires, the mouse leaves the menu containing A and B and enters the submenus.
+// Now when the timer fires, |mPopups| is null (!= |mTimerMenu|) so we have to
+// see if anything in A's children is selected (recall that even disabled items
+// are selected, the style just doesn't show it). If that is the case, we need
+// to set the selected item back to A.
+//
+// case (2);
+// Item A has an open submenu, and in it there is an item (B) which also has an
+// open submenu (so there are 3 menus displayed right now). The mouse then
+// leaves B's child submenu and selects an item that is a sibling of A, call it
+// C. When the mouse enters C, the timer is set and |mTimerMenu| is A and
+// |mPopups| is C. As the timer fires, the mouse is still within C. The correct
+// behavior is to set the current item to C and close up the chain parented at
+// A.
+//
+// This brings up the question of is the logic of case (1) enough? The answer
+// is no, and is discussed in bugzilla bug 29400. Case (1) asks if A's submenu
+// has a selected child, and if it does, set the selected item to A. Because B
+// has a submenu open, it is selected and as a result, A is set to be the
+// selected item even though the mouse rests in C -- very wrong.
+//
+// The solution is to use the same idea, but instead of only checking one
+// level, drill all the way down to the deepest open submenu and check if it
+// has something selected. Since the mouse is in a grandparent, it won't, and
+// we know that we can safely close up A and all its children.
+//
+// The code below melds the two cases together.
+//
+void nsXULPopupManager::KillMenuTimer() {
+ if (mCloseTimer && mTimerMenu) {
+ mCloseTimer->Cancel();
+ mCloseTimer = nullptr;
+
+ if (mTimerMenu->IsOpen()) {
+ HidePopup(mTimerMenu->GetContent(), false, false, true, false);
+ }
+ }
+
+ mTimerMenu = nullptr;
+}
+
+void nsXULPopupManager::CancelMenuTimer(nsMenuPopupFrame* aMenu) {
+ if (mCloseTimer && mTimerMenu == aMenu) {
+ mCloseTimer->Cancel();
+ mCloseTimer = nullptr;
+ mTimerMenu = nullptr;
+ }
+}
+
+bool nsXULPopupManager::HandleShortcutNavigation(KeyboardEvent& aKeyEvent,
+ nsMenuPopupFrame* aFrame) {
+ // On Windows, don't check shortcuts when the accelerator key is down.
+#ifdef XP_WIN
+ WidgetInputEvent* evt = aKeyEvent.WidgetEventPtr()->AsInputEvent();
+ if (evt && evt->IsAccel()) {
+ return false;
+ }
+#endif
+
+ if (!aFrame) {
+ if (nsMenuChainItem* item = GetTopVisibleMenu()) {
+ aFrame = item->Frame();
+ }
+ }
+
+ if (aFrame) {
+ bool action = false;
+ RefPtr result = aFrame->FindMenuWithShortcut(aKeyEvent, action);
+ if (!result) {
+ return false;
+ }
+ RefPtr popup = &aFrame->PopupElement();
+ popup->SetActiveMenuChild(result, XULMenuParentElement::ByKey::Yes);
+ if (action) {
+ WidgetEvent* evt = aKeyEvent.WidgetEventPtr();
+ result->HandleEnterKeyPress(*evt);
+ }
+ return true;
+ }
+
+ if (mActiveMenuBar) {
+ RefPtr menubar = &mActiveMenuBar->MenubarElement();
+ if (RefPtr result = menubar->FindMenuWithShortcut(aKeyEvent)) {
+ result->OpenMenuPopup(true);
+ return true;
+ }
+#ifdef XP_WIN
+ // Behavior on Windows - this item is on the menu bar, beep and deactivate
+ // the menu bar.
+ // TODO(emilio): This is rather odd, and I cannot get the beep to work,
+ // but this matches what old code was doing...
+ if (nsCOMPtr<nsISound> sound = do_GetService("@mozilla.org/sound;1")) {
+ sound->Beep();
+ }
+ mActiveMenuBar->SetActive(false);
+#endif
+ }
+ return false;
+}
+
+bool nsXULPopupManager::HandleKeyboardNavigation(uint32_t aKeyCode) {
+ if (nsMenuChainItem* nextitem = GetTopVisibleMenu()) {
+ nextitem->Content()->OwnerDoc()->FlushPendingNotifications(
+ FlushType::Frames);
+ }
+
+ // navigate up through the open menus, looking for the topmost one
+ // in the same hierarchy
+ nsMenuChainItem* item = nullptr;
+ nsMenuChainItem* nextitem = GetTopVisibleMenu();
+ while (nextitem) {
+ item = nextitem;
+ nextitem = item->GetParent();
+
+ if (!nextitem) {
+ break;
+ }
+ // stop if the parent isn't a menu
+ if (!nextitem->IsMenu()) {
+ break;
+ }
+
+ // Check to make sure that the parent is actually the parent menu. It won't
+ // be if the parent is in a different frame hierarchy, for example, for a
+ // context menu opened on another menu.
+ XULPopupElement& expectedParent = nextitem->Frame()->PopupElement();
+ auto* menu = item->Frame()->PopupElement().GetContainingMenu();
+ if (!menu || menu->GetMenuParent() != &expectedParent) {
+ break;
+ }
+ }
+
+ nsIFrame* itemFrame;
+ if (item) {
+ itemFrame = item->Frame();
+ } else if (mActiveMenuBar) {
+ itemFrame = mActiveMenuBar;
+ } else {
+ return false;
+ }
+
+ nsNavigationDirection theDirection;
+ NS_ASSERTION(aKeyCode >= KeyboardEvent_Binding::DOM_VK_END &&
+ aKeyCode <= KeyboardEvent_Binding::DOM_VK_DOWN,
+ "Illegal key code");
+ theDirection = NS_DIRECTION_FROM_KEY_CODE(itemFrame, aKeyCode);
+
+ bool selectFirstItem = true;
+#ifdef MOZ_WIDGET_GTK
+ {
+ XULButtonElement* currentItem = nullptr;
+ if (item && mActiveMenuBar && NS_DIRECTION_IS_INLINE(theDirection)) {
+ currentItem = item->Frame()->PopupElement().GetActiveMenuChild();
+ // If nothing is selected in the menu and we have a menubar, let it
+ // handle the movement not to steal focus from it.
+ if (!currentItem) {
+ item = nullptr;
+ }
+ }
+ // On menu change, only select first item if an item is already selected.
+ selectFirstItem = !!currentItem;
+ }
+#endif
+
+ // if a popup is open, first check for navigation within the popup
+ if (item && HandleKeyboardNavigationInPopup(item, theDirection)) {
+ return true;
+ }
+
+ // no popup handled the key, so check the active menubar, if any
+ if (!mActiveMenuBar) {
+ return false;
+ }
+ RefPtr menubar = XULMenuParentElement::FromNode(mActiveMenuBar->GetContent());
+ if (NS_DIRECTION_IS_INLINE(theDirection)) {
+ RefPtr prevActiveItem = menubar->GetActiveMenuChild();
+ const bool open = prevActiveItem && prevActiveItem->IsMenuPopupOpen();
+ RefPtr nextItem = theDirection == eNavigationDirection_End
+ ? menubar->GetNextMenuItem()
+ : menubar->GetPrevMenuItem();
+ menubar->SetActiveMenuChild(nextItem, XULMenuParentElement::ByKey::Yes);
+ if (open && nextItem) {
+ nextItem->OpenMenuPopup(selectFirstItem);
+ }
+ return true;
+ }
+ if (NS_DIRECTION_IS_BLOCK(theDirection)) {
+ // Open the menu and select its first item.
+ if (RefPtr currentMenu = menubar->GetActiveMenuChild()) {
+ ShowMenu(currentMenu, selectFirstItem);
+ }
+ return true;
+ }
+ return false;
+}
+
+bool nsXULPopupManager::HandleKeyboardNavigationInPopup(
+ nsMenuChainItem* item, nsMenuPopupFrame* aFrame,
+ nsNavigationDirection aDir) {
+ NS_ASSERTION(aFrame, "aFrame is null");
+ NS_ASSERTION(!item || item->Frame() == aFrame,
+ "aFrame is expected to be equal to item->Frame()");
+
+ using Wrap = XULMenuParentElement::Wrap;
+ RefPtr<XULPopupElement> menu = &aFrame->PopupElement();
+
+ aFrame->ClearIncrementalString();
+ RefPtr currentItem = aFrame->GetCurrentMenuItem();
+
+ // This method only gets called if we're open.
+ if (!currentItem && NS_DIRECTION_IS_INLINE(aDir)) {
+ // We've been opened, but we haven't had anything selected.
+ // We can handle End, but our parent handles Start.
+ if (aDir == eNavigationDirection_End) {
+ if (RefPtr nextItem = menu->GetNextMenuItem(Wrap::No)) {
+ menu->SetActiveMenuChild(nextItem, XULMenuParentElement::ByKey::Yes);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ const bool isContainer = currentItem && !currentItem->IsMenuItem();
+ const bool isOpen = currentItem && currentItem->IsMenuPopupOpen();
+ if (isOpen) {
+ // For an open popup, have the child process the event
+ nsMenuChainItem* child = item ? item->GetChild() : nullptr;
+ if (child && HandleKeyboardNavigationInPopup(child, aDir)) {
+ return true;
+ }
+ } else if (aDir == eNavigationDirection_End && isContainer &&
+ !currentItem->IsDisabled()) {
+ currentItem->OpenMenuPopup(true);
+ return true;
+ }
+
+ // For block progression, we can move in either direction
+ if (NS_DIRECTION_IS_BLOCK(aDir) || NS_DIRECTION_IS_BLOCK_TO_EDGE(aDir)) {
+ RefPtr<XULButtonElement> nextItem = nullptr;
+
+ if (aDir == eNavigationDirection_Before ||
+ aDir == eNavigationDirection_After) {
+ // Cursor navigation does not wrap on Mac or for menulists on Windows.
+ auto wrap =
+#ifdef XP_WIN
+ aFrame->IsMenuList() ? Wrap::No : Wrap::Yes;
+#elif defined XP_MACOSX
+ Wrap::No;
+#else
+ Wrap::Yes;
+#endif
+
+ if (aDir == eNavigationDirection_Before) {
+ nextItem = menu->GetPrevMenuItem(wrap);
+ } else {
+ nextItem = menu->GetNextMenuItem(wrap);
+ }
+ } else if (aDir == eNavigationDirection_First) {
+ nextItem = menu->GetFirstMenuItem();
+ } else {
+ nextItem = menu->GetLastMenuItem();
+ }
+
+ if (nextItem) {
+ menu->SetActiveMenuChild(nextItem, XULMenuParentElement::ByKey::Yes);
+ return true;
+ }
+ } else if (currentItem && isOpen && aDir == eNavigationDirection_Start) {
+ // close a submenu when Left is pressed
+ if (nsMenuPopupFrame* popupFrame =
+ currentItem->GetMenuPopup(FlushType::None)) {
+ HidePopup(popupFrame->GetContent(), /* aHideChain = */ false,
+ /* aDeselectMenu = */ false, /* aAsynchronous = */ false,
+ /* aIsCancel = */ false);
+ }
+ return true;
+ }
+
+ return false;
+}
+
+bool nsXULPopupManager::HandleKeyboardEventWithKeyCode(
+ KeyboardEvent* aKeyEvent, nsMenuChainItem* aTopVisibleMenuItem) {
+ uint32_t keyCode = aKeyEvent->KeyCode();
+
+ // Escape should close panels, but the other keys should have no effect.
+ if (aTopVisibleMenuItem &&
+ aTopVisibleMenuItem->PopupType() != ePopupTypeMenu) {
+ if (keyCode == KeyboardEvent_Binding::DOM_VK_ESCAPE) {
+ HidePopup(aTopVisibleMenuItem->Content(), false, false, false, true);
+ aKeyEvent->StopPropagation();
+ aKeyEvent->StopCrossProcessForwarding();
+ aKeyEvent->PreventDefault();
+ }
+ return true;
+ }
+
+ bool consume = (aTopVisibleMenuItem || mActiveMenuBar);
+ switch (keyCode) {
+ case KeyboardEvent_Binding::DOM_VK_UP:
+ case KeyboardEvent_Binding::DOM_VK_DOWN:
+#ifndef XP_MACOSX
+ // roll up the popup when alt+up/down are pressed within a menulist.
+ if (aKeyEvent->AltKey() && aTopVisibleMenuItem &&
+ aTopVisibleMenuItem->Frame()->IsMenuList()) {
+ Rollup(0, false, nullptr, nullptr);
+ break;
+ }
+ [[fallthrough]];
+#endif
+
+ case KeyboardEvent_Binding::DOM_VK_LEFT:
+ case KeyboardEvent_Binding::DOM_VK_RIGHT:
+ case KeyboardEvent_Binding::DOM_VK_HOME:
+ case KeyboardEvent_Binding::DOM_VK_END:
+ HandleKeyboardNavigation(keyCode);
+ break;
+
+ case KeyboardEvent_Binding::DOM_VK_PAGE_DOWN:
+ case KeyboardEvent_Binding::DOM_VK_PAGE_UP:
+ if (aTopVisibleMenuItem) {
+ aTopVisibleMenuItem->Frame()->ChangeByPage(
+ keyCode == KeyboardEvent_Binding::DOM_VK_PAGE_UP);
+ }
+ break;
+
+ case KeyboardEvent_Binding::DOM_VK_ESCAPE:
+ // Pressing Escape hides one level of menus only. If no menu is open,
+ // check if a menubar is active and inform it that a menu closed. Even
+ // though in this latter case, a menu didn't actually close, the effect
+ // ends up being the same. Similar for the tab key below.
+ if (aTopVisibleMenuItem) {
+ HidePopup(aTopVisibleMenuItem->Content(), false, false, false, true);
+ } else if (mActiveMenuBar) {
+ mActiveMenuBar->MenuClosed();
+ }
+ break;
+
+ case KeyboardEvent_Binding::DOM_VK_TAB:
+#ifndef XP_MACOSX
+ case KeyboardEvent_Binding::DOM_VK_F10:
+#endif
+ if (aTopVisibleMenuItem &&
+ !aTopVisibleMenuItem->Frame()->PopupElement().AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::activateontab, nsGkAtoms::_true,
+ eCaseMatters)) {
+ // Close popups or deactivate menubar when Tab or F10 are pressed
+ Rollup(0, false, nullptr, nullptr);
+ break;
+ } else if (mActiveMenuBar) {
+ mActiveMenuBar->MenuClosed();
+ break;
+ }
+ // Intentional fall-through to RETURN case
+ [[fallthrough]];
+
+ case KeyboardEvent_Binding::DOM_VK_RETURN: {
+ // If there is a popup open, check if the current item needs to be opened.
+ // Otherwise, tell the active menubar, if any, to activate the menu. The
+ // Enter method will return a menu if one needs to be opened as a result.
+ WidgetEvent* event = aKeyEvent->WidgetEventPtr();
+ if (aTopVisibleMenuItem) {
+ aTopVisibleMenuItem->Frame()->HandleEnterKeyPress(*event);
+ } else if (mActiveMenuBar) {
+ mActiveMenuBar->HandleEnterKeyPress(*event);
+ }
+ break;
+ }
+
+ default:
+ return false;
+ }
+
+ if (consume) {
+ aKeyEvent->StopPropagation();
+ aKeyEvent->StopCrossProcessForwarding();
+ aKeyEvent->PreventDefault();
+ }
+ return true;
+}
+
+nsresult nsXULPopupManager::HandleEvent(Event* aEvent) {
+ RefPtr<KeyboardEvent> keyEvent = aEvent->AsKeyboardEvent();
+ NS_ENSURE_TRUE(keyEvent, NS_ERROR_UNEXPECTED);
+
+ // handlers shouldn't be triggered by non-trusted events.
+ if (!keyEvent->IsTrusted()) {
+ return NS_OK;
+ }
+
+ nsAutoString eventType;
+ keyEvent->GetType(eventType);
+ if (eventType.EqualsLiteral("keyup")) {
+ return KeyUp(keyEvent);
+ }
+ if (eventType.EqualsLiteral("keydown")) {
+ return KeyDown(keyEvent);
+ }
+ if (eventType.EqualsLiteral("keypress")) {
+ return KeyPress(keyEvent);
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Unexpected eventType");
+ return NS_OK;
+}
+
+nsresult nsXULPopupManager::UpdateIgnoreKeys(bool aIgnoreKeys) {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (item) {
+ item->SetIgnoreKeys(aIgnoreKeys ? eIgnoreKeys_True : eIgnoreKeys_Shortcuts);
+ }
+ UpdateKeyboardListeners();
+ return NS_OK;
+}
+
+nsPopupState nsXULPopupManager::GetPopupState(
+ mozilla::dom::Element* aPopupElement) {
+ if (mNativeMenu && mNativeMenu->Element()->Contains(aPopupElement)) {
+ if (aPopupElement != mNativeMenu->Element()) {
+ // Submenu state is stored in mNativeMenuSubmenuStates.
+ return mNativeMenuSubmenuStates.MaybeGet(aPopupElement)
+ .valueOr(ePopupClosed);
+ }
+ // mNativeMenu->Element()'s state is stored in its nsMenuPopupFrame.
+ }
+
+ nsMenuPopupFrame* menuPopupFrame =
+ do_QueryFrame(aPopupElement->GetPrimaryFrame());
+ if (menuPopupFrame) {
+ return menuPopupFrame->PopupState();
+ }
+ return ePopupClosed;
+}
+
+nsresult nsXULPopupManager::KeyUp(KeyboardEvent* aKeyEvent) {
+ // don't do anything if a menu isn't open or a menubar isn't active
+ if (!mActiveMenuBar) {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (!item || item->PopupType() != ePopupTypeMenu) return NS_OK;
+
+ if (item->IgnoreKeys() == eIgnoreKeys_Shortcuts) {
+ aKeyEvent->StopCrossProcessForwarding();
+ return NS_OK;
+ }
+ }
+
+ aKeyEvent->StopPropagation();
+ aKeyEvent->StopCrossProcessForwarding();
+ aKeyEvent->PreventDefault();
+
+ return NS_OK; // I am consuming event
+}
+
+nsresult nsXULPopupManager::KeyDown(KeyboardEvent* aKeyEvent) {
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (item && item->Frame()->PopupElement().IsLocked()) {
+ return NS_OK;
+ }
+
+ if (HandleKeyboardEventWithKeyCode(aKeyEvent, item)) {
+ return NS_OK;
+ }
+
+ // don't do anything if a menu isn't open or a menubar isn't active
+ if (!mActiveMenuBar && (!item || item->PopupType() != ePopupTypeMenu))
+ return NS_OK;
+
+ // Since a menu was open, stop propagation of the event to keep other event
+ // listeners from becoming confused.
+ if (!item || item->IgnoreKeys() != eIgnoreKeys_Shortcuts) {
+ aKeyEvent->StopPropagation();
+ }
+
+ // If the key just pressed is the access key (usually Alt),
+ // dismiss and unfocus the menu.
+ int32_t menuAccessKey = nsMenuBarListener::GetMenuAccessKey();
+ if (menuAccessKey) {
+ uint32_t theChar = aKeyEvent->KeyCode();
+
+ if (theChar == (uint32_t)menuAccessKey) {
+ bool ctrl = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_CONTROL &&
+ aKeyEvent->CtrlKey());
+ bool alt = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_ALT &&
+ aKeyEvent->AltKey());
+ bool shift = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_SHIFT &&
+ aKeyEvent->ShiftKey());
+ bool meta = (menuAccessKey != KeyboardEvent_Binding::DOM_VK_META &&
+ aKeyEvent->MetaKey());
+ if (!(ctrl || alt || shift || meta)) {
+ // The access key just went down and no other
+ // modifiers are already down.
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (item && !item->Frame()->IsMenuList()) {
+ Rollup(0, false, nullptr, nullptr);
+ } else if (mActiveMenuBar) {
+ mActiveMenuBar->MenuClosed();
+ }
+
+ // Clear the item to avoid bugs as it may have been deleted during
+ // rollup.
+ item = nullptr;
+ }
+ aKeyEvent->StopPropagation();
+ aKeyEvent->PreventDefault();
+ }
+ }
+
+ aKeyEvent->StopCrossProcessForwarding();
+ return NS_OK;
+}
+
+nsresult nsXULPopupManager::KeyPress(KeyboardEvent* aKeyEvent) {
+ // Don't check prevent default flag -- menus always get first shot at key
+ // events.
+
+ nsMenuChainItem* item = GetTopVisibleMenu();
+ if (item && (item->Frame()->PopupElement().IsLocked() ||
+ item->PopupType() != ePopupTypeMenu)) {
+ return NS_OK;
+ }
+
+ // if a menu is open or a menubar is active, it consumes the key event
+ bool consume = (item || mActiveMenuBar);
+
+ WidgetInputEvent* evt = aKeyEvent->WidgetEventPtr()->AsInputEvent();
+ bool isAccel = evt && evt->IsAccel();
+
+ // When ignorekeys="shortcuts" is used, we don't call preventDefault on the
+ // key event when the accelerator key is pressed. This allows another
+ // listener to handle keys. For instance, this allows global shortcuts to
+ // still apply while a menu is open.
+ if (item && item->IgnoreKeys() == eIgnoreKeys_Shortcuts && isAccel) {
+ consume = false;
+ }
+
+ HandleShortcutNavigation(*aKeyEvent, nullptr);
+
+ aKeyEvent->StopCrossProcessForwarding();
+ if (consume) {
+ aKeyEvent->StopPropagation();
+ aKeyEvent->PreventDefault();
+ }
+
+ return NS_OK; // I am consuming event
+}
+
+NS_IMETHODIMP
+nsXULPopupHidingEvent::Run() {
+ RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance();
+ Document* document = mPopup->GetUncomposedDoc();
+ if (pm && document) {
+ if (RefPtr<nsPresContext> presContext = document->GetPresContext()) {
+ nsCOMPtr<nsIContent> popup = mPopup;
+ nsCOMPtr<nsIContent> nextPopup = mNextPopup;
+ nsCOMPtr<nsIContent> lastPopup = mLastPopup;
+ pm->FirePopupHidingEvent(popup, nextPopup, lastPopup, presContext,
+ mPopupType, mDeselectMenu, mIsRollup);
+ }
+ }
+ return NS_OK;
+}
+
+bool nsXULPopupPositionedEvent::DispatchIfNeeded(nsIContent* aPopup) {
+ // The popuppositioned event only fires on arrow panels for now.
+ if (aPopup->IsElement() &&
+ aPopup->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ nsGkAtoms::arrow, eCaseMatters)) {
+ nsCOMPtr<nsIRunnable> event = new nsXULPopupPositionedEvent(aPopup);
+ aPopup->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
+ return true;
+ }
+
+ return false;
+}
+
+static void AlignmentPositionToString(nsMenuPopupFrame* aFrame,
+ nsAString& aString) {
+ aString.Truncate();
+ int8_t position = aFrame->GetAlignmentPosition();
+ switch (position) {
+ case POPUPPOSITION_AFTERSTART:
+ return aString.AssignLiteral("after_start");
+ case POPUPPOSITION_AFTEREND:
+ return aString.AssignLiteral("after_end");
+ case POPUPPOSITION_BEFORESTART:
+ return aString.AssignLiteral("before_start");
+ case POPUPPOSITION_BEFOREEND:
+ return aString.AssignLiteral("before_end");
+ case POPUPPOSITION_STARTBEFORE:
+ return aString.AssignLiteral("start_before");
+ case POPUPPOSITION_ENDBEFORE:
+ return aString.AssignLiteral("end_before");
+ case POPUPPOSITION_STARTAFTER:
+ return aString.AssignLiteral("start_after");
+ case POPUPPOSITION_ENDAFTER:
+ return aString.AssignLiteral("end_after");
+ case POPUPPOSITION_OVERLAP:
+ return aString.AssignLiteral("overlap");
+ case POPUPPOSITION_AFTERPOINTER:
+ return aString.AssignLiteral("after_pointer");
+ case POPUPPOSITION_SELECTION:
+ return aString.AssignLiteral("selection");
+ default:
+ // Leave as an empty string.
+ break;
+ }
+}
+
+NS_IMETHODIMP
+MOZ_CAN_RUN_SCRIPT_BOUNDARY
+nsXULPopupPositionedEvent::Run() {
+ RefPtr<nsXULPopupManager> pm = nsXULPopupManager::GetInstance();
+ if (!pm) {
+ return NS_OK;
+ }
+ nsMenuPopupFrame* popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
+ if (!popupFrame) {
+ return NS_OK;
+ }
+
+ popupFrame->WillDispatchPopupPositioned();
+
+ // At this point, hidePopup may have been called but it currently has no
+ // way to stop this event. However, if hidePopup was called, the popup
+ // will now be in the hiding or closed state. If we are in the shown or
+ // positioning state instead, we can assume that we are still clear to
+ // open/move the popup
+ nsPopupState state = popupFrame->PopupState();
+ if (state != ePopupPositioning && state != ePopupShown) {
+ return NS_OK;
+ }
+
+ // Note that the offset might be along either the X or Y axis, but for the
+ // sake of simplicity we use a point with only the X axis set so we can
+ // use ToNearestPixels().
+ int32_t popupOffset = nsPoint(popupFrame->GetAlignmentOffset(), 0)
+ .ToNearestPixels(AppUnitsPerCSSPixel())
+ .x;
+
+ PopupPositionedEventInit init;
+ init.mComposed = true;
+ init.mIsAnchored = popupFrame->IsAnchored();
+ init.mAlignmentOffset = popupOffset;
+ AlignmentPositionToString(popupFrame, init.mAlignmentPosition);
+ RefPtr<PopupPositionedEvent> event =
+ PopupPositionedEvent::Constructor(mPopup, u"popuppositioned"_ns, init);
+ event->SetTrusted(true);
+
+ mPopup->DispatchEvent(*event);
+
+ // Get the popup frame and make sure it is still in the positioning
+ // state. If it isn't, someone may have tried to reshow or hide it
+ // during the popuppositioned event.
+ // Alternately, this event may have been fired in reponse to moving the
+ // popup rather than opening it. In that case, we are done.
+ popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
+ if (popupFrame && popupFrame->PopupState() == ePopupPositioning) {
+ pm->ShowPopupCallback(mPopup, popupFrame, false, false);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsXULMenuCommandEvent::Run() {
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (!pm) {
+ return NS_OK;
+ }
+
+ RefPtr menu = XULButtonElement::FromNode(mMenu);
+ MOZ_ASSERT(menu);
+ if (mFlipChecked) {
+ if (menu->GetXULBoolAttr(nsGkAtoms::checked)) {
+ menu->UnsetAttr(kNameSpaceID_None, nsGkAtoms::checked, true);
+ } else {
+ menu->SetAttr(kNameSpaceID_None, nsGkAtoms::checked, u"true"_ns, true);
+ }
+ }
+
+ // The order of the nsViewManager and PresShell COM pointers is
+ // important below. We want the pres shell to get released before the
+ // associated view manager on exit from this function.
+ // See bug 54233.
+ // XXXndeakin is this still needed?
+ RefPtr<nsPresContext> presContext = menu->OwnerDoc()->GetPresContext();
+ RefPtr<PresShell> presShell =
+ presContext ? presContext->PresShell() : nullptr;
+ RefPtr<nsViewManager> kungFuDeathGrip =
+ presShell ? presShell->GetViewManager() : nullptr;
+ Unused << kungFuDeathGrip; // Not referred to directly within this function
+
+ // Deselect ourselves.
+ if (mCloseMenuMode != CloseMenuMode_None) {
+ if (RefPtr parent = menu->GetMenuParent()) {
+ if (parent->GetActiveMenuChild() == menu) {
+ parent->SetActiveMenuChild(nullptr);
+ }
+ }
+ }
+
+ AutoHandlingUserInputStatePusher userInpStatePusher(mUserInput);
+ nsContentUtils::DispatchXULCommand(
+ menu, mIsTrusted, nullptr, presShell, mModifiers & MODIFIER_CONTROL,
+ mModifiers & MODIFIER_ALT, mModifiers & MODIFIER_SHIFT,
+ mModifiers & MODIFIER_META, 0, mButton);
+
+ if (mCloseMenuMode != CloseMenuMode_None) {
+ if (RefPtr popup = menu->GetContainingPopupElement()) {
+ pm->HidePopup(popup, mCloseMenuMode == CloseMenuMode_Auto, true, false,
+ false);
+ }
+ }
+
+ return NS_OK;
+}
diff --git a/layout/xul/nsXULPopupManager.h b/layout/xul/nsXULPopupManager.h
new file mode 100644
index 0000000000..9100ad7738
--- /dev/null
+++ b/layout/xul/nsXULPopupManager.h
@@ -0,0 +1,881 @@
+/* -*- 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/. */
+
+/**
+ * The XUL Popup Manager keeps track of all open popups.
+ */
+
+#ifndef nsXULPopupManager_h__
+#define nsXULPopupManager_h__
+
+#include "mozilla/Logging.h"
+#include "nsHashtablesFwd.h"
+#include "nsIContent.h"
+#include "nsIRollupListener.h"
+#include "nsIDOMEventListener.h"
+#include "nsPoint.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "nsIObserver.h"
+#include "nsITimer.h"
+#include "nsIReflowCallback.h"
+#include "nsThreadUtils.h"
+#include "nsPresContext.h"
+#include "nsStyleConsts.h"
+#include "nsWidgetInitData.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/widget/NativeMenu.h"
+#include "Units.h"
+
+// XXX Avoid including this here by moving function bodies to the cpp file.
+#include "mozilla/dom/Element.h"
+
+// X.h defines KeyPress
+#ifdef KeyPress
+# undef KeyPress
+#endif
+
+/**
+ * There are two types that are used:
+ * - dismissable popups such as menus, which should close up when there is a
+ * click outside the popup. In this situation, the entire chain of menus
+ * above should also be closed.
+ * - panels, which stay open until a request is made to close them. This
+ * type is used by tooltips.
+ *
+ * When a new popup is opened, it is appended to the popup chain, stored in a
+ * linked list in mPopups.
+ * Popups are stored in this list linked from newest to oldest. When a click
+ * occurs outside one of the open dismissable popups, the chain is closed by
+ * calling Rollup.
+ */
+
+class nsContainerFrame;
+class nsMenuPopupFrame;
+class nsMenuBarFrame;
+class nsIDocShellTreeItem;
+class nsPIDOMWindowOuter;
+class nsRefreshDriver;
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class Event;
+class KeyboardEvent;
+class UIEvent;
+class XULButtonElement;
+} // namespace dom
+} // namespace mozilla
+
+// XUL popups can be in several different states. When opening a popup, the
+// state changes as follows:
+// ePopupClosed - initial state
+// ePopupShowing - during the period when the popupshowing event fires
+// ePopupOpening - between the popupshowing event and being visible. Creation
+// of the child frames, layout and reflow occurs in this
+// state. The popup is stored in the popup manager's list of
+// open popups during this state.
+// ePopupVisible - layout is done and the popup's view and widget are made
+// visible. The popup is visible on screen but may be
+// transitioning. The popupshown event has not yet fired.
+// ePopupShown - the popup has been shown and is fully ready. This state is
+// assigned just before the popupshown event fires.
+// When closing a popup:
+// ePopupHidden - during the period when the popuphiding event fires and
+// the popup is removed.
+// ePopupClosed - the popup's widget is made invisible.
+enum nsPopupState {
+ // state when a popup is not open
+ ePopupClosed,
+ // state from when a popup is requested to be shown to after the
+ // popupshowing event has been fired.
+ ePopupShowing,
+ // state while a popup is waiting to be laid out and positioned
+ ePopupPositioning,
+ // state while a popup is open but the widget is not yet visible
+ ePopupOpening,
+ // state while a popup is visible and waiting for the popupshown event
+ ePopupVisible,
+ // state while a popup is open and visible on screen
+ ePopupShown,
+ // state from when a popup is requested to be hidden to when it is closed.
+ ePopupHiding,
+ // state which indicates that the popup was hidden without firing the
+ // popuphiding or popuphidden events. It is used when executing a menu
+ // command because the menu needs to be hidden before the command event
+ // fires, yet the popuphiding and popuphidden events are fired after. This
+ // state can also occur when the popup is removed because the document is
+ // unloaded.
+ ePopupInvisible
+};
+
+// when a menu command is executed, the closemenu attribute may be used
+// to define how the menu should be closed up
+enum CloseMenuMode {
+ CloseMenuMode_Auto, // close up the chain of menus, default value
+ CloseMenuMode_None, // don't close up any menus
+ CloseMenuMode_Single // close up only the menu the command is inside
+};
+
+/**
+ * nsNavigationDirection: an enum expressing navigation through the menus in
+ * terms which are independent of the directionality of the chrome. The
+ * terminology, derived from XSL-FO and CSS3 (e.g.
+ * http://www.w3.org/TR/css3-text/#TextLayout), is BASE (Before, After, Start,
+ * End), with the addition of First and Last (mapped to Home and End
+ * respectively).
+ *
+ * In languages such as English where the inline progression is left-to-right
+ * and the block progression is top-to-bottom (lr-tb), these terms will map out
+ * as in the following diagram
+ *
+ * --- inline progression --->
+ *
+ * First |
+ * ... |
+ * Before |
+ * +--------+ block
+ * Start | | End progression
+ * +--------+ |
+ * After |
+ * ... |
+ * Last V
+ *
+ */
+
+enum nsNavigationDirection {
+ eNavigationDirection_Last,
+ eNavigationDirection_First,
+ eNavigationDirection_Start,
+ eNavigationDirection_Before,
+ eNavigationDirection_End,
+ eNavigationDirection_After
+};
+
+enum nsIgnoreKeys {
+ eIgnoreKeys_False,
+ eIgnoreKeys_True,
+ eIgnoreKeys_Shortcuts,
+};
+
+#define NS_DIRECTION_IS_INLINE(dir) \
+ (dir == eNavigationDirection_Start || dir == eNavigationDirection_End)
+#define NS_DIRECTION_IS_BLOCK(dir) \
+ (dir == eNavigationDirection_Before || dir == eNavigationDirection_After)
+#define NS_DIRECTION_IS_BLOCK_TO_EDGE(dir) \
+ (dir == eNavigationDirection_First || dir == eNavigationDirection_Last)
+
+static_assert(static_cast<uint8_t>(mozilla::StyleDirection::Ltr) == 0 &&
+ static_cast<uint8_t>(mozilla::StyleDirection::Rtl) == 1,
+ "Left to Right should be 0 and Right to Left should be 1");
+
+/**
+ * DirectionFromKeyCodeTable: two arrays, the first for left-to-right and the
+ * other for right-to-left, that map keycodes to values of
+ * nsNavigationDirection.
+ */
+extern const nsNavigationDirection DirectionFromKeyCodeTable[2][6];
+
+#define NS_DIRECTION_FROM_KEY_CODE(frame, keycode) \
+ (DirectionFromKeyCodeTable[static_cast<uint8_t>( \
+ (frame)->StyleVisibility()->mDirection)][( \
+ keycode)-mozilla::dom::KeyboardEvent_Binding::DOM_VK_END])
+
+// Used to hold information about a popup that is about to be opened.
+struct PendingPopup {
+ PendingPopup(nsIContent* aPopup, mozilla::dom::Event* aEvent);
+
+ const RefPtr<nsIContent> mPopup;
+ const RefPtr<mozilla::dom::Event> mEvent;
+
+ // Device pixels relative to the showing popup's presshell's
+ // root prescontext's root frame.
+ mozilla::LayoutDeviceIntPoint mMousePoint;
+
+ // Cached modifiers used to trigger the popup.
+ mozilla::Modifiers mModifiers;
+
+ already_AddRefed<nsIContent> GetTriggerContent() const;
+
+ void InitMousePoint();
+
+ void SetMousePoint(mozilla::LayoutDeviceIntPoint aMousePoint) {
+ mMousePoint = aMousePoint;
+ }
+
+ uint16_t MouseInputSource() const;
+};
+
+// nsMenuChainItem holds info about an open popup. Items are stored in a
+// doubly linked list. Note that the linked list is stored beginning from
+// the lowest child in a chain of menus, as this is the active submenu.
+class nsMenuChainItem {
+ private:
+ nsMenuPopupFrame* mFrame; // the popup frame
+ nsPopupType mPopupType; // the popup type of the frame
+ bool mNoAutoHide; // true for noautohide panels
+ bool mIsContext; // true for context menus
+ bool mOnMenuBar; // true if the menu is on a menu bar
+ nsIgnoreKeys mIgnoreKeys; // indicates how keyboard listeners should be used
+
+ // True if the popup should maintain its position relative to the anchor when
+ // the anchor moves.
+ bool mFollowAnchor;
+
+ // The last seen position of the anchor, relative to the screen.
+ nsRect mCurrentRect;
+
+ mozilla::UniquePtr<nsMenuChainItem> mParent;
+ // Back pointer, safe because mChild keeps us alive.
+ nsMenuChainItem* mChild = nullptr;
+
+ public:
+ nsMenuChainItem(nsMenuPopupFrame* aFrame, bool aNoAutoHide, bool aIsContext,
+ nsPopupType aPopupType)
+ : mFrame(aFrame),
+ mPopupType(aPopupType),
+ mNoAutoHide(aNoAutoHide),
+ mIsContext(aIsContext),
+ mOnMenuBar(false),
+ mIgnoreKeys(eIgnoreKeys_False),
+ mFollowAnchor(false) {
+ NS_ASSERTION(aFrame, "null frame passed to nsMenuChainItem constructor");
+ MOZ_COUNT_CTOR(nsMenuChainItem);
+ }
+
+ MOZ_COUNTED_DTOR(nsMenuChainItem)
+
+ nsIContent* Content();
+ nsMenuPopupFrame* Frame() { return mFrame; }
+ nsPopupType PopupType() { return mPopupType; }
+ bool IsNoAutoHide() { return mNoAutoHide; }
+ void SetNoAutoHide(bool aNoAutoHide) { mNoAutoHide = aNoAutoHide; }
+ bool IsMenu() { return mPopupType == ePopupTypeMenu; }
+ bool IsContextMenu() { return mIsContext; }
+ nsIgnoreKeys IgnoreKeys() { return mIgnoreKeys; }
+ void SetIgnoreKeys(nsIgnoreKeys aIgnoreKeys) { mIgnoreKeys = aIgnoreKeys; }
+ bool IsOnMenuBar() { return mOnMenuBar; }
+ void SetOnMenuBar(bool aOnMenuBar) { mOnMenuBar = aOnMenuBar; }
+ nsMenuChainItem* GetParent() { return mParent.get(); }
+ nsMenuChainItem* GetChild() { return mChild; }
+ bool FollowsAnchor() { return mFollowAnchor; }
+ void UpdateFollowAnchor();
+ void CheckForAnchorChange();
+
+ // set the parent of this item to aParent, also changing the parent
+ // to have this as a child.
+ void SetParent(mozilla::UniquePtr<nsMenuChainItem> aParent);
+ // Removes the parent pointer and returns it.
+ mozilla::UniquePtr<nsMenuChainItem> Detach();
+};
+
+// this class is used for dispatching popuphiding events asynchronously.
+class nsXULPopupHidingEvent : public mozilla::Runnable {
+ public:
+ nsXULPopupHidingEvent(nsIContent* aPopup, nsIContent* aNextPopup,
+ nsIContent* aLastPopup, nsPopupType aPopupType,
+ bool aDeselectMenu, bool aIsCancel)
+ : mozilla::Runnable("nsXULPopupHidingEvent"),
+ mPopup(aPopup),
+ mNextPopup(aNextPopup),
+ mLastPopup(aLastPopup),
+ mPopupType(aPopupType),
+ mDeselectMenu(aDeselectMenu),
+ mIsRollup(aIsCancel) {
+ NS_ASSERTION(aPopup,
+ "null popup supplied to nsXULPopupHidingEvent constructor");
+ // aNextPopup and aLastPopup may be null
+ }
+
+ NS_IMETHOD Run() override;
+
+ private:
+ nsCOMPtr<nsIContent> mPopup;
+ nsCOMPtr<nsIContent> mNextPopup;
+ nsCOMPtr<nsIContent> mLastPopup;
+ nsPopupType mPopupType;
+ bool mDeselectMenu;
+ bool mIsRollup;
+};
+
+// this class is used for dispatching popuppositioned events asynchronously.
+class nsXULPopupPositionedEvent : public mozilla::Runnable {
+ public:
+ explicit nsXULPopupPositionedEvent(nsIContent* aPopup)
+ : mozilla::Runnable("nsXULPopupPositionedEvent"), mPopup(aPopup) {
+ MOZ_ASSERT(aPopup);
+ }
+
+ NS_IMETHOD Run() override;
+
+ // Asynchronously dispatch a popuppositioned event at aPopup if this is a
+ // panel that should receieve such events. Return true if the event was sent.
+ static bool DispatchIfNeeded(nsIContent* aPopup);
+
+ private:
+ const nsCOMPtr<nsIContent> mPopup;
+};
+
+// this class is used for dispatching menu command events asynchronously.
+class nsXULMenuCommandEvent : public mozilla::Runnable {
+ public:
+ nsXULMenuCommandEvent(mozilla::dom::Element* aMenu, bool aIsTrusted,
+ mozilla::Modifiers aModifiers, bool aUserInput,
+ bool aFlipChecked, int16_t aButton)
+ : mozilla::Runnable("nsXULMenuCommandEvent"),
+ mMenu(aMenu),
+ mModifiers(aModifiers),
+ mButton(aButton),
+ mIsTrusted(aIsTrusted),
+ mUserInput(aUserInput),
+ mFlipChecked(aFlipChecked),
+ mCloseMenuMode(CloseMenuMode_Auto) {
+ NS_ASSERTION(aMenu,
+ "null menu supplied to nsXULMenuCommandEvent constructor");
+ }
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override;
+
+ void SetCloseMenuMode(CloseMenuMode aCloseMenuMode) {
+ mCloseMenuMode = aCloseMenuMode;
+ }
+
+ private:
+ RefPtr<mozilla::dom::Element> mMenu;
+
+ mozilla::Modifiers mModifiers;
+ int16_t mButton;
+ bool mIsTrusted;
+ bool mUserInput;
+ bool mFlipChecked;
+ CloseMenuMode mCloseMenuMode;
+};
+
+class nsXULPopupManager final : public nsIDOMEventListener,
+ public nsIRollupListener,
+ public nsIObserver,
+ public mozilla::widget::NativeMenu::Observer {
+ public:
+ friend class nsXULPopupHidingEvent;
+ friend class nsXULPopupPositionedEvent;
+ friend class nsXULMenuCommandEvent;
+ friend class TransitionEnder;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ // nsIRollupListener
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual bool Rollup(uint32_t aCount, bool aFlush,
+ const mozilla::LayoutDeviceIntPoint* pos,
+ nsIContent** aLastRolledUp) override;
+ virtual bool ShouldRollupOnMouseWheelEvent() override;
+ virtual bool ShouldConsumeOnMouseWheelEvent() override;
+ virtual bool ShouldRollupOnMouseActivate() override;
+ virtual uint32_t GetSubmenuWidgetChain(
+ nsTArray<nsIWidget*>* aWidgetChain) override;
+ virtual nsIWidget* GetRollupWidget() override;
+ virtual bool RollupNativeMenu() override;
+
+ // NativeMenu::Observer
+ void OnNativeMenuOpened() override;
+ void OnNativeMenuClosed() override;
+ void OnNativeSubMenuWillOpen(mozilla::dom::Element* aPopupElement) override;
+ void OnNativeSubMenuDidOpen(mozilla::dom::Element* aPopupElement) override;
+ void OnNativeSubMenuClosed(mozilla::dom::Element* aPopupElement) override;
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void OnNativeMenuWillActivateItem(
+ mozilla::dom::Element* aMenuItemElement) override;
+
+ static nsXULPopupManager* sInstance;
+
+ // initialize and shutdown methods called by nsLayoutStatics
+ static nsresult Init();
+ static void Shutdown();
+
+ // returns a weak reference to the popup manager instance, could return null
+ // if a popup manager could not be allocated
+ static nsXULPopupManager* GetInstance();
+
+ // This should be called when a window is moved or resized to adjust the
+ // popups accordingly.
+ void AdjustPopupsOnWindowChange(nsPIDOMWindowOuter* aWindow);
+ void AdjustPopupsOnWindowChange(mozilla::PresShell* aPresShell);
+
+ // inform the popup manager that a menu bar has been activated or deactivated,
+ // either because one of its menus has opened or closed, or that the menubar
+ // has been focused such that its menus may be navigated with the keyboard.
+ // aActivate should be true when the menubar should be focused, and false
+ // when the active menu bar should be defocused. In the latter case, if
+ // aMenuBar isn't currently active, yet another menu bar is, that menu bar
+ // will remain active.
+ void SetActiveMenuBar(nsMenuBarFrame* aMenuBar, bool aActivate);
+
+ struct MayShowMenuResult {
+ const bool mIsNative = false;
+ mozilla::dom::XULButtonElement* const mMenuButton = nullptr;
+ nsMenuPopupFrame* const mMenuPopupFrame = nullptr;
+
+ explicit operator bool() const {
+ MOZ_ASSERT(!!mMenuButton == !!mMenuPopupFrame);
+ return mIsNative || mMenuButton;
+ }
+ };
+
+ MayShowMenuResult MayShowMenu(nsIContent* aMenu);
+
+ /**
+ * Open a <menu> given its content node. If aSelectFirstItem is
+ * set to true, the first item on the menu will automatically be
+ * selected.
+ */
+ void ShowMenu(nsIContent* aMenu, bool aSelectFirstItem);
+
+ /**
+ * Open a popup, either anchored or unanchored. If aSelectFirstItem is
+ * true, then the first item in the menu is selected. The arguments are
+ * similar to those for XULPopupElement::OpenPopup.
+ *
+ * aTriggerEvent should be the event that triggered the event. This is used
+ * to determine the coordinates and trigger node for the popup. This may be
+ * null if the popup was not triggered by an event.
+ *
+ * This fires the popupshowing event synchronously.
+ */
+ void ShowPopup(nsIContent* aPopup, nsIContent* aAnchorContent,
+ const nsAString& aPosition, int32_t aXPos, int32_t aYPos,
+ bool aIsContextMenu, bool aAttributesOverride,
+ bool aSelectFirstItem, mozilla::dom::Event* aTriggerEvent);
+
+ /**
+ * Open a popup at a specific screen position specified by aXPos and aYPos,
+ * measured in CSS pixels.
+ *
+ * This fires the popupshowing event synchronously.
+ *
+ * If aIsContextMenu is true, the popup is positioned at a slight
+ * offset from aXPos/aYPos to ensure that it is not under the mouse
+ * cursor.
+ */
+ void ShowPopupAtScreen(nsIContent* aPopup, int32_t aXPos, int32_t aYPos,
+ bool aIsContextMenu,
+ mozilla::dom::Event* aTriggerEvent);
+
+ /* Open a popup anchored at a screen rectangle specified by aRect.
+ * The remaining arguments are similar to ShowPopup.
+ */
+ void ShowPopupAtScreenRect(nsIContent* aPopup, const nsAString& aPosition,
+ const nsIntRect& aRect, bool aIsContextMenu,
+ bool aAttributesOverride,
+ mozilla::dom::Event* aTriggerEvent);
+
+ /**
+ * Open a popup as a native menu, at a specific screen position specified by
+ * aXPos and aYPos, measured in CSS pixels.
+ *
+ * This fires the popupshowing event synchronously.
+ *
+ * Returns whether native menus are supported for aPopup on this platform.
+ * TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY bool ShowPopupAsNativeMenu(
+ nsIContent* aPopup, int32_t aXPos, int32_t aYPos, bool aIsContextMenu,
+ mozilla::dom::Event* aTriggerEvent);
+
+ /**
+ * Open a tooltip at a specific screen position specified by aXPos and aYPos,
+ * measured in device pixels. This fires the popupshowing event synchronously.
+ */
+ void ShowTooltipAtScreen(nsIContent* aPopup, nsIContent* aTriggerContent,
+ const mozilla::LayoutDeviceIntPoint&);
+
+ /*
+ * Hide a popup aPopup. If the popup is in a <menu>, then also inform the
+ * menu that the popup is being hidden.
+ *
+ * aHideChain - true if the entire chain of menus should be closed. If false,
+ * only this popup is closed.
+ * aDeselectMenu - true if the parent <menu> of the popup should be
+ * deselected. This will be false when the menu is closed by
+ * pressing the Escape key.
+ * aAsynchronous - true if the first popuphiding event should be sent
+ * asynchrously. This should be true if HidePopup is called
+ * from a frame.
+ * aIsCancel - true if this popup is hiding due to being cancelled.
+ * aLastPopup - optional popup to close last when hiding a chain of menus.
+ * If null, then all popups will be closed.
+ */
+ void HidePopup(nsIContent* aPopup, bool aHideChain, bool aDeselectMenu,
+ bool aAsynchronous, bool aIsCancel,
+ nsIContent* aLastPopup = nullptr);
+
+ /*
+ * Hide the popup of a <menu>.
+ */
+ void HideMenu(nsIContent* aMenu);
+
+ /**
+ * Hide a popup after a short delay. This is used when rolling over menu
+ * items. This timer is stored in mCloseTimer. The timer may be cancelled and
+ * the popup closed by calling KillMenuTimer.
+ */
+ void HidePopupAfterDelay(nsMenuPopupFrame* aPopup, int32_t aDelay);
+
+ /**
+ * Hide all of the popups from a given docshell. This should be called when
+ * the document is hidden.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void HidePopupsInDocShell(nsIDocShellTreeItem* aDocShellToHide);
+
+ /**
+ * Check if any popups need to be repositioned or hidden after a style or
+ * layout change. This will update, for example, any arrow type panels when
+ * the anchor that is is pointing to has moved, resized or gone away.
+ * Only those popups that pertain to the supplied aRefreshDriver are updated.
+ */
+ void UpdatePopupPositions(nsRefreshDriver* aRefreshDriver);
+
+ /**
+ * Enable or disable anchor following on the popup if needed.
+ */
+ void UpdateFollowAnchor(nsMenuPopupFrame* aPopup);
+
+ /**
+ * Execute a menu command from the triggering event aEvent.
+ *
+ * aMenu - a menuitem to execute
+ * aEvent - an nsXULMenuCommandEvent that contains all the info from the mouse
+ * event which triggered the menu to be executed, may not be null
+ */
+ MOZ_CAN_RUN_SCRIPT void ExecuteMenu(nsIContent* aMenu,
+ nsXULMenuCommandEvent* aEvent);
+
+ /**
+ * If a native menu is open, and aItem is an item in the menu's subtree,
+ * execute the item with the help of the native menu and close the menu.
+ * Returns true if a native menu was open.
+ */
+ bool ActivateNativeMenuItem(nsIContent* aItem, mozilla::Modifiers aModifiers,
+ int16_t aButton, mozilla::ErrorResult& aRv);
+
+ /**
+ * Return true if the popup for the supplied content node is open.
+ */
+ bool IsPopupOpen(nsIContent* aPopup);
+
+ /**
+ * Return the frame for the topmost open popup of a given type, or null if
+ * no popup of that type is open. If aType is ePopupTypeAny, a menu of any
+ * type is returned.
+ */
+ nsIFrame* GetTopPopup(nsPopupType aType);
+
+ /**
+ * Returns the topmost active menuitem that's currently visible, if any.
+ */
+ nsIContent* GetTopActiveMenuItemContent();
+
+ /**
+ * Return an array of all the open and visible popup frames for
+ * menus, in order from top to bottom.
+ */
+ void GetVisiblePopups(nsTArray<nsIFrame*>& aPopups);
+
+ /**
+ * Get the node that last triggered a popup or tooltip in the document
+ * aDocument. aDocument must be non-null and be a document contained within
+ * the same window hierarchy as the popup to retrieve.
+ */
+ already_AddRefed<nsINode> GetLastTriggerPopupNode(
+ mozilla::dom::Document* aDocument) {
+ return GetLastTriggerNode(aDocument, false);
+ }
+
+ already_AddRefed<nsINode> GetLastTriggerTooltipNode(
+ mozilla::dom::Document* aDocument) {
+ return GetLastTriggerNode(aDocument, true);
+ }
+
+ /**
+ * Return false if a popup may not be opened. This will return false if the
+ * popup is already open, if the popup is in a content shell that is not
+ * focused, or if it is a submenu of another menu that isn't open.
+ */
+ bool MayShowPopup(nsMenuPopupFrame* aFrame);
+
+ /**
+ * Indicate that the popup associated with aView has been moved to the
+ * specified screen coordinates.
+ */
+ void PopupMoved(nsIFrame* aFrame, nsIntPoint aPoint, bool aByMoveToRect);
+
+ /**
+ * Indicate that the popup associated with aView has been resized to the
+ * given device pixel size aSize.
+ */
+ void PopupResized(nsIFrame* aFrame, mozilla::LayoutDeviceIntSize aSize);
+
+ /**
+ * Called when a popup frame is destroyed. In this case, just remove the
+ * item and later popups from the list. No point going through HidePopup as
+ * the frames have gone away.
+ */
+ MOZ_CAN_RUN_SCRIPT void PopupDestroyed(nsMenuPopupFrame* aFrame);
+
+ /**
+ * Returns true if there is a context menu open. If aPopup is specified,
+ * then the context menu must be later in the chain than aPopup. If aPopup
+ * is null, returns true if any context menu at all is open.
+ */
+ bool HasContextMenu(nsMenuPopupFrame* aPopup);
+
+ /**
+ * Update the commands for the menus within the menu popup for a given
+ * content node. aPopup should be a XUL menupopup element. This method
+ * changes attributes on the children of aPopup, and deals only with the
+ * content of the popup, not the frames.
+ */
+ void UpdateMenuItems(nsIContent* aPopup);
+
+ /**
+ * Stop the timer which hides a popup after a delay, started by a previous
+ * call to HidePopupAfterDelay. In addition, the popup awaiting to be hidden
+ * is closed asynchronously.
+ */
+ void KillMenuTimer();
+
+ /**
+ * Cancel the timer which closes menus after delay, but only if the menu to
+ * close is aMenuParent. When a submenu is opened, the user might move the
+ * mouse over a sibling menuitem which would normally close the menu. This
+ * menu is closed via a timer. However, if the user moves the mouse over the
+ * submenu before the timer fires, we should instead cancel the timer. This
+ * ensures that the user can move the mouse diagonally over a menu.
+ */
+ void CancelMenuTimer(nsMenuPopupFrame*);
+
+ /**
+ * Handles navigation for menu accelkeys. If aFrame is specified, then the
+ * key is handled by that popup, otherwise if aFrame is null, the key is
+ * handled by the active popup or menubar.
+ */
+ MOZ_CAN_RUN_SCRIPT bool HandleShortcutNavigation(
+ mozilla::dom::KeyboardEvent& aKeyEvent, nsMenuPopupFrame* aFrame);
+
+ /**
+ * Handles cursor navigation within a menu. Returns true if the key has
+ * been handled.
+ */
+ MOZ_CAN_RUN_SCRIPT bool HandleKeyboardNavigation(uint32_t aKeyCode);
+
+ /**
+ * Handle keyboard navigation within a menu popup specified by aFrame.
+ * Returns true if the key was handled and other default handling
+ * should not occur.
+ */
+ MOZ_CAN_RUN_SCRIPT bool HandleKeyboardNavigationInPopup(
+ nsMenuPopupFrame* aFrame, nsNavigationDirection aDir) {
+ return HandleKeyboardNavigationInPopup(nullptr, aFrame, aDir);
+ }
+
+ /**
+ * Handles the keyboard event with keyCode value. Returns true if the event
+ * has been handled.
+ */
+ MOZ_CAN_RUN_SCRIPT bool HandleKeyboardEventWithKeyCode(
+ mozilla::dom::KeyboardEvent* aKeyEvent,
+ nsMenuChainItem* aTopVisibleMenuItem);
+
+ // Sets mIgnoreKeys of the Top Visible Menu Item
+ nsresult UpdateIgnoreKeys(bool aIgnoreKeys);
+
+ nsPopupState GetPopupState(mozilla::dom::Element* aPopupElement);
+
+ mozilla::dom::Event* GetOpeningPopupEvent() const {
+ return mPendingPopup->mEvent.get();
+ }
+
+ MOZ_CAN_RUN_SCRIPT nsresult KeyUp(mozilla::dom::KeyboardEvent* aKeyEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult KeyDown(mozilla::dom::KeyboardEvent* aKeyEvent);
+ MOZ_CAN_RUN_SCRIPT nsresult KeyPress(mozilla::dom::KeyboardEvent* aKeyEvent);
+
+ protected:
+ nsXULPopupManager();
+ ~nsXULPopupManager();
+
+ // get the nsMenuPopupFrame, if any, for the given content node
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ nsMenuPopupFrame* GetPopupFrameForContent(nsIContent* aContent,
+ bool aShouldFlush);
+
+ // return the topmost menu, skipping over invisible popups
+ nsMenuChainItem* GetTopVisibleMenu();
+
+ // Removes the chain item from the chain and deletes it.
+ void RemoveMenuChainItem(nsMenuChainItem*);
+
+ // Hide all of the visible popups from the given list. This function can
+ // cause style changes and frame destruction.
+ MOZ_CAN_RUN_SCRIPT void HidePopupsInList(
+ const nsTArray<nsMenuPopupFrame*>& aFrames);
+
+ // Hide, but don't close, visible menus. Called before executing a menu item.
+ // The caller promises to close the menus properly (with a call to HidePopup)
+ // once the item has been executed.
+ MOZ_CAN_RUN_SCRIPT void HideOpenMenusBeforeExecutingMenu(CloseMenuMode aMode);
+
+ // callbacks for ShowPopup and HidePopup as events may be done asynchronously
+ MOZ_CAN_RUN_SCRIPT void ShowPopupCallback(nsIContent* aPopup,
+ nsMenuPopupFrame* aPopupFrame,
+ bool aIsContextMenu,
+ bool aSelectFirstItem);
+ MOZ_CAN_RUN_SCRIPT void HidePopupCallback(
+ nsIContent* aPopup, nsMenuPopupFrame* aPopupFrame, nsIContent* aNextPopup,
+ nsIContent* aLastPopup, nsPopupType aPopupType, bool aDeselectMenu);
+
+ /**
+ * Trigger frame construction and reflow in the popup, fire a popupshowing
+ * event on the popup and then open the popup.
+ *
+ * aPendingPopup - information about the popup to open
+ * aIsContextMenu - true for context menus
+ * aSelectFirstItem - true to select the first item in the menu
+ * TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void BeginShowingPopup(
+ const PendingPopup& aPendingPopup, bool aIsContextMenu,
+ bool aSelectFirstItem);
+
+ /**
+ * Fire a popuphiding event and then hide the popup. This will be called
+ * recursively if aNextPopup and aLastPopup are set in order to hide a chain
+ * of open menus. If these are not set, only one popup is closed. However,
+ * if the popup type indicates a menu, yet the next popup is not a menu,
+ * then this ends the closing of popups. This allows a menulist inside a
+ * non-menu to close up the menu but not close up the panel it is contained
+ * within.
+ *
+ * The caller must keep a strong reference to aPopup, aNextPopup and
+ * aLastPopup.
+ *
+ * aPopup - the popup to hide
+ * aNextPopup - the next popup to hide
+ * aLastPopup - the last popup in the chain to hide
+ * aPresContext - nsPresContext for the popup's frame
+ * aPopupType - the PopupType of the frame.
+ * aDeselectMenu - true to unhighlight the menu when hiding it
+ * aIsCancel - true if this popup is hiding due to being cancelled.
+ */
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ void FirePopupHidingEvent(nsIContent* aPopup, nsIContent* aNextPopup,
+ nsIContent* aLastPopup, nsPresContext* aPresContext,
+ nsPopupType aPopupType, bool aDeselectMenu,
+ bool aIsCancel);
+
+ /**
+ * Handle keyboard navigation within a menu popup specified by aItem.
+ */
+ MOZ_CAN_RUN_SCRIPT
+ bool HandleKeyboardNavigationInPopup(nsMenuChainItem* aItem,
+ nsNavigationDirection aDir) {
+ return HandleKeyboardNavigationInPopup(aItem, aItem->Frame(), aDir);
+ }
+
+ private:
+ /**
+ * Handle keyboard navigation within a menu popup aFrame. If aItem is
+ * supplied, then it is expected to have a frame equal to aFrame.
+ * If aItem is non-null, then the navigation may be redirected to
+ * an open submenu if one exists. Returns true if the key was
+ * handled and other default handling should not occur.
+ */
+ MOZ_CAN_RUN_SCRIPT bool HandleKeyboardNavigationInPopup(
+ nsMenuChainItem* aItem, nsMenuPopupFrame* aFrame,
+ nsNavigationDirection aDir);
+
+ protected:
+ already_AddRefed<nsINode> GetLastTriggerNode(
+ mozilla::dom::Document* aDocument, bool aIsTooltip);
+
+ /**
+ * Fire a popupshowing event for aPopup.
+ */
+ MOZ_CAN_RUN_SCRIPT nsEventStatus FirePopupShowingEvent(
+ const PendingPopup& aPendingPopup, nsPresContext* aPresContext);
+
+ /**
+ * Set mouse capturing for the current popup. This traps mouse clicks that
+ * occur outside the popup so that it can be closed up. aOldPopup should be
+ * set to the popup that was previously the current popup.
+ */
+ void SetCaptureState(nsIContent* aOldPopup);
+
+ /**
+ * Key event listeners are attached to the document containing the current
+ * menu for menu and shortcut navigation. Only one listener is needed at a
+ * time, stored in mKeyListener, so switch it only if the document changes.
+ * Having menus in different documents is very rare, so the listeners will
+ * usually only be attached when the first menu opens and removed when all
+ * menus have closed.
+ *
+ * This is also used when only a menubar is active without any open menus,
+ * so that keyboard navigation between menus on the menubar may be done.
+ */
+ // TODO: Convert UpdateKeyboardListeners() to MOZ_CAN_RUN_SCRIPT and get rid
+ // of the kungFuDeathGrip in it.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void UpdateKeyboardListeners();
+
+ /*
+ * Returns true if the docshell for aDoc is aExpected or a child of aExpected.
+ */
+ bool IsChildOfDocShell(mozilla::dom::Document* aDoc,
+ nsIDocShellTreeItem* aExpected);
+
+ // Finds a chain item in mPopups.
+ nsMenuChainItem* FindPopup(nsIContent* aPopup) const;
+
+ // the document the key event listener is attached to
+ nsCOMPtr<mozilla::dom::EventTarget> mKeyListener;
+
+ // widget that is currently listening to rollup events
+ nsCOMPtr<nsIWidget> mWidget;
+
+ // set to the currently active menu bar, if any
+ nsMenuBarFrame* mActiveMenuBar;
+
+ // linked list of normal menus and panels. mPopups points to the innermost
+ // popup, which keeps alive all their parents.
+ mozilla::UniquePtr<nsMenuChainItem> mPopups;
+
+ // timer used for HidePopupAfterDelay
+ nsCOMPtr<nsITimer> mCloseTimer;
+ nsMenuPopupFrame* mTimerMenu = nullptr;
+
+ // Information about the popup that is currently firing a popupshowing event.
+ const PendingPopup* mPendingPopup;
+
+ // If a popup is displayed as a native menu, this is non-null while the
+ // native menu is open.
+ // mNativeMenu has a strong reference to the menupopup nsIContent.
+ RefPtr<mozilla::widget::NativeMenu> mNativeMenu;
+
+ // If the currently open native menu activated an item, this is the item's
+ // close menu mode. Nothing() if mNativeMenu is null or if no item was
+ // activated.
+ mozilla::Maybe<CloseMenuMode> mNativeMenuActivatedItemCloseMenuMode;
+
+ // If a popup is displayed as a native menu, this map contains the popup state
+ // for any of its non-closed submenus. This state cannot be stored on the
+ // submenus' nsMenuPopupFrames, because we usually don't generate frames for
+ // the contents of native menus.
+ // If a submenu is not present in this map, it means it's closed.
+ // This map is empty if mNativeMenu is null.
+ nsTHashMap<RefPtr<mozilla::dom::Element>, nsPopupState>
+ mNativeMenuSubmenuStates;
+};
+
+#endif
diff --git a/layout/xul/nsXULTooltipListener.cpp b/layout/xul/nsXULTooltipListener.cpp
new file mode 100644
index 0000000000..4e9e62790d
--- /dev/null
+++ b/layout/xul/nsXULTooltipListener.cpp
@@ -0,0 +1,678 @@
+/* -*- 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 "nsXULTooltipListener.h"
+
+#include "XULButtonElement.h"
+#include "nsXULElement.h"
+#include "mozilla/dom/Document.h"
+#include "nsGkAtoms.h"
+#include "nsMenuPopupFrame.h"
+#include "nsIDragService.h"
+#include "nsIDragSession.h"
+#include "nsITreeView.h"
+#include "nsIScriptContext.h"
+#include "nsPIDOMWindow.h"
+#include "nsXULPopupManager.h"
+#include "nsIPopupContainer.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTreeColumns.h"
+#include "nsContentUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Event.h" // for Event
+#include "mozilla/dom/MouseEvent.h"
+#include "mozilla/dom/TreeColumnBinding.h"
+#include "mozilla/dom/XULTreeElementBinding.h"
+#include "mozilla/TextEvents.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+nsXULTooltipListener* nsXULTooltipListener::sInstance = nullptr;
+
+//////////////////////////////////////////////////////////////////////////
+//// nsISupports
+
+nsXULTooltipListener::nsXULTooltipListener()
+ : mTooltipShownOnce(false),
+ mIsSourceTree(false),
+ mNeedTitletip(false),
+ mLastTreeRow(-1) {
+ // FIXME(emilio): This can be faster, this should use static prefs.
+ //
+ // register the callback so we get notified of updates
+ Preferences::RegisterCallback(ToolbarTipsPrefChanged,
+ "browser.chrome.toolbar_tips");
+
+ // Call the pref callback to initialize our state.
+ ToolbarTipsPrefChanged("browser.chrome.toolbar_tips", nullptr);
+}
+
+nsXULTooltipListener::~nsXULTooltipListener() {
+ MOZ_ASSERT(sInstance == this);
+ sInstance = nullptr;
+
+ HideTooltip();
+
+ // Unregister our pref observer
+ Preferences::UnregisterCallback(ToolbarTipsPrefChanged,
+ "browser.chrome.toolbar_tips");
+}
+
+NS_IMPL_ISUPPORTS(nsXULTooltipListener, nsIDOMEventListener)
+
+void nsXULTooltipListener::MouseOut(Event* aEvent) {
+ // reset flag so that tooltip will display on the next MouseMove
+ mTooltipShownOnce = false;
+ mPreviousMouseMoveTarget = nullptr;
+
+ // if the timer is running and no tooltip is shown, we
+ // have to cancel the timer here so that it doesn't
+ // show the tooltip if we move the mouse out of the window
+ nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip);
+ if (mTooltipTimer && !currentTooltip) {
+ mTooltipTimer->Cancel();
+ mTooltipTimer = nullptr;
+ return;
+ }
+
+#ifdef DEBUG_crap
+ if (mNeedTitletip) return;
+#endif
+
+ // check to see if the mouse left the targetNode, and if so,
+ // hide the tooltip
+ if (currentTooltip) {
+ // which node did the mouse leave?
+ EventTarget* eventTarget = aEvent->GetComposedTarget();
+ nsCOMPtr<nsINode> targetNode = nsINode::FromEventTargetOrNull(eventTarget);
+ if (targetNode && targetNode->IsContent() &&
+ !targetNode->AsContent()->GetContainingShadow()) {
+ eventTarget = aEvent->GetTarget();
+ }
+
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (pm) {
+ nsCOMPtr<nsINode> tooltipNode =
+ pm->GetLastTriggerTooltipNode(currentTooltip->GetComposedDoc());
+
+ // If the target node is the current tooltip target node, the mouse
+ // left the node the tooltip appeared on, so close the tooltip. However,
+ // don't do this if the mouse moved onto the tooltip in case the
+ // tooltip appears positioned near the mouse.
+ nsCOMPtr<EventTarget> relatedTarget =
+ aEvent->AsMouseEvent()->GetRelatedTarget();
+ nsIContent* relatedContent =
+ nsIContent::FromEventTargetOrNull(relatedTarget);
+ if (tooltipNode == targetNode && relatedContent != currentTooltip) {
+ HideTooltip();
+ // reset special tree tracking
+ if (mIsSourceTree) {
+ mLastTreeRow = -1;
+ mLastTreeCol = nullptr;
+ }
+ }
+ }
+ }
+}
+
+void nsXULTooltipListener::MouseMove(Event* aEvent) {
+ if (!sShowTooltips) return;
+
+ // stash the coordinates of the event so that we can still get back to it from
+ // within the timer callback. On win32, we'll get a MouseMove event even when
+ // a popup goes away -- even when the mouse doesn't change position! To get
+ // around this, we make sure the mouse has really moved before proceeding.
+ MouseEvent* mouseEvent = aEvent->AsMouseEvent();
+ if (!mouseEvent) {
+ return;
+ }
+ auto newMouseScreenPoint = mouseEvent->ScreenPointLayoutDevicePix();
+
+ // filter out false win32 MouseMove event
+ if (mMouseScreenPoint == newMouseScreenPoint) {
+ return;
+ }
+
+ nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip);
+ nsCOMPtr<EventTarget> eventTarget = aEvent->GetComposedTarget();
+ nsIContent* content = nsIContent::FromEventTargetOrNull(eventTarget);
+
+ bool isSameTarget = true;
+ nsCOMPtr<nsIContent> tempContent = do_QueryReferent(mPreviousMouseMoveTarget);
+ if (tempContent && tempContent != content) {
+ isSameTarget = false;
+ }
+
+ // filter out minor movements due to crappy optical mice and shaky hands
+ // to prevent tooltips from hiding prematurely. Do not filter out movements
+ // if we are changing targets, as they may register new tooltips.
+ if (currentTooltip && isSameTarget &&
+ abs(mMouseScreenPoint.x - newMouseScreenPoint.x) <=
+ kTooltipMouseMoveTolerance &&
+ abs(mMouseScreenPoint.y - newMouseScreenPoint.y) <=
+ kTooltipMouseMoveTolerance) {
+ return;
+ }
+ mMouseScreenPoint = newMouseScreenPoint;
+ mPreviousMouseMoveTarget = do_GetWeakReference(content);
+
+ nsCOMPtr<nsIContent> sourceContent =
+ do_QueryInterface(aEvent->GetCurrentTarget());
+ mSourceNode = do_GetWeakReference(sourceContent);
+ mIsSourceTree = sourceContent->IsXULElement(nsGkAtoms::treechildren);
+ if (mIsSourceTree) CheckTreeBodyMove(mouseEvent);
+
+ // as the mouse moves, we want to make sure we reset the timer to show it,
+ // so that the delay is from when the mouse stops moving, not when it enters
+ // the node.
+ KillTooltipTimer();
+
+ // Hide the current tooltip if we change target nodes. If the new target
+ // has the same tooltip, we will open it again. We cannot compare
+ // the targets' tooltips because popupshowing events can set the tooltip.
+ if (!isSameTarget) {
+ HideTooltip();
+ mTooltipShownOnce = false;
+ }
+
+ // If the mouse moves while the tooltip is up, hide it. If nothing is
+ // showing and the tooltip hasn't been displayed since the mouse entered
+ // the node, then start the timer to show the tooltip.
+ // If we have moved to a different target, we need to display the new tooltip,
+ // as the previous target's tooltip will have just been hidden.
+ if ((!currentTooltip && !mTooltipShownOnce) || !isSameTarget) {
+ // don't show tooltips attached to elements outside of a menu popup
+ // when hovering over an element inside it. The popupsinherittooltip
+ // attribute may be used to disable this behaviour, which is useful for
+ // large menu hierarchies such as bookmarks.
+ if (!sourceContent->IsElement() ||
+ !sourceContent->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::popupsinherittooltip,
+ nsGkAtoms::_true, eCaseMatters)) {
+ for (nsIContent* targetContent =
+ nsIContent::FromEventTargetOrNull(eventTarget);
+ targetContent && targetContent != sourceContent;
+ targetContent = targetContent->GetParent()) {
+ if (targetContent->IsAnyOfXULElements(
+ nsGkAtoms::menupopup, nsGkAtoms::panel, nsGkAtoms::tooltip)) {
+ mSourceNode = nullptr;
+ return;
+ }
+ }
+ }
+
+ mTargetNode = do_GetWeakReference(eventTarget);
+ if (mTargetNode) {
+ nsresult rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mTooltipTimer), sTooltipCallback, this,
+ LookAndFeel::GetInt(LookAndFeel::IntID::TooltipDelay, 500),
+ nsITimer::TYPE_ONE_SHOT, "sTooltipCallback",
+ sourceContent->OwnerDoc()->EventTargetFor(TaskCategory::Other));
+ if (NS_FAILED(rv)) {
+ mTargetNode = nullptr;
+ mSourceNode = nullptr;
+ }
+ }
+ return;
+ }
+
+ if (mIsSourceTree) return;
+ // Hide the tooltip if it is currently showing.
+ if (currentTooltip) {
+ HideTooltip();
+ // set a flag so that the tooltip is only displayed once until the mouse
+ // leaves the node
+ mTooltipShownOnce = true;
+ }
+}
+
+NS_IMETHODIMP
+nsXULTooltipListener::HandleEvent(Event* aEvent) {
+ nsAutoString type;
+ aEvent->GetType(type);
+ if (type.EqualsLiteral("wheel") || type.EqualsLiteral("mousedown") ||
+ type.EqualsLiteral("mouseup") || type.EqualsLiteral("dragstart")) {
+ HideTooltip();
+ return NS_OK;
+ }
+
+ if (type.EqualsLiteral("keydown")) {
+ // Hide the tooltip if a non-modifier key is pressed.
+ WidgetKeyboardEvent* keyEvent = aEvent->WidgetEventPtr()->AsKeyboardEvent();
+ if (!keyEvent->IsModifierKeyEvent()) {
+ HideTooltip();
+ }
+
+ return NS_OK;
+ }
+
+ if (type.EqualsLiteral("popuphiding")) {
+ DestroyTooltip();
+ return NS_OK;
+ }
+
+ // Note that mousemove, mouseover and mouseout might be
+ // fired even during dragging due to widget's bug.
+ nsCOMPtr<nsIDragService> dragService =
+ do_GetService("@mozilla.org/widget/dragservice;1");
+ NS_ENSURE_TRUE(dragService, NS_OK);
+ nsCOMPtr<nsIDragSession> dragSession;
+ dragService->GetCurrentSession(getter_AddRefs(dragSession));
+ if (dragSession) {
+ return NS_OK;
+ }
+
+ // Not dragging.
+
+ if (type.EqualsLiteral("mousemove")) {
+ MouseMove(aEvent);
+ return NS_OK;
+ }
+
+ if (type.EqualsLiteral("mouseout")) {
+ MouseOut(aEvent);
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+//////////////////////////////////////////////////////////////////////////
+//// nsXULTooltipListener
+
+// static
+void nsXULTooltipListener::ToolbarTipsPrefChanged(const char* aPref,
+ void* aClosure) {
+ sShowTooltips =
+ Preferences::GetBool("browser.chrome.toolbar_tips", sShowTooltips);
+}
+
+//////////////////////////////////////////////////////////////////////////
+//// nsXULTooltipListener
+
+bool nsXULTooltipListener::sShowTooltips = false;
+
+void nsXULTooltipListener::AddTooltipSupport(nsIContent* aNode) {
+ MOZ_ASSERT(aNode);
+ MOZ_ASSERT(this == sInstance);
+
+ aNode->AddSystemEventListener(u"mouseout"_ns, this, false, false);
+ aNode->AddSystemEventListener(u"mousemove"_ns, this, false, false);
+ aNode->AddSystemEventListener(u"mousedown"_ns, this, false, false);
+ aNode->AddSystemEventListener(u"mouseup"_ns, this, false, false);
+ aNode->AddSystemEventListener(u"dragstart"_ns, this, true, false);
+}
+
+void nsXULTooltipListener::RemoveTooltipSupport(nsIContent* aNode) {
+ MOZ_ASSERT(aNode);
+ MOZ_ASSERT(this == sInstance);
+
+ // The last reference to us can go after some of these calls.
+ RefPtr<nsXULTooltipListener> instance = this;
+
+ aNode->RemoveSystemEventListener(u"mouseout"_ns, this, false);
+ aNode->RemoveSystemEventListener(u"mousemove"_ns, this, false);
+ aNode->RemoveSystemEventListener(u"mousedown"_ns, this, false);
+ aNode->RemoveSystemEventListener(u"mouseup"_ns, this, false);
+ aNode->RemoveSystemEventListener(u"dragstart"_ns, this, true);
+}
+
+void nsXULTooltipListener::CheckTreeBodyMove(MouseEvent* aMouseEvent) {
+ nsCOMPtr<nsIContent> sourceNode = do_QueryReferent(mSourceNode);
+ if (!sourceNode) return;
+
+ // get the documentElement of the document the tree is in
+ Document* doc = sourceNode->GetComposedDoc();
+
+ RefPtr<XULTreeElement> tree = GetSourceTree();
+ Element* root = doc ? doc->GetRootElement() : nullptr;
+ if (root && root->GetPrimaryFrame() && tree) {
+ CSSIntPoint pos = aMouseEvent->ScreenPoint(CallerType::System);
+
+ // subtract off the documentElement's position
+ // XXX Isn't this just converting to client points?
+ CSSIntRect rect = root->GetPrimaryFrame()->GetScreenRect();
+ pos -= rect.TopLeft();
+
+ ErrorResult rv;
+ TreeCellInfo cellInfo;
+ tree->GetCellAt(pos.x, pos.y, cellInfo, rv);
+
+ int32_t row = cellInfo.mRow;
+ RefPtr<nsTreeColumn> col = cellInfo.mCol;
+
+ // determine if we are going to need a titletip
+ // XXX check the disabletitletips attribute on the tree content
+ mNeedTitletip = false;
+ if (row >= 0 && cellInfo.mChildElt.EqualsLiteral("text")) {
+ mNeedTitletip = tree->IsCellCropped(row, col, rv);
+ }
+
+ nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip);
+ if (currentTooltip && (row != mLastTreeRow || col != mLastTreeCol)) {
+ HideTooltip();
+ }
+
+ mLastTreeRow = row;
+ mLastTreeCol = col;
+ }
+}
+
+nsresult nsXULTooltipListener::ShowTooltip() {
+ nsCOMPtr<nsIContent> sourceNode = do_QueryReferent(mSourceNode);
+
+ // get the tooltip content designated for the target node
+ nsCOMPtr<nsIContent> tooltipNode;
+ GetTooltipFor(sourceNode, getter_AddRefs(tooltipNode));
+ if (!tooltipNode || sourceNode == tooltipNode)
+ return NS_ERROR_FAILURE; // the target node doesn't need a tooltip
+
+ // set the node in the document that triggered the tooltip and show it
+ if (tooltipNode->GetComposedDoc() &&
+ nsContentUtils::IsChromeDoc(tooltipNode->GetComposedDoc())) {
+ // Make sure the target node is still attached to some document.
+ // It might have been deleted.
+ if (sourceNode->IsInComposedDoc()) {
+ if (!mIsSourceTree) {
+ mLastTreeRow = -1;
+ mLastTreeCol = nullptr;
+ }
+
+ mCurrentTooltip = do_GetWeakReference(tooltipNode);
+ LaunchTooltip();
+ mTargetNode = nullptr;
+
+ nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip);
+ if (!currentTooltip) return NS_OK;
+
+ // listen for popuphidden on the tooltip node, so that we can
+ // be sure DestroyPopup is called even if someone else closes the tooltip
+ currentTooltip->AddSystemEventListener(u"popuphiding"_ns, this, false,
+ false);
+
+ // listen for mousedown, mouseup, keydown, and mouse events at
+ // document level
+ Document* doc = sourceNode->GetComposedDoc();
+ if (doc) {
+ // Probably, we should listen to untrusted events for hiding tooltips
+ // on content since tooltips might disturb something of web
+ // applications. If we don't specify the aWantsUntrusted of
+ // AddSystemEventListener(), the event target sets it to TRUE if the
+ // target is in content.
+ doc->AddSystemEventListener(u"wheel"_ns, this, true);
+ doc->AddSystemEventListener(u"mousedown"_ns, this, true);
+ doc->AddSystemEventListener(u"mouseup"_ns, this, true);
+#ifndef XP_WIN
+ // On Windows, key events don't close tooltips.
+ doc->AddSystemEventListener(u"keydown"_ns, this, true);
+#endif
+ }
+ mSourceNode = nullptr;
+ }
+ }
+
+ return NS_OK;
+}
+
+static void SetTitletipLabel(XULTreeElement* aTree, Element* aTooltip,
+ int32_t aRow, nsTreeColumn* aCol) {
+ nsCOMPtr<nsITreeView> view = aTree->GetView();
+ if (view) {
+ nsAutoString label;
+#ifdef DEBUG
+ nsresult rv =
+#endif
+ view->GetCellText(aRow, aCol, label);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Couldn't get the cell text!");
+ aTooltip->SetAttr(kNameSpaceID_None, nsGkAtoms::label, label, true);
+ }
+}
+
+void nsXULTooltipListener::LaunchTooltip() {
+ RefPtr<Element> currentTooltip = do_QueryReferent(mCurrentTooltip);
+ if (!currentTooltip) {
+ return;
+ }
+
+ if (mIsSourceTree && mNeedTitletip) {
+ RefPtr<XULTreeElement> tree = GetSourceTree();
+
+ SetTitletipLabel(tree, currentTooltip, mLastTreeRow, mLastTreeCol);
+ if (!(currentTooltip = do_QueryReferent(mCurrentTooltip))) {
+ // Because of mutation events, currentTooltip can be null.
+ return;
+ }
+ currentTooltip->SetAttr(kNameSpaceID_None, nsGkAtoms::titletip, u"true"_ns,
+ true);
+ } else {
+ currentTooltip->UnsetAttr(kNameSpaceID_None, nsGkAtoms::titletip, true);
+ }
+
+ if (!(currentTooltip = do_QueryReferent(mCurrentTooltip))) {
+ // Because of mutation events, currentTooltip can be null.
+ return;
+ }
+
+ nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
+ if (!pm) {
+ return;
+ }
+
+ auto cleanup = MakeScopeExit([&] {
+ // Clear the current tooltip if the popup was not opened successfully.
+ if (!pm->IsPopupOpen(currentTooltip)) {
+ mCurrentTooltip = nullptr;
+ }
+ });
+
+ RefPtr<Element> target = do_QueryReferent(mTargetNode);
+ if (!target) {
+ return;
+ }
+
+ pm->ShowTooltipAtScreen(currentTooltip, target, mMouseScreenPoint);
+}
+
+nsresult nsXULTooltipListener::HideTooltip() {
+ if (nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip)) {
+ if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
+ pm->HidePopup(currentTooltip, false, false, false, false);
+ }
+ }
+
+ DestroyTooltip();
+ return NS_OK;
+}
+
+static void GetImmediateChild(nsIContent* aContent, nsAtom* aTag,
+ nsIContent** aResult) {
+ *aResult = nullptr;
+ for (nsCOMPtr<nsIContent> childContent = aContent->GetFirstChild();
+ childContent; childContent = childContent->GetNextSibling()) {
+ if (childContent->IsXULElement(aTag)) {
+ childContent.forget(aResult);
+ return;
+ }
+ }
+}
+
+nsresult nsXULTooltipListener::FindTooltip(nsIContent* aTarget,
+ nsIContent** aTooltip) {
+ if (!aTarget) return NS_ERROR_NULL_POINTER;
+
+ // before we go on, make sure that target node still has a window
+ Document* document = aTarget->GetComposedDoc();
+ if (!document) {
+ NS_WARNING("Unable to retrieve the tooltip node document.");
+ return NS_ERROR_FAILURE;
+ }
+ nsPIDOMWindowOuter* window = document->GetWindow();
+ if (!window) {
+ return NS_OK;
+ }
+
+ if (window->Closed()) {
+ return NS_OK;
+ }
+
+ // non-XUL elements should just use the default tooltip
+ if (!aTarget->IsXULElement()) {
+ nsIPopupContainer* popupContainer =
+ nsIPopupContainer::GetPopupContainer(document->GetPresShell());
+ NS_ENSURE_STATE(popupContainer);
+ if (RefPtr<Element> tooltip = popupContainer->GetDefaultTooltip()) {
+ tooltip.forget(aTooltip);
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+ }
+
+ // On Windows, the OS shows the tooltip, so we don't want Gecko to do it
+#ifdef XP_WIN
+ if (nsIFrame* f = aTarget->GetPrimaryFrame()) {
+ if (f->StyleDisplay()->GetWindowButtonType()) {
+ return NS_OK;
+ }
+ }
+#endif
+
+ nsAutoString tooltipText;
+ aTarget->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::tooltiptext,
+ tooltipText);
+
+ if (!tooltipText.IsEmpty()) {
+ // specifying tooltiptext means we will always use the default tooltip
+ nsIPopupContainer* popupContainer =
+ nsIPopupContainer::GetPopupContainer(document->GetPresShell());
+ NS_ENSURE_STATE(popupContainer);
+ if (RefPtr<Element> tooltip = popupContainer->GetDefaultTooltip()) {
+ tooltip->SetAttr(kNameSpaceID_None, nsGkAtoms::label, tooltipText, true);
+ tooltip.forget(aTooltip);
+ }
+ return NS_OK;
+ }
+
+ nsAutoString tooltipId;
+ aTarget->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::tooltip,
+ tooltipId);
+
+ // if tooltip == _child, look for first <tooltip> child
+ if (tooltipId.EqualsLiteral("_child")) {
+ GetImmediateChild(aTarget, nsGkAtoms::tooltip, aTooltip);
+ return NS_OK;
+ }
+
+ if (!tooltipId.IsEmpty()) {
+ DocumentOrShadowRoot* documentOrShadowRoot =
+ aTarget->GetUncomposedDocOrConnectedShadowRoot();
+ // tooltip must be an id, use getElementById to find it
+ if (documentOrShadowRoot) {
+ nsCOMPtr<nsIContent> tooltipEl =
+ documentOrShadowRoot->GetElementById(tooltipId);
+
+ if (tooltipEl) {
+ mNeedTitletip = false;
+ tooltipEl.forget(aTooltip);
+ return NS_OK;
+ }
+ }
+ }
+
+ // titletips should just use the default tooltip
+ if (mIsSourceTree && mNeedTitletip) {
+ nsIPopupContainer* popupContainer =
+ nsIPopupContainer::GetPopupContainer(document->GetPresShell());
+ NS_ENSURE_STATE(popupContainer);
+ NS_IF_ADDREF(*aTooltip = popupContainer->GetDefaultTooltip());
+ }
+
+ return NS_OK;
+}
+
+nsresult nsXULTooltipListener::GetTooltipFor(nsIContent* aTarget,
+ nsIContent** aTooltip) {
+ *aTooltip = nullptr;
+ nsCOMPtr<nsIContent> tooltip;
+ nsresult rv = FindTooltip(aTarget, getter_AddRefs(tooltip));
+ if (NS_FAILED(rv) || !tooltip) {
+ return rv;
+ }
+
+ // Submenus can't be used as tooltips, see bug 288763.
+ if (nsIContent* parent = tooltip->GetParent()) {
+ if (auto* button = XULButtonElement::FromNode(parent)) {
+ if (button->IsMenu()) {
+ NS_WARNING("Menu cannot be used as a tooltip");
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ tooltip.swap(*aTooltip);
+ return rv;
+}
+
+nsresult nsXULTooltipListener::DestroyTooltip() {
+ nsCOMPtr<nsIDOMEventListener> kungFuDeathGrip(this);
+ nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip);
+ if (currentTooltip) {
+ // release tooltip before removing listener to prevent our destructor from
+ // being called recursively (bug 120863)
+ mCurrentTooltip = nullptr;
+
+ // clear out the tooltip node on the document
+ nsCOMPtr<Document> doc = currentTooltip->GetComposedDoc();
+ if (doc) {
+ // remove the mousedown and keydown listener from document
+ doc->RemoveSystemEventListener(u"wheel"_ns, this, true);
+ doc->RemoveSystemEventListener(u"mousedown"_ns, this, true);
+ doc->RemoveSystemEventListener(u"mouseup"_ns, this, true);
+#ifndef XP_WIN
+ doc->RemoveSystemEventListener(u"keydown"_ns, this, true);
+#endif
+ }
+
+ // remove the popuphidden listener from tooltip
+ currentTooltip->RemoveSystemEventListener(u"popuphiding"_ns, this, false);
+ }
+
+ // kill any ongoing timers
+ KillTooltipTimer();
+ mSourceNode = nullptr;
+ mLastTreeCol = nullptr;
+
+ return NS_OK;
+}
+
+void nsXULTooltipListener::KillTooltipTimer() {
+ if (mTooltipTimer) {
+ mTooltipTimer->Cancel();
+ mTooltipTimer = nullptr;
+ mTargetNode = nullptr;
+ }
+}
+
+void nsXULTooltipListener::sTooltipCallback(nsITimer* aTimer, void* aListener) {
+ RefPtr<nsXULTooltipListener> instance = sInstance;
+ if (instance) instance->ShowTooltip();
+}
+
+XULTreeElement* nsXULTooltipListener::GetSourceTree() {
+ nsCOMPtr<nsIContent> sourceNode = do_QueryReferent(mSourceNode);
+ if (mIsSourceTree && sourceNode) {
+ RefPtr<XULTreeElement> xulEl =
+ XULTreeElement::FromNodeOrNull(sourceNode->GetParent());
+ return xulEl;
+ }
+
+ return nullptr;
+}
diff --git a/layout/xul/nsXULTooltipListener.h b/layout/xul/nsXULTooltipListener.h
new file mode 100644
index 0000000000..1810accf81
--- /dev/null
+++ b/layout/xul/nsXULTooltipListener.h
@@ -0,0 +1,98 @@
+/* -*- 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 nsXULTooltipListener_h__
+#define nsXULTooltipListener_h__
+
+#include "nsIDOMEventListener.h"
+#include "nsITimer.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "XULTreeElement.h"
+#include "nsIWeakReferenceUtils.h"
+#include "mozilla/Attributes.h"
+
+class nsIContent;
+class nsTreeColumn;
+
+namespace mozilla {
+namespace dom {
+class Event;
+class MouseEvent;
+} // namespace dom
+} // namespace mozilla
+
+class nsXULTooltipListener final : public nsIDOMEventListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ void MouseOut(mozilla::dom::Event* aEvent);
+ void MouseMove(mozilla::dom::Event* aEvent);
+
+ void AddTooltipSupport(nsIContent* aNode);
+ void RemoveTooltipSupport(nsIContent* aNode);
+ static nsXULTooltipListener* GetInstance() {
+ if (!sInstance) sInstance = new nsXULTooltipListener();
+ return sInstance;
+ }
+
+ protected:
+ nsXULTooltipListener();
+ ~nsXULTooltipListener();
+
+ // pref callback for when the "show tooltips" pref changes
+ static bool sShowTooltips;
+
+ void KillTooltipTimer();
+
+ void CheckTreeBodyMove(mozilla::dom::MouseEvent* aMouseEvent);
+ mozilla::dom::XULTreeElement* GetSourceTree();
+
+ nsresult ShowTooltip();
+ void LaunchTooltip();
+ nsresult HideTooltip();
+ nsresult DestroyTooltip();
+ // This method tries to find a tooltip for aTarget.
+ nsresult FindTooltip(nsIContent* aTarget, nsIContent** aTooltip);
+ // This method calls FindTooltip and checks that the tooltip
+ // can be really used (i.e. tooltip is not a menu).
+ nsresult GetTooltipFor(nsIContent* aTarget, nsIContent** aTooltip);
+
+ static nsXULTooltipListener* sInstance;
+ static void ToolbarTipsPrefChanged(const char* aPref, void* aClosure);
+
+ nsWeakPtr mSourceNode;
+ nsWeakPtr mTargetNode;
+ nsWeakPtr mCurrentTooltip;
+ nsWeakPtr mPreviousMouseMoveTarget;
+
+ // a timer for showing the tooltip
+ nsCOMPtr<nsITimer> mTooltipTimer;
+ static void sTooltipCallback(nsITimer* aTimer, void* aListener);
+
+ // Screen coordinates of the last mousemove event, stored so that the tooltip
+ // can be opened at this location.
+ //
+ // TODO(emilio): This duplicates a lot of code with ChromeTooltipListener.
+ mozilla::LayoutDeviceIntPoint mMouseScreenPoint;
+
+ // Tolerance for mousemove event
+ static constexpr mozilla::LayoutDeviceIntCoord kTooltipMouseMoveTolerance = 7;
+
+ // flag specifying if the tooltip has already been displayed by a MouseMove
+ // event. The flag is reset on MouseOut so that the tooltip will display
+ // the next time the mouse enters the node (bug #395668).
+ bool mTooltipShownOnce;
+
+ // special members for handling trees
+ bool mIsSourceTree;
+ bool mNeedTitletip;
+ int32_t mLastTreeRow;
+ RefPtr<nsTreeColumn> mLastTreeCol;
+};
+
+#endif // nsXULTooltipListener
diff --git a/layout/xul/reftest/checkbox-dynamic-change-ref.xhtml b/layout/xul/reftest/checkbox-dynamic-change-ref.xhtml
new file mode 100644
index 0000000000..a790928f92
--- /dev/null
+++ b/layout/xul/reftest/checkbox-dynamic-change-ref.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <checkbox id="c1"/>
+ <checkbox id="c2" checked="true"/>
+</window>
diff --git a/layout/xul/reftest/checkbox-dynamic-change.xhtml b/layout/xul/reftest/checkbox-dynamic-change.xhtml
new file mode 100644
index 0000000000..116e142a3c
--- /dev/null
+++ b/layout/xul/reftest/checkbox-dynamic-change.xhtml
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait">
+ <checkbox id="c1" checked="true"/>
+ <checkbox id="c2"/>
+ <script>
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ let c1 = document.getElementById("c1");
+ let c2 = document.getElementById("c2");
+ c1.removeAttribute("checked");
+ c2.setAttribute("checked", true);
+ document.documentElement.className = "";
+ });
+ });
+ </script>
+</window>
diff --git a/layout/xul/reftest/image-scaling-min-height-1-ref.xhtml b/layout/xul/reftest/image-scaling-min-height-1-ref.xhtml
new file mode 100644
index 0000000000..595450fe47
--- /dev/null
+++ b/layout/xul/reftest/image-scaling-min-height-1-ref.xhtml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+<html:style><![CDATA[
+
+window { -moz-box-align: start; -moz-box-pack: start }
+hbox { background: yellow }
+vbox { background: blue; width: 15px; height: 15px }
+
+]]></html:style>
+
+<hbox><vbox /><label value="a b c d e f" /></hbox>
+
+</window>
diff --git a/layout/xul/reftest/image-scaling-min-height-1.xhtml b/layout/xul/reftest/image-scaling-min-height-1.xhtml
new file mode 100644
index 0000000000..5c45d6b0c9
--- /dev/null
+++ b/layout/xul/reftest/image-scaling-min-height-1.xhtml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+<html:style><![CDATA[
+
+window { -moz-box-align: start; -moz-box-pack: start }
+hbox { background: yellow }
+image { background: blue; min-width: 15px; min-height: 15px }
+
+]]></html:style>
+
+<hbox><image /><label value="a b c d e f" /></hbox>
+
+</window>
diff --git a/layout/xul/reftest/image-size-ref.xhtml b/layout/xul/reftest/image-size-ref.xhtml
new file mode 100644
index 0000000000..f2ecbec30e
--- /dev/null
+++ b/layout/xul/reftest/image-size-ref.xhtml
@@ -0,0 +1,115 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+<html:style>
+div { margin: 0px; line-height: 0px; }
+div div { background: blue; display: inline; float: left; }
+</html:style>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 40px; height: 30px;"/><html:img
+ src="image4x3.png" style="width: 80px; height: 20px;"/><html:img
+ src="image4x3.png" style="width: 10px; height: 70px;"/><html:img
+ src="image4x3.png" style="width: 80px; height: 60px;"/><html:img
+ src="image4x3.png" style="width: 80px; height: 60px;"/><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/><html:img
+ src="image4x3.png" style="width: 40px; height: 30px; border: 8px solid green;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 80px; height: 64px; border: 8px solid yellow;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 72px; height: 58px; border: 8px solid green;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 24px; height: 22px; border: 8px solid yellow;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 24px; height: 22px; border: 8px solid green;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 74px; height: 53px; border: solid yellow; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 18px; height: 11px; border: solid green; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 40px; height: 30px;"/><html:img
+ src="image4x3.png" style="width: 80px; height: 20px;"/><html:img
+ src="image4x3.png" style="width: 10px; height: 70px;"/><html:img
+ src="image4x3.png" style="width: 80px; height: 60px;"/><html:img
+ src="image4x3.png" style="height: 80px; height: 60px;"/><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/><html:img
+ src="image4x3.png" style="width: 60px; height: 25px;"/><html:img
+ src="image4x3.png" style="width: 20px; height: 75px;"/><html:img
+ src="image4x3.png" style="width: 80px; height: 64px; padding: 8px; box-sizing: border-box;"/><html:img
+ src="image4x3.png" style="width: 72px; height: 58px; padding: 8px; box-sizing: border-box;"/><html:img
+ src="image4x3.png" style="width: 24px; height: 22px; padding: 8px; box-sizing: border-box;"/><html:img
+ src="image4x3.png" style="width: 24px; height: 22px; padding: 8px; box-sizing: border-box;"/><html:img
+ src="image4x3.png" style="width: 67px; height: 60px; padding: 4px 2px 8px 1px; box-sizing: border-box;"/><html:img
+ src="image4x3.png" style="width: 11px; height: 18px; padding: 4px 2px 8px 1px; box-sizing: border-box;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 30px; height: 22.5px"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 20px; height: 15px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width 30px; height: 22.5px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 24px; height: 22px; border: 8px solid green;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 24px; height: 22px; border: 8px solid green;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 40px; height: 30px;"/><html:img
+ src="image4x3.png" style="width: 40px; height: 30px;"/><html:img
+ src="image4x3.png" style="width: 40px; height: 30px;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 60px; height: 49px; border: 8px solid green;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 112px; height: 88px; border: 8px solid yellow;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 96px; height: 76px; border: 8px solid green;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 112px; height: 88px; border: 8px solid yellow;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 106px; height: 77px; border: solid yellow; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/>
+</html:div>
+
+<html:div><html:img
+ src="image4x3.png" style="width: 60px; height: 45px;"/><html:img
+ src="image4x3.png" style="width: 120px; height: 90px;"/><html:img
+ src="image4x3.png" style="width 60px; height: 45px;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 60px; height: 49px; padding: 8px;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 112px; height: 88px; padding: 8px;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 96px; height: 76px; padding: 8px;"/><html:img
+ src="image4x3.png" style="box-sizing: border-box; width: 112px; height: 88px; padding: 8px;"/>
+</html:div>
+
+<html:div><html:div
+ style="width: 20px; height: 15px;"/><html:div
+ style="width: 80px; height: 60px;"/><html:div
+ style="width: 40px; height: 30px;"/><html:div
+ style="width: 10px; height: 8px;"/><html:div
+ style="width: 10px; height: 8px;"/>
+</html:div>
+
+<html:div><html:div style="width: 20px; height: 15px;"/></html:div>
+
+<html:div><html:div style="width: 20px; height: 15px;"/></html:div>
+
+<html:div><html:div style="box-sizing: border-box; width: 24px; height: 22px; border: 8px solid green;"/></html:div>
+
+<html:div><html:div style="box-sizing: border-box; width: 24px; height: 22px; border: 8px solid green;"/></html:div>
+
+</window>
diff --git a/layout/xul/reftest/image-size.xhtml b/layout/xul/reftest/image-size.xhtml
new file mode 100644
index 0000000000..3d372dc3cd
--- /dev/null
+++ b/layout/xul/reftest/image-size.xhtml
@@ -0,0 +1,123 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<hbox align="end">
+ <image src="image4x3.png"/>
+ <image src="image4x3.png" width="80" height="20"/>
+ <image src="image4x3.png" width="10" height="70"/>
+ <image src="image4x3.png" width="80"/>
+ <image src="image4x3.png" height="60"/>
+ <image src="image4x3.png" width="20"/>
+ <image src="image4x3.png" height="15"/>
+ <image src="image4x3.png" style="border: 8px solid green;"/>
+ <image src="image4x3.png" width="80" style="border: 8px solid yellow;"/>
+ <image src="image4x3.png" height="58" style="border: 8px solid green;"/>
+ <image src="image4x3.png" width="24" style="border: 8px solid yellow;"/>
+ <image src="image4x3.png" height="22" style="border: 8px solid green;"/>
+ <image src="image4x3.png" width="74"
+ style="border: 1px solid yellow; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/>
+ <image src="image4x3.png" height="11"
+ style="border: 1px solid green; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="width: auto; height: auto;"/>
+ <image src="image4x3.png" style="width: 80px; height: 20px;"/>
+ <image src="image4x3.png" style="width: 10px; height: 70px;"/>
+ <image src="image4x3.png" style="width: 80px;"/>
+ <image src="image4x3.png" style="height: 60px;"/>
+ <image src="image4x3.png" style="width: 20px;"/>
+ <image src="image4x3.png" style="height: 15px;"/>
+ <image src="image4x3.png" style="width: 80px; height: 20px;" width="60" height="25"/>
+ <image src="image4x3.png" style="width: 10px; height: 70px;" width="20" height="75"/>
+ <image src="image4x3.png" style="width: 80px; padding: 8px;"/>
+ <image src="image4x3.png" style="height: 58px; padding: 8px;"/>
+ <image src="image4x3.png" style="width: 24px; padding: 8px;"/>
+ <image src="image4x3.png" style="height: 22px; padding: 8px;"/>
+ <image src="image4x3.png" style="width: 67px; padding: 4px 2px 8px 1px"/>
+ <image src="image4x3.png" style="height: 18px; padding: 4px 2px 8px 1px"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" maxwidth="20"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" maxheight="15"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" maxwidth="30" maxheight="25"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="max-width: 20px;"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="max-height: 15px;"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="max-width: 30px; max-height: 25px;"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" maxwidth="24" style="border: 8px solid green;"/>
+</hbox>
+<hbox align="end">
+ <image src="image4x3.png" maxheight="22" style="border: 8px solid green;"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" minwidth="20"/>
+ <image src="image4x3.png" minheight="20"/>
+ <image src="image4x3.png" minwidth="20" minheight="25"/>
+ <image src="image4x3.png" minwidth="60" style="border: 8px solid green;"/>
+ <image src="image4x3.png" minheight="88" style="border: 8px solid yellow;"/>
+ <image src="image4x3.png" minwidth="90" minheight="76" style="border: 8px solid green;"/>
+ <image src="image4x3.png" minwidth="112" minheight="76" style="border: 8px solid yellow;"/>
+ <image src="image4x3.png" minwidth="106"
+ style="border: 1px solid yellow; border-top-width: 1px; border-right-width: 2px; border-bottom-width: 4px; border-left-width: 8px;"/>
+</hbox>
+
+<hbox align="end">
+ <image src="image4x3.png" style="min-width: 60px;"/>
+ <image src="image4x3.png" style="min-height: 90px;"/>
+ <image src="image4x3.png" style="min-width 41px; min-height: 45px;"/>
+ <image src="image4x3.png" style="min-width: 60px; padding: 8px;"/>
+ <image src="image4x3.png" style="min-height: 88px; padding: 8px;"/>
+ <image src="image4x3.png" style="min-width: 90px; min-height: 76px; padding: 8px;"/>
+ <image src="image4x3.png" style="min-width: 112px; min-height: 76px; padding: 8px;"/>
+</hbox>
+
+<hbox align="start">
+ <image style="width: auto; height: auto; list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px);"/>
+ <image width="80" style="list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px);"/>
+ <image height="30" style="list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px);"/>
+ <image style="width: 10px; list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 21px, 5px);"/>
+ <image style="height: 8px; list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 21px, 5px);"/>
+</hbox>
+
+<hbox align="end">
+ <image maxwidth="20"
+ style="list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px);"/>
+</hbox>
+
+<hbox align="end">
+ <image maxheight="15"
+ style="list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px);"/>
+</hbox>
+
+<hbox align="end">
+ <image maxwidth="24"
+ style="list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px); border: 8px solid green;"/>
+</hbox>
+
+<hbox align="end">
+ <image maxheight="22"
+ style="list-style-image: url(image4x3.png); -moz-image-region: rect(5px, 25px, 20px, 5px); border: 8px solid green;"/>
+</hbox>
+
+</window>
diff --git a/layout/xul/reftest/image4x3.png b/layout/xul/reftest/image4x3.png
new file mode 100644
index 0000000000..6719bf5cec
--- /dev/null
+++ b/layout/xul/reftest/image4x3.png
Binary files differ
diff --git a/layout/xul/reftest/popup-explicit-size-ref.xhtml b/layout/xul/reftest/popup-explicit-size-ref.xhtml
new file mode 100644
index 0000000000..85a8a6832a
--- /dev/null
+++ b/layout/xul/reftest/popup-explicit-size-ref.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window align="start" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <label value="One"/>
+ <label value="Two"/>
+</window>
diff --git a/layout/xul/reftest/popup-explicit-size.xhtml b/layout/xul/reftest/popup-explicit-size.xhtml
new file mode 100644
index 0000000000..a4a87c2c8b
--- /dev/null
+++ b/layout/xul/reftest/popup-explicit-size.xhtml
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window align="start" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <label value="One"/>
+ <menupopup height="40"/>
+ <label value="Two"/>
+</window>
diff --git a/layout/xul/reftest/radio-dynamic-change-ref.xhtml b/layout/xul/reftest/radio-dynamic-change-ref.xhtml
new file mode 100644
index 0000000000..73ff14d6cd
--- /dev/null
+++ b/layout/xul/reftest/radio-dynamic-change-ref.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <radio id="r1"/>
+ <radio id="r2" selected="true"/>
+</window>
diff --git a/layout/xul/reftest/radio-dynamic-change.xhtml b/layout/xul/reftest/radio-dynamic-change.xhtml
new file mode 100644
index 0000000000..508e99ec02
--- /dev/null
+++ b/layout/xul/reftest/radio-dynamic-change.xhtml
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait">
+ <radio id="r1" selected="true"/>
+ <radio id="r2"/>
+ <script>
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ let r1 = document.getElementById("r1");
+ let r2 = document.getElementById("r2");
+ r1.removeAttribute("selected");
+ r2.setAttribute("selected", true);
+ document.documentElement.className = "";
+ });
+ });
+ </script>
+</window>
diff --git a/layout/xul/reftest/reftest.list b/layout/xul/reftest/reftest.list
new file mode 100644
index 0000000000..a2b0b6c6fe
--- /dev/null
+++ b/layout/xul/reftest/reftest.list
@@ -0,0 +1,14 @@
+== chrome://reftest/content/xul/reftest/popup-explicit-size.xhtml chrome://reftest/content/xul/reftest/popup-explicit-size-ref.xhtml
+fuzzy(0-16,0-128) random-if(Android) == chrome://reftest/content/xul/reftest/image-size.xhtml chrome://reftest/content/xul/reftest/image-size-ref.xhtml
+== chrome://reftest/content/xul/reftest/image-scaling-min-height-1.xhtml chrome://reftest/content/xul/reftest/image-scaling-min-height-1-ref.xhtml
+== chrome://reftest/content/xul/reftest/textbox-text-transform.xhtml chrome://reftest/content/xul/reftest/textbox-text-transform-ref.xhtml
+
+== chrome://reftest/content/xul/reftest/checkbox-dynamic-change.xhtml chrome://reftest/content/xul/reftest/checkbox-dynamic-change-ref.xhtml
+== chrome://reftest/content/xul/reftest/radio-dynamic-change.xhtml chrome://reftest/content/xul/reftest/radio-dynamic-change-ref.xhtml
+
+# These test find marks appearing on the scrollbar
+fails-if(useDrawSnapshot) != chrome://reftest/content/xul/reftest/scrollbar-marks.html chrome://reftest/content/xul/reftest/scrollbar-marks-ref.html
+fails-if(useDrawSnapshot) != chrome://reftest/content/xul/reftest/scrollbar-marks2.html chrome://reftest/content/xul/reftest/scrollbar-marks-ref.html
+fails-if(useDrawSnapshot) != chrome://reftest/content/xul/reftest/scrollbar-marks2.html chrome://reftest/content/xul/reftest/scrollbar-marks.html
+# This test is fuzzy as the marks cannot be positioned exactly as the real ones are measured in dev pixels.
+fuzzy(0-10,0-170) fuzzy-if(winWidget&&isDebugBuild&&layersGPUAccelerated&&!is64Bit,1-1,74-170) == chrome://reftest/content/xul/reftest/scrollbar-marks-overlay.html chrome://reftest/content/xul/reftest/scrollbar-marks-overlay-ref.html
diff --git a/layout/xul/reftest/scrollbar-marks-overlay-ref.html b/layout/xul/reftest/scrollbar-marks-overlay-ref.html
new file mode 100644
index 0000000000..8d940c64d6
--- /dev/null
+++ b/layout/xul/reftest/scrollbar-marks-overlay-ref.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script>
+ // Account for scrollbar buttons on Windows
+const hasScrollbarButtons = navigator.platform.indexOf("Win") >= 0;
+const scrollbarButtonSize = 16;
+
+function assignMarks()
+{
+ let frame0 = document.getElementById('frame0');
+ let width = frame0.getBoundingClientRect().width;
+
+ let innerRect0 = frame0.contentDocument.documentElement.getBoundingClientRect();
+ let markWidth = width - innerRect0.width - 2;
+
+ let scrollButtonHeight = hasScrollbarButtons ? scrollbarButtonSize : 0;
+ let sliderHeight = 200 - scrollButtonHeight * 2;
+
+ let one = document.getElementById('one');
+ one.style.width = markWidth + "px";
+ one.style.top = (Math.floor(30 / frames[0].scrollMaxY * sliderHeight) + scrollButtonHeight) + "px";
+
+ let two = document.getElementById('two');
+ two.style.width = markWidth + "px";
+ two.style.top = (Math.floor(70 / frames[0].scrollMaxY * sliderHeight) + scrollButtonHeight) + "px";
+
+ let three = document.getElementById('three');
+ three.style.width = markWidth + "px";
+ three.style.top = (Math.floor(110 / frames[0].scrollMaxY * sliderHeight) + scrollButtonHeight) + "px";
+
+ let frame1 = document.getElementById('frame1');
+ let height = frame1.getBoundingClientRect().height;
+
+ let innerRect1 = frame1.contentDocument.documentElement.getBoundingClientRect();
+ let markHeight = height - innerRect1.height - 2;
+
+ let scrollButtonWidth = hasScrollbarButtons ? scrollbarButtonSize : 0;
+ let sliderWidth = 300 - scrollButtonWidth * 2;
+
+ let four = document.getElementById('four');
+ four.style.height = markHeight + "px";
+ four.style.left = (Math.floor(45 / frames[1].scrollMaxX * sliderWidth) + scrollButtonWidth) + "px";
+
+ let five = document.getElementById('five');
+ five.style.height = markHeight + "px";
+ five.style.left = (Math.floor(165 / frames[1].scrollMaxX * sliderWidth) + scrollButtonWidth) + "px";
+
+ document.documentElement.removeAttribute("class");
+}
+</script>
+</head>
+<body onload="assignMarks()">
+<div style='border: 1px solid red; position: absolute; width: 300px; padding: 0;'>
+ <iframe id='frame0' style='position: relative; border: none; height: 200px; vertical-align: middle;' src='data:text/html,<p style="height: 400px;"></p>'></iframe>
+ <div id='one' style='border: 1px solid #ef0fff; opacity: 0.3; position: absolute; right: 0px;'></div>
+ <div id='two' style='border: 1px solid #ef0fff; opacity: 0.3; position: absolute; right: 0px;'></div>
+ <div id='three' style='border: 1px solid #ef0fff; opacity: 0.3; position: absolute; right: 0px;'></div>
+ <iframe id='frame1' style='position: relative; border: none; height: 200px; vertical-align: middle;' src='data:text/html,<p style="height: 100%; width: 600px;"></p>'></iframe>
+ <div id='four' style='border: 1px solid #ef0fff; opacity: 0.3; position: absolute; bottom: 0px;'></div>
+ <div id='five' style='border: 1px solid #ef0fff; opacity: 0.3; position: absolute; bottom: 0px;'></div>
+</div>
+</body>
+</html>
diff --git a/layout/xul/reftest/scrollbar-marks-overlay.html b/layout/xul/reftest/scrollbar-marks-overlay.html
new file mode 100644
index 0000000000..823fd6fd52
--- /dev/null
+++ b/layout/xul/reftest/scrollbar-marks-overlay.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script>
+ function doTest() {
+ frames[0].setScrollMarks([30, 70, 110]);
+ frames[1].setScrollMarks([45, 165], true);
+ document.documentElement.removeAttribute("class");
+ }
+</script>
+</head>
+<body onload="doTest()">
+<div style='border: 1px solid red; position: absolute; width: 300px; padding: 0;'>
+ <iframe style='position: relative; border: none; height: 200px; vertical-align: middle;' src='data:text/html,<p style="height: 400px;"></p>'></iframe>
+ <iframe style='position: relative; border: none; height: 200px; vertical-align: middle;' src='data:text/html,<p style="height: 100%; width: 600px;"></p>'></iframe>
+</div>
+</body>
+</html>
diff --git a/layout/xul/reftest/scrollbar-marks-ref.html b/layout/xul/reftest/scrollbar-marks-ref.html
new file mode 100644
index 0000000000..204c5db7fa
--- /dev/null
+++ b/layout/xul/reftest/scrollbar-marks-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+</head>
+<body>
+ <p>This is some text</p>
+ <p style="height: 1000px;">Box 1</p>
+ <p>This is some text</p>
+ <p style="height: 1000px;">Box 2</p>
+ <p>This is some text</p>
+</body>
+</html>
+
diff --git a/layout/xul/reftest/scrollbar-marks.html b/layout/xul/reftest/scrollbar-marks.html
new file mode 100644
index 0000000000..c60d06c804
--- /dev/null
+++ b/layout/xul/reftest/scrollbar-marks.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script>
+ function doTest() {
+ window.setScrollMarks([20]);
+ document.documentElement.removeAttribute("class");
+ }
+</script>
+</head>
+<body onload="doTest()">
+ <p>This is some text</p>
+ <p style="height: 1000px;">Box 1</p>
+ <p>This is some text</p>
+ <p style="height: 1000px;">Box 2</p>
+ <p>This is some text</p>
+</body>
+</html>
diff --git a/layout/xul/reftest/scrollbar-marks2.html b/layout/xul/reftest/scrollbar-marks2.html
new file mode 100644
index 0000000000..e39c6e1192
--- /dev/null
+++ b/layout/xul/reftest/scrollbar-marks2.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script>
+ function doTest() {
+ // Two find marks should be drawn.
+ window.setScrollMarks([20, 140]);
+ document.documentElement.removeAttribute("class");
+ }
+</script>
+</head>
+<body onload="doTest()">
+ <p>This is some text</p>
+ <p style="height: 1000px;">Box 1</p>
+ <p>This is some text</p>
+ <p style="height: 1000px;">Box 2</p>
+ <p>This is some text</p>
+</body>
+</html>
diff --git a/layout/xul/reftest/textbox-text-transform-ref.xhtml b/layout/xul/reftest/textbox-text-transform-ref.xhtml
new file mode 100644
index 0000000000..74d03a1ec9
--- /dev/null
+++ b/layout/xul/reftest/textbox-text-transform-ref.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<label value="UPPERCASE"/>
+<label value="lowercase"/>
+</window>
diff --git a/layout/xul/reftest/textbox-text-transform.xhtml b/layout/xul/reftest/textbox-text-transform.xhtml
new file mode 100644
index 0000000000..5c542cf80e
--- /dev/null
+++ b/layout/xul/reftest/textbox-text-transform.xhtml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<label style="text-transform: uppercase" value="uppercase"/>
+<label style="text-transform: lowercase" value="LOWERCASE"/>
+</window>
diff --git a/layout/xul/test/browser.ini b/layout/xul/test/browser.ini
new file mode 100644
index 0000000000..0f79707843
--- /dev/null
+++ b/layout/xul/test/browser.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+
+[browser_bug685470.js]
+[browser_bug703210.js]
+skip-if = true # Bugs 1382428, 1567736, 1565339
+[browser_bug706743.js]
+skip-if = true # Bug 1157576
+[browser_bug1163304.js]
+skip-if =
+ os != 'linux' && os != 'win' // Due to testing menubar behavior with keyboard
+[browser_bug1754298.js]
diff --git a/layout/xul/test/browser_bug1163304.js b/layout/xul/test/browser_bug1163304.js
new file mode 100644
index 0000000000..ae29389e0f
--- /dev/null
+++ b/layout/xul/test/browser_bug1163304.js
@@ -0,0 +1,83 @@
+const { CustomizableUITestUtils } = ChromeUtils.import(
+ "resource://testing-common/CustomizableUITestUtils.jsm"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+add_task(async function test_setup() {
+ await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+add_task(async function() {
+ const promiseFocusInSearchBar = BrowserTestUtils.waitForEvent(
+ BrowserSearch.searchBar.textbox,
+ "focus"
+ );
+ BrowserSearch.searchBar.focus();
+ await promiseFocusInSearchBar;
+
+ let DOMWindowUtils = EventUtils._getDOMWindowUtils();
+ is(
+ DOMWindowUtils.IMEStatus,
+ DOMWindowUtils.IME_STATUS_ENABLED,
+ "IME should be available when searchbar has focus"
+ );
+
+ let searchPopup = document.getElementById("PopupSearchAutoComplete");
+
+ // Open popup of the searchbar
+ // Oddly, F4 key press is sometimes not handled by the search bar.
+ // It's out of scope of this test, so, let's retry to open it if failed.
+ await (async () => {
+ async function tryToOpen() {
+ try {
+ BrowserSearch.searchBar.focus();
+ EventUtils.synthesizeKey("KEY_F4");
+ await TestUtils.waitForCondition(
+ () => searchPopup.state == "open",
+ "The popup isn't opened",
+ 5,
+ 100
+ );
+ } catch (e) {
+ // timed out, let's just return false without asserting the failure.
+ return false;
+ }
+ return true;
+ }
+ for (let i = 0; i < 5; i++) {
+ if (await tryToOpen()) {
+ return;
+ }
+ }
+ ok(false, "Failed to open the popup of searchbar");
+ })();
+
+ is(
+ DOMWindowUtils.IMEStatus,
+ DOMWindowUtils.IME_STATUS_ENABLED,
+ "IME should be available even when the popup of searchbar is open"
+ );
+
+ // Activate the menubar, then, the popup should be closed
+ is(searchPopup.state, "open", "The popup of searchbar shouldn't be closed");
+ let hiddenPromise = BrowserTestUtils.waitForEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Alt");
+ await hiddenPromise;
+ await new Promise(r => setTimeout(r, 0));
+
+ is(
+ DOMWindowUtils.IMEStatus,
+ DOMWindowUtils.IME_STATUS_DISABLED,
+ "IME should not be available when menubar is active"
+ );
+ // Inactivate the menubar (and restore the focus to the searchbar
+ EventUtils.synthesizeKey("KEY_Escape");
+ is(
+ DOMWindowUtils.IMEStatus,
+ DOMWindowUtils.IME_STATUS_ENABLED,
+ "IME should be available after focus is back to the searchbar"
+ );
+});
diff --git a/layout/xul/test/browser_bug1754298.js b/layout/xul/test/browser_bug1754298.js
new file mode 100644
index 0000000000..1fad9ebe1a
--- /dev/null
+++ b/layout/xul/test/browser_bug1754298.js
@@ -0,0 +1,35 @@
+add_task(async function() {
+ const PAGE = `
+<!doctype html>
+<select>
+<option value="1">AA Option</option>
+<option value="2">BB Option</option>
+<option value="3">&nbsp;CC Option</option>
+<option value="4">&nbsp;&nbsp;DD Option</option>
+<option value="5">&nbsp;&nbsp;&nbsp;EE Option</option>
+</select>`;
+ const url = "data:text/html," + encodeURI(PAGE);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url,
+ },
+ async function(browser) {
+ let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+ await BrowserTestUtils.synthesizeMouseAtCenter("select", {}, browser);
+ let popup = await popupShownPromise;
+ EventUtils.sendString("C", window);
+ EventUtils.sendKey("RETURN", window);
+ ok(
+ await TestUtils.waitForCondition(() => {
+ return SpecialPowers.spawn(
+ browser,
+ [],
+ () => content.document.querySelector("select").value
+ ).then(value => value == 3);
+ }),
+ "Unexpected value for select element (expected 3)!"
+ );
+ }
+ );
+});
diff --git a/layout/xul/test/browser_bug685470.js b/layout/xul/test/browser_bug685470.js
new file mode 100644
index 0000000000..8542fbaeb6
--- /dev/null
+++ b/layout/xul/test/browser_bug685470.js
@@ -0,0 +1,38 @@
+add_task(async function() {
+ const html =
+ '<p id="p1" title="tooltip is here">This paragraph has a tooltip.</p>';
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/html," + html
+ );
+
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({ set: [["ui.tooltipDelay", 0]] }, resolve);
+ });
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#p1",
+ { type: "mousemove" },
+ gBrowser.selectedBrowser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#p1",
+ {},
+ gBrowser.selectedBrowser
+ );
+
+ // Wait until the tooltip timeout triggers that would normally have opened the popup.
+ await new Promise(resolve => setTimeout(resolve, 0));
+ is(
+ document.getElementById("aHTMLTooltip").state,
+ "closed",
+ "local tooltip is closed"
+ );
+ is(
+ document.getElementById("remoteBrowserTooltip").state,
+ "closed",
+ "remote tooltip is closed"
+ );
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/layout/xul/test/browser_bug703210.js b/layout/xul/test/browser_bug703210.js
new file mode 100644
index 0000000000..c02b7aba08
--- /dev/null
+++ b/layout/xul/test/browser_bug703210.js
@@ -0,0 +1,56 @@
+add_task(async function() {
+ const url =
+ "data:text/html," +
+ "<html onmousemove='event.stopPropagation()'" +
+ " onmouseenter='event.stopPropagation()' onmouseleave='event.stopPropagation()'" +
+ " onmouseover='event.stopPropagation()' onmouseout='event.stopPropagation()'>" +
+ '<p id="p1" title="tooltip is here">This paragraph has a tooltip.</p>' +
+ '<p id="p2">This paragraph doesn\'t have tooltip.</p></html>';
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let browser = gBrowser.selectedBrowser;
+
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({ set: [["ui.tooltipDelay", 0]] }, resolve);
+ });
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popupshown",
+ false,
+ event => {
+ is(event.originalTarget.localName, "tooltip", "tooltip is showing");
+ return true;
+ }
+ );
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popuphidden",
+ false,
+ event => {
+ is(event.originalTarget.localName, "tooltip", "tooltip is hidden");
+ return true;
+ }
+ );
+
+ // Send a mousemove at a known position to start the test.
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#p2",
+ { type: "mousemove" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#p1",
+ { type: "mousemove" },
+ browser
+ );
+ await popupShownPromise;
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#p2",
+ { type: "mousemove" },
+ browser
+ );
+ await popupHiddenPromise;
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/layout/xul/test/browser_bug706743.js b/layout/xul/test/browser_bug706743.js
new file mode 100644
index 0000000000..cb7e487a26
--- /dev/null
+++ b/layout/xul/test/browser_bug706743.js
@@ -0,0 +1,158 @@
+add_task(async function() {
+ const url =
+ "data:text/html,<html><head></head><body>" +
+ '<a id="target" href="about:blank" title="This is tooltip text" ' +
+ 'style="display:block;height:20px;margin:10px;" ' +
+ 'onclick="return false;">here is an anchor element</a></body></html>';
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let browser = gBrowser.selectedBrowser;
+
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({ set: [["ui.tooltipDelay", 0]] }, resolve);
+ });
+
+ // Send a mousemove at a known position to start the test.
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ -5,
+ -5,
+ { type: "mousemove" },
+ browser
+ );
+
+ // show tooltip by mousemove into target.
+ let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown");
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ 5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+ await popupShownPromise;
+
+ // hide tooltip by mousemove to outside.
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popuphidden"
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ -5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+ await popupHiddenPromise;
+
+ // mousemove into the target and start drag by emulation via nsIDragService.
+ // Note that on some platforms, we cannot actually start the drag by
+ // synthesized events. E.g., Windows waits an actual mousemove event after
+ // dragstart.
+
+ // Emulate a buggy mousemove event. widget might dispatch mousemove event
+ // during drag.
+
+ function tooltipNotExpected() {
+ ok(false, "tooltip is shown during drag");
+ }
+ addEventListener("popupshown", tooltipNotExpected, true);
+
+ let dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
+ Ci.nsIDragService
+ );
+ dragService.startDragSessionForTests(
+ Ci.nsIDragService.DRAGDROP_ACTION_MOVE |
+ Ci.nsIDragService.DRAGDROP_ACTION_COPY |
+ Ci.nsIDragService.DRAGDROP_ACTION_LINK
+ );
+ try {
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ 5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 100));
+ } finally {
+ removeEventListener("popupshown", tooltipNotExpected, true);
+ dragService.endDragSession(true);
+ }
+
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ -5,
+ -5,
+ { type: "mousemove" },
+ browser
+ );
+
+ // If tooltip listener used a flag for managing D&D state, we would need
+ // to test if the tooltip is shown after drag.
+
+ // show tooltip by mousemove into target.
+ popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown");
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ 5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+ await popupShownPromise;
+
+ // hide tooltip by mousemove to outside.
+ popupHiddenPromise = BrowserTestUtils.waitForEvent(document, "popuphidden");
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ -5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+ await popupHiddenPromise;
+
+ // Show tooltip after mousedown
+ popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown");
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ 5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+ await popupShownPromise;
+
+ popupHiddenPromise = BrowserTestUtils.waitForEvent(document, "popuphidden");
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ 5,
+ 15,
+ { type: "mousedown" },
+ browser
+ );
+ await popupHiddenPromise;
+
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ 5,
+ 15,
+ { type: "mouseup" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeMouse(
+ "#target",
+ -5,
+ 15,
+ { type: "mousemove" },
+ browser
+ );
+
+ ok(true, "tooltips appear properly");
+
+ gBrowser.removeCurrentTab();
+});
diff --git a/layout/xul/test/chrome.ini b/layout/xul/test/chrome.ini
new file mode 100644
index 0000000000..b0ebb99b07
--- /dev/null
+++ b/layout/xul/test/chrome.ini
@@ -0,0 +1,39 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+ windowminmaxsize1.xhtml
+ windowminmaxsize2.xhtml
+ windowminmaxsize3.xhtml
+ windowminmaxsize4.xhtml
+ windowminmaxsize5.xhtml
+ windowminmaxsize6.xhtml
+ windowminmaxsize7.xhtml
+ windowminmaxsize8.xhtml
+ windowminmaxsize9.xhtml
+ windowminmaxsize10.xhtml
+ titledpanelwindow.xhtml
+
+[test_blockify_moz_box.html]
+[test_bug159346.xhtml]
+[test_bug381167.xhtml]
+[test_bug398982-1.xhtml]
+[test_bug398982-2.xhtml]
+[test_bug467442.xhtml]
+[test_bug477754.xhtml]
+[test_bug703150.xhtml]
+[test_bug987230.xhtml]
+skip-if = os == 'linux' # No native mousedown event on Linux
+[test_bug1197913.xhtml]
+[test_popupReflowPos.xhtml]
+[test_popupSizeTo.xhtml]
+[test_popupZoom.xhtml]
+[test_submenuClose.xhtml]
+[test_windowminmaxsize.xhtml]
+[test_resizer_ctrl_click.xhtml]
+[test_resizer_incontent.xhtml]
+[test_splitter.xhtml]
+skip-if = toolkit == 'android' # no XUL theme
+[test_splitter_sibling.xhtml]
+skip-if = toolkit == 'android' # no XUL theme
+[test_toolbarbutton_ctrl_click.xhtml]
+[test_menuitem_ctrl_click.xhtml]
diff --git a/layout/xul/test/file_bug386386.sjs b/layout/xul/test/file_bug386386.sjs
new file mode 100644
index 0000000000..4cd23a7909
--- /dev/null
+++ b/layout/xul/test/file_bug386386.sjs
@@ -0,0 +1,14 @@
+// SJS file for test_bug386386.html
+"use strict";
+
+function handleRequest(request, response) {
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader(
+ "Content-Type",
+ "application/xhtml+xml;charset=utf-8",
+ false
+ );
+ response.write(
+ "%3C%3Fxml%20version%3D%221.0%22%3F%3E%0A%3Cwindow%3E%3C/window%3E"
+ );
+}
diff --git a/layout/xul/test/mochitest.ini b/layout/xul/test/mochitest.ini
new file mode 100644
index 0000000000..599abe9b07
--- /dev/null
+++ b/layout/xul/test/mochitest.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+support-files =
+ file_bug386386.sjs
+[test_bug386386.html]
+allow_xul_xbl = true
+[test_bug394800.xhtml]
+allow_xul_xbl = true
+[test_bug511075.html]
+skip-if = toolkit == 'android' #bug 798806
+[test_bug563416.html]
+skip-if = toolkit == 'android'
+[test_drag_thumb_in_link.html]
+skip-if = toolkit == 'android'
diff --git a/layout/xul/test/test_blockify_moz_box.html b/layout/xul/test/test_blockify_moz_box.html
new file mode 100644
index 0000000000..3f4a4546b8
--- /dev/null
+++ b/layout/xul/test/test_blockify_moz_box.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1580012
+-->
+<head>
+ <title>Test for Bug 1580012</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <style>
+ /* Styling for parents that blockify their children: */
+ .grid { display: grid; }
+ .flex { display: flex; }
+
+ /* Styling that blockifies an element itself: */
+ .float { float: left; }
+ .abs { position: absolute; }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1580012">Mozilla Bug 1580012</a>
+<p id="display"></p>
+<div id="content">
+ <!-- Boxes that have no reason to be blockified: -->
+ <div class="moz-box" id="regularMozBox"></div>
+ <div class="moz-inline-box" id="regularMozInlineBox"></div>
+
+ <!-- A grid container with a -moz-box and a -moz-inline-box grid item (which
+ should both end up with display:-moz-box), and a -moz-inline-box
+ grandchild (which should preserve its -moz-inline-box display val): -->
+ <div class="grid">
+ <div class="moz-box" id="gridItemMozBox"></div>
+ <div class="moz-inline-box" id="gridItemMozInlineBox"></div>
+ <div><div class="moz-inline-box" id="gridGrandchildMozInlineBox"></div></div>
+ </div>
+
+ <!-- A flex container with a -moz-box and a -moz-inline-box flex item (which
+ should both end up with display:-moz-box), and a -moz-inline-box
+ grandchild (which should preserve its -moz-inline-box display val): -->
+ <div class="flex">
+ <div class="moz-box" id="flexItemMozBox"></div>
+ <div class="moz-inline-box" id="flexItemMozInlineBox"></div>
+ <div><div class="moz-inline-box" id="flexGrandchildMozInlineBox"></div></div>
+ </div>
+
+ <!-- Boxes that are directly blockified via other styling on them: -->
+ <!-- XXXdholbert commenting these out -- see notes below about assertion
+ failures for floated -moz-box.
+ <div class="float moz-box" id="floatMozBox"></div>
+ <div class="float moz-inline-box" id="floatMozInlineBox"></div>
+ -->
+ <!-- XXXdholbert commenting these out -- see notes below about assertion
+ failures for positioned -moz-box.
+ <div class="abs moz-box" id="absMozBox"></div>
+ <div class="abs moz-inline-box" id="absMozInlineBox"></div>
+ -->
+</div>
+<pre id="test">
+<script>
+
+/** Test for Bug 1580012 **/
+
+function checkDisp(elemId, expectedDisplay) {
+ var elem = document.getElementById(elemId);
+ ok(elem, "should have a valid ID for an element");
+
+ is(getComputedStyle(elem).display, expectedDisplay,
+ "Element with ID " + elemId + " should have expected display value");
+}
+
+// Create CSS Style rules to add -moz-box / -moz-inline-box styling.
+// Note that these style won't parse correctly until after we've flipped
+// the prefs via pushPrefEnv(). That's why I'm creating these style rules
+// here rather than just putting them inline in the <style> element.
+var sheet = document.styleSheets[0];
+sheet.insertRule(".moz-box { display: -moz-box; }");
+sheet.insertRule(".moz-inline-box { display: -moz-inline-box; }");
+
+// Check the computed 'display' of the various elements.
+checkDisp("regularMozBox", "-moz-box");
+checkDisp("regularMozInlineBox", "-moz-inline-box");
+
+checkDisp("gridItemMozBox", "-moz-box");
+checkDisp("gridItemMozInlineBox", "-moz-box");
+checkDisp("gridGrandchildMozInlineBox", "-moz-inline-box");
+
+checkDisp("flexItemMozBox", "-moz-box");
+checkDisp("flexItemMozInlineBox", "-moz-box");
+checkDisp("flexGrandchildMozInlineBox", "-moz-inline-box");
+
+// XXXdholbert The floated boxes trigger assertion failures where
+// nsLineLayout thinks it somehow ended up with an inline-level (really, just
+// a non-'block') floated thing. In practice this isn't really a concern
+// since -moz-box display values are disabled in content and since XUL
+// doesn't use 'float' for layout. So: I've added a fatal assertion in
+// ReflowInput.cpp to validate that we never actually encounter a floated
+// -moz-box/-moz-inline-box, and I'm commenting out these lines (which
+// trigger that fatal assertion).
+//
+// checkDisp("floatMozBox", "-moz-box");
+// checkDisp("floatMozInlineBox", "-moz-box");
+
+
+// XXXdholbert These abspos boxes trigger a diagnostic assertion added in
+// bug 1582819 which is intended to flush out XUL content that is positioned
+// and hence was previously blockified to 'block' but will now be '-moz-box'.
+// The diagnostic assertion doesn't need to stay around forever, but while
+// it exists, we can't test this scenario without triggering it.
+//
+// checkDisp("absMozBox", "-moz-box");
+// checkDisp("absMozInlineBox", "-moz-box");
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/xul/test/test_bug1197913.xhtml b/layout/xul/test/test_bug1197913.xhtml
new file mode 100644
index 0000000000..770c70113d
--- /dev/null
+++ b/layout/xul/test/test_bug1197913.xhtml
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1197913
+-->
+<window title="Mozilla Bug 1197913"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="SimpleTest.waitForFocus(nextTest, window)">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1197913"
+ target="_blank">Mozilla Bug 1197913</a>
+ </body>
+
+ <hbox align="center" pack="center">
+ <menulist>
+ <menupopup>
+ <menuitem label="Car" />
+ <menuitem label="Taxi" id="target" />
+ <menuitem label="Bus" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ let menulist = document.getElementsByTagName("menulist")[0];
+ let menuitem = document.getElementById("target");
+
+ function onDOMMenuItemActive(e) {
+ menuitem.removeEventListener("DOMMenuItemActive", onDOMMenuItemActive);
+
+ synthesizeMouse(menuitem, 0, 0, { type: "mousemove" });
+ synthesizeMouse(menuitem, -1, 0, { type: "mousemove" });
+
+ setTimeout(() => {
+ ok(menuitem.getAttribute("_moz-menuactive"), "Should be active");
+ SimpleTest.finish();
+ });
+ }
+
+ function onPopupShown(e) {
+ menulist.removeEventListener("popupshown", onPopupShown);
+ menuitem.addEventListener("DOMMenuItemActive", onDOMMenuItemActive);
+ synthesizeMouse(menuitem, 0, 0, { type: "mousemove" });
+ synthesizeMouse(menuitem, 1, 0, { type: "mousemove" });
+ }
+
+ function nextTest(e) {
+ menulist.addEventListener("popupshown", onPopupShown);
+ synthesizeMouseAtCenter(menulist, {});
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/layout/xul/test/test_bug159346.xhtml b/layout/xul/test/test_bug159346.xhtml
new file mode 100644
index 0000000000..03dc5d5d1b
--- /dev/null
+++ b/layout/xul/test/test_bug159346.xhtml
@@ -0,0 +1,143 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Test for Bug 159346">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=159346
+-->
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+<scrollbar id="scrollbar" curpos="0" maxpos="500"/>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+var scrollbar = document.getElementById("scrollbar");
+var downButton;
+
+var domWinUtils = SpecialPowers.DOMWindowUtils;
+domWinUtils.loadSheetUsingURIString('data:text/css,@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); scrollbarbutton[type="increment"][sbattr="scrollbar-down-bottom"] { display: -moz-box; min-width: 3px; min-height: 3px; }', domWinUtils.AGENT_SHEET);
+
+function init()
+{
+ downButton = SpecialPowers.unwrap(
+ SpecialPowers.InspectorUtils.getChildrenForNode(scrollbar, true)[4]);
+ if (!downButton) {
+ ok(navigator.userAgent.indexOf("Linux") !== -1 ||
+ navigator.userAgent.indexOf("Mac") !== -1, "Theme doesn't support scrollbar buttons");
+ SimpleTest.finish();
+ return;
+ }
+ SimpleTest.executeSoon(doTest1);
+}
+
+function getCurrentPos()
+{
+ return Number(scrollbar.getAttribute("curpos"));
+}
+
+function doTest1()
+{
+ var lastPos = 0;
+
+ synthesizeMouseAtCenter(downButton, { type: "mousedown" });
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by mousedown #1");
+ lastPos = getCurrentPos();
+
+ setTimeout(function () {
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by auto repeat #1");
+ synthesizeMouseAtCenter(downButton, { type: "mouseup" });
+ lastPos = getCurrentPos();
+
+ setTimeout(function () {
+ is(getCurrentPos(), lastPos,
+ "scrollbar changed curpos after mouseup #1");
+ SimpleTest.executeSoon(doTest2);
+ }, 1000);
+ }, 1000);
+}
+
+function doTest2()
+{
+ SpecialPowers.setIntPref("ui.scrollbarButtonAutoRepeatBehavior", 0);
+
+ scrollbar.setAttribute("curpos", 0);
+ var lastPos = 0;
+
+ synthesizeMouseAtCenter(downButton, { type: "mousedown" });
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by mousedown #2");
+ lastPos = getCurrentPos();
+
+ synthesizeMouse(downButton, -10, -10, { type: "mousemove" });
+ lastPos = getCurrentPos();
+
+ setTimeout(function () {
+ is(getCurrentPos(), lastPos,
+ "scrollbar changed curpos by auto repeat when cursor is outside of scrollbar button #2");
+ synthesizeMouseAtCenter(downButton, { type: "mousemove" });
+ lastPos = getCurrentPos();
+
+ setTimeout(function () {
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by mousemove after cursor is back on the scrollbar button #2");
+ synthesizeMouseAtCenter(downButton, { type: "mouseup" });
+ SimpleTest.executeSoon(doTest3);
+ }, 1000);
+ }, 1000);
+}
+
+function doTest3()
+{
+ SpecialPowers.setIntPref("ui.scrollbarButtonAutoRepeatBehavior", 1);
+
+ scrollbar.setAttribute("curpos", 0);
+ var lastPos = 0;
+
+ synthesizeMouseAtCenter(downButton, { type: "mousedown" });
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by mousedown #3");
+ synthesizeMouse(downButton, -10, -10, { type: "mousemove" });
+ lastPos = getCurrentPos();
+
+ setTimeout(function () {
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by auto repeat when cursor is outside of scrollbar button #3");
+ synthesizeMouseAtCenter(downButton, { type: "mousemove" });
+ lastPos = getCurrentPos();
+
+ setTimeout(function () {
+ ok(getCurrentPos() > lastPos,
+ "scrollbar didn't change curpos by mousemove after cursor is back on the scrollbar button #3");
+ synthesizeMouseAtCenter(downButton, { type: "mouseup" });
+
+ SpecialPowers.clearUserPref("ui.scrollbarButtonAutoRepeatBehavior");
+ SimpleTest.finish();
+ }, 1000);
+ }, 1000);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+]]>
+</script>
+
+<body id="html_body" xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=159346">Mozilla Bug 159346</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+<script>
+addLoadEvent(init);
+</script>
+</body>
+
+
+</window>
diff --git a/layout/xul/test/test_bug381167.xhtml b/layout/xul/test/test_bug381167.xhtml
new file mode 100644
index 0000000000..750dabae33
--- /dev/null
+++ b/layout/xul/test/test_bug381167.xhtml
@@ -0,0 +1,52 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=381167
+-->
+<head>
+ <title>Test for Bug 381167</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=381167">Mozilla Bug 381167</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<xul:tree>
+ <xul:tree>
+ <xul:treechildren/>
+ <xul:treecol/>
+ </xul:tree>
+</xul:tree>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 381167 **/
+
+SimpleTest.waitForExplicitFinish();
+
+function closeit() {
+ var evt = new KeyboardEvent("keypress", {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ ctrlKey: true,
+ keyCode: 'W'.charCodeAt(0),
+ charCode: 0,
+ });
+ window.dispatchEvent(evt);
+
+ setTimeout(finish, 200);
+}
+window.addEventListener('load', closeit);
+
+function finish()
+{
+ ok(true, "This is a mochikit version of a crash test. To complete is to pass.");
+ SimpleTest.finish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/xul/test/test_bug386386.html b/layout/xul/test/test_bug386386.html
new file mode 100644
index 0000000000..d3187c9142
--- /dev/null
+++ b/layout/xul/test/test_bug386386.html
@@ -0,0 +1,34 @@
+<html>
+<head><title>Testcase for bug 386386</title>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=386386
+-->
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+
+<iframe id="test386386" src="file_bug386386.sjs"></iframe>
+
+<script class="testbody" type="application/javascript">
+
+function boom()
+{
+ var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var doc = document.getElementById("test386386").contentDocument;
+ var observes = doc.createElementNS(XUL_NS, 'observes');
+ doc.removeChild(doc.documentElement);
+ doc.appendChild(observes);
+ is(0, 0, "Test is successful if we get here without crashing");
+ SimpleTest.finish();
+}
+
+function do_test() {
+ setTimeout(boom, 200);
+}
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+addLoadEvent(do_test);
+</script>
+
+</body>
+</html>
diff --git a/layout/xul/test/test_bug394800.xhtml b/layout/xul/test/test_bug394800.xhtml
new file mode 100644
index 0000000000..26fc50f771
--- /dev/null
+++ b/layout/xul/test/test_bug394800.xhtml
@@ -0,0 +1,39 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=394800
+-->
+ <title>Test Mozilla bug 394800</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<script class="testbody" type="application/javascript">
+
+function do_test()
+{
+ var x = document.getElementById("x");
+ x.parentNode.removeChild(x);
+ is(0, 0, "this is a crash/assertion test, so we're ok if we survived this far");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</head>
+
+<body>
+
+<xul:menulist><xul:tooltip/><div><span><xul:hbox id="x"/></span></div></xul:menulist>
+
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=394800">Mozilla Bug 394800</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+
+<script>
+ addLoadEvent(do_test);
+</script>
+
+</body>
+</html>
diff --git a/layout/xul/test/test_bug398982-1.xhtml b/layout/xul/test/test_bug398982-1.xhtml
new file mode 100644
index 0000000000..da6598b70d
--- /dev/null
+++ b/layout/xul/test/test_bug398982-1.xhtml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<menuitem xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="position: absolute; display: block;">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=398982
+-->
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<tooltip type="zzz">
+<treecols/>
+</tooltip>
+
+<script xmlns="http://www.w3.org/1999/xhtml" class="testbody" type="application/javascript">
+<![CDATA[
+function doe() {
+ document.getElementsByTagName('menuitem')[0].removeAttribute('style');
+ is(0, 0, "Test is successful if we get here without crashing");
+ SimpleTest.finish();
+}
+function do_test() {
+ setTimeout(doe, 200);
+}
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(do_test);
+]]>
+</script>
+<html:body></html:body> <!-- XXX SimpleTest.showReport() requires a html:body -->
+</menuitem>
diff --git a/layout/xul/test/test_bug398982-2.xhtml b/layout/xul/test/test_bug398982-2.xhtml
new file mode 100644
index 0000000000..865e688ea3
--- /dev/null
+++ b/layout/xul/test/test_bug398982-2.xhtml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Test for Bug 398982">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=398982
+-->
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<popupgroup style="position: absolute; display: block;">
+<tooltip type="zzz">
+<treecols/>
+</tooltip>
+
+<script xmlns="http://www.w3.org/1999/xhtml" class="testbody" type="application/javascript">
+<![CDATA[
+function doe() {
+ document.getElementsByTagName('popupgroup')[0].removeAttribute('style');
+ is(0, 0, "Test is successful if we get here without crashing");
+ SimpleTest.finish();
+}
+function do_test() {
+ setTimeout(doe, 200);
+}
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(do_test);
+]]>
+</script>
+</popupgroup>
+<html:body></html:body> <!-- XXX SimpleTest.showReport() requires a html:body -->
+</window>
diff --git a/layout/xul/test/test_bug467442.xhtml b/layout/xul/test/test_bug467442.xhtml
new file mode 100644
index 0000000000..f0f84c3f86
--- /dev/null
+++ b/layout/xul/test/test_bug467442.xhtml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=467442
+-->
+<window title="Mozilla Bug 467442"
+ onload="onload()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- test code goes here -->
+ <popupset>
+ <panel id="panel">
+ Hello.
+ </panel>
+ </popupset>
+ <hbox>
+ <button id="anchor" label="Anchor hello on here" style="transform: translate(100px, 0)"/>
+ </hbox>
+ <script type="application/javascript">
+ <![CDATA[
+
+ SimpleTest.waitForExplicitFinish();
+
+ function onload() {
+ /** Test for Bug 467442 **/
+ let panel = document.getElementById("panel");
+ let anchor = document.getElementById("anchor");
+
+ panel.addEventListener("popupshown", function onpopupshown() {
+ let panelRect = panel.getBoundingClientRect();
+ let marginLeft = parseFloat(getComputedStyle(panel).marginLeft);
+ let anchorRect = anchor.getBoundingClientRect();
+ is(panelRect.left - marginLeft, anchorRect.left, "Panel should be anchored to the button");
+ panel.addEventListener("popuphidden", function onpopuphidden() {
+ SimpleTest.finish();
+ }, { once: true });
+ panel.hidePopup();
+ }, { once: true });
+
+ panel.openPopup(anchor, "after_start", 0, 0, false, false);
+ }
+
+ ]]>
+ </script>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=467442"
+ target="_blank">Mozilla Bug 467442</a>
+ </body>
+</window>
diff --git a/layout/xul/test/test_bug477754.xhtml b/layout/xul/test/test_bug477754.xhtml
new file mode 100644
index 0000000000..338f95c62e
--- /dev/null
+++ b/layout/xul/test/test_bug477754.xhtml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=477754
+-->
+<window title="Mozilla Bug 477754"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=477754"
+ target="_blank">Mozilla Bug 477754</a>
+ </body>
+
+ <hbox pack="center">
+ <label id="anchor" style="direction: rtl;" value="Anchor"/>
+ </hbox>
+ <panel id="testPopup" onpopupshown="doTest();">
+ <label value="I am a popup"/>
+ </panel>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ /** Test for Bug 477754 **/
+ SimpleTest.waitForExplicitFinish();
+
+ let testPopup, testAnchor;
+
+ addEventListener("load", function () {
+ removeEventListener("load", arguments.callee, false);
+
+ testPopup = document.getElementById("testPopup");
+ testAnchor = document.getElementById("anchor");
+
+ testPopup.openPopup(testAnchor, "after_start", 10, 0, false, false);
+ }, false);
+
+ function doTest() {
+ let anchorRect = testAnchor.getBoundingClientRect();
+ let popupRect = testPopup.getBoundingClientRect();
+ let marginRight = parseFloat(getComputedStyle(testPopup).marginRight)
+ is(Math.round(anchorRect.right - popupRect.right - marginRight), 10,
+ "RTL popup's right offset should be equal to the x offset passed to openPopup");
+ testPopup.hidePopup();
+ SimpleTest.finish();
+ }
+
+ ]]></script>
+</window>
diff --git a/layout/xul/test/test_bug511075.html b/layout/xul/test/test_bug511075.html
new file mode 100644
index 0000000000..34e784ba56
--- /dev/null
+++ b/layout/xul/test/test_bug511075.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=511075
+-->
+<head>
+ <title>Test for Bug 511075</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #scroller {
+ border: 1px solid black;
+ }
+ </style>
+</head>
+<body onload="runTests()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=511075">Mozilla Bug 511075</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 511075 **/
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+var tests = [
+ function() {
+ ok(true, "Setting location.hash should scroll.");
+ nextTest();
+ // Click the top scroll arrow.
+ var x = scroller.getBoundingClientRect().width - 5;
+ var y = 5;
+ // On MacOSX the top scroll arrow can be below the slider just above
+ // the bottom scroll arrow.
+ if (navigator.platform.includes("Mac"))
+ y = scroller.getBoundingClientRect().height - 40;
+ synthesizeMouse(scroller, x, y, { type : "mousedown" }, window);
+ synthesizeMouse(scroller, x, y, { type: "mouseup" }, window);
+ },
+ function() {
+ ok(true, "Clicking the top scroll arrow should scroll.");
+ nextTest();
+ // Click the bottom scroll arrow.
+ var x = scroller.getBoundingClientRect().width - 5;
+ var y = scroller.getBoundingClientRect().height - 25;
+ synthesizeMouse(scroller, x, y, { type : "mousedown" }, window);
+ synthesizeMouse(scroller, x, y, { type: "mouseup" }, window);
+ },
+ function() {
+ ok(true, "Clicking the bottom scroll arrow should scroll.");
+ nextTest();
+ // Click the scrollbar.
+ var x = scroller.getBoundingClientRect().width - 5;
+ synthesizeMouse(scroller, x, 40, { type : "mousedown" }, window);
+ synthesizeMouse(scroller, x, 40, { type: "mouseup" }, window);
+ },
+ function() {
+ ok(true, "Clicking the scrollbar should scroll");
+ nextTest();
+ // Click the scrollbar.
+ var x = scroller.getBoundingClientRect().width - 5;
+ var y = scroller.getBoundingClientRect().height - 50;
+ synthesizeMouse(scroller, x, y, { type : "mousedown" }, window);
+ synthesizeMouse(scroller, x, y, { type: "mouseup" }, window);
+ },
+ function() {
+ scroller.onscroll = null;
+ ok(true, "Clicking the scrollbar should scroll");
+ finish();
+ }
+];
+
+document.onmousedown = function () { return false; };
+document.onmouseup = function () { return true; };
+
+
+var scroller;
+var timer = 0;
+
+function failure() {
+ ok(false, scroller.onscroll + " did not run!");
+ scroller.onscroll = null;
+ finish();
+}
+
+function nextTest() {
+ clearTimeout(timer);
+ scroller.onscroll = tests.shift();
+ timer = setTimeout(failure, 2000);
+}
+
+function runTests() {
+ scroller = document.getElementById("scroller");
+ nextTest();
+ window.location.hash = "initialPosition";
+}
+
+function finish() {
+ document.onmousedown = null;
+ document.onmouseup = null;
+ clearTimeout(timer);
+ window.location.hash = "topPosition";
+ SimpleTest.finish();
+}
+
+
+</script>
+</pre>
+<div id="scroller" style="overflow: scroll; width: 100px; height: 150px;">
+<a id="topPosition" name="topPosition">top</a>
+<div style="width: 20000px; height: 20000px;"></div>
+<a id="initialPosition" name="initialPosition">initialPosition</a>
+<div style="width: 20000px; height: 20000px;"></div>
+</div>
+</body>
+</html>
diff --git a/layout/xul/test/test_bug563416.html b/layout/xul/test/test_bug563416.html
new file mode 100644
index 0000000000..22abb5bdc3
--- /dev/null
+++ b/layout/xul/test/test_bug563416.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=563416
+-->
+<head>
+ <title>Test for Bug 563416</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=563416">Mozilla Bug 563416</a>
+<p id="display"><iframe id="test" srcdoc='<textarea style="box-sizing:content-box; overflow: hidden; -moz-appearance:none; height: 0px; padding: 0px;" cols="20" rows="10">hsldkjvmshlkkajskdlfksdjflskdjflskdjflskdjflskdjfddddddddd</textarea>'></iframe></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 563416 **/
+
+var result = -1;
+var expected = -2;
+var i = 0;
+
+function runTest() {
+ i = 0;
+ var frame = document.getElementById('test');
+ frame.onload = function() {
+ var t = frame.contentDocument.documentElement.getElementsByTagName("textarea")[0];
+ expected = t.clientWidth + 10;
+ t.style.width = expected + 'px';
+ result = t.clientWidth;
+ if (i == 0) {
+ i++;
+ setTimeout(function(){frame.contentWindow.location.reload();},0);
+ }
+ else {
+ is(result, expected, "setting style.width changes clientWidth");
+ SimpleTest.finish();
+ }
+ }
+ frame.contentWindow.location.reload();
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(runTest);
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/layout/xul/test/test_bug703150.xhtml b/layout/xul/test/test_bug703150.xhtml
new file mode 100644
index 0000000000..c5c0fc0dce
--- /dev/null
+++ b/layout/xul/test/test_bug703150.xhtml
@@ -0,0 +1,74 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Test for Bug 703150">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=703150
+-->
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+<scrollbar id="scrollbar" curpos="0" maxpos="500"/>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+function doTest()
+{
+ var scrollbar = document.getElementById("scrollbar");
+ var scrollbarThumb = null;
+ for (let child of SpecialPowers.InspectorUtils.getChildrenForNode(scrollbar, true)) {
+ if (child.nodeName === "slider") {
+ scrollbarThumb = SpecialPowers.unwrap(child.childNodes[0]);
+ }
+ }
+
+ ok(scrollbarThumb, "Should find thumb");
+ is(scrollbarThumb.nodeName, "thumb", "Should find thumb");
+
+ function mousedownHandler(aEvent)
+ {
+ aEvent.stopPropagation();
+ }
+ window.addEventListener("mousedown", mousedownHandler, true);
+
+ // Wait for finishing reflow...
+ SimpleTest.executeSoon(function () {
+ synthesizeMouseAtCenter(scrollbarThumb, { type: "mousedown" });
+
+ is(scrollbar.getAttribute("curpos"), "0",
+ "scrollbar thumb has been moved already");
+
+ synthesizeMouseAtCenter(scrollbar, { type: "mousemove" });
+
+ ok(scrollbar.getAttribute("curpos") > 0,
+ "scrollbar thumb hasn't been dragged");
+
+ synthesizeMouseAtCenter(scrollbarThumb, { type: "mouseup" });
+
+ window.removeEventListener("mousedown", mousedownHandler, true);
+
+ SimpleTest.finish();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+
+]]>
+</script>
+
+<body id="html_body" xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=703150">Mozilla Bug 703150</a>
+<p id="display"></p>
+
+<pre id="test">
+</pre>
+<script>
+addLoadEvent(doTest);
+</script>
+</body>
+
+
+</window>
diff --git a/layout/xul/test/test_bug987230.xhtml b/layout/xul/test/test_bug987230.xhtml
new file mode 100644
index 0000000000..3161ad9d0e
--- /dev/null
+++ b/layout/xul/test/test_bug987230.xhtml
@@ -0,0 +1,109 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=987230
+-->
+<window title="Mozilla Bug 987230"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="SimpleTest.waitForFocus(startTest, window)">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=987230"
+ target="_blank">Mozilla Bug 987230</a>
+ </body>
+
+ <vbox>
+ <toolbar>
+ <toolbarbutton id="toolbarbutton-anchor"
+ label="Anchor"
+ consumeanchor="toolbarbutton-anchor"
+ onclick="onAnchorClick(event)"
+ style="padding: 50px !important; list-style-image: url(chrome://branding/content/icon32.png)"/>
+ </toolbar>
+ <spacer flex="1"/>
+ <hbox id="hbox-anchor"
+ style="padding: 20px"
+ onclick="onAnchorClick(event)">
+ <hbox id="inner-anchor"
+ consumeanchor="hbox-anchor"
+ >
+ Another anchor
+ </hbox>
+ </hbox>
+ <spacer flex="1"/>
+ </vbox>
+
+ <panel id="mypopup"
+ type="arrow"
+ onpopupshown="onMyPopupShown(event)"
+ onpopuphidden="onMyPopupHidden(event)">This is a test popup</panel>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ /** Test for Bug 987230 **/
+ SimpleTest.waitForExplicitFinish();
+
+ SimpleTest.requestCompleteLog();
+
+ function onMyPopupHidden(e) {
+ ok(true, "Popup hidden");
+ if (outerAnchor.id == "toolbarbutton-anchor") {
+ popupHasShown = false;
+ outerAnchor = document.getElementById("hbox-anchor");
+ anchor = document.getElementById("inner-anchor");
+ nextTest();
+ } else {
+ //XXXgijs set mouse position back outside the iframe:
+ let frameRect = window.frameElement.getBoundingClientRect();
+ let scale = window.devicePixelRatio;
+ let outsideOfFrameX = (window.mozInnerScreenX + frameRect.width + 100) * scale;
+ let outsideOfFrameY = Math.max(0, window.mozInnerScreenY - 100) * scale;
+
+ info("Mousemove: " + outsideOfFrameX + ", " + outsideOfFrameY +
+ " (from innerscreen " + window.mozInnerScreenX + ", " + window.mozInnerScreenY +
+ " and rect width " + frameRect.width + " and scale " + scale + ")");
+ synthesizeNativeMouseEvent({
+ type: "mousemove",
+ screenX: outsideOfFrameX,
+ screenY: outsideOfFrameY,
+ scale: "inScreenPixels",
+ elementOnWidget: null,
+ });
+ SimpleTest.finish();
+ }
+ }
+
+ let popupHasShown = false;
+ function onMyPopupShown(e) {
+ popupHasShown = true;
+ synthesizeNativeMouseEvent({ type: "click", target: outerAnchor, offsetX: 5, offsetY: 5 });
+ }
+
+ function onAnchorClick(e) {
+ info("click: " + e.target.id);
+ ok(!popupHasShown, "Popup should only be shown once");
+ popup.openPopup(anchor, "bottomcenter topright");
+ }
+
+ let popup, outerAnchor, anchor;
+
+ function startTest() {
+ popup = document.getElementById("mypopup");
+ outerAnchor = document.getElementById("toolbarbutton-anchor");
+ anchor = outerAnchor.icon;
+ nextTest();
+ }
+
+ function nextTest(e) {
+ synthesizeMouse(outerAnchor, 5, 5, {});
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/layout/xul/test/test_drag_thumb_in_link.html b/layout/xul/test/test_drag_thumb_in_link.html
new file mode 100644
index 0000000000..7c39fd0f28
--- /dev/null
+++ b/layout/xul/test/test_drag_thumb_in_link.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=367028
+-->
+<head>
+<title>Test for Bug 367028</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<style>
+#scroller {
+ display: block;
+ width: 200px;
+ height: 100px;
+ overflow: scroll;
+ background: beige;
+ border: 1px solid black;
+}
+
+#biggerblock {
+ display: block;
+ width: 100px;
+ height: 150px;
+ line-height: 150px;
+ white-space: nowrap;
+ overflow: hidden;
+ background: khaki;
+}
+</style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=367028">Mozilla Bug 367028</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<a id="scroller" href="#">
+ block anchor<span id="biggerblock">bigger block</span>
+</a>
+<script type="application/javascript">
+
+function waitForEvent(aTarget, aEvent) {
+ return new Promise(aResolve => {
+ aTarget.addEventListener(aEvent, aResolve, { once: true });
+ });
+}
+
+/** Test for Bug 367028 **/
+
+add_task(async function drag_thumb_in_link() {
+ let scroller = document.getElementById("scroller");
+ scroller.ondragstart = function(e) {
+ e.preventDefault();
+ ok(false, "dragging on scroller bar should not trigger drag-and-drop operation");
+ scroller.ondragstart = null;
+ };
+
+ // Click the scroll bar.
+ let x = scroller.getBoundingClientRect().width - 5;
+ let y = scroller.getBoundingClientRect().height - 70;
+ synthesizeMouse(scroller, x, y, { type : "mousedown" }, window);
+ synthesizeMouse(scroller, x, y, { type : "mousemove" }, window);
+
+ let scrollPromise = waitForEvent(scroller, "scroll");
+ x = scroller.getBoundingClientRect().width + 20;
+ y = scroller.getBoundingClientRect().height - 30;
+ synthesizeMouse(scroller, x, y, { type : "mousemove" }, window);
+ synthesizeMouse(scroller, x, y, { type : "mouseup" }, window);
+ await scrollPromise;
+
+ ok(true, "Dragging scroller bar should scroll");
+ scroller.ondragstart = null;
+});
+
+</script>
+</body>
+</html>
diff --git a/layout/xul/test/test_menuitem_ctrl_click.xhtml b/layout/xul/test/test_menuitem_ctrl_click.xhtml
new file mode 100644
index 0000000000..af0b82a6ed
--- /dev/null
+++ b/layout/xul/test/test_menuitem_ctrl_click.xhtml
@@ -0,0 +1,80 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1630828
+-->
+<window title="Mozilla Bug 1630828"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="">
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<!-- test results are displayed in the html:body -->
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1630828"
+ target="_blank">Mozilla Bug 1630828</a>
+</body>
+
+<hbox align="center" pack="center">
+ <menulist id="menu">
+ <menupopup id="popup">
+ <menuitem label="Target" id="target" />
+ </menupopup>
+ </menulist>
+</hbox>
+<!-- test code goes here -->
+<script type="application/javascript">
+<![CDATA[
+
+const { AppConstants } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+function waitForEvent(target, event) {
+ info(`Waiting for ${event} event.`);
+ return new Promise(resolve => {
+ target.addEventListener(event, resolve, { once: true });
+ });
+}
+
+function waitForIdle() {
+ return new Promise(resolve => {
+ SpecialPowers.Services.tm.idleDispatchToMainThread(resolve);
+ });
+}
+
+add_setup(async function() {
+ await SimpleTest.promiseFocus();
+});
+
+add_task(async function test_ctrl_click() {
+ const isMac = AppConstants.platform === "macosx";
+
+ let popup = document.getElementById("popup");
+ let promise = waitForEvent(popup, "popupshown");
+ let menu = document.getElementById("menu");
+ synthesizeMouseAtCenter(menu, {});
+ // Wait for popup open.
+ await promise;
+
+ let commandReceived = false;
+ menu.addEventListener("command", function(e) {
+ ok(!isMac, `${AppConstants.platform} receives command event`);
+ commandReceived = true;
+ });
+
+ // Ctrl click in Mac won't dispatch command event and close popup, so we wait
+ // for idle instead.
+ promise = isMac ? waitForIdle() : waitForEvent(popup, "popuphidden");
+ let target = document.getElementById("target");
+ synthesizeMouseAtCenter(target, { ctrlKey: true });
+ await promise;
+
+ is(commandReceived, !isMac, `Check command event for ${AppConstants.platform}`);
+ is(popup.state, isMac ? "open" : "closed", `Check popup state for ${AppConstants.platform}`);
+});
+
+]]>
+</script>
+</window>
diff --git a/layout/xul/test/test_popupReflowPos.xhtml b/layout/xul/test/test_popupReflowPos.xhtml
new file mode 100644
index 0000000000..a26a833d13
--- /dev/null
+++ b/layout/xul/test/test_popupReflowPos.xhtml
@@ -0,0 +1,77 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<window title="XUL Panel reflow placement test"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <script><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ function openPopup()
+ {
+ synthesizeMouseAtCenter(document.getElementById("thebutton"), {}, window);
+ }
+
+ function popupShown(event)
+ {
+ document.getElementById("parent").className = "";
+ var popup = document.getElementById("thepopup");
+
+ var buttonbcr = document.getElementById("thebutton").getBoundingClientRect();
+ var popupbcr = popup.getOuterScreenRect();
+ var popupMarginLeft = parseFloat(getComputedStyle(popup).marginLeft);
+ var popupMarginTop = parseFloat(getComputedStyle(popup).marginTop);
+
+ ok(Math.abs(popupbcr.x - popupMarginLeft - window.mozInnerScreenX - buttonbcr.x) < 3, "x pos is correct");
+ ok(Math.abs(popupbcr.y - popupMarginTop - window.mozInnerScreenY - buttonbcr.bottom) < 3, "y pos is correct");
+
+ event.target.hidePopup();
+ }
+
+ SimpleTest.waitForFocus(openPopup);
+ ]]></script>
+
+ <html:style>
+ .mbox {
+ display: inline-block;
+ width: 33%;
+ height: 50px;
+ background: green;
+ vertical-align: middle;
+ }
+ .orange {
+ background: orange;
+ }
+ .change > .mbox {
+ width: 60px;
+ }
+ </html:style>
+
+ <html:div style="width: 300px; height: 200px;">
+ <html:div id="parent" class="change" style="background: red; border: 1px solid black; width: 300px; height: 200px;">
+ <html:div class="mbox"></html:div>
+ <html:div class="mbox"></html:div>
+ <html:div class="mbox"></html:div>
+ <html:div class="mbox orange">
+
+ <button label="Show" type="menu" id="thebutton">
+ <menupopup id="thepopup" onpopupshown="popupShown(event)" onpopuphidden="SimpleTest.finish()">
+ <menuitem label="New"/>
+ <menuitem label="Open"/>
+ <menuitem label="Save"/>
+ <menuseparator/>
+ <menuitem label="Exit"/>
+ </menupopup>
+ </button>
+
+ </html:div>
+ </html:div>
+ </html:div>
+
+</window>
diff --git a/layout/xul/test/test_popupSizeTo.xhtml b/layout/xul/test/test_popupSizeTo.xhtml
new file mode 100644
index 0000000000..6e60f28e0a
--- /dev/null
+++ b/layout/xul/test/test_popupSizeTo.xhtml
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+XUL Panel sizeTo tests
+-->
+<window title="XUL Panel sizeTo tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ function openPopup()
+ {
+ document.getElementById("panel").
+ openPopupAtScreen(Math.round(window.mozInnerScreenX) + window.innerWidth - 130,
+ Math.round(window.mozInnerScreenY) + window.innerHeight - 130);
+ }
+
+ function sizeAndCheck(width, height) {
+ var panel = document.getElementById("panel");
+ panel.sizeTo(width, height);
+ is(panel.getBoundingClientRect().width, width, "width is correct");
+ is(panel.getBoundingClientRect().height, height, "height is correct");
+
+ }
+ function popupShown(event)
+ {
+ var panel = document.getElementById("panel");
+ var bcr = panel.getBoundingClientRect();
+ // resize to 10px bigger in both dimensions.
+ sizeAndCheck(bcr.width+10, bcr.height+10);
+ // Same width, different height (based on *new* size from last sizeAndCheck)
+ sizeAndCheck(bcr.width+10, bcr.height);
+ // Same height, different width (also based on *new* size from last sizeAndCheck)
+ sizeAndCheck(bcr.width, bcr.height);
+ event.target.hidePopup();
+ }
+
+ SimpleTest.waitForFocus(openPopup);
+ ]]></script>
+
+<panel id="panel" onpopupshown="popupShown(event)" onpopuphidden="SimpleTest.finish()">
+ <resizer id="resizer" dir="bottomend" width="16" height="16"/>
+ <hbox width="50" height="50" flex="1"/>
+</panel>
+
+</window>
diff --git a/layout/xul/test/test_popupZoom.xhtml b/layout/xul/test/test_popupZoom.xhtml
new file mode 100644
index 0000000000..641ed0756c
--- /dev/null
+++ b/layout/xul/test/test_popupZoom.xhtml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<window title="XUL Panel zoom test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <script><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ var savedzoom;
+
+ function openPopup()
+ {
+ docviewer = window.docShell.contentViewer;
+ savedzoom = SpecialPowers.getFullZoom(window);
+ SpecialPowers.setFullZoom(window, 2);
+
+ document.getElementById("panel").
+ openPopup(document.getElementById("anchor"), "after_start", 0, 0, false, false, null);
+ }
+
+ function popupShown(event)
+ {
+ var panel = document.getElementById("panel");
+ var panelMarginLeft = parseFloat(getComputedStyle(panel).marginLeft);
+ var panelMarginTop = parseFloat(getComputedStyle(panel).marginTop);
+ var panelbcr = panel.getBoundingClientRect();
+ var anchorbcr = document.getElementById("anchor").getBoundingClientRect();
+
+ ok(Math.abs(panelbcr.x - panelMarginLeft - anchorbcr.x) < 3, "x pos is correct");
+ ok(Math.abs(panelbcr.y - panelMarginTop - anchorbcr.bottom) < 3, "y pos is correct");
+
+ SpecialPowers.setFullZoom(window, savedzoom);
+
+ event.target.hidePopup();
+ }
+
+ SimpleTest.waitForFocus(openPopup);
+ ]]></script>
+
+<description id="anchor" value="Sometext to this some texts"/>
+<panel id="panel" onpopupshown="popupShown(event)" onpopuphidden="SimpleTest.finish()">
+ <resizer id="resizer" dir="bottomend" width="16" height="16"/>
+ <hbox width="50" height="50" flex="1"/>
+</panel>
+
+
+</window>
diff --git a/layout/xul/test/test_resizer_ctrl_click.xhtml b/layout/xul/test/test_resizer_ctrl_click.xhtml
new file mode 100644
index 0000000000..225d3c6518
--- /dev/null
+++ b/layout/xul/test/test_resizer_ctrl_click.xhtml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+ XUL Widget Test for the resizer element
+ -->
+<window title="Titlebar" width="200" height="200"
+ onload="setTimeout(test_resizer_ctrl_click, 0);"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<resizer id="resizer" dir="bottomend" width="16" height="16"/>
+
+<!-- test code goes here -->
+<script type="application/javascript"><![CDATA[
+
+const { AppConstants } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+SimpleTest.waitForExplicitFinish();
+
+function test_resizer_ctrl_click()
+{
+ let resizer = document.getElementById("resizer");
+ let isCommandFired = false;
+
+ resizer.addEventListener("click", function(aEvent) {
+ // Delay check for command event, because it is fired after click event.
+ setTimeout(() => {
+ ok(isCommandFired, "Check if command event is fired");
+ SimpleTest.finish();
+ }, 0);
+ });
+ resizer.addEventListener("command", function(aEvent) {
+ isCommandFired = true;
+ ok(aEvent.ctrlKey, "Check ctrlKey for command event");
+ ok(!aEvent.shiftKey, "Check shiftKey for command event");
+ ok(!aEvent.altKey, "Check altKey for command event");
+ ok(!aEvent.metaKey, "Check metaKey for command event");
+ is(aEvent.inputSource, MouseEvent.MOZ_SOURCE_MOUSE,
+ "Check inputSource for command event");
+ });
+ synthesizeMouseAtCenter(resizer, { ctrlKey: true });
+}
+
+]]>
+</script>
+
+</window>
diff --git a/layout/xul/test/test_resizer_incontent.xhtml b/layout/xul/test/test_resizer_incontent.xhtml
new file mode 100644
index 0000000000..2d29dd3f8d
--- /dev/null
+++ b/layout/xul/test/test_resizer_incontent.xhtml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+This test ensures that a resizer in content doesn't resize the window.
+-->
+<window title="XUL resizer in content test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" />
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ function testResizer()
+ {
+ var oldScreenX = window.screenX;
+ var oldScreenY = window.screenY;
+ var oldWidth = window.outerWidth;
+ var oldHeight = window.outerHeight;
+ var resizer = document.getElementById("resizer");
+ synthesizeMouseAtCenter(resizer, { type:"mousedown" });
+ synthesizeMouse(resizer, 32, 32, { type:"mousemove" });
+ synthesizeMouse(resizer, 32, 32, { type:"mouseup" });
+ is(window.screenX, oldScreenX, "window not moved for non-chrome window screenX");
+ is(window.screenY, oldScreenY, "window not moved for non-chrome window screenY");
+ is(window.outerWidth, oldWidth, "window not moved for non-chrome window outerWidth");
+ is(window.outerHeight, oldHeight, "window not moved for non-chrome window outerHeight");
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForFocus(testResizer);
+ ]]></script>
+
+ <resizer id="resizer" dir="bottomend" width="16" height="16"/>
+
+</window>
diff --git a/layout/xul/test/test_splitter.xhtml b/layout/xul/test/test_splitter.xhtml
new file mode 100644
index 0000000000..8ee74177e9
--- /dev/null
+++ b/layout/xul/test/test_splitter.xhtml
@@ -0,0 +1,131 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<?xml-stylesheet href="data:text/css, hbox { border: 1px solid red; } vbox { border: 1px solid green }" type="text/css"?>
+<!--
+XUL <splitter> collapsing tests
+-->
+<window title="XUL splitter collapsing tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ orient="horizontal">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <!-- test code goes here -->
+ <script type="application/javascript"><![CDATA[
+ SimpleTest.waitForExplicitFinish();
+
+ async function dragSplitter(offsetX) {
+ info(`Dragging splitter ${splitter.id} to ${offsetX}`);
+
+ const splitterRect = splitter.getBoundingClientRect();
+ const splitterWidth = splitterRect.width;
+ synthesizeMouse(splitter, splitterWidth / 2, 2, {type: "mousedown"});
+ synthesizeMouse(splitter, splitterWidth / 2, 1, {type: "mousemove"});
+ await new Promise(SimpleTest.executeSoon);
+ is(splitter.getAttribute("state"), "dragging", "The splitter should be dragged");
+ synthesizeMouse(splitter, offsetX, 1, {type: "mousemove"});
+ synthesizeMouse(splitter, offsetX, 1, {type: "mouseup"});
+ await new Promise(SimpleTest.executeSoon);
+ const newSplitterRect = splitter.getBoundingClientRect();
+ is(
+ offsetX > 0,
+ newSplitterRect.left > splitterRect.left,
+ `Should move in the right direction ${splitterRect.left} -> ${newSplitterRect.left}, ${offsetX}`
+ );
+ }
+
+ function shouldBeCollapsed(where) {
+ is(splitter.getAttribute("state"), "collapsed", "The splitter should be collapsed");
+ is(splitter.getAttribute("substate"), where, "The splitter should be collapsed " + where);
+ }
+
+ function shouldNotBeCollapsed() {
+ is(splitter.getAttribute("state"), "", "The splitter should not be collapsed");
+ }
+
+ async function runPass(isRTL, rightCollapsed, leftCollapsed) {
+ const containerWidth = splitter.parentNode.getBoundingClientRect().width;
+ await dragSplitter(containerWidth);
+ if (rightCollapsed) {
+ shouldBeCollapsed(isRTL ? "before" : "after");
+ } else {
+ shouldNotBeCollapsed();
+ }
+ await dragSplitter(-containerWidth * 2);
+ if (leftCollapsed) {
+ shouldBeCollapsed(isRTL ? "after" : "before");
+ } else {
+ shouldNotBeCollapsed();
+ }
+ await dragSplitter(containerWidth / 2);
+ // the splitter should never be collapsed in the middle
+ shouldNotBeCollapsed();
+ }
+
+ var splitter;
+ var activeBox = null;
+ function setActiveBox(element) {
+ if (activeBox) {
+ activeBox.style.display = "none";
+ }
+ if (element) {
+ element.style.display = "";
+ element.getBoundingClientRect();
+ }
+ activeBox = element;
+ }
+
+ async function runTests(rtl, splitterId) {
+ info(`Running tests for ${splitterId}`);
+ splitter = document.getElementById(splitterId);
+ setActiveBox(splitter.parentNode);
+ await runPass(rtl, false, false);
+ splitter.setAttribute("collapse", "before");
+ await runPass(rtl, rtl, !rtl);
+ splitter.setAttribute("collapse", "after");
+ await runPass(rtl, !rtl, rtl);
+ splitter.setAttribute("collapse", "both");
+ await runPass(rtl, true, true);
+ }
+
+ async function runAllTests() {
+ await runTests(false, "ltr-splitter");
+ await runTests(true, "rtl-splitter");
+ await runTests(false, "ltr-flex-splitter");
+ await runTests(true, "rtl-flex-splitter");
+ SimpleTest.finish();
+ }
+
+ addLoadEvent(function() {SimpleTest.executeSoon(runAllTests);});
+ ]]></script>
+
+ <hbox style="display: none; width: 200px; height: 300px; direction: ltr;">
+ <vbox style="height: 300px;" flex="1"/>
+ <splitter id="ltr-splitter" width="5"/>
+ <vbox style="height: 300px;" flex="1"/>
+ </hbox>
+
+ <hbox style="display: none; width: 200px; height: 300px; direction: rtl;">
+ <vbox style="height: 300px;" flex="1"/>
+ <splitter id="rtl-splitter" width="5"/>
+ <vbox style="height: 300px;" flex="1"/>
+ </hbox>
+
+ <hbox style="display: none; width: 200px; height: 300px; direction: ltr; -moz-box-layout: flex">
+ <vbox style="height: 300px;" flex="1"/>
+ <splitter id="ltr-flex-splitter" width="5"/>
+ <vbox style="height: 300px;" flex="1"/>
+ </hbox>
+
+ <hbox style="display: none; width: 200px; height: 300px; direction: rtl; -moz-box-layout: flex">
+ <vbox style="height: 300px;" flex="1"/>
+ <splitter id="rtl-flex-splitter" width="5"/>
+ <vbox style="height: 300px;" flex="1"/>
+ </hbox>
+
+</window>
diff --git a/layout/xul/test/test_splitter_sibling.xhtml b/layout/xul/test/test_splitter_sibling.xhtml
new file mode 100644
index 0000000000..9e139309e7
--- /dev/null
+++ b/layout/xul/test/test_splitter_sibling.xhtml
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<?xml-stylesheet href="data:text/css, hbox { border: 1px solid red; } vbox { border: 1px solid green }" type="text/css"?>
+<window title="XUL splitter resizebefore/after tests"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ orient="horizontal">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ </body>
+
+ <hbox style="width: 200px; height: 200px; direction: ltr; display: none">
+ <vbox style="height: 200px; width: 40px" />
+ <splitter id="ltr-splitter-before" width="5" resizebefore="sibling" resizeafter="none"/>
+ <vbox style="height: 200px;" flex="1"/>
+ </hbox>
+
+ <hbox style="width: 200px; height: 200px; direction: rtl; display: none">
+ <vbox style="height: 200px; width: 40px" />
+ <splitter id="rtl-splitter-before" width="5" resizebefore="sibling" resizeafter="none"/>
+ <vbox style="height: 200px;" flex="1"/>
+ </hbox>
+
+ <hbox style="width: 200px; height: 200px; direction: ltr; display: none">
+ <vbox style="height: 200px;" flex="1"/>
+ <splitter id="ltr-splitter-after" width="5" resizeafter="sibling" resizebefore="none"/>
+ <vbox style="height: 200px; width: 40px" />
+ </hbox>
+
+ <hbox style="width: 200px; height: 200px; direction: rtl; display: none">
+ <vbox style="height: 200px;" flex="1"/>
+ <splitter id="rtl-splitter-after" width="5" resizeafter="sibling" resizebefore="none"/>
+ <vbox style="height: 200px; width: 40px" />
+ </hbox>
+
+ <script><![CDATA[
+ async function dragSplitter(splitter, offsetX) {
+ info(`Dragging splitter ${splitter.id} to ${offsetX}`);
+
+ const splitterRect = splitter.getBoundingClientRect();
+ const splitterWidth = splitterRect.width;
+ synthesizeMouse(splitter, splitterWidth / 2, 2, {type: "mousedown"});
+ synthesizeMouse(splitter, splitterWidth / 2, 1, {type: "mousemove"});
+ await new Promise(SimpleTest.executeSoon);
+ is(splitter.getAttribute("state"), "dragging", "The splitter should be dragged");
+ synthesizeMouse(splitter, offsetX, 1, {type: "mousemove"});
+ synthesizeMouse(splitter, offsetX, 1, {type: "mouseup"});
+ await new Promise(SimpleTest.executeSoon);
+ const newSplitterRect = splitter.getBoundingClientRect();
+ is(
+ offsetX > 0,
+ newSplitterRect.left > splitterRect.left,
+ `Should move in the right direction ${splitterRect.left} -> ${newSplitterRect.left}, ${offsetX}`
+ );
+ }
+
+ add_task(async function() {
+ for (let splitter of document.querySelectorAll("splitter")) {
+ info(`Testing ${splitter.id}`);
+ splitter.parentNode.style.display = "";
+ const isBefore = splitter.getAttribute("resizebefore") == "sibling";
+ const isRtl = getComputedStyle(splitter).direction == "rtl";
+
+ const resizableElement = isBefore ? splitter.previousElementSibling : splitter.nextElementSibling;
+ const nonResizableElement = isBefore ? splitter.nextElementSibling : splitter.previousElementSibling;
+ const oldWidth = resizableElement.getBoundingClientRect().width;
+
+ await dragSplitter(splitter, 10);
+
+ is(nonResizableElement.style.width, "", "Shouldn't have set width");
+ isnot(resizableElement.style.width, "40px", "Should've changed width");
+
+ const newWidth = resizableElement.getBoundingClientRect().width;
+
+ info(`Went from ${oldWidth} -> ${newWidth}\n`);
+
+ if (isRtl == isBefore) {
+ ok(newWidth < oldWidth, "Should've shrunk");
+ } else {
+ ok(newWidth > oldWidth, "Should've grown");
+ }
+ splitter.parentNode.style.display = "none";
+ }
+ });
+ ]]></script>
+</window>
diff --git a/layout/xul/test/test_submenuClose.xhtml b/layout/xul/test/test_submenuClose.xhtml
new file mode 100644
index 0000000000..47337e61b9
--- /dev/null
+++ b/layout/xul/test/test_submenuClose.xhtml
@@ -0,0 +1,91 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1181560
+-->
+<window title="Mozilla Bug 1181560"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="SimpleTest.waitForFocus(nextTest, window)">
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+
+ <!-- test results are displayed in the html:body -->
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181560"
+ target="_blank">Mozilla Bug 1181560</a>
+ </body>
+
+ <vbox>
+ <menubar>
+ <menu id="menu" label="MyMenu">
+ <menupopup>
+ <menuitem label="A"/>
+ <menu id="b" label="B">
+ <menupopup>
+ <menuitem label="B1"/>
+ </menupopup>
+ </menu>
+ <menu id="c" label="C">
+ <menupopup>
+ <menuitem label="C1"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ </menubar>
+ </vbox>
+
+ <!-- test code goes here -->
+ <script type="application/javascript">
+ <![CDATA[
+ /** Test for Bug 1181560 **/
+ SimpleTest.waitForExplicitFinish();
+
+ let menuB, menuC, mainMenu, menuBOpen, menuCOpen;
+ let menuBOpenCount = 0;
+
+ function handleBOpens() {
+ menuBOpenCount++;
+ menuBOpen = true;
+ ok(!menuCOpen, "Menu C should not be open when menu B has opened");
+ if (menuBOpenCount >= 2) {
+ SimpleTest.finish();
+ return;
+ }
+ sendKey("LEFT", window);
+ sendKey("DOWN", window);
+ sendKey("RIGHT", window);
+ }
+
+ function handleBCloses() {
+ menuBOpen = false;
+ }
+
+ function handleCOpens() {
+ menuCOpen = true;
+ ok(!menuBOpen, "Menu B should not be open when menu C has opened");
+ synthesizeMouseAtCenter(menuB, {}, window);
+ }
+
+ function handleCCloses() {
+ menuCOpen = false;
+ }
+
+ function nextTest(e) {
+ mainMenu = document.getElementById("menu");
+ menuB = document.getElementById("b");
+ menuC = document.getElementById("c");
+ menuB.menupopup.addEventListener("popupshown", handleBOpens);
+ menuB.menupopup.addEventListener("popuphidden", handleBCloses);
+ menuC.menupopup.addEventListener("popupshown", handleCOpens);
+ menuC.menupopup.addEventListener("popuphidden", handleCCloses);
+ mainMenu.addEventListener("popupshown", ev => {
+ synthesizeMouseAtCenter(menuB, {}, window);
+ });
+ mainMenu.open = true;
+ }
+ ]]>
+ </script>
+</window>
diff --git a/layout/xul/test/test_toolbarbutton_ctrl_click.xhtml b/layout/xul/test/test_toolbarbutton_ctrl_click.xhtml
new file mode 100644
index 0000000000..6ad5f18ae7
--- /dev/null
+++ b/layout/xul/test/test_toolbarbutton_ctrl_click.xhtml
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+ XUL Widget Test for the toolbarbutton element
+ -->
+<window title="Titlebar" width="200" height="200"
+ onload="setTimeout(test_resizer_ctrl_click, 0);"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<toolbarbutton id="toolbarbutton" width="16" height="16"/>
+
+<!-- test code goes here -->
+<script type="application/javascript"><![CDATA[
+
+const { AppConstants } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+SimpleTest.waitForExplicitFinish();
+
+function test_resizer_ctrl_click()
+{
+ let toolbarbutton = document.getElementById("toolbarbutton");
+ let isCommandFired = false;
+
+ toolbarbutton.addEventListener("click", function(aEvent) {
+ // Delay check for command event, because it is fired after click event.
+ setTimeout(() => {
+ ok(isCommandFired, "Check if command event is fired");
+ SimpleTest.finish();
+ }, 0);
+ });
+ toolbarbutton.addEventListener("command", function(aEvent) {
+ isCommandFired = true;
+ ok(aEvent.ctrlKey, "Check ctrlKey for command event");
+ ok(!aEvent.shiftKey, "Check shiftKey for command event");
+ ok(!aEvent.altKey, "Check altKey for command event");
+ ok(!aEvent.metaKey, "Check metaKey for command event");
+ is(aEvent.inputSource, MouseEvent.MOZ_SOURCE_MOUSE,
+ "Check inputSource for command event");
+ });
+ synthesizeMouseAtCenter(toolbarbutton, { ctrlKey: true });
+}
+
+]]>
+</script>
+
+</window>
diff --git a/layout/xul/test/test_windowminmaxsize.xhtml b/layout/xul/test/test_windowminmaxsize.xhtml
new file mode 100644
index 0000000000..d502fd1248
--- /dev/null
+++ b/layout/xul/test/test_windowminmaxsize.xhtml
@@ -0,0 +1,191 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Window Minimum and Maximum Size Tests" onload="nextTest()"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+
+<html:style>
+<![CDATA[
+ panel::part(content) {
+ border: 0;
+ padding: 0;
+ margin: 0;
+ }
+]]>
+</html:style>
+
+<panel id="panel" onpopupshown="doPanelTest(this)" onpopuphidden="nextPopupTest(this)"
+ orient="vertical"
+ align="start" pack="start" style="appearance: none; margin: 0; border: 0; padding: 0;">
+ <hbox id="popupresizer" dir="bottomright" flex="1" width="60" height="60"
+ style="appearance: none; margin: 0; border: 0; padding: 0;"/>
+</panel>
+
+<script>
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+var gTestId = -1;
+
+// width and height in the tests below specify the expected size of the window.
+// note, win8 has a minimum inner window size of around 122 pixels. Don't go below this on min-width tests.
+var tests = [
+ { testname: "unconstrained",
+ src: "windowminmaxsize1.xhtml",
+ width: 150, height: 150 },
+ { testname: "constraint min style",
+ src: "windowminmaxsize2.xhtml",
+ width: 180, height: 210 },
+ { testname: "constraint max style",
+ src: "windowminmaxsize3.xhtml",
+ width: 125, height: 140 },
+ { testname: "constraint min attributes",
+ src: "windowminmaxsize4.xhtml",
+ width: 240, height: 220 },
+ { testname: "constraint min attributes with width and height set",
+ src: "windowminmaxsize5.xhtml",
+ width: 215, height: 235 },
+ { testname: "constraint max attributes",
+ src: "windowminmaxsize6.xhtml",
+ width: 125, height: 95 },
+ // this gets the inner width as <window minwidth='210'> makes the box 210 pixels wide
+ { testname: "constraint min width attribute only",
+ src: "windowminmaxsize7.xhtml",
+ width: 210, height: 150 },
+ { testname: "constraint max width attribute only",
+ src: "windowminmaxsize8.xhtml",
+ width: 128, height: 150 },
+ { testname: "constraint max width attribute with minheight",
+ src: "windowminmaxsize9.xhtml",
+ width: 195, height: 180 },
+ { testname: "constraint minwidth, minheight, maxwidth and maxheight set",
+ src: "windowminmaxsize10.xhtml",
+ width: 150, height: 150 }
+];
+
+var popupTests = [
+ { testname: "popup unconstrained",
+ width: 60, height: 60
+ },
+ { testname: "popup with minimum size",
+ minwidth: 150, minheight: 180,
+ width: 150, height: 180
+ },
+ { testname: "popup with maximum size",
+ maxwidth: 50, maxheight: 45,
+ width: 50, height: 45,
+ }
+];
+
+function nextTest()
+{
+ // Run through each of the tests above by opening a simple window with
+ // the attributes or style defined for that test. The comparisons will be
+ // done by windowOpened. gTestId holds the index into the tests array.
+ if (++gTestId >= tests.length) {
+ // Now do the popup tests
+ gTestId = -1;
+ SimpleTest.waitForFocus(function () { nextPopupTest(document.getElementById("panel")) } );
+ }
+ else {
+ tests[gTestId].window = window.browsingContext.topChromeWindow.open(tests[gTestId].src, "_blank", "chrome,resizable=yes");
+ SimpleTest.waitForFocus(windowOpened, tests[gTestId].window);
+ }
+}
+
+function windowOpened(otherWindow)
+{
+ // Check the width and the width plus one due to bug 696746.
+ ok(otherWindow.innerWidth == tests[gTestId].width ||
+ otherWindow.innerWidth == tests[gTestId].width + 1,
+ tests[gTestId].testname + " width of " + otherWindow.innerWidth + " matches " + tests[gTestId].width);
+ is(otherWindow.innerHeight, tests[gTestId].height, tests[gTestId].testname + " height");
+
+ otherWindow.close();
+ nextTest();
+}
+
+function doPanelTest(panel)
+{
+ var rect = panel.getBoundingClientRect();
+ is(rect.width, popupTests[gTestId].width, popupTests[gTestId].testname + " width");
+ is(rect.height, popupTests[gTestId].height, popupTests[gTestId].testname + " height");
+
+ panel.hidePopup();
+}
+
+function nextPopupTest(panel)
+{
+ if (++gTestId >= popupTests.length) {
+ // Next, check a panel that has a titlebar to ensure that it is accounted for
+ // properly in the size.
+ var titledPanelWindow = window.browsingContext.topChromeWindow.open("titledpanelwindow.xhtml", "_blank", "chrome,resizable=yes");
+ SimpleTest.waitForFocus(titledPanelWindowOpened, titledPanelWindow);
+ }
+ else {
+ function setattr(attr) {
+ if (attr in popupTests[gTestId])
+ panel.setAttribute(attr, popupTests[gTestId][attr]);
+ else
+ panel.removeAttribute(attr);
+ }
+ setattr("minwidth");
+ setattr("minheight");
+ setattr("maxwidth");
+ setattr("maxheight");
+
+ // Prevent event loop starvation as a result of popup events being
+ // synchronous. See bug 1131576.
+ SimpleTest.executeSoon(() => {
+ // Non-chrome shells require focus to open a popup.
+ SimpleTest.waitForFocus(() => { panel.openPopup() });
+ });
+ }
+}
+
+function titledPanelWindowOpened(panelwindow)
+{
+ info("titledPanelWindowOpened");
+ var panel = panelwindow.document.documentElement.firstChild;
+ panel.addEventListener("popupshown", () => doTitledPanelTest(panel));
+ panel.addEventListener("popuphidden", () => done(panelwindow));
+ // See above as for why.
+ SimpleTest.executeSoon(() => {
+ SimpleTest.waitForFocus(() => { panel.openPopup() }, panelwindow);
+ });
+}
+
+function doTitledPanelTest(panel)
+{
+ info("doTitledPanelTest");
+ var rect = panel.getBoundingClientRect();
+ is(rect.width, 120, "panel with titlebar width");
+ is(rect.height, 140, "panel with titlebar height");
+ panel.hidePopup();
+}
+
+function done(panelwindow)
+{
+ panelwindow.close();
+ SimpleTest.finish();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<p id="display">
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+</window>
diff --git a/layout/xul/test/titledpanelwindow.xhtml b/layout/xul/test/titledpanelwindow.xhtml
new file mode 100644
index 0000000000..ab3e8fcf99
--- /dev/null
+++ b/layout/xul/test/titledpanelwindow.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0;'>
+<panel style="background: white" noautohide='true' titlebar='normal' minwidth='120' minheight='140'/><label value='Test'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize1.xhtml b/layout/xul/test/windowminmaxsize1.xhtml
new file mode 100644
index 0000000000..fff337da23
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize1.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0;'>
+<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize10.xhtml b/layout/xul/test/windowminmaxsize10.xhtml
new file mode 100644
index 0000000000..bf20fd4ce2
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize10.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0;' minwidth='120' maxwidth='480' minheight='110' maxheight='470'>
+<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize2.xhtml b/layout/xul/test/windowminmaxsize2.xhtml
new file mode 100644
index 0000000000..96053b76f8
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize2.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0; min-width: 180px; min-height: 210px;'>
+<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize3.xhtml b/layout/xul/test/windowminmaxsize3.xhtml
new file mode 100644
index 0000000000..2eca783041
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize3.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0; max-width: 125px; max-height: 140px'>
+<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize4.xhtml b/layout/xul/test/windowminmaxsize4.xhtml
new file mode 100644
index 0000000000..81b07ca5c0
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize4.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0;' minwidth='240' minheight='220'>
+<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize5.xhtml b/layout/xul/test/windowminmaxsize5.xhtml
new file mode 100644
index 0000000000..b4365e5388
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize5.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0;' width='190' height='220' minwidth='215' minheight='235'>
+<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize6.xhtml b/layout/xul/test/windowminmaxsize6.xhtml
new file mode 100644
index 0000000000..76a8b6b3f0
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize6.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0;' maxwidth='125' maxheight='95'>
+<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize7.xhtml b/layout/xul/test/windowminmaxsize7.xhtml
new file mode 100644
index 0000000000..9d037fd27d
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize7.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0;' minwidth='210'>
+<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize8.xhtml b/layout/xul/test/windowminmaxsize8.xhtml
new file mode 100644
index 0000000000..52847e91af
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize8.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0;' maxwidth='128'>
+<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/test/windowminmaxsize9.xhtml b/layout/xul/test/windowminmaxsize9.xhtml
new file mode 100644
index 0000000000..515fe1b243
--- /dev/null
+++ b/layout/xul/test/windowminmaxsize9.xhtml
@@ -0,0 +1,4 @@
+<?xml-stylesheet href='chrome://global/skin' type='text/css'?>
+<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' align='start' pack='start' style='-moz-appearance: none; margin: 0; padding: 0; border: 0;' maxwidth='195' width='230' height='120' minheight='180'>
+<resizer dir='bottomright' flex='1' width='150' height='150' style='-moz-appearance: none; margin: 0; border: 0; padding: 0;'/>
+</window>
diff --git a/layout/xul/tree/crashtests/307298-1.xhtml b/layout/xul/tree/crashtests/307298-1.xhtml
new file mode 100644
index 0000000000..6c04a01321
--- /dev/null
+++ b/layout/xul/tree/crashtests/307298-1.xhtml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="var tree = document.getElementById('tree'), treeitem = document.getElementById('treeitem'); tree.parentNode.insertBefore(treeitem, tree);">
+
+<tree flex="1" id="tree">
+ <treecols>
+ <treecol id="name" label="Name" primary="true" flex="1"/>
+ </treecols>
+
+ <treechildren>
+ <treeitem id="treeitem">
+ <treerow>
+ <treecell label="Click the button below to crash"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+</tree>
+
+</window>
diff --git a/layout/xul/tree/crashtests/309732-1.xhtml b/layout/xul/tree/crashtests/309732-1.xhtml
new file mode 100644
index 0000000000..a7e40b75b9
--- /dev/null
+++ b/layout/xul/tree/crashtests/309732-1.xhtml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait" onload="setTimeout(boom, 30)">
+
+
+ <script>
+ function boom()
+ {
+ document.documentElement.appendChild(document.getElementById("TC"));
+ document.documentElement.appendChild(document.getElementById("TI"));
+
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+
+<tree flex="1">
+ <treecols>
+ <treecol label="Name"/>
+ </treecols>
+ <treechildren id="TC">
+ <treeitem id="TI">
+ <treerow>
+ <treecell label="First treecell"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </tree>
+</window>
diff --git a/layout/xul/tree/crashtests/309732-2.xhtml b/layout/xul/tree/crashtests/309732-2.xhtml
new file mode 100644
index 0000000000..354c58dacf
--- /dev/null
+++ b/layout/xul/tree/crashtests/309732-2.xhtml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait" onload="setTimeout(boom, 30)">
+
+ <script>
+ function boom()
+ {
+ document.documentElement.appendChild(document.getElementById('TC'));
+ document.getElementById('TI').hidden = false;
+
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+
+
+ <tree flex="1">
+ <treecols>
+ <treecol label="Name" flex="1"/>
+ </treecols>
+ <treechildren id="TC">
+ <treeitem>
+ <treerow>
+ <treecell label="First treecell"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </tree>
+ <treeitem id="TI" hidden="true"/>
+</window>
diff --git a/layout/xul/tree/crashtests/366583-1.xhtml b/layout/xul/tree/crashtests/366583-1.xhtml
new file mode 100644
index 0000000000..fd12709905
--- /dev/null
+++ b/layout/xul/tree/crashtests/366583-1.xhtml
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="boom1();"
+ class="reftest-wait">
+
+<script>
+
+var tree;
+
+function boom1()
+{
+ tree = document.getElementById("tree");
+ tree.style.position = "fixed";
+ setTimeout(boom2, 30);
+}
+
+function boom2()
+{
+ tree.style.overflow = "visible";
+ document.documentElement.removeAttribute("class");
+}
+</script>
+
+<tree rows="6" id="tree" style="display: list-item; overflow: auto; visibility: collapse;">
+ <treecols>
+ <treecol id="firstname" label="First Name" primary="true" style="-moz-box-flex: 3"/>
+ <treecol id="lastname" label="Last Name" style="-moz-box-flex: 7"/>
+ </treecols>
+
+ <treechildren>
+ <treeitem container="true" open="true">
+ <treerow>
+ <treecell label="Foo"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+</tree>
+
+
+</window>
diff --git a/layout/xul/tree/crashtests/380217-1.xhtml b/layout/xul/tree/crashtests/380217-1.xhtml
new file mode 100644
index 0000000000..251b3c450d
--- /dev/null
+++ b/layout/xul/tree/crashtests/380217-1.xhtml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="document.documentElement.style.content = '\'a\'';">
+
+<html:style>
+* { position: fixed; }
+*:not(style) {
+ /* At the time this testcase was added, the above `float` styling would
+ have automatically forced "display:block" for these elements, so we
+ should preserve that styling to preserve the integrity of the crashtest
+ since blockification behavior for -moz-box is changing. */
+ display: block;
+}
+</html:style>
+
+<tree rows="6">
+ <treecols>
+ <treecol id="firstname" label="First Name" primary="true"/>
+ </treecols>
+ <treechildren>
+ <treeitem>
+ <treerow>
+ <treecell/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+</tree>
+
+</window>
diff --git a/layout/xul/tree/crashtests/382444-1-inner.html b/layout/xul/tree/crashtests/382444-1-inner.html
new file mode 100644
index 0000000000..01805e6b34
--- /dev/null
+++ b/layout/xul/tree/crashtests/382444-1-inner.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+<title>Testcase bug - Crash [@ nsINodeInfo::Equals] with underflow event, tree stuff and removing window</title>
+</head>
+<body>
+<iframe src="data:application/xhtml+xml;charset=utf-8,%3Cwindow%20xmlns%3D%22http%3A//www.mozilla.org/keymaster/gatekeeper/there.is.only.xul%22%3E%0A%3Ctree%20style%3D%22overflow%3A%20auto%3B%20display%3A%20-moz-inline-box%3B%22%3E%0A%3Ctreeitem%20style%3D%22overflow%3A%20scroll%3B%20display%3A%20table-cell%3B%22%3E%0A%3Ctreechildren%20style%3D%22%20display%3A%20table-row%3B%22%3E%0A%3Ctreeitem%20id%3D%22a%22%20style%3D%22display%3A%20table-cell%3B%22%3E%0A%3C/treeitem%3E%0A%3C/treechildren%3E%0A%3C/treeitem%3E%0A%0A%3C/tree%3E%0A%0A%3Cscript%20xmlns%3D%22http%3A//www.w3.org/1999/xhtml%22%3E%0Afunction%20doe%28%29%20%7B%0Adocument.getElementById%28%27a%27%29.parentNode.removeChild%28document.getElementById%28%27a%27%29%29%3B%0A%7D%0AsetTimeout%28doe%2C%20100%29%3B%0Adocument.addEventListener%28%27underflow%27%2C%20function%28e%29%20%7Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%20%7D%2C%20true%29%3B%0Awindow.addEventListener%28%27underflow%27%2C%20function%28e%29%20%7Bwindow.frameElement.parentNode.removeChild%28window.frameElement%29%20%7D%2C%20true%29%3B%0A%3C/script%3E%0A%3C/window%3E" id="content"></iframe>
+
+<script>
+function doe() {
+window.location.reload();
+}
+setTimeout(doe, 500);
+</script>
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/382444-1.html b/layout/xul/tree/crashtests/382444-1.html
new file mode 100644
index 0000000000..8926cf16d7
--- /dev/null
+++ b/layout/xul/tree/crashtests/382444-1.html
@@ -0,0 +1,9 @@
+<html class="reftest-wait">
+<head>
+<script>
+setTimeout('document.documentElement.className = ""', 500);
+</script>
+<body>
+<iframe src="382444-1-inner.html"></iframe>
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/391178-1.xhtml b/layout/xul/tree/crashtests/391178-1.xhtml
new file mode 100644
index 0000000000..0f4b16cd99
--- /dev/null
+++ b/layout/xul/tree/crashtests/391178-1.xhtml
@@ -0,0 +1,41 @@
+<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait">
+<head>
+<script>
+
+var ccc;
+
+function boom()
+{
+ var XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ var hbox = document.createElementNS(XUL_NS, 'hbox');
+ var tree = document.createElementNS(XUL_NS, 'tree');
+ var treecol = document.createElementNS(XUL_NS, 'treecol');
+
+ ccc = document.getElementById("ccc");
+
+ ccc.style.position = "fixed";
+
+ hbox.appendChild(treecol);
+ tree.appendChild(hbox);
+ ccc.appendChild(tree);
+
+ setTimeout(boom2, 200);
+}
+
+function boom2()
+{
+ ccc.style.position = "";
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+
+<body onload="boom();">
+
+<div id="ccc">
+</div>
+
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/391178-2.xhtml b/layout/xul/tree/crashtests/391178-2.xhtml
new file mode 100644
index 0000000000..423b5d1bfe
--- /dev/null
+++ b/layout/xul/tree/crashtests/391178-2.xhtml
@@ -0,0 +1,20 @@
+<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml" class="reftest-wait">
+
+<tree id="a" style="display: block; position: fixed;">
+ <box style=" display: block; position: fixed;">
+ <treecol style=" display: -moz-box;"/>
+ </box>
+ <box style="display: block; position: fixed;">
+ <treechildren style="display: block; position: absolute;"/>
+ </box>
+</tree>
+
+<script xmlns="http://www.w3.org/1999/xhtml">
+function removestyles(){
+ document.getElementById('a').removeAttribute('style');
+ document.documentElement.removeAttribute("class");
+}
+setTimeout(removestyles, 100);
+</script>
+</window>
diff --git a/layout/xul/tree/crashtests/393665-1.xhtml b/layout/xul/tree/crashtests/393665-1.xhtml
new file mode 100644
index 0000000000..6fb5ec0c9e
--- /dev/null
+++ b/layout/xul/tree/crashtests/393665-1.xhtml
@@ -0,0 +1,3 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <treechildren style="display: block" />
+</window>
diff --git a/layout/xul/tree/crashtests/399227-1.xhtml b/layout/xul/tree/crashtests/399227-1.xhtml
new file mode 100644
index 0000000000..3ae4dfa764
--- /dev/null
+++ b/layout/xul/tree/crashtests/399227-1.xhtml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" class="reftest-wait" onload="setTimeout(boom, 30)">
+
+
+ <script>
+ function boom()
+ {
+ var tree = document.getElementById("thetree");
+ var selection = tree.view.selection;
+
+ selection.select(0);
+ tree.parentNode.removeChild(tree);
+
+ // This is expected to throw an error (it used to crash).
+ try {
+ selection.rangedSelect(1, 1, false);
+ }
+ catch (ex) {}
+
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+
+<tree flex="1" id="thetree">
+ <treecols>
+ <treecol label="Name"/>
+ </treecols>
+ <treechildren id="TC">
+ <treeitem id="TI1">
+ <treerow>
+ <treecell label="First treecell"/>
+ </treerow>
+ </treeitem>
+ <treeitem id="TI2">
+ <treerow>
+ <treecell label="Second treecell"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+ </tree>
+</window>
diff --git a/layout/xul/tree/crashtests/399692-1.xhtml b/layout/xul/tree/crashtests/399692-1.xhtml
new file mode 100644
index 0000000000..97eec26742
--- /dev/null
+++ b/layout/xul/tree/crashtests/399692-1.xhtml
@@ -0,0 +1,10 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+</head>
+<body>
+
+<xul:treechildren style="display: inline;" />
+
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/399715-1.xhtml b/layout/xul/tree/crashtests/399715-1.xhtml
new file mode 100644
index 0000000000..ea0a20cfa2
--- /dev/null
+++ b/layout/xul/tree/crashtests/399715-1.xhtml
@@ -0,0 +1,9 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<body style="float: right;" onload="document.body.style.cssFloat = '';">
+
+<xul:tree><xul:hbox><xul:treecol /></xul:hbox></xul:tree>
+
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/409807-1.xhtml b/layout/xul/tree/crashtests/409807-1.xhtml
new file mode 100644
index 0000000000..a3af3da41b
--- /dev/null
+++ b/layout/xul/tree/crashtests/409807-1.xhtml
@@ -0,0 +1,25 @@
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" onload="boom();">
+
+<script type="text/javascript">
+
+function boom()
+{
+ var tree = document.getElementById("tree");
+ var tc = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "treechildren");
+
+ document.addEventListener("DOMAttrModified", m, false);
+
+ tree.appendChild(tc);
+
+ function m()
+ {
+ document.removeEventListener("DOMAttrModified", m, false);
+ tree.removeChild(tc);
+ }
+}
+
+</script>
+
+<tree id="tree" />
+
+</window>
diff --git a/layout/xul/tree/crashtests/414170-1.xhtml b/layout/xul/tree/crashtests/414170-1.xhtml
new file mode 100644
index 0000000000..82ea63bcfd
--- /dev/null
+++ b/layout/xul/tree/crashtests/414170-1.xhtml
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="boom();">
+
+<script type="text/javascript">
+
+function boom()
+{
+ var option = document.createElementNS("http://www.w3.org/1999/xhtml", "option");
+ document.getElementById("tc").appendChild(option);
+}
+
+</script>
+
+<tree><treechildren id="tc"><hbox/></treechildren></tree>
+
+</window>
diff --git a/layout/xul/tree/crashtests/479931-1.xhtml b/layout/xul/tree/crashtests/479931-1.xhtml
new file mode 100644
index 0000000000..458a192501
--- /dev/null
+++ b/layout/xul/tree/crashtests/479931-1.xhtml
@@ -0,0 +1,19 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ var o = document.createElementNS("http://www.w3.org/1999/xhtml", "option");
+ var q = document.getElementById("q");
+ q.appendChild(o);
+}
+
+</script>
+</head>
+<body onload="boom();">
+
+<xul:tree><xul:treechildren id="q"><div/></xul:treechildren></xul:tree>
+
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/585815-iframe.xhtml b/layout/xul/tree/crashtests/585815-iframe.xhtml
new file mode 100644
index 0000000000..90c20fca80
--- /dev/null
+++ b/layout/xul/tree/crashtests/585815-iframe.xhtml
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="setInterval(run, 25)">
+
+<tree style="-moz-box-flex: 1" rows="2">
+ <treecols>
+ <treecol id="sender" label="Sender" style="-moz-box-flex: 1"/>
+ <treecol id="subject" label="Subject" style="-moz-box-flex: 2"/>
+ </treecols>
+ <treechildren>
+ <treeitem>
+ <treerow>
+ <treecell label="joe@somewhere.com"/>
+ <treecell label="Top secret plans"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ <treeitem>
+ <treerow>
+ <treecell label="mel@whereever.com"/>
+ <treecell label="Let's do lunch"/>
+ </treerow>
+ </treeitem>
+ </treechildren>
+</tree>
+
+<script type="text/javascript"><![CDATA[
+function run() {
+ var tree = document.getElementsByTagName("tree")[0];
+ var sel = tree.view.selection;
+ sel.rangedSelect(0, 0, true);
+ sel.rangedSelect(1000, 1001, true);
+ sel.adjustSelection(1, 0x7fffffff);
+}
+]]></script>
+
+</window>
diff --git a/layout/xul/tree/crashtests/585815.html b/layout/xul/tree/crashtests/585815.html
new file mode 100644
index 0000000000..7c3b27f6aa
--- /dev/null
+++ b/layout/xul/tree/crashtests/585815.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait"><head>
+ <meta charset="utf-8">
+ <title>Testcase for bug 585815</title>
+<script>
+function done()
+{
+ document.documentElement.removeAttribute("class");
+}
+</script>
+</head>
+<body onload="setTimeout(done,1000)">
+
+<iframe src="585815-iframe.xhtml"></iframe>
+
+
+</body>
+</html>
diff --git a/layout/xul/tree/crashtests/601427.html b/layout/xul/tree/crashtests/601427.html
new file mode 100644
index 0000000000..2a2999052e
--- /dev/null
+++ b/layout/xul/tree/crashtests/601427.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+
+var onPaintFunctions =
+[
+ function() { document.documentElement.style.MozAppearance = "treeheadersortarrow"; },
+ function() { document.documentElement.style.position = "fixed"; },
+ function() { document.documentElement.removeAttribute("class"); }
+];
+
+var i = 0;
+
+function advance()
+{
+ var f = onPaintFunctions[i++];
+ if (f)
+ f();
+}
+
+function start()
+{
+ window.addEventListener("MozAfterPaint", advance, true);
+ advance();
+}
+
+window.addEventListener("load", start);
+
+</script>
+</html>
diff --git a/layout/xul/tree/crashtests/730441-3.xhtml b/layout/xul/tree/crashtests/730441-3.xhtml
new file mode 100644
index 0000000000..c3fe199a83
--- /dev/null
+++ b/layout/xul/tree/crashtests/730441-3.xhtml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!--
+###!!! ASSERTION: You can't dereference a NULL nsCOMPtr with operator->().: 'mRawPtr != 0', file ../../../../dist/include/nsCOMPtr.h, line 796
+
+Program received signal SIGSEGV, Segmentation fault.
+0xb6b7463a in nsTreeContentView::SetTree (this=0xb0ba2510, aTree=0xaaecece0) at layout/xul/base/src/tree/src/nsTreeContentView.cpp:571
+571 boxObject->GetElement(getter_AddRefs(element));
+(gdb) bt 3
+#0 0xb6b7463a in nsTreeContentView::SetTree (this=0xb0ba2510, aTree=0xaaecece0) at layout/xul/base/src/tree/src/nsTreeContentView.cpp:571
+#1 0xb736c76f in NS_InvokeByIndex_P () at xpcom/reflect/xptcall/md/unix/xptcinvoke_gcc_x86_unix.cpp:69
+#2 0xb6171901 in XPCWrappedNative::CallMethod (ccx=..., mode=XPCWrappedNative::CALL_METHOD)
+ at js/src/xpconnect/src/xpcwrappednative.cpp:2722
+(More stack frames follow...)
+(gdb) list 566
+561 nsTreeContentView::SetTree(nsITreeBoxObject* aTree)
+562 {
+563 ClearRows();
+564
+565 mBoxObject = aTree;
+566
+567 if (aTree && !mRoot) {
+568 // Get our root element
+569 nsCOMPtr<nsIBoxObject> boxObject = do_QueryInterface(mBoxObject);
+570 nsCOMPtr<Element> element;
+571 boxObject->GetElement(getter_AddRefs(element));
+(gdb) p boxObject
+$16 = {mRawPtr = 0x0}
+
+|aTree| does not implement |nsIBoxObject|, so |do_QueryInterface(mBoxObject)|
+returns null. Then we have |null->GetElement()|.
+-->
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="document.getElementById('tree').view.setTree({});">
+<tree id="tree">
+ <treechildren/>
+</tree>
+</window>
+
diff --git a/layout/xul/tree/crashtests/crashtests.list b/layout/xul/tree/crashtests/crashtests.list
new file mode 100644
index 0000000000..8ed749aca4
--- /dev/null
+++ b/layout/xul/tree/crashtests/crashtests.list
@@ -0,0 +1,18 @@
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/307298-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/309732-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/309732-2.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/366583-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/380217-1.xhtml
+load 382444-1.html
+load 391178-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/391178-2.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/393665-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/399227-1.xhtml
+load 399692-1.xhtml
+load 399715-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/409807-1.xhtml
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/414170-1.xhtml
+load 479931-1.xhtml
+load 585815.html
+load 601427.html
+load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/730441-3.xhtml
diff --git a/layout/xul/tree/moz.build b/layout/xul/tree/moz.build
new file mode 100644
index 0000000000..04385a9d4f
--- /dev/null
+++ b/layout/xul/tree/moz.build
@@ -0,0 +1,46 @@
+# -*- 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", "XUL")
+
+XPIDL_SOURCES += [
+ "nsITreeSelection.idl",
+ "nsITreeView.idl",
+]
+
+XPIDL_MODULE = "layout_xul_tree"
+
+EXPORTS += [
+ "nsTreeColFrame.h",
+ "nsTreeColumns.h",
+ "nsTreeUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "nsTreeBodyFrame.cpp",
+ "nsTreeColFrame.cpp",
+ "nsTreeColumns.cpp",
+ "nsTreeContentView.cpp",
+ "nsTreeImageListener.cpp",
+ "nsTreeSelection.cpp",
+ "nsTreeStyleCache.cpp",
+ "nsTreeUtils.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "..",
+ "../../base",
+ "../../forms",
+ "../../generic",
+ "../../painting",
+ "../../style",
+ "/dom/base",
+ "/dom/xul",
+]
diff --git a/layout/xul/tree/nsITreeSelection.idl b/layout/xul/tree/nsITreeSelection.idl
new file mode 100644
index 0000000000..d265b639ee
--- /dev/null
+++ b/layout/xul/tree/nsITreeSelection.idl
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+webidl XULTreeElement;
+
+[scriptable, uuid(ab6fe746-300b-4ab4-abb9-1c0e3977874c)]
+interface nsITreeSelection : nsISupports
+{
+ /**
+ * The tree widget for this selection.
+ */
+ attribute XULTreeElement tree;
+
+ /**
+ * This attribute is a boolean indicating single selection.
+ */
+ readonly attribute boolean single;
+
+ /**
+ * The number of rows currently selected in this tree.
+ */
+ readonly attribute long count;
+
+ /**
+ * Indicates whether or not the row at the specified index is
+ * part of the selection.
+ */
+ boolean isSelected(in long index);
+
+ /**
+ * Deselect all rows and select the row at the specified index.
+ */
+ void select(in long index);
+
+ /**
+ * Perform a timed select.
+ */
+ void timedSelect(in long index, in long delay);
+
+ /**
+ * Toggle the selection state of the row at the specified index.
+ */
+ void toggleSelect(in long index);
+
+ /**
+ * Select the range specified by the indices. If augment is true,
+ * then we add the range to the selection without clearing out anything
+ * else. If augment is false, everything is cleared except for the specified range.
+ */
+ void rangedSelect(in long startIndex, in long endIndex, in boolean augment);
+
+ /**
+ * Clears the range.
+ */
+ void clearRange(in long startIndex, in long endIndex);
+
+ /**
+ * Clears the selection.
+ */
+ void clearSelection();
+
+ /**
+ * Selects all rows.
+ */
+ void selectAll();
+
+ /**
+ * Iterate the selection using these methods.
+ */
+ long getRangeCount();
+ void getRangeAt(in long i, out long min, out long max);
+
+ /**
+ * Can be used to invalidate the selection.
+ */
+ void invalidateSelection();
+
+ /**
+ * Called when the row count changes to adjust selection indices.
+ */
+ void adjustSelection(in long index, in long count);
+
+ /**
+ * This attribute is a boolean indicating whether or not the
+ * "select" event should fire when the selection is changed using
+ * one of our methods. A view can use this to temporarily suppress
+ * the selection while manipulating all of the indices, e.g., on
+ * a sort.
+ * Note: setting this attribute to false will fire a select event.
+ */
+ attribute boolean selectEventsSuppressed;
+
+ /**
+ * The current item (the one that gets a focus rect in addition to being
+ * selected).
+ */
+ attribute long currentIndex;
+
+ /**
+ * The selection "pivot". This is the first item the user selected as
+ * part of a ranged select.
+ */
+ readonly attribute long shiftSelectPivot;
+};
+
+/**
+ * The following interface is not scriptable and MUST NEVER BE MADE scriptable.
+ * Native treeselections implement it, and we use this to check whether a
+ * treeselection is native (and therefore suitable for use by untrusted content).
+ */
+[uuid(1bd59678-5cb3-4316-b246-31a91b19aabe)]
+interface nsINativeTreeSelection : nsITreeSelection
+{
+ [noscript] void ensureNative();
+};
diff --git a/layout/xul/tree/nsITreeView.idl b/layout/xul/tree/nsITreeView.idl
new file mode 100644
index 0000000000..7f2480ceaf
--- /dev/null
+++ b/layout/xul/tree/nsITreeView.idl
@@ -0,0 +1,173 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsITreeSelection;
+
+webidl DataTransfer;
+webidl TreeColumn;
+webidl XULTreeElement;
+
+[scriptable, uuid(091116f0-0bdc-4b32-b9c8-c8d5a37cb088)]
+interface nsITreeView : nsISupports
+{
+ /**
+ * The total number of rows in the tree (including the offscreen rows).
+ */
+ readonly attribute long rowCount;
+
+ /**
+ * The selection for this view.
+ */
+ attribute nsITreeSelection selection;
+
+ /**
+ * A whitespace delimited list of properties. For each property X the view
+ * gives back will cause the pseudoclasses ::-moz-tree-cell(x),
+ * ::-moz-tree-row(x), ::-moz-tree-twisty(x), ::-moz-tree-image(x),
+ * ::-moz-tree-cell-text(x). to be matched on the pseudoelement
+ * ::moz-tree-row.
+ */
+ AString getRowProperties(in long index);
+
+ /**
+ * A whitespace delimited list of properties for a given cell. Each
+ * property, x, that the view gives back will cause the pseudoclasses
+ * ::-moz-tree-cell(x), ::-moz-tree-row(x), ::-moz-tree-twisty(x),
+ * ::-moz-tree-image(x), ::-moz-tree-cell-text(x). to be matched on the
+ * cell.
+ */
+ AString getCellProperties(in long row, in TreeColumn col);
+
+ /**
+ * Called to get properties to paint a column background. For shading the sort
+ * column, etc.
+ */
+ AString getColumnProperties(in TreeColumn col);
+
+ /**
+ * Methods that can be used to test whether or not a twisty should be drawn,
+ * and if so, whether an open or closed twisty should be used.
+ */
+ boolean isContainer(in long index);
+ boolean isContainerOpen(in long index);
+ boolean isContainerEmpty(in long index);
+
+ /**
+ * isSeparator is used to determine if the row at index is a separator.
+ * A value of true will result in the tree drawing a horizontal separator.
+ * The tree uses the ::moz-tree-separator pseudoclass to draw the separator.
+ */
+ boolean isSeparator(in long index);
+
+ /**
+ * Specifies if there is currently a sort on any column. Used mostly by dragdrop
+ * to affect drop feedback.
+ */
+ boolean isSorted();
+
+ const short DROP_BEFORE = -1;
+ const short DROP_ON = 0;
+ const short DROP_AFTER = 1;
+ /**
+ * Methods used by the drag feedback code to determine if a drag is allowable at
+ * the current location. To get the behavior where drops are only allowed on
+ * items, such as the mailNews folder pane, always return false when
+ * the orientation is not DROP_ON.
+ */
+ boolean canDrop(in long index, in long orientation, in DataTransfer dataTransfer);
+
+ /**
+ * Called when the user drops something on this view. The |orientation| param
+ * specifies before/on/after the given |row|.
+ */
+ void drop(in long row, in long orientation, in DataTransfer dataTransfer);
+
+ /**
+ * Methods used by the tree to draw thread lines in the tree.
+ * getParentIndex is used to obtain the index of a parent row.
+ * If there is no parent row, getParentIndex returns -1.
+ */
+ long getParentIndex(in long rowIndex);
+
+ /**
+ * hasNextSibling is used to determine if the row at rowIndex has a nextSibling
+ * that occurs *after* the index specified by afterIndex. Code that is forced
+ * to march down the view looking at levels can optimize the march by starting
+ * at afterIndex+1.
+ */
+ boolean hasNextSibling(in long rowIndex, in long afterIndex);
+
+ /**
+ * The level is an integer value that represents
+ * the level of indentation. It is multiplied by the width specified in the
+ * :moz-tree-indentation pseudoelement to compute the exact indendation.
+ */
+ long getLevel(in long index);
+
+ /**
+ * The image path for a given cell. For defining an icon for a cell.
+ * If the empty string is returned, the :moz-tree-image pseudoelement
+ * will be used.
+ */
+ AString getImageSrc(in long row, in TreeColumn col);
+
+ /**
+ * The value for a given cell. This method is only called for columns
+ * of type other than |text|.
+ */
+ AString getCellValue(in long row, in TreeColumn col);
+
+ /**
+ * The text for a given cell. If a column consists only of an image, then
+ * the empty string is returned.
+ */
+ AString getCellText(in long row, in TreeColumn col);
+
+ /**
+ * Called during initialization to link the view to the front end box object.
+ */
+ void setTree(in XULTreeElement tree);
+
+ /**
+ * Called on the view when an item is opened or closed.
+ */
+ void toggleOpenState(in long index);
+
+ /**
+ * Called on the view when a header is clicked.
+ */
+ void cycleHeader(in TreeColumn col);
+
+ /**
+ * Should be called from a XUL onselect handler whenever the selection changes.
+ */
+ [binaryname(SelectionChangedXPCOM)]
+ void selectionChanged();
+
+ /**
+ * Called on the view when a cell in a non-selectable cycling column (e.g., unread/flag/etc.) is clicked.
+ */
+ void cycleCell(in long row, in TreeColumn col);
+
+ /**
+ * isEditable is called to ask the view if the cell contents are editable.
+ * A value of true will result in the tree popping up a text field when
+ * the user tries to inline edit the cell.
+ */
+ boolean isEditable(in long row, in TreeColumn col);
+
+ /**
+ * setCellValue is called when the value of the cell has been set by the user.
+ * This method is only called for columns of type other than |text|.
+ */
+ void setCellValue(in long row, in TreeColumn col, in AString value);
+
+ /**
+ * setCellText is called when the contents of the cell have been edited by the user.
+ */
+ void setCellText(in long row, in TreeColumn col, in AString value);
+};
diff --git a/layout/xul/tree/nsTreeBodyFrame.cpp b/layout/xul/tree/nsTreeBodyFrame.cpp
new file mode 100644
index 0000000000..f20d39b263
--- /dev/null
+++ b/layout/xul/tree/nsTreeBodyFrame.cpp
@@ -0,0 +1,4348 @@
+/* -*- 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/AsyncEventDispatcher.h"
+#include "mozilla/ContentEvents.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/PathHelpers.h"
+#include "mozilla/Likely.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/MouseEvents.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/intl/Segmenter.h"
+
+#include "gfxUtils.h"
+#include "nsAlgorithm.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsFontMetrics.h"
+#include "nsITreeView.h"
+#include "nsPresContext.h"
+#include "nsNameSpaceManager.h"
+
+#include "nsTreeBodyFrame.h"
+#include "nsTreeSelection.h"
+#include "nsTreeImageListener.h"
+
+#include "nsGkAtoms.h"
+#include "nsCSSAnonBoxes.h"
+
+#include "gfxContext.h"
+#include "nsIContent.h"
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/dom/Document.h"
+#include "nsCSSRendering.h"
+#include "nsString.h"
+#include "nsContainerFrame.h"
+#include "nsView.h"
+#include "nsViewManager.h"
+#include "nsVariant.h"
+#include "nsWidgetsCID.h"
+#include "nsIFrameInlines.h"
+#include "nsBoxFrame.h"
+#include "nsBoxLayoutState.h"
+#include "nsTextBoxFrame.h"
+#include "nsTreeContentView.h"
+#include "nsTreeUtils.h"
+#include "nsStyleConsts.h"
+#include "nsITheme.h"
+#include "imgIRequest.h"
+#include "imgIContainer.h"
+#include "mozilla/dom/NodeInfo.h"
+#include "nsContentUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsIScrollableFrame.h"
+#include "nsDisplayList.h"
+#include "mozilla/dom/CustomEvent.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/dom/TreeColumnBinding.h"
+#include <algorithm>
+#include "ScrollbarActivity.h"
+
+#ifdef ACCESSIBILITY
+# include "nsAccessibilityService.h"
+# include "nsIWritablePropertyBag2.h"
+#endif
+#include "nsBidiUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+using namespace mozilla::layout;
+
+// Function that cancels all the image requests in our cache.
+void nsTreeBodyFrame::CancelImageRequests() {
+ for (nsTreeImageCacheEntry entry : mImageCache.Values()) {
+ // If our imgIRequest object was registered with the refresh driver
+ // then we need to deregister it.
+ nsLayoutUtils::DeregisterImageRequest(PresContext(), entry.request,
+ nullptr);
+ entry.request->UnlockImage();
+ entry.request->CancelAndForgetObserver(NS_BINDING_ABORTED);
+ }
+}
+
+//
+// NS_NewTreeFrame
+//
+// Creates a new tree frame
+//
+nsIFrame* NS_NewTreeBodyFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsTreeBodyFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsTreeBodyFrame)
+
+NS_QUERYFRAME_HEAD(nsTreeBodyFrame)
+ NS_QUERYFRAME_ENTRY(nsIScrollbarMediator)
+ NS_QUERYFRAME_ENTRY(nsTreeBodyFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsLeafBoxFrame)
+
+// Constructor
+nsTreeBodyFrame::nsTreeBodyFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsLeafBoxFrame(aStyle, aPresContext, kClassID),
+ mImageCache(),
+ mTopRowIndex(0),
+ mPageLength(0),
+ mHorzPosition(0),
+ mOriginalHorzWidth(-1),
+ mHorzWidth(0),
+ mAdjustWidth(0),
+ mRowHeight(0),
+ mIndentation(0),
+ mStringWidth(-1),
+ mUpdateBatchNest(0),
+ mRowCount(0),
+ mMouseOverRow(-1),
+ mFocused(false),
+ mHasFixedRowCount(false),
+ mVerticalOverflow(false),
+ mHorizontalOverflow(false),
+ mReflowCallbackPosted(false),
+ mCheckingOverflow(false) {
+ mColumns = new nsTreeColumns(this);
+}
+
+// Destructor
+nsTreeBodyFrame::~nsTreeBodyFrame() {
+ CancelImageRequests();
+ DetachImageListeners();
+}
+
+static void GetBorderPadding(ComputedStyle* aStyle, nsMargin& aMargin) {
+ aMargin.SizeTo(0, 0, 0, 0);
+ aStyle->StylePadding()->GetPadding(aMargin);
+ aMargin += aStyle->StyleBorder()->GetComputedBorder();
+}
+
+static void AdjustForBorderPadding(ComputedStyle* aStyle, nsRect& aRect) {
+ nsMargin borderPadding(0, 0, 0, 0);
+ GetBorderPadding(aStyle, borderPadding);
+ aRect.Deflate(borderPadding);
+}
+
+void nsTreeBodyFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsLeafBoxFrame::Init(aContent, aParent, aPrevInFlow);
+
+ mIndentation = GetIndentation();
+ mRowHeight = GetRowHeight();
+
+ // Call GetBaseElement so that mTree is assigned.
+ GetBaseElement();
+
+ if (LookAndFeel::GetInt(LookAndFeel::IntID::UseOverlayScrollbars) != 0) {
+ mScrollbarActivity =
+ new ScrollbarActivity(static_cast<nsIScrollbarMediator*>(this));
+ }
+}
+
+nsSize nsTreeBodyFrame::GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) {
+ EnsureView();
+
+ RefPtr<XULTreeElement> tree(GetBaseElement());
+
+ nsSize min(0, 0);
+ int32_t desiredRows;
+ if (MOZ_UNLIKELY(!tree)) {
+ desiredRows = 0;
+ } else {
+ nsAutoString rows;
+ tree->GetAttr(kNameSpaceID_None, nsGkAtoms::rows, rows);
+ if (!rows.IsEmpty()) {
+ nsresult err;
+ desiredRows = rows.ToInteger(&err);
+ mPageLength = desiredRows;
+ } else {
+ desiredRows = 0;
+ }
+ }
+
+ min.height = mRowHeight * desiredRows;
+
+ AddXULBorderAndPadding(min);
+ bool widthSet, heightSet;
+ nsIFrame::AddXULMinSize(this, min, widthSet, heightSet);
+
+ return min;
+}
+
+nscoord nsTreeBodyFrame::CalcMaxRowWidth() {
+ if (mStringWidth != -1) return mStringWidth;
+
+ if (!mView) {
+ return 0;
+ }
+
+ ComputedStyle* rowContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow());
+ nsMargin rowMargin(0, 0, 0, 0);
+ GetBorderPadding(rowContext, rowMargin);
+
+ nscoord rowWidth;
+ nsTreeColumn* col;
+
+ RefPtr<gfxContext> rc = PresShell()->CreateReferenceRenderingContext();
+
+ for (int32_t row = 0; row < mRowCount; ++row) {
+ rowWidth = 0;
+
+ for (col = mColumns->GetFirstColumn(); col; col = col->GetNext()) {
+ nscoord desiredWidth, currentWidth;
+ nsresult rv = GetCellWidth(row, col, rc, desiredWidth, currentWidth);
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT_UNREACHABLE("invalid column");
+ continue;
+ }
+ rowWidth += desiredWidth;
+ }
+
+ if (rowWidth > mStringWidth) mStringWidth = rowWidth;
+ }
+
+ mStringWidth += rowMargin.left + rowMargin.right;
+ return mStringWidth;
+}
+
+void nsTreeBodyFrame::DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) {
+ if (mScrollbarActivity) {
+ mScrollbarActivity->Destroy();
+ mScrollbarActivity = nullptr;
+ }
+
+ mScrollEvent.Revoke();
+ // Make sure we cancel any posted callbacks.
+ if (mReflowCallbackPosted) {
+ PresShell()->CancelReflowCallback(this);
+ mReflowCallbackPosted = false;
+ }
+
+ if (mColumns) mColumns->SetTree(nullptr);
+
+ if (mTree) {
+ mTree->BodyDestroyed(mTopRowIndex);
+ }
+
+ if (nsCOMPtr<nsITreeView> view = std::move(mView)) {
+ nsCOMPtr<nsITreeSelection> sel;
+ view->GetSelection(getter_AddRefs(sel));
+ if (sel) {
+ sel->SetTree(nullptr);
+ }
+ view->SetTree(nullptr);
+ }
+
+ nsLeafBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
+}
+
+void nsTreeBodyFrame::EnsureView() {
+ if (mView) {
+ return;
+ }
+
+ if (PresShell()->IsReflowLocked()) {
+ if (!mReflowCallbackPosted) {
+ mReflowCallbackPosted = true;
+ PresShell()->PostReflowCallback(this);
+ }
+ return;
+ }
+
+ AutoWeakFrame weakFrame(this);
+
+ RefPtr<XULTreeElement> tree = GetBaseElement();
+ if (!tree) {
+ return;
+ }
+ nsCOMPtr<nsITreeView> treeView = tree->GetView();
+ if (!treeView || !weakFrame.IsAlive()) {
+ return;
+ }
+ int32_t rowIndex = tree->GetCachedTopVisibleRow();
+
+ // Set our view.
+ SetView(treeView);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ // Scroll to the given row.
+ // XXX is this optimal if we haven't laid out yet?
+ ScrollToRow(rowIndex);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+}
+
+void nsTreeBodyFrame::ManageReflowCallback(const nsRect& aRect,
+ nscoord aHorzWidth) {
+ if (!mReflowCallbackPosted &&
+ (!aRect.IsEqualEdges(mRect) || mHorzWidth != aHorzWidth)) {
+ PresShell()->PostReflowCallback(this);
+ mReflowCallbackPosted = true;
+ mOriginalHorzWidth = mHorzWidth;
+ } else if (mReflowCallbackPosted && mHorzWidth != aHorzWidth &&
+ mOriginalHorzWidth == aHorzWidth) {
+ PresShell()->CancelReflowCallback(this);
+ mReflowCallbackPosted = false;
+ mOriginalHorzWidth = -1;
+ }
+}
+
+void nsTreeBodyFrame::SetXULBounds(nsBoxLayoutState& aBoxLayoutState,
+ const nsRect& aRect,
+ bool aRemoveOverflowArea) {
+ nscoord horzWidth = CalcHorzWidth(GetScrollParts());
+ ManageReflowCallback(aRect, horzWidth);
+ mHorzWidth = horzWidth;
+
+ nsLeafBoxFrame::SetXULBounds(aBoxLayoutState, aRect, aRemoveOverflowArea);
+}
+
+bool nsTreeBodyFrame::ReflowFinished() {
+ if (!mView) {
+ AutoWeakFrame weakFrame(this);
+ EnsureView();
+ NS_ENSURE_TRUE(weakFrame.IsAlive(), false);
+ }
+ if (mView) {
+ CalcInnerBox();
+ ScrollParts parts = GetScrollParts();
+ mHorzWidth = CalcHorzWidth(parts);
+ if (!mHasFixedRowCount) {
+ mPageLength =
+ (mRowHeight > 0) ? (mInnerBox.height / mRowHeight) : mRowCount;
+ }
+
+ int32_t lastPageTopRow = std::max(0, mRowCount - mPageLength);
+ if (mTopRowIndex > lastPageTopRow)
+ ScrollToRowInternal(parts, lastPageTopRow);
+
+ XULTreeElement* treeContent = GetBaseElement();
+ if (treeContent && treeContent->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::keepcurrentinview,
+ nsGkAtoms::_true, eCaseMatters)) {
+ // make sure that the current selected item is still
+ // visible after the tree changes size.
+ if (nsCOMPtr<nsITreeSelection> sel = GetSelection()) {
+ int32_t currentIndex;
+ sel->GetCurrentIndex(&currentIndex);
+ if (currentIndex != -1) {
+ EnsureRowIsVisibleInternal(parts, currentIndex);
+ }
+ }
+ }
+
+ if (!FullScrollbarsUpdate(false)) {
+ return false;
+ }
+ }
+
+ mReflowCallbackPosted = false;
+ return false;
+}
+
+void nsTreeBodyFrame::ReflowCallbackCanceled() {
+ mReflowCallbackPosted = false;
+}
+
+nsresult nsTreeBodyFrame::GetView(nsITreeView** aView) {
+ *aView = nullptr;
+ AutoWeakFrame weakFrame(this);
+ EnsureView();
+ NS_ENSURE_STATE(weakFrame.IsAlive());
+ NS_IF_ADDREF(*aView = mView);
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::SetView(nsITreeView* aView) {
+ // First clear out the old view.
+ nsCOMPtr<nsITreeView> oldView = std::move(mView);
+ if (oldView) {
+ AutoWeakFrame weakFrame(this);
+
+ nsCOMPtr<nsITreeSelection> sel;
+ oldView->GetSelection(getter_AddRefs(sel));
+ if (sel) {
+ sel->SetTree(nullptr);
+ }
+ oldView->SetTree(nullptr);
+
+ NS_ENSURE_STATE(weakFrame.IsAlive());
+
+ // Only reset the top row index and delete the columns if we had an old
+ // non-null view.
+ mTopRowIndex = 0;
+ }
+
+ // Tree, meet the view.
+ mView = aView;
+
+ // Changing the view causes us to refetch our data. This will
+ // necessarily entail a full invalidation of the tree.
+ Invalidate();
+
+ RefPtr<XULTreeElement> treeContent = GetBaseElement();
+ if (treeContent) {
+#ifdef ACCESSIBILITY
+ if (nsAccessibilityService* accService = GetAccService()) {
+ accService->TreeViewChanged(PresContext()->GetPresShell(), treeContent,
+ mView);
+ }
+#endif // #ifdef ACCESSIBILITY
+ FireDOMEvent(u"TreeViewChanged"_ns, treeContent);
+ }
+
+ if (aView) {
+ // Give the view a new empty selection object to play with, but only if it
+ // doesn't have one already.
+ nsCOMPtr<nsITreeSelection> sel;
+ aView->GetSelection(getter_AddRefs(sel));
+ if (sel) {
+ sel->SetTree(treeContent);
+ } else {
+ NS_NewTreeSelection(treeContent, getter_AddRefs(sel));
+ aView->SetSelection(sel);
+ }
+
+ // View, meet the tree.
+ AutoWeakFrame weakFrame(this);
+ aView->SetTree(treeContent);
+ NS_ENSURE_STATE(weakFrame.IsAlive());
+ aView->GetRowCount(&mRowCount);
+
+ if (!PresShell()->IsReflowLocked()) {
+ // The scrollbar will need to be updated.
+ FullScrollbarsUpdate(false);
+ } else if (!mReflowCallbackPosted) {
+ mReflowCallbackPosted = true;
+ PresShell()->PostReflowCallback(this);
+ }
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<nsITreeSelection> nsTreeBodyFrame::GetSelection() const {
+ nsCOMPtr<nsITreeSelection> sel;
+ if (nsCOMPtr<nsITreeView> view = GetExistingView()) {
+ view->GetSelection(getter_AddRefs(sel));
+ }
+ return sel.forget();
+}
+
+nsresult nsTreeBodyFrame::SetFocused(bool aFocused) {
+ if (mFocused != aFocused) {
+ mFocused = aFocused;
+ if (nsCOMPtr<nsITreeSelection> sel = GetSelection()) {
+ sel->InvalidateSelection();
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::GetTreeBody(Element** aElement) {
+ // NS_ASSERTION(mContent, "no content, see bug #104878");
+ if (!mContent) return NS_ERROR_NULL_POINTER;
+
+ RefPtr<Element> element = mContent->AsElement();
+ element.forget(aElement);
+ return NS_OK;
+}
+
+int32_t nsTreeBodyFrame::RowHeight() const {
+ return nsPresContext::AppUnitsToIntCSSPixels(mRowHeight);
+}
+
+int32_t nsTreeBodyFrame::RowWidth() {
+ return nsPresContext::AppUnitsToIntCSSPixels(CalcHorzWidth(GetScrollParts()));
+}
+
+int32_t nsTreeBodyFrame::GetHorizontalPosition() const {
+ return nsPresContext::AppUnitsToIntCSSPixels(mHorzPosition);
+}
+
+Maybe<CSSIntRegion> nsTreeBodyFrame::GetSelectionRegion() {
+ if (!mView) {
+ return Nothing();
+ }
+
+ AutoWeakFrame wf(this);
+ nsCOMPtr<nsITreeSelection> selection = GetSelection();
+ if (!selection || !wf.IsAlive()) {
+ return Nothing();
+ }
+
+ RefPtr<nsPresContext> presContext = PresContext();
+ nsIntRect rect = mRect.ToOutsidePixels(AppUnitsPerCSSPixel());
+
+ nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame();
+ nsPoint origin = GetOffsetTo(rootFrame);
+
+ CSSIntRegion region;
+
+ // iterate through the visible rows and add the selected ones to the
+ // drag region
+ int32_t x = nsPresContext::AppUnitsToIntCSSPixels(origin.x);
+ int32_t y = nsPresContext::AppUnitsToIntCSSPixels(origin.y);
+ int32_t top = y;
+ int32_t end = LastVisibleRow();
+ int32_t rowHeight = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight);
+ for (int32_t i = mTopRowIndex; i <= end; i++) {
+ bool isSelected;
+ selection->IsSelected(i, &isSelected);
+ if (isSelected) {
+ region.OrWith(CSSIntRect(x, y, rect.width, rowHeight));
+ }
+ y += rowHeight;
+ }
+
+ // clip to the tree boundary in case one row extends past it
+ region.AndWith(CSSIntRect(x, top, rect.width, rect.height));
+
+ return Some(region);
+}
+
+nsresult nsTreeBodyFrame::Invalidate() {
+ if (mUpdateBatchNest) return NS_OK;
+
+ InvalidateFrame();
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::InvalidateColumn(nsTreeColumn* aCol) {
+ if (mUpdateBatchNest) return NS_OK;
+
+ if (!aCol) return NS_ERROR_INVALID_ARG;
+
+#ifdef ACCESSIBILITY
+ if (GetAccService()) {
+ FireInvalidateEvent(-1, -1, aCol, aCol);
+ }
+#endif // #ifdef ACCESSIBILITY
+
+ nsRect columnRect;
+ nsresult rv = aCol->GetRect(this, mInnerBox.y, mInnerBox.height, &columnRect);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // When false then column is out of view
+ if (OffsetForHorzScroll(columnRect, true))
+ InvalidateFrameWithRect(columnRect);
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::InvalidateRow(int32_t aIndex) {
+ if (mUpdateBatchNest) return NS_OK;
+
+#ifdef ACCESSIBILITY
+ if (GetAccService()) {
+ FireInvalidateEvent(aIndex, aIndex, nullptr, nullptr);
+ }
+#endif // #ifdef ACCESSIBILITY
+
+ aIndex -= mTopRowIndex;
+ if (aIndex < 0 || aIndex > mPageLength) return NS_OK;
+
+ nsRect rowRect(mInnerBox.x, mInnerBox.y + mRowHeight * aIndex,
+ mInnerBox.width, mRowHeight);
+ InvalidateFrameWithRect(rowRect);
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::InvalidateCell(int32_t aIndex, nsTreeColumn* aCol) {
+ if (mUpdateBatchNest) return NS_OK;
+
+#ifdef ACCESSIBILITY
+ if (GetAccService()) {
+ FireInvalidateEvent(aIndex, aIndex, aCol, aCol);
+ }
+#endif // #ifdef ACCESSIBILITY
+
+ aIndex -= mTopRowIndex;
+ if (aIndex < 0 || aIndex > mPageLength) return NS_OK;
+
+ if (!aCol) return NS_ERROR_INVALID_ARG;
+
+ nsRect cellRect;
+ nsresult rv = aCol->GetRect(this, mInnerBox.y + mRowHeight * aIndex,
+ mRowHeight, &cellRect);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (OffsetForHorzScroll(cellRect, true)) InvalidateFrameWithRect(cellRect);
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::InvalidateRange(int32_t aStart, int32_t aEnd) {
+ if (mUpdateBatchNest) return NS_OK;
+
+ if (aStart == aEnd) return InvalidateRow(aStart);
+
+ int32_t last = LastVisibleRow();
+ if (aStart > aEnd || aEnd < mTopRowIndex || aStart > last) return NS_OK;
+
+ if (aStart < mTopRowIndex) aStart = mTopRowIndex;
+
+ if (aEnd > last) aEnd = last;
+
+#ifdef ACCESSIBILITY
+ if (GetAccService()) {
+ int32_t end =
+ mRowCount > 0 ? ((mRowCount <= aEnd) ? mRowCount - 1 : aEnd) : 0;
+ FireInvalidateEvent(aStart, end, nullptr, nullptr);
+ }
+#endif // #ifdef ACCESSIBILITY
+
+ nsRect rangeRect(mInnerBox.x,
+ mInnerBox.y + mRowHeight * (aStart - mTopRowIndex),
+ mInnerBox.width, mRowHeight * (aEnd - aStart + 1));
+ InvalidateFrameWithRect(rangeRect);
+
+ return NS_OK;
+}
+
+static void FindScrollParts(nsIFrame* aCurrFrame,
+ nsTreeBodyFrame::ScrollParts* aResult) {
+ if (!aResult->mColumnsScrollFrame) {
+ nsIScrollableFrame* f = do_QueryFrame(aCurrFrame);
+ if (f) {
+ aResult->mColumnsFrame = aCurrFrame;
+ aResult->mColumnsScrollFrame = f;
+ }
+ }
+
+ nsScrollbarFrame* sf = do_QueryFrame(aCurrFrame);
+ if (sf) {
+ if (!aCurrFrame->IsXULHorizontal()) {
+ if (!aResult->mVScrollbar) {
+ aResult->mVScrollbar = sf;
+ }
+ } else {
+ if (!aResult->mHScrollbar) {
+ aResult->mHScrollbar = sf;
+ }
+ }
+ // don't bother searching inside a scrollbar
+ return;
+ }
+
+ nsIFrame* child = aCurrFrame->PrincipalChildList().FirstChild();
+ while (child && !child->GetContent()->IsRootOfNativeAnonymousSubtree() &&
+ (!aResult->mVScrollbar || !aResult->mHScrollbar ||
+ !aResult->mColumnsScrollFrame)) {
+ FindScrollParts(child, aResult);
+ child = child->GetNextSibling();
+ }
+}
+
+nsTreeBodyFrame::ScrollParts nsTreeBodyFrame::GetScrollParts() {
+ ScrollParts result = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
+ XULTreeElement* tree = GetBaseElement();
+ nsIFrame* treeFrame = tree ? tree->GetPrimaryFrame() : nullptr;
+ if (treeFrame) {
+ // The way we do this, searching through the entire frame subtree, is pretty
+ // dumb! We should know where these frames are.
+ FindScrollParts(treeFrame, &result);
+ if (result.mHScrollbar) {
+ result.mHScrollbar->SetScrollbarMediatorContent(GetContent());
+ nsIFrame* f = do_QueryFrame(result.mHScrollbar);
+ result.mHScrollbarContent = f->GetContent()->AsElement();
+ }
+ if (result.mVScrollbar) {
+ result.mVScrollbar->SetScrollbarMediatorContent(GetContent());
+ nsIFrame* f = do_QueryFrame(result.mVScrollbar);
+ result.mVScrollbarContent = f->GetContent()->AsElement();
+ }
+ }
+ return result;
+}
+
+void nsTreeBodyFrame::UpdateScrollbars(const ScrollParts& aParts) {
+ nscoord rowHeightAsPixels = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight);
+
+ AutoWeakFrame weakFrame(this);
+
+ if (aParts.mVScrollbar) {
+ nsAutoString curPos;
+ curPos.AppendInt(mTopRowIndex * rowHeightAsPixels);
+ aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos,
+ curPos, true);
+ // 'this' might be deleted here
+ }
+
+ if (weakFrame.IsAlive() && aParts.mHScrollbar) {
+ nsAutoString curPos;
+ curPos.AppendInt(mHorzPosition);
+ aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos,
+ curPos, true);
+ // 'this' might be deleted here
+ }
+
+ if (weakFrame.IsAlive() && mScrollbarActivity) {
+ mScrollbarActivity->ActivityOccurred();
+ }
+}
+
+void nsTreeBodyFrame::CheckOverflow(const ScrollParts& aParts) {
+ bool verticalOverflowChanged = false;
+ bool horizontalOverflowChanged = false;
+
+ if (!mVerticalOverflow && mRowCount > mPageLength) {
+ mVerticalOverflow = true;
+ verticalOverflowChanged = true;
+ } else if (mVerticalOverflow && mRowCount <= mPageLength) {
+ mVerticalOverflow = false;
+ verticalOverflowChanged = true;
+ }
+
+ if (aParts.mColumnsFrame) {
+ nsRect bounds = aParts.mColumnsFrame->GetRect();
+ if (bounds.width != 0) {
+ /* Ignore overflows that are less than half a pixel. Yes these happen
+ all over the place when flex boxes are compressed real small.
+ Probably a result of a rounding errors somewhere in the layout code. */
+ bounds.width += nsPresContext::CSSPixelsToAppUnits(0.5f);
+ if (!mHorizontalOverflow && bounds.width < mHorzWidth) {
+ mHorizontalOverflow = true;
+ horizontalOverflowChanged = true;
+ } else if (mHorizontalOverflow && bounds.width >= mHorzWidth) {
+ mHorizontalOverflow = false;
+ horizontalOverflowChanged = true;
+ }
+ }
+ }
+
+ if (!horizontalOverflowChanged && !verticalOverflowChanged) {
+ return;
+ }
+
+ AutoWeakFrame weakFrame(this);
+
+ RefPtr<nsPresContext> presContext = PresContext();
+ RefPtr<mozilla::PresShell> presShell = presContext->GetPresShell();
+ nsCOMPtr<nsIContent> content = mContent;
+
+ if (verticalOverflowChanged) {
+ InternalScrollPortEvent event(
+ true, mVerticalOverflow ? eScrollPortOverflow : eScrollPortUnderflow,
+ nullptr);
+ event.mOrient = InternalScrollPortEvent::eVertical;
+ EventDispatcher::Dispatch(content, presContext, &event);
+ }
+
+ if (horizontalOverflowChanged) {
+ InternalScrollPortEvent event(
+ true, mHorizontalOverflow ? eScrollPortOverflow : eScrollPortUnderflow,
+ nullptr);
+ event.mOrient = InternalScrollPortEvent::eHorizontal;
+ EventDispatcher::Dispatch(content, presContext, &event);
+ }
+
+ // The synchronous event dispatch above can trigger reflow notifications.
+ // Flush those explicitly now, so that we can guard against potential infinite
+ // recursion. See bug 905909.
+ if (!weakFrame.IsAlive()) {
+ return;
+ }
+ NS_ASSERTION(!mCheckingOverflow,
+ "mCheckingOverflow should not already be set");
+ // Don't use AutoRestore since we want to not touch mCheckingOverflow if we
+ // fail the weakFrame.IsAlive() check below
+ mCheckingOverflow = true;
+ presShell->FlushPendingNotifications(FlushType::Layout);
+ if (!weakFrame.IsAlive()) {
+ return;
+ }
+ mCheckingOverflow = false;
+}
+
+void nsTreeBodyFrame::InvalidateScrollbars(const ScrollParts& aParts,
+ AutoWeakFrame& aWeakColumnsFrame) {
+ if (mUpdateBatchNest || !mView) return;
+ AutoWeakFrame weakFrame(this);
+
+ if (aParts.mVScrollbar) {
+ // Do Vertical Scrollbar
+ nsAutoString maxposStr;
+
+ nscoord rowHeightAsPixels =
+ nsPresContext::AppUnitsToIntCSSPixels(mRowHeight);
+
+ int32_t size = rowHeightAsPixels *
+ (mRowCount > mPageLength ? mRowCount - mPageLength : 0);
+ maxposStr.AppendInt(size);
+ aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::maxpos,
+ maxposStr, true);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ // Also set our page increment and decrement.
+ nscoord pageincrement = mPageLength * rowHeightAsPixels;
+ nsAutoString pageStr;
+ pageStr.AppendInt(pageincrement);
+ aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None,
+ nsGkAtoms::pageincrement, pageStr, true);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+ }
+
+ if (aParts.mHScrollbar && aParts.mColumnsFrame &&
+ aWeakColumnsFrame.IsAlive()) {
+ // And now Horizontal scrollbar
+ nsRect bounds = aParts.mColumnsFrame->GetRect();
+ nsAutoString maxposStr;
+
+ maxposStr.AppendInt(mHorzWidth > bounds.width ? mHorzWidth - bounds.width
+ : 0);
+ aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::maxpos,
+ maxposStr, true);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ nsAutoString pageStr;
+ pageStr.AppendInt(bounds.width);
+ aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None,
+ nsGkAtoms::pageincrement, pageStr, true);
+ NS_ENSURE_TRUE_VOID(weakFrame.IsAlive());
+
+ pageStr.Truncate();
+ pageStr.AppendInt(nsPresContext::CSSPixelsToAppUnits(16));
+ aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::increment,
+ pageStr, true);
+ }
+
+ if (weakFrame.IsAlive() && mScrollbarActivity) {
+ mScrollbarActivity->ActivityOccurred();
+ }
+}
+
+// Takes client x/y in pixels, converts them to appunits, and converts into
+// values relative to this nsTreeBodyFrame frame.
+nsPoint nsTreeBodyFrame::AdjustClientCoordsToBoxCoordSpace(int32_t aX,
+ int32_t aY) {
+ nsPoint point(nsPresContext::CSSPixelsToAppUnits(aX),
+ nsPresContext::CSSPixelsToAppUnits(aY));
+
+ nsPresContext* presContext = PresContext();
+ point -= GetOffsetTo(presContext->GetPresShell()->GetRootFrame());
+
+ // Adjust by the inner box coords, so that we're in the inner box's
+ // coordinate space.
+ point -= mInnerBox.TopLeft();
+ return point;
+} // AdjustClientCoordsToBoxCoordSpace
+
+int32_t nsTreeBodyFrame::GetRowAt(int32_t aX, int32_t aY) {
+ if (!mView) {
+ return 0;
+ }
+
+ nsPoint point = AdjustClientCoordsToBoxCoordSpace(aX, aY);
+
+ // Check if the coordinates are above our visible space.
+ if (point.y < 0) {
+ return -1;
+ }
+
+ return GetRowAtInternal(point.x, point.y);
+}
+
+nsresult nsTreeBodyFrame::GetCellAt(int32_t aX, int32_t aY, int32_t* aRow,
+ nsTreeColumn** aCol,
+ nsACString& aChildElt) {
+ if (!mView) return NS_OK;
+
+ nsPoint point = AdjustClientCoordsToBoxCoordSpace(aX, aY);
+
+ // Check if the coordinates are above our visible space.
+ if (point.y < 0) {
+ *aRow = -1;
+ return NS_OK;
+ }
+
+ nsTreeColumn* col;
+ nsCSSAnonBoxPseudoStaticAtom* child;
+ GetCellAt(point.x, point.y, aRow, &col, &child);
+
+ if (col) {
+ NS_ADDREF(*aCol = col);
+ if (child == nsCSSAnonBoxes::mozTreeCell())
+ aChildElt.AssignLiteral("cell");
+ else if (child == nsCSSAnonBoxes::mozTreeTwisty())
+ aChildElt.AssignLiteral("twisty");
+ else if (child == nsCSSAnonBoxes::mozTreeImage())
+ aChildElt.AssignLiteral("image");
+ else if (child == nsCSSAnonBoxes::mozTreeCellText())
+ aChildElt.AssignLiteral("text");
+ }
+
+ return NS_OK;
+}
+
+//
+// GetCoordsForCellItem
+//
+// Find the x/y location and width/height (all in PIXELS) of the given object
+// in the given column.
+//
+// XXX IMPORTANT XXX:
+// Hyatt says in the bug for this, that the following needs to be done:
+// (1) You need to deal with overflow when computing cell rects. See other
+// column iteration examples... if you don't deal with this, you'll mistakenly
+// extend the cell into the scrollbar's rect.
+//
+// (2) You are adjusting the cell rect by the *row" border padding. That's
+// wrong. You need to first adjust a row rect by its border/padding, and then
+// the cell rect fits inside the adjusted row rect. It also can have
+// border/padding as well as margins. The vertical direction isn't that
+// important, but you need to get the horizontal direction right.
+//
+// (3) GetImageSize() does not include margins (but it does include
+// border/padding). You need to make sure to add in the image's margins as well.
+//
+nsresult nsTreeBodyFrame::GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol,
+ const nsACString& aElement,
+ int32_t* aX, int32_t* aY,
+ int32_t* aWidth,
+ int32_t* aHeight) {
+ *aX = 0;
+ *aY = 0;
+ *aWidth = 0;
+ *aHeight = 0;
+
+ bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
+ nscoord currX = mInnerBox.x - mHorzPosition;
+
+ // The Rect for the requested item.
+ nsRect theRect;
+
+ nsPresContext* presContext = PresContext();
+
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+
+ for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol;
+ currCol = currCol->GetNext()) {
+ // The Rect for the current cell.
+ nscoord colWidth;
+#ifdef DEBUG
+ nsresult rv =
+#endif
+ currCol->GetWidthInTwips(this, &colWidth);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "invalid column");
+
+ nsRect cellRect(currX, mInnerBox.y + mRowHeight * (aRow - mTopRowIndex),
+ colWidth, mRowHeight);
+
+ // Check the ID of the current column to see if it matches. If it doesn't
+ // increment the current X value and continue to the next column.
+ if (currCol != aCol) {
+ currX += cellRect.width;
+ continue;
+ }
+ // Now obtain the properties for our cell.
+ PrefillPropertyArray(aRow, currCol);
+
+ nsAutoString properties;
+ view->GetCellProperties(aRow, currCol, properties);
+ nsTreeUtils::TokenizeProperties(properties, mScratchArray);
+
+ ComputedStyle* rowContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow());
+
+ // We don't want to consider any of the decorations that may be present
+ // on the current row, so we have to deflate the rect by the border and
+ // padding and offset its left and top coordinates appropriately.
+ AdjustForBorderPadding(rowContext, cellRect);
+
+ ComputedStyle* cellContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell());
+
+ constexpr auto cell = "cell"_ns;
+ if (currCol->IsCycler() || cell.Equals(aElement)) {
+ // If the current Column is a Cycler, then the Rect is just the cell - the
+ // margins. Similarly, if we're just being asked for the cell rect,
+ // provide it.
+
+ theRect = cellRect;
+ nsMargin cellMargin;
+ cellContext->StyleMargin()->GetMargin(cellMargin);
+ theRect.Deflate(cellMargin);
+ break;
+ }
+
+ // Since we're not looking for the cell, and since the cell isn't a cycler,
+ // we're looking for some subcomponent, and now we need to subtract the
+ // borders and padding of the cell from cellRect so this does not
+ // interfere with our computations.
+ AdjustForBorderPadding(cellContext, cellRect);
+
+ RefPtr<gfxContext> rc =
+ presContext->PresShell()->CreateReferenceRenderingContext();
+
+ // Now we'll start making our way across the cell, starting at the edge of
+ // the cell and proceeding until we hit the right edge. |cellX| is the
+ // working X value that we will increment as we crawl from left to right.
+ nscoord cellX = cellRect.x;
+ nscoord remainWidth = cellRect.width;
+
+ if (currCol->IsPrimary()) {
+ // If the current Column is a Primary, then we need to take into account
+ // the indentation and possibly a twisty.
+
+ // The amount of indentation is the indentation width (|mIndentation|) by
+ // the level.
+ int32_t level;
+ view->GetLevel(aRow, &level);
+ if (!isRTL) cellX += mIndentation * level;
+ remainWidth -= mIndentation * level;
+
+ // Find the twisty rect by computing its size.
+ nsRect imageRect;
+ nsRect twistyRect(cellRect);
+ ComputedStyle* twistyContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty());
+ GetTwistyRect(aRow, currCol, imageRect, twistyRect, presContext,
+ twistyContext);
+
+ if ("twisty"_ns.Equals(aElement)) {
+ // If we're looking for the twisty Rect, just return the size
+ theRect = twistyRect;
+ break;
+ }
+
+ // Now we need to add in the margins of the twisty element, so that we
+ // can find the offset of the next element in the cell.
+ nsMargin twistyMargin;
+ twistyContext->StyleMargin()->GetMargin(twistyMargin);
+ twistyRect.Inflate(twistyMargin);
+
+ // Adjust our working X value with the twisty width (image size, margins,
+ // borders, padding.
+ if (!isRTL) cellX += twistyRect.width;
+ }
+
+ // Cell Image
+ ComputedStyle* imageContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage());
+
+ nsRect imageSize = GetImageSize(aRow, currCol, false, imageContext);
+ if ("image"_ns.Equals(aElement)) {
+ theRect = imageSize;
+ theRect.x = cellX;
+ theRect.y = cellRect.y;
+ break;
+ }
+
+ // Add in the margins of the cell image.
+ nsMargin imageMargin;
+ imageContext->StyleMargin()->GetMargin(imageMargin);
+ imageSize.Inflate(imageMargin);
+
+ // Increment cellX by the image width
+ if (!isRTL) cellX += imageSize.width;
+
+ // Cell Text
+ nsAutoString cellText;
+ view->GetCellText(aRow, currCol, cellText);
+ // We're going to measure this text so we need to ensure bidi is enabled if
+ // necessary
+ CheckTextForBidi(cellText);
+
+ // Create a scratch rect to represent the text rectangle, with the current
+ // X and Y coords, and a guess at the width and height. The width is the
+ // remaining width we have left to traverse in the cell, which will be the
+ // widest possible value for the text rect, and the row height.
+ nsRect textRect(cellX, cellRect.y, remainWidth, cellRect.height);
+
+ // Measure the width of the text. If the width of the text is greater than
+ // the remaining width available, then we just assume that the text has
+ // been cropped and use the remaining rect as the text Rect. Otherwise,
+ // we add in borders and padding to the text dimension and give that back.
+ ComputedStyle* textContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText());
+
+ RefPtr<nsFontMetrics> fm =
+ nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, presContext);
+ nscoord height = fm->MaxHeight();
+
+ nsMargin textMargin;
+ textContext->StyleMargin()->GetMargin(textMargin);
+ textRect.Deflate(textMargin);
+
+ // Center the text. XXX Obey vertical-align style prop?
+ if (height < textRect.height) {
+ textRect.y += (textRect.height - height) / 2;
+ textRect.height = height;
+ }
+
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(textContext, bp);
+ textRect.height += bp.top + bp.bottom;
+
+ AdjustForCellText(cellText, aRow, currCol, *rc, *fm, textRect);
+
+ theRect = textRect;
+ }
+
+ if (isRTL) theRect.x = mInnerBox.width - theRect.x - theRect.width;
+
+ *aX = nsPresContext::AppUnitsToIntCSSPixels(theRect.x);
+ *aY = nsPresContext::AppUnitsToIntCSSPixels(theRect.y);
+ *aWidth = nsPresContext::AppUnitsToIntCSSPixels(theRect.width);
+ *aHeight = nsPresContext::AppUnitsToIntCSSPixels(theRect.height);
+
+ return NS_OK;
+}
+
+int32_t nsTreeBodyFrame::GetRowAtInternal(nscoord aX, nscoord aY) {
+ if (mRowHeight <= 0) return -1;
+
+ // Now just mod by our total inner box height and add to our top row index.
+ int32_t row = (aY / mRowHeight) + mTopRowIndex;
+
+ // Check if the coordinates are below our visible space (or within our visible
+ // space but below any row).
+ if (row > mTopRowIndex + mPageLength || row >= mRowCount) return -1;
+
+ return row;
+}
+
+void nsTreeBodyFrame::CheckTextForBidi(nsAutoString& aText) {
+ // We could check to see whether the prescontext already has bidi enabled,
+ // but usually it won't, so it's probably faster to avoid the call to
+ // GetPresContext() when it's not needed.
+ if (HasRTLChars(aText)) {
+ PresContext()->SetBidiEnabled();
+ }
+}
+
+void nsTreeBodyFrame::AdjustForCellText(nsAutoString& aText, int32_t aRowIndex,
+ nsTreeColumn* aColumn,
+ gfxContext& aRenderingContext,
+ nsFontMetrics& aFontMetrics,
+ nsRect& aTextRect) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+
+ nscoord maxWidth = aTextRect.width;
+ bool widthIsGreater = nsLayoutUtils::StringWidthIsGreaterThan(
+ aText, aFontMetrics, drawTarget, maxWidth);
+
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ if (aColumn->Overflow()) {
+ DebugOnly<nsresult> rv;
+ nsTreeColumn* nextColumn = aColumn->GetNext();
+ while (nextColumn && widthIsGreater) {
+ while (nextColumn) {
+ nscoord width;
+ rv = nextColumn->GetWidthInTwips(this, &width);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "nextColumn is invalid");
+
+ if (width != 0) {
+ break;
+ }
+
+ nextColumn = nextColumn->GetNext();
+ }
+
+ if (nextColumn) {
+ nsAutoString nextText;
+ view->GetCellText(aRowIndex, nextColumn, nextText);
+ // We don't measure or draw this text so no need to check it for
+ // bidi-ness
+
+ if (nextText.Length() == 0) {
+ nscoord width;
+ rv = nextColumn->GetWidthInTwips(this, &width);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "nextColumn is invalid");
+
+ maxWidth += width;
+ widthIsGreater = nsLayoutUtils::StringWidthIsGreaterThan(
+ aText, aFontMetrics, drawTarget, maxWidth);
+
+ nextColumn = nextColumn->GetNext();
+ } else {
+ nextColumn = nullptr;
+ }
+ }
+ }
+ }
+
+ using CroppingStyle = nsTextBoxFrame::CroppingStyle;
+ CroppingStyle cropType = CroppingStyle::CropRight;
+ if (aColumn->GetCropStyle() == 1) {
+ cropType = CroppingStyle::CropCenter;
+ } else if (aColumn->GetCropStyle() == 2) {
+ cropType = CroppingStyle::CropLeft;
+ }
+ nsTextBoxFrame::CropStringForWidth(aText, aRenderingContext, aFontMetrics,
+ maxWidth, cropType);
+
+ nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi(
+ aText, this, aFontMetrics, aRenderingContext);
+
+ switch (aColumn->GetTextAlignment()) {
+ case mozilla::StyleTextAlign::Right:
+ aTextRect.x += aTextRect.width - width;
+ break;
+ case mozilla::StyleTextAlign::Center:
+ aTextRect.x += (aTextRect.width - width) / 2;
+ break;
+ default:
+ break;
+ }
+
+ aTextRect.width = width;
+}
+
+nsCSSAnonBoxPseudoStaticAtom* nsTreeBodyFrame::GetItemWithinCellAt(
+ nscoord aX, const nsRect& aCellRect, int32_t aRowIndex,
+ nsTreeColumn* aColumn) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ // Obtain the properties for our cell.
+ PrefillPropertyArray(aRowIndex, aColumn);
+ nsAutoString properties;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ view->GetCellProperties(aRowIndex, aColumn, properties);
+ nsTreeUtils::TokenizeProperties(properties, mScratchArray);
+
+ // Resolve style for the cell.
+ ComputedStyle* cellContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell());
+
+ // Obtain the margins for the cell and then deflate our rect by that
+ // amount. The cell is assumed to be contained within the deflated rect.
+ nsRect cellRect(aCellRect);
+ nsMargin cellMargin;
+ cellContext->StyleMargin()->GetMargin(cellMargin);
+ cellRect.Deflate(cellMargin);
+
+ // Adjust the rect for its border and padding.
+ AdjustForBorderPadding(cellContext, cellRect);
+
+ if (aX < cellRect.x || aX >= cellRect.x + cellRect.width) {
+ // The user clicked within the cell's margins/borders/padding. This
+ // constitutes a click on the cell.
+ return nsCSSAnonBoxes::mozTreeCell();
+ }
+
+ nscoord currX = cellRect.x;
+ nscoord remainingWidth = cellRect.width;
+
+ // Handle right alignment hit testing.
+ bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
+
+ nsPresContext* presContext = PresContext();
+ RefPtr<gfxContext> rc =
+ presContext->PresShell()->CreateReferenceRenderingContext();
+
+ if (aColumn->IsPrimary()) {
+ // If we're the primary column, we have indentation and a twisty.
+ int32_t level;
+ view->GetLevel(aRowIndex, &level);
+
+ if (!isRTL) currX += mIndentation * level;
+ remainingWidth -= mIndentation * level;
+
+ if ((isRTL && aX > currX + remainingWidth) || (!isRTL && aX < currX)) {
+ // The user clicked within the indentation.
+ return nsCSSAnonBoxes::mozTreeCell();
+ }
+
+ // Always leave space for the twisty.
+ nsRect twistyRect(currX, cellRect.y, remainingWidth, cellRect.height);
+ bool hasTwisty = false;
+ bool isContainer = false;
+ view->IsContainer(aRowIndex, &isContainer);
+ if (isContainer) {
+ bool isContainerEmpty = false;
+ view->IsContainerEmpty(aRowIndex, &isContainerEmpty);
+ if (!isContainerEmpty) hasTwisty = true;
+ }
+
+ // Resolve style for the twisty.
+ ComputedStyle* twistyContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty());
+
+ nsRect imageSize;
+ GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, presContext,
+ twistyContext);
+
+ // We will treat a click as hitting the twisty if it happens on the margins,
+ // borders, padding, or content of the twisty object. By allowing a "slop"
+ // into the margin, we make it a little bit easier for a user to hit the
+ // twisty. (We don't want to be too picky here.)
+ nsMargin twistyMargin;
+ twistyContext->StyleMargin()->GetMargin(twistyMargin);
+ twistyRect.Inflate(twistyMargin);
+ if (isRTL) twistyRect.x = currX + remainingWidth - twistyRect.width;
+
+ // Now we test to see if aX is actually within the twistyRect. If it is,
+ // and if the item should have a twisty, then we return "twisty". If it is
+ // within the rect but we shouldn't have a twisty, then we return "cell".
+ if (aX >= twistyRect.x && aX < twistyRect.x + twistyRect.width) {
+ if (hasTwisty)
+ return nsCSSAnonBoxes::mozTreeTwisty();
+ else
+ return nsCSSAnonBoxes::mozTreeCell();
+ }
+
+ if (!isRTL) currX += twistyRect.width;
+ remainingWidth -= twistyRect.width;
+ }
+
+ // Now test to see if the user hit the icon for the cell.
+ nsRect iconRect(currX, cellRect.y, remainingWidth, cellRect.height);
+
+ // Resolve style for the image.
+ ComputedStyle* imageContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage());
+
+ nsRect iconSize = GetImageSize(aRowIndex, aColumn, false, imageContext);
+ nsMargin imageMargin;
+ imageContext->StyleMargin()->GetMargin(imageMargin);
+ iconSize.Inflate(imageMargin);
+ iconRect.width = iconSize.width;
+ if (isRTL) iconRect.x = currX + remainingWidth - iconRect.width;
+
+ if (aX >= iconRect.x && aX < iconRect.x + iconRect.width) {
+ // The user clicked on the image.
+ return nsCSSAnonBoxes::mozTreeImage();
+ }
+
+ if (!isRTL) currX += iconRect.width;
+ remainingWidth -= iconRect.width;
+
+ nsAutoString cellText;
+ view->GetCellText(aRowIndex, aColumn, cellText);
+ // We're going to measure this text so we need to ensure bidi is enabled if
+ // necessary
+ CheckTextForBidi(cellText);
+
+ nsRect textRect(currX, cellRect.y, remainingWidth, cellRect.height);
+
+ ComputedStyle* textContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText());
+
+ nsMargin textMargin;
+ textContext->StyleMargin()->GetMargin(textMargin);
+ textRect.Deflate(textMargin);
+
+ AdjustForBorderPadding(textContext, textRect);
+
+ RefPtr<nsFontMetrics> fm =
+ nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, presContext);
+ AdjustForCellText(cellText, aRowIndex, aColumn, *rc, *fm, textRect);
+
+ if (aX >= textRect.x && aX < textRect.x + textRect.width)
+ return nsCSSAnonBoxes::mozTreeCellText();
+ else
+ return nsCSSAnonBoxes::mozTreeCell();
+}
+
+void nsTreeBodyFrame::GetCellAt(nscoord aX, nscoord aY, int32_t* aRow,
+ nsTreeColumn** aCol,
+ nsCSSAnonBoxPseudoStaticAtom** aChildElt) {
+ *aCol = nullptr;
+ *aChildElt = nullptr;
+
+ *aRow = GetRowAtInternal(aX, aY);
+ if (*aRow < 0) return;
+
+ // Determine the column hit.
+ for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol;
+ currCol = currCol->GetNext()) {
+ nsRect cellRect;
+ nsresult rv = currCol->GetRect(
+ this, mInnerBox.y + mRowHeight * (*aRow - mTopRowIndex), mRowHeight,
+ &cellRect);
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT_UNREACHABLE("column has no frame");
+ continue;
+ }
+
+ if (!OffsetForHorzScroll(cellRect, false)) continue;
+
+ if (aX >= cellRect.x && aX < cellRect.x + cellRect.width) {
+ // We know the column hit now.
+ *aCol = currCol;
+
+ if (currCol->IsCycler())
+ // Cyclers contain only images. Fill this in immediately and return.
+ *aChildElt = nsCSSAnonBoxes::mozTreeImage();
+ else
+ *aChildElt = GetItemWithinCellAt(aX, cellRect, *aRow, currCol);
+ break;
+ }
+ }
+}
+
+nsresult nsTreeBodyFrame::GetCellWidth(int32_t aRow, nsTreeColumn* aCol,
+ gfxContext* aRenderingContext,
+ nscoord& aDesiredSize,
+ nscoord& aCurrentSize) {
+ MOZ_ASSERT(aCol, "aCol must not be null");
+ MOZ_ASSERT(aRenderingContext, "aRenderingContext must not be null");
+
+ // The rect for the current cell.
+ nscoord colWidth;
+ nsresult rv = aCol->GetWidthInTwips(this, &colWidth);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsRect cellRect(0, 0, colWidth, mRowHeight);
+
+ int32_t overflow =
+ cellRect.x + cellRect.width - (mInnerBox.x + mInnerBox.width);
+ if (overflow > 0) cellRect.width -= overflow;
+
+ // Adjust borders and padding for the cell.
+ ComputedStyle* cellContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell());
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(cellContext, bp);
+
+ aCurrentSize = cellRect.width;
+ aDesiredSize = bp.left + bp.right;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+
+ if (aCol->IsPrimary()) {
+ // If the current Column is a Primary, then we need to take into account
+ // the indentation and possibly a twisty.
+
+ // The amount of indentation is the indentation width (|mIndentation|) by
+ // the level.
+ int32_t level;
+ view->GetLevel(aRow, &level);
+ aDesiredSize += mIndentation * level;
+
+ // Find the twisty rect by computing its size.
+ ComputedStyle* twistyContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty());
+
+ nsRect imageSize;
+ nsRect twistyRect(cellRect);
+ GetTwistyRect(aRow, aCol, imageSize, twistyRect, PresContext(),
+ twistyContext);
+
+ // Add in the margins of the twisty element.
+ nsMargin twistyMargin;
+ twistyContext->StyleMargin()->GetMargin(twistyMargin);
+ twistyRect.Inflate(twistyMargin);
+
+ aDesiredSize += twistyRect.width;
+ }
+
+ ComputedStyle* imageContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage());
+
+ // Account for the width of the cell image.
+ nsRect imageSize = GetImageSize(aRow, aCol, false, imageContext);
+ // Add in the margins of the cell image.
+ nsMargin imageMargin;
+ imageContext->StyleMargin()->GetMargin(imageMargin);
+ imageSize.Inflate(imageMargin);
+
+ aDesiredSize += imageSize.width;
+
+ // Get the cell text.
+ nsAutoString cellText;
+ view->GetCellText(aRow, aCol, cellText);
+ // We're going to measure this text so we need to ensure bidi is enabled if
+ // necessary
+ CheckTextForBidi(cellText);
+
+ ComputedStyle* textContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText());
+
+ // Get the borders and padding for the text.
+ GetBorderPadding(textContext, bp);
+
+ RefPtr<nsFontMetrics> fm =
+ nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, PresContext());
+ // Get the width of the text itself
+ nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi(cellText, this, *fm,
+ *aRenderingContext);
+ nscoord totalTextWidth = width + bp.left + bp.right;
+ aDesiredSize += totalTextWidth;
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::IsCellCropped(int32_t aRow, nsTreeColumn* aCol,
+ bool* _retval) {
+ nscoord currentSize, desiredSize;
+ nsresult rv;
+
+ if (!aCol) return NS_ERROR_INVALID_ARG;
+
+ RefPtr<gfxContext> rc = PresShell()->CreateReferenceRenderingContext();
+
+ rv = GetCellWidth(aRow, aCol, rc, desiredSize, currentSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *_retval = desiredSize > currentSize;
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::CreateTimer(const LookAndFeel::IntID aID,
+ nsTimerCallbackFunc aFunc, int32_t aType,
+ nsITimer** aTimer, const char* aName) {
+ // Get the delay from the look and feel service.
+ int32_t delay = LookAndFeel::GetInt(aID, 0);
+
+ nsCOMPtr<nsITimer> timer;
+
+ // Create a new timer only if the delay is greater than zero.
+ // Zero value means that this feature is completely disabled.
+ if (delay > 0) {
+ MOZ_TRY_VAR(timer,
+ NS_NewTimerWithFuncCallback(
+ aFunc, this, delay, aType, aName,
+ mContent->OwnerDoc()->EventTargetFor(TaskCategory::Other)));
+ }
+
+ timer.forget(aTimer);
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::RowCountChanged(int32_t aIndex, int32_t aCount) {
+ if (aCount == 0 || !mView) {
+ return NS_OK; // Nothing to do.
+ }
+
+#ifdef ACCESSIBILITY
+ if (GetAccService()) {
+ FireRowCountChangedEvent(aIndex, aCount);
+ }
+#endif // #ifdef ACCESSIBILITY
+
+ AutoWeakFrame weakFrame(this);
+
+ // Adjust our selection.
+ if (nsCOMPtr<nsITreeSelection> sel = GetSelection()) {
+ sel->AdjustSelection(aIndex, aCount);
+ }
+
+ NS_ENSURE_STATE(weakFrame.IsAlive());
+
+ if (mUpdateBatchNest) return NS_OK;
+
+ mRowCount += aCount;
+#ifdef DEBUG
+ int32_t rowCount = mRowCount;
+ mView->GetRowCount(&rowCount);
+ NS_ASSERTION(
+ rowCount == mRowCount,
+ "row count did not change by the amount suggested, check caller");
+#endif
+
+ int32_t count = Abs(aCount);
+ int32_t last = LastVisibleRow();
+ if (aIndex >= mTopRowIndex && aIndex <= last) InvalidateRange(aIndex, last);
+
+ ScrollParts parts = GetScrollParts();
+
+ if (mTopRowIndex == 0) {
+ // Just update the scrollbar and return.
+ FullScrollbarsUpdate(false);
+ return NS_OK;
+ }
+
+ bool needsInvalidation = false;
+ // Adjust our top row index.
+ if (aCount > 0) {
+ if (mTopRowIndex > aIndex) {
+ // Rows came in above us. Augment the top row index.
+ mTopRowIndex += aCount;
+ }
+ } else if (aCount < 0) {
+ if (mTopRowIndex > aIndex + count - 1) {
+ // No need to invalidate. The remove happened
+ // completely above us (offscreen).
+ mTopRowIndex -= count;
+ } else if (mTopRowIndex >= aIndex) {
+ // This is a full-blown invalidate.
+ if (mTopRowIndex + mPageLength > mRowCount - 1) {
+ mTopRowIndex = std::max(0, mRowCount - 1 - mPageLength);
+ }
+ needsInvalidation = true;
+ }
+ }
+
+ FullScrollbarsUpdate(needsInvalidation);
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::BeginUpdateBatch() {
+ ++mUpdateBatchNest;
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::EndUpdateBatch() {
+ NS_ASSERTION(mUpdateBatchNest > 0, "badly nested update batch");
+
+ if (--mUpdateBatchNest != 0) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ if (!view) {
+ return NS_OK;
+ }
+
+ Invalidate();
+ int32_t countBeforeUpdate = mRowCount;
+ view->GetRowCount(&mRowCount);
+ if (countBeforeUpdate != mRowCount) {
+ if (mTopRowIndex + mPageLength > mRowCount - 1) {
+ mTopRowIndex = std::max(0, mRowCount - 1 - mPageLength);
+ }
+ FullScrollbarsUpdate(false);
+ }
+
+ return NS_OK;
+}
+
+void nsTreeBodyFrame::PrefillPropertyArray(int32_t aRowIndex,
+ nsTreeColumn* aCol) {
+ MOZ_ASSERT(!aCol || aCol->GetFrame(), "invalid column passed");
+ mScratchArray.Clear();
+
+ // focus
+ if (mFocused)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::focus);
+ else
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::blur);
+
+ // sort
+ bool sorted = false;
+ mView->IsSorted(&sorted);
+ if (sorted) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::sorted);
+
+ // drag session
+ if (mSlots && mSlots->mIsDragging)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dragSession);
+
+ if (aRowIndex != -1) {
+ if (aRowIndex == mMouseOverRow)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::hover);
+
+ nsCOMPtr<nsITreeSelection> selection = GetSelection();
+ if (selection) {
+ // selected
+ bool isSelected;
+ selection->IsSelected(aRowIndex, &isSelected);
+ if (isSelected)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::selected);
+
+ // current
+ int32_t currentIndex;
+ selection->GetCurrentIndex(&currentIndex);
+ if (aRowIndex == currentIndex)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::current);
+ }
+
+ // container or leaf
+ bool isContainer = false;
+ mView->IsContainer(aRowIndex, &isContainer);
+ if (isContainer) {
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::container);
+
+ // open or closed
+ bool isOpen = false;
+ mView->IsContainerOpen(aRowIndex, &isOpen);
+ if (isOpen)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::open);
+ else
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::closed);
+ } else {
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::leaf);
+ }
+
+ // drop orientation
+ if (mSlots && mSlots->mDropAllowed && mSlots->mDropRow == aRowIndex) {
+ if (mSlots->mDropOrient == nsITreeView::DROP_BEFORE)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropBefore);
+ else if (mSlots->mDropOrient == nsITreeView::DROP_ON)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropOn);
+ else if (mSlots->mDropOrient == nsITreeView::DROP_AFTER)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropAfter);
+ }
+
+ // odd or even
+ if (aRowIndex % 2)
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::odd);
+ else
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::even);
+
+ XULTreeElement* tree = GetBaseElement();
+ if (tree && tree->HasAttr(kNameSpaceID_None, nsGkAtoms::editing)) {
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::editing);
+ }
+
+ // multiple columns
+ if (mColumns->GetColumnAt(1))
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::multicol);
+ }
+
+ if (aCol) {
+ mScratchArray.AppendElement(aCol->GetAtom());
+
+ if (aCol->IsPrimary())
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::primary);
+
+ if (aCol->GetType() == TreeColumn_Binding::TYPE_CHECKBOX) {
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::checkbox);
+
+ if (aRowIndex != -1) {
+ nsAutoString value;
+ mView->GetCellValue(aRowIndex, aCol, value);
+ if (value.EqualsLiteral("true"))
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::checked);
+ }
+ }
+
+ // Read special properties from attributes on the column content node
+ if (aCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::insertbefore,
+ nsGkAtoms::_true, eCaseMatters))
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::insertbefore);
+ if (aCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::insertafter,
+ nsGkAtoms::_true, eCaseMatters))
+ mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::insertafter);
+ }
+}
+
+nsITheme* nsTreeBodyFrame::GetTwistyRect(int32_t aRowIndex,
+ nsTreeColumn* aColumn,
+ nsRect& aImageRect,
+ nsRect& aTwistyRect,
+ nsPresContext* aPresContext,
+ ComputedStyle* aTwistyContext) {
+ // The twisty rect extends all the way to the end of the cell. This is
+ // incorrect. We need to determine the twisty rect's true width. This is
+ // done by examining the ComputedStyle for a width first. If it has one, we
+ // use that. If it doesn't, we use the image's natural width. If the image
+ // hasn't loaded and if no width is specified, then we just bail. If there is
+ // a -moz-appearance involved, adjust the rect by the minimum widget size
+ // provided by the theme implementation.
+ aImageRect = GetImageSize(aRowIndex, aColumn, true, aTwistyContext);
+ if (aImageRect.height > aTwistyRect.height)
+ aImageRect.height = aTwistyRect.height;
+ if (aImageRect.width > aTwistyRect.width)
+ aImageRect.width = aTwistyRect.width;
+ else
+ aTwistyRect.width = aImageRect.width;
+
+ bool useTheme = false;
+ nsITheme* theme = nullptr;
+ StyleAppearance appearance =
+ aTwistyContext->StyleDisplay()->EffectiveAppearance();
+ if (appearance != StyleAppearance::None) {
+ theme = aPresContext->Theme();
+ if (theme->ThemeSupportsWidget(aPresContext, nullptr, appearance))
+ useTheme = true;
+ }
+
+ if (useTheme) {
+ LayoutDeviceIntSize minTwistySizePx =
+ theme->GetMinimumWidgetSize(aPresContext, this, appearance);
+
+ // GMWS() returns size in pixels, we need to convert it back to app units
+ nsSize minTwistySize;
+ minTwistySize.width =
+ aPresContext->DevPixelsToAppUnits(minTwistySizePx.width);
+ minTwistySize.height =
+ aPresContext->DevPixelsToAppUnits(minTwistySizePx.height);
+
+ if (aTwistyRect.width < minTwistySize.width) {
+ aTwistyRect.width = minTwistySize.width;
+ }
+ }
+
+ return useTheme ? theme : nullptr;
+}
+
+nsresult nsTreeBodyFrame::GetImage(int32_t aRowIndex, nsTreeColumn* aCol,
+ bool aUseContext,
+ ComputedStyle* aComputedStyle,
+ bool& aAllowImageRegions,
+ imgIContainer** aResult) {
+ *aResult = nullptr;
+
+ nsAutoString imageSrc;
+ mView->GetImageSrc(aRowIndex, aCol, imageSrc);
+ RefPtr<imgRequestProxy> styleRequest;
+ if (!aUseContext && !imageSrc.IsEmpty()) {
+ aAllowImageRegions = false;
+ } else {
+ // Obtain the URL from the ComputedStyle.
+ aAllowImageRegions = true;
+ styleRequest =
+ aComputedStyle->StyleList()->mListStyleImage.GetImageRequest();
+ if (!styleRequest) return NS_OK;
+ nsCOMPtr<nsIURI> uri;
+ styleRequest->GetURI(getter_AddRefs(uri));
+ nsAutoCString spec;
+ nsresult rv = uri->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ CopyUTF8toUTF16(spec, imageSrc);
+ }
+
+ // Look the image up in our cache.
+ nsTreeImageCacheEntry entry;
+ if (mImageCache.Get(imageSrc, &entry)) {
+ // Find out if the image has loaded.
+ uint32_t status;
+ imgIRequest* imgReq = entry.request;
+ imgReq->GetImageStatus(&status);
+ imgReq->GetImage(aResult); // We hand back the image here. The GetImage
+ // call addrefs *aResult.
+ bool animated = true; // Assuming animated is the safe option
+
+ // We can only call GetAnimated if we're decoded
+ if (*aResult && (status & imgIRequest::STATUS_DECODE_COMPLETE))
+ (*aResult)->GetAnimated(&animated);
+
+ if ((!(status & imgIRequest::STATUS_LOAD_COMPLETE)) || animated) {
+ // We either aren't done loading, or we're animating. Add our row as a
+ // listener for invalidations.
+ nsCOMPtr<imgINotificationObserver> obs;
+ imgReq->GetNotificationObserver(getter_AddRefs(obs));
+
+ if (obs) {
+ static_cast<nsTreeImageListener*>(obs.get())->AddCell(aRowIndex, aCol);
+ }
+
+ return NS_OK;
+ }
+ }
+
+ if (!*aResult) {
+ // Create a new nsTreeImageListener object and pass it our row and column
+ // information.
+ nsTreeImageListener* listener = new nsTreeImageListener(this);
+ if (!listener) return NS_ERROR_OUT_OF_MEMORY;
+
+ mCreatedListeners.Insert(listener);
+
+ listener->AddCell(aRowIndex, aCol);
+ nsCOMPtr<imgINotificationObserver> imgNotificationObserver = listener;
+
+ Document* doc = mContent->GetComposedDoc();
+ if (!doc)
+ // The page is currently being torn down. Why bother.
+ return NS_ERROR_FAILURE;
+
+ RefPtr<imgRequestProxy> imageRequest;
+ if (styleRequest) {
+ styleRequest->SyncClone(imgNotificationObserver, doc,
+ getter_AddRefs(imageRequest));
+ } else {
+ nsCOMPtr<nsIURI> srcURI;
+ nsContentUtils::NewURIWithDocumentCharset(
+ getter_AddRefs(srcURI), imageSrc, doc, mContent->GetBaseURI());
+ if (!srcURI) return NS_ERROR_FAILURE;
+
+ auto referrerInfo = MakeRefPtr<mozilla::dom::ReferrerInfo>(*doc);
+
+ // XXXbz what's the origin principal for this stuff that comes from our
+ // view? I guess we should assume that it's the node's principal...
+ nsresult rv = nsContentUtils::LoadImage(
+ srcURI, mContent, doc, mContent->NodePrincipal(), 0, referrerInfo,
+ imgNotificationObserver, nsIRequest::LOAD_NORMAL, u""_ns,
+ getter_AddRefs(imageRequest));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // NOTE(heycam): If it's an SVG image, and we need to want the image to
+ // able to respond to media query changes, it needs to be added to the
+ // document's ImageTracker (like nsImageBoxFrame does). For now, assume
+ // we don't need this.
+ }
+ listener->UnsuppressInvalidation();
+
+ if (!imageRequest) return NS_ERROR_FAILURE;
+
+ // We don't want discarding/decode-on-draw for xul images
+ imageRequest->StartDecoding(imgIContainer::FLAG_ASYNC_NOTIFY);
+ imageRequest->LockImage();
+
+ // In a case it was already cached.
+ imageRequest->GetImage(aResult);
+ nsTreeImageCacheEntry cacheEntry(imageRequest, imgNotificationObserver);
+ mImageCache.InsertOrUpdate(imageSrc, cacheEntry);
+ }
+ return NS_OK;
+}
+
+nsRect nsTreeBodyFrame::GetImageSize(int32_t aRowIndex, nsTreeColumn* aCol,
+ bool aUseContext,
+ ComputedStyle* aComputedStyle) {
+ // XXX We should respond to visibility rules for collapsed vs. hidden.
+
+ // This method returns the width of the twisty INCLUDING borders and padding.
+ // It first checks the ComputedStyle for a width. If none is found, it tries
+ // to use the default image width for the twisty. If no image is found, it
+ // defaults to border+padding.
+ nsRect r(0, 0, 0, 0);
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(aComputedStyle, bp);
+ r.Inflate(bp);
+
+ // Now r contains our border+padding info. We now need to get our width and
+ // height.
+ bool needWidth = false;
+ bool needHeight = false;
+
+ // We have to load image even though we already have a size.
+ // Don't change this, otherwise things start to go awry.
+ bool useImageRegion = true;
+ nsCOMPtr<imgIContainer> image;
+ GetImage(aRowIndex, aCol, aUseContext, aComputedStyle, useImageRegion,
+ getter_AddRefs(image));
+
+ const nsStylePosition* myPosition = aComputedStyle->StylePosition();
+ const nsStyleList* myList = aComputedStyle->StyleList();
+ nsRect imageRegion = myList->GetImageRegion();
+ if (useImageRegion) {
+ r.x += imageRegion.x;
+ r.y += imageRegion.y;
+ }
+
+ if (myPosition->mWidth.ConvertsToLength()) {
+ int32_t val = myPosition->mWidth.ToLength();
+ r.width += val;
+ } else if (useImageRegion && imageRegion.width > 0) {
+ r.width += imageRegion.width;
+ } else {
+ needWidth = true;
+ }
+
+ if (myPosition->mHeight.ConvertsToLength()) {
+ int32_t val = myPosition->mHeight.ToLength();
+ r.height += val;
+ } else if (useImageRegion && imageRegion.height > 0)
+ r.height += imageRegion.height;
+ else
+ needHeight = true;
+
+ if (image) {
+ if (needWidth || needHeight) {
+ // Get the natural image size.
+
+ if (needWidth) {
+ // Get the size from the image.
+ nscoord width;
+ image->GetWidth(&width);
+ r.width += nsPresContext::CSSPixelsToAppUnits(width);
+ }
+
+ if (needHeight) {
+ nscoord height;
+ image->GetHeight(&height);
+ r.height += nsPresContext::CSSPixelsToAppUnits(height);
+ }
+ }
+ }
+
+ return r;
+}
+
+// GetImageDestSize returns the destination size of the image.
+// The width and height do not include borders and padding.
+// The width and height have not been adjusted to fit in the row height
+// or cell width.
+// The width and height reflect the destination size specified in CSS,
+// or the image region specified in CSS, or the natural size of the
+// image.
+// If only the destination width has been specified in CSS, the height is
+// calculated to maintain the aspect ratio of the image.
+// If only the destination height has been specified in CSS, the width is
+// calculated to maintain the aspect ratio of the image.
+nsSize nsTreeBodyFrame::GetImageDestSize(ComputedStyle* aComputedStyle,
+ bool useImageRegion,
+ imgIContainer* image) {
+ nsSize size(0, 0);
+
+ // We need to get the width and height.
+ bool needWidth = false;
+ bool needHeight = false;
+
+ // Get the style position to see if the CSS has specified the
+ // destination width/height.
+ const nsStylePosition* myPosition = aComputedStyle->StylePosition();
+
+ if (myPosition->mWidth.ConvertsToLength()) {
+ // CSS has specified the destination width.
+ size.width = myPosition->mWidth.ToLength();
+ } else {
+ // We'll need to get the width of the image/region.
+ needWidth = true;
+ }
+
+ if (myPosition->mHeight.ConvertsToLength()) {
+ // CSS has specified the destination height.
+ size.height = myPosition->mHeight.ToLength();
+ } else {
+ // We'll need to get the height of the image/region.
+ needHeight = true;
+ }
+
+ if (needWidth || needHeight) {
+ // We need to get the size of the image/region.
+ nsSize imageSize(0, 0);
+
+ const nsStyleList* myList = aComputedStyle->StyleList();
+ nsRect imageRegion = myList->GetImageRegion();
+ if (useImageRegion && imageRegion.width > 0) {
+ // CSS has specified an image region.
+ // Use the width of the image region.
+ imageSize.width = imageRegion.width;
+ } else if (image) {
+ nscoord width;
+ image->GetWidth(&width);
+ imageSize.width = nsPresContext::CSSPixelsToAppUnits(width);
+ }
+
+ if (useImageRegion && imageRegion.height > 0) {
+ // CSS has specified an image region.
+ // Use the height of the image region.
+ imageSize.height = imageRegion.height;
+ } else if (image) {
+ nscoord height;
+ image->GetHeight(&height);
+ imageSize.height = nsPresContext::CSSPixelsToAppUnits(height);
+ }
+
+ if (needWidth) {
+ if (!needHeight && imageSize.height != 0) {
+ // The CSS specified the destination height, but not the destination
+ // width. We need to calculate the width so that we maintain the
+ // image's aspect ratio.
+ size.width = imageSize.width * size.height / imageSize.height;
+ } else {
+ size.width = imageSize.width;
+ }
+ }
+
+ if (needHeight) {
+ if (!needWidth && imageSize.width != 0) {
+ // The CSS specified the destination width, but not the destination
+ // height. We need to calculate the height so that we maintain the
+ // image's aspect ratio.
+ size.height = imageSize.height * size.width / imageSize.width;
+ } else {
+ size.height = imageSize.height;
+ }
+ }
+ }
+
+ return size;
+}
+
+// GetImageSourceRect returns the source rectangle of the image to be
+// displayed.
+// The width and height reflect the image region specified in CSS, or
+// the natural size of the image.
+// The width and height do not include borders and padding.
+// The width and height do not reflect the destination size specified
+// in CSS.
+nsRect nsTreeBodyFrame::GetImageSourceRect(ComputedStyle* aComputedStyle,
+ bool useImageRegion,
+ imgIContainer* image) {
+ const nsStyleList* myList = aComputedStyle->StyleList();
+ // CSS has specified an image region.
+ if (useImageRegion && myList->mImageRegion.IsRect()) {
+ return myList->GetImageRegion();
+ }
+
+ if (!image) {
+ return nsRect();
+ }
+
+ nsRect r;
+ // Use the actual image size.
+ nscoord coord;
+ if (NS_SUCCEEDED(image->GetWidth(&coord))) {
+ r.width = nsPresContext::CSSPixelsToAppUnits(coord);
+ }
+ if (NS_SUCCEEDED(image->GetHeight(&coord))) {
+ r.height = nsPresContext::CSSPixelsToAppUnits(coord);
+ }
+ return r;
+}
+
+int32_t nsTreeBodyFrame::GetRowHeight() {
+ // Look up the correct height. It is equal to the specified height
+ // + the specified margins.
+ mScratchArray.Clear();
+ ComputedStyle* rowContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow());
+ if (rowContext) {
+ const nsStylePosition* myPosition = rowContext->StylePosition();
+
+ nscoord minHeight = 0;
+ if (myPosition->mMinHeight.ConvertsToLength()) {
+ minHeight = myPosition->mMinHeight.ToLength();
+ }
+
+ nscoord height = 0;
+ if (myPosition->mHeight.ConvertsToLength()) {
+ height = myPosition->mHeight.ToLength();
+ }
+
+ if (height < minHeight) height = minHeight;
+
+ if (height > 0) {
+ height = nsPresContext::AppUnitsToIntCSSPixels(height);
+ height += height % 2;
+ height = nsPresContext::CSSPixelsToAppUnits(height);
+
+ // XXX Check box-sizing to determine if border/padding should augment the
+ // height Inflate the height by our margins.
+ nsRect rowRect(0, 0, 0, height);
+ nsMargin rowMargin;
+ rowContext->StyleMargin()->GetMargin(rowMargin);
+ rowRect.Inflate(rowMargin);
+ height = rowRect.height;
+ return height;
+ }
+ }
+
+ return nsPresContext::CSSPixelsToAppUnits(18); // As good a default as any.
+}
+
+int32_t nsTreeBodyFrame::GetIndentation() {
+ // Look up the correct indentation. It is equal to the specified indentation
+ // width.
+ mScratchArray.Clear();
+ ComputedStyle* indentContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeIndentation());
+ if (indentContext) {
+ const nsStylePosition* myPosition = indentContext->StylePosition();
+ if (myPosition->mWidth.ConvertsToLength()) {
+ return myPosition->mWidth.ToLength();
+ }
+ }
+
+ return nsPresContext::CSSPixelsToAppUnits(16); // As good a default as any.
+}
+
+void nsTreeBodyFrame::CalcInnerBox() {
+ mInnerBox.SetRect(0, 0, mRect.width, mRect.height);
+ AdjustForBorderPadding(mComputedStyle, mInnerBox);
+}
+
+nscoord nsTreeBodyFrame::CalcHorzWidth(const ScrollParts& aParts) {
+ // Compute the adjustment to the last column. This varies depending on the
+ // visibility of the columnpicker and the scrollbar.
+ if (aParts.mColumnsFrame)
+ mAdjustWidth = mRect.width - aParts.mColumnsFrame->GetRect().width;
+ else
+ mAdjustWidth = 0;
+
+ nscoord width = 0;
+
+ // We calculate this from the scrollable frame, so that it
+ // properly covers all contingencies of what could be
+ // scrollable (columns, body, etc...)
+
+ if (aParts.mColumnsScrollFrame) {
+ width = aParts.mColumnsScrollFrame->GetScrollRange().width +
+ aParts.mColumnsScrollFrame->GetScrollPortRect().width;
+ }
+
+ // If no horz scrolling periphery is present, then just return our width
+ if (width == 0) width = mRect.width;
+
+ return width;
+}
+
+Maybe<nsIFrame::Cursor> nsTreeBodyFrame::GetCursor(const nsPoint& aPoint) {
+ // Check the GetScriptHandlingObject so we don't end up running code when
+ // the document is a zombie.
+ bool dummy;
+ if (mView && GetContent()->GetComposedDoc()->GetScriptHandlingObject(dummy)) {
+ int32_t row;
+ nsTreeColumn* col;
+ nsCSSAnonBoxPseudoStaticAtom* child;
+ GetCellAt(aPoint.x, aPoint.y, &row, &col, &child);
+
+ if (child) {
+ // Our scratch array is already prefilled.
+ RefPtr<ComputedStyle> childContext = GetPseudoComputedStyle(child);
+ StyleCursorKind kind = childContext->StyleUI()->Cursor().keyword;
+ if (kind == StyleCursorKind::Auto) {
+ kind = StyleCursorKind::Default;
+ }
+ return Some(
+ Cursor{kind, AllowCustomCursorImage::Yes, std::move(childContext)});
+ }
+ }
+ return nsLeafBoxFrame::GetCursor(aPoint);
+}
+
+static uint32_t GetDropEffect(WidgetGUIEvent* aEvent) {
+ NS_ASSERTION(aEvent->mClass == eDragEventClass, "wrong event type");
+ WidgetDragEvent* dragEvent = aEvent->AsDragEvent();
+ nsContentUtils::SetDataTransferInEvent(dragEvent);
+
+ uint32_t action = 0;
+ if (dragEvent->mDataTransfer) {
+ action = dragEvent->mDataTransfer->DropEffectInt();
+ }
+ return action;
+}
+
+nsresult nsTreeBodyFrame::HandleEvent(nsPresContext* aPresContext,
+ WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) {
+ if (aEvent->mMessage == eMouseOver || aEvent->mMessage == eMouseMove) {
+ nsPoint pt =
+ nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this});
+ int32_t xTwips = pt.x - mInnerBox.x;
+ int32_t yTwips = pt.y - mInnerBox.y;
+ int32_t newrow = GetRowAtInternal(xTwips, yTwips);
+ if (mMouseOverRow != newrow) {
+ // redraw the old and the new row
+ if (mMouseOverRow != -1) InvalidateRow(mMouseOverRow);
+ mMouseOverRow = newrow;
+ if (mMouseOverRow != -1) InvalidateRow(mMouseOverRow);
+ }
+ } else if (aEvent->mMessage == eMouseOut) {
+ if (mMouseOverRow != -1) {
+ InvalidateRow(mMouseOverRow);
+ mMouseOverRow = -1;
+ }
+ } else if (aEvent->mMessage == eDragEnter) {
+ if (!mSlots) {
+ mSlots = MakeUnique<Slots>();
+ }
+
+ // Cache several things we'll need throughout the course of our work. These
+ // will all get released on a drag exit.
+
+ if (mSlots->mTimer) {
+ mSlots->mTimer->Cancel();
+ mSlots->mTimer = nullptr;
+ }
+
+ // Cache the drag session.
+ mSlots->mIsDragging = true;
+ mSlots->mDropRow = -1;
+ mSlots->mDropOrient = -1;
+ mSlots->mDragAction = GetDropEffect(aEvent);
+ } else if (aEvent->mMessage == eDragOver) {
+ // The mouse is hovering over this tree. If we determine things are
+ // different from the last time, invalidate the drop feedback at the old
+ // position, query the view to see if the current location is droppable,
+ // and then invalidate the drop feedback at the new location if it is.
+ // The mouse may or may not have changed position from the last time
+ // we were called, so optimize out a lot of the extra notifications by
+ // checking if anything changed first. For drop feedback we use drop,
+ // dropBefore and dropAfter property.
+ if (!mView || !mSlots) {
+ return NS_OK;
+ }
+
+ // Save last values, we will need them.
+ int32_t lastDropRow = mSlots->mDropRow;
+ int16_t lastDropOrient = mSlots->mDropOrient;
+#ifndef XP_MACOSX
+ int16_t lastScrollLines = mSlots->mScrollLines;
+#endif
+
+ // Find out the current drag action
+ uint32_t lastDragAction = mSlots->mDragAction;
+ mSlots->mDragAction = GetDropEffect(aEvent);
+
+ // Compute the row mouse is over and the above/below/on state.
+ // Below we'll use this to see if anything changed.
+ // Also check if we want to auto-scroll.
+ ComputeDropPosition(aEvent, &mSlots->mDropRow, &mSlots->mDropOrient,
+ &mSlots->mScrollLines);
+
+ // While we're here, handle tracking of scrolling during a drag.
+ if (mSlots->mScrollLines) {
+ if (mSlots->mDropAllowed) {
+ // Invalidate primary cell at old location.
+ mSlots->mDropAllowed = false;
+ InvalidateDropFeedback(lastDropRow, lastDropOrient);
+ }
+#ifdef XP_MACOSX
+ ScrollByLines(mSlots->mScrollLines);
+#else
+ if (!lastScrollLines) {
+ // Cancel any previously initialized timer.
+ if (mSlots->mTimer) {
+ mSlots->mTimer->Cancel();
+ mSlots->mTimer = nullptr;
+ }
+
+ // Set a timer to trigger the tree scrolling.
+ CreateTimer(LookAndFeel::IntID::TreeLazyScrollDelay, LazyScrollCallback,
+ nsITimer::TYPE_ONE_SHOT, getter_AddRefs(mSlots->mTimer),
+ "nsTreeBodyFrame::LazyScrollCallback");
+ }
+#endif
+ // Bail out to prevent spring loaded timer and feedback line settings.
+ return NS_OK;
+ }
+
+ // If changed from last time, invalidate primary cell at the old location
+ // and if allowed, invalidate primary cell at the new location. If nothing
+ // changed, just bail.
+ if (mSlots->mDropRow != lastDropRow ||
+ mSlots->mDropOrient != lastDropOrient ||
+ mSlots->mDragAction != lastDragAction) {
+ // Invalidate row at the old location.
+ if (mSlots->mDropAllowed) {
+ mSlots->mDropAllowed = false;
+ InvalidateDropFeedback(lastDropRow, lastDropOrient);
+ }
+
+ if (mSlots->mTimer) {
+ // Timer is active but for a different row than the current one, kill
+ // it.
+ mSlots->mTimer->Cancel();
+ mSlots->mTimer = nullptr;
+ }
+
+ if (mSlots->mDropRow >= 0) {
+ if (!mSlots->mTimer && mSlots->mDropOrient == nsITreeView::DROP_ON) {
+ // Either there wasn't a timer running or it was just killed above.
+ // If over a folder, start up a timer to open the folder.
+ bool isContainer = false;
+ mView->IsContainer(mSlots->mDropRow, &isContainer);
+ if (isContainer) {
+ bool isOpen = false;
+ mView->IsContainerOpen(mSlots->mDropRow, &isOpen);
+ if (!isOpen) {
+ // This node isn't expanded, set a timer to expand it.
+ CreateTimer(LookAndFeel::IntID::TreeOpenDelay, OpenCallback,
+ nsITimer::TYPE_ONE_SHOT,
+ getter_AddRefs(mSlots->mTimer),
+ "nsTreeBodyFrame::OpenCallback");
+ }
+ }
+ }
+
+ // The dataTransfer was initialized by the call to GetDropEffect above.
+ bool canDropAtNewLocation = false;
+ mView->CanDrop(mSlots->mDropRow, mSlots->mDropOrient,
+ aEvent->AsDragEvent()->mDataTransfer,
+ &canDropAtNewLocation);
+
+ if (canDropAtNewLocation) {
+ // Invalidate row at the new location.
+ mSlots->mDropAllowed = canDropAtNewLocation;
+ InvalidateDropFeedback(mSlots->mDropRow, mSlots->mDropOrient);
+ }
+ }
+ }
+
+ // Indicate that the drop is allowed by preventing the default behaviour.
+ if (mSlots->mDropAllowed) *aEventStatus = nsEventStatus_eConsumeNoDefault;
+ } else if (aEvent->mMessage == eDrop) {
+ // this event was meant for another frame, so ignore it
+ if (!mSlots) return NS_OK;
+
+ // Tell the view where the drop happened.
+
+ // Remove the drop folder and all its parents from the array.
+ int32_t parentIndex;
+ nsresult rv = mView->GetParentIndex(mSlots->mDropRow, &parentIndex);
+ while (NS_SUCCEEDED(rv) && parentIndex >= 0) {
+ mSlots->mArray.RemoveElement(parentIndex);
+ rv = mView->GetParentIndex(parentIndex, &parentIndex);
+ }
+
+ NS_ASSERTION(aEvent->mClass == eDragEventClass, "wrong event type");
+ WidgetDragEvent* dragEvent = aEvent->AsDragEvent();
+ nsContentUtils::SetDataTransferInEvent(dragEvent);
+
+ mView->Drop(mSlots->mDropRow, mSlots->mDropOrient,
+ dragEvent->mDataTransfer);
+ mSlots->mDropRow = -1;
+ mSlots->mDropOrient = -1;
+ mSlots->mIsDragging = false;
+ *aEventStatus =
+ nsEventStatus_eConsumeNoDefault; // already handled the drop
+ } else if (aEvent->mMessage == eDragExit) {
+ // this event was meant for another frame, so ignore it
+ if (!mSlots) return NS_OK;
+
+ // Clear out all our tracking vars.
+
+ if (mSlots->mDropAllowed) {
+ mSlots->mDropAllowed = false;
+ InvalidateDropFeedback(mSlots->mDropRow, mSlots->mDropOrient);
+ } else
+ mSlots->mDropAllowed = false;
+ mSlots->mIsDragging = false;
+ mSlots->mScrollLines = 0;
+ // If a drop is occuring, the exit event will fire just before the drop
+ // event, so don't reset mDropRow or mDropOrient as these fields are used
+ // by the drop event.
+ if (mSlots->mTimer) {
+ mSlots->mTimer->Cancel();
+ mSlots->mTimer = nullptr;
+ }
+
+ if (!mSlots->mArray.IsEmpty()) {
+ // Close all spring loaded folders except the drop folder.
+ CreateTimer(LookAndFeel::IntID::TreeCloseDelay, CloseCallback,
+ nsITimer::TYPE_ONE_SHOT, getter_AddRefs(mSlots->mTimer),
+ "nsTreeBodyFrame::CloseCallback");
+ }
+ }
+
+ return NS_OK;
+}
+
+namespace mozilla {
+
+class nsDisplayTreeBody final : public nsPaintedDisplayItem {
+ public:
+ nsDisplayTreeBody(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame)
+ : nsPaintedDisplayItem(aBuilder, aFrame) {
+ MOZ_COUNT_CTOR(nsDisplayTreeBody);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayTreeBody)
+
+ nsDisplayItemGeometry* AllocateGeometry(
+ nsDisplayListBuilder* aBuilder) override {
+ return new nsDisplayTreeBodyGeometry(this, aBuilder, IsWindowActive());
+ }
+
+ void Destroy(nsDisplayListBuilder* aBuilder) override {
+ aBuilder->UnregisterThemeGeometry(this);
+ nsPaintedDisplayItem::Destroy(aBuilder);
+ }
+
+ bool IsWindowActive() const {
+ DocumentState docState =
+ mFrame->PresContext()->Document()->GetDocumentState();
+ return !docState.HasState(DocumentState::WINDOW_INACTIVE);
+ }
+
+ void ComputeInvalidationRegion(nsDisplayListBuilder* aBuilder,
+ const nsDisplayItemGeometry* aGeometry,
+ nsRegion* aInvalidRegion) const override {
+ auto geometry = static_cast<const nsDisplayTreeBodyGeometry*>(aGeometry);
+
+ if (IsWindowActive() != geometry->mWindowIsActive) {
+ bool snap;
+ aInvalidRegion->Or(*aInvalidRegion, GetBounds(aBuilder, &snap));
+ }
+
+ nsPaintedDisplayItem::ComputeInvalidationRegion(aBuilder, aGeometry,
+ aInvalidRegion);
+ }
+
+ virtual void Paint(nsDisplayListBuilder* aBuilder,
+ gfxContext* aCtx) override {
+ MOZ_ASSERT(aBuilder);
+ Unused << static_cast<nsTreeBodyFrame*>(mFrame)->PaintTreeBody(
+ *aCtx, GetPaintRect(aBuilder, aCtx), ToReferenceFrame(), aBuilder);
+ }
+
+ NS_DISPLAY_DECL_NAME("XULTreeBody", TYPE_XUL_TREE_BODY)
+
+ virtual nsRect GetComponentAlphaBounds(
+ nsDisplayListBuilder* aBuilder) const override {
+ bool snap;
+ return GetBounds(aBuilder, &snap);
+ }
+};
+
+} // namespace mozilla
+
+#ifdef XP_MACOSX
+static bool IsInSourceList(nsIFrame* aFrame) {
+ for (nsIFrame* frame = aFrame; frame;
+ frame = nsLayoutUtils::GetCrossDocParentFrameInProcess(frame)) {
+ if (frame->StyleDisplay()->EffectiveAppearance() ==
+ StyleAppearance::MozMacSourceList) {
+ return true;
+ }
+ }
+ return false;
+}
+#endif
+
+// Painting routines
+void nsTreeBodyFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ // REVIEW: why did we paint if we were collapsed? that makes no sense!
+ if (!IsVisibleForPainting()) return; // We're invisible. Don't paint.
+
+ // Handles painting our background, border, and outline.
+ nsLeafBoxFrame::BuildDisplayList(aBuilder, aLists);
+
+ // Bail out now if there's no view or we can't run script because the
+ // document is a zombie
+ if (!mView || !GetContent()->GetComposedDoc()->GetWindow()) return;
+
+ nsDisplayItem* item = MakeDisplayItem<nsDisplayTreeBody>(aBuilder, this);
+ if (!item) {
+ return;
+ }
+ aLists.Content()->AppendToTop(item);
+
+#ifdef XP_MACOSX
+ XULTreeElement* tree = GetBaseElement();
+ nsIFrame* treeFrame = tree ? tree->GetPrimaryFrame() : nullptr;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ nsCOMPtr<nsITreeSelection> selection = GetSelection();
+ nsITheme* theme = PresContext()->Theme();
+ // On Mac, we support native theming of selected rows. On 10.10 and higher,
+ // this means applying vibrancy which require us to register the theme
+ // geometrics for the row. In order to make the vibrancy effect to work
+ // properly, we also need an ancestor frame to be themed as a source list.
+ if (selection && theme && IsInSourceList(treeFrame)) {
+ // Loop through our onscreen rows. If the row is selected and a
+ // -moz-appearance is provided, RegisterThemeGeometry might be necessary.
+ const auto end = std::min(mRowCount, LastVisibleRow() + 1);
+ for (auto i = FirstVisibleRow(); i < end; i++) {
+ bool isSelected;
+ selection->IsSelected(i, &isSelected);
+ if (isSelected) {
+ PrefillPropertyArray(i, nullptr);
+ nsAutoString properties;
+ view->GetRowProperties(i, properties);
+ nsTreeUtils::TokenizeProperties(properties, mScratchArray);
+ ComputedStyle* rowContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow());
+ auto appearance = rowContext->StyleDisplay()->EffectiveAppearance();
+ if (appearance != StyleAppearance::None) {
+ if (theme->ThemeSupportsWidget(PresContext(), this, appearance)) {
+ nsITheme::ThemeGeometryType type =
+ theme->ThemeGeometryTypeForWidget(this, appearance);
+ if (type != nsITheme::eThemeGeometryTypeUnknown) {
+ nsRect rowRect(mInnerBox.x,
+ mInnerBox.y + mRowHeight * (i - FirstVisibleRow()),
+ mInnerBox.width, mRowHeight);
+ aBuilder->RegisterThemeGeometry(
+ type, item,
+ LayoutDeviceIntRect::FromUnknownRect(
+ (rowRect + aBuilder->ToReferenceFrame(this))
+ .ToNearestPixels(
+ PresContext()->AppUnitsPerDevPixel())));
+ }
+ }
+ }
+ }
+ }
+ }
+#endif
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintTreeBody(gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect,
+ nsPoint aPt,
+ nsDisplayListBuilder* aBuilder) {
+ // Update our available height and our page count.
+ CalcInnerBox();
+
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+
+ aRenderingContext.Save();
+ aRenderingContext.Clip(NSRectToSnappedRect(
+ mInnerBox + aPt, PresContext()->AppUnitsPerDevPixel(), *drawTarget));
+ int32_t oldPageCount = mPageLength;
+ if (!mHasFixedRowCount)
+ mPageLength =
+ (mRowHeight > 0) ? (mInnerBox.height / mRowHeight) : mRowCount;
+
+ if (oldPageCount != mPageLength ||
+ mHorzWidth != CalcHorzWidth(GetScrollParts())) {
+ // Schedule a ResizeReflow that will update our info properly.
+ PresShell()->FrameNeedsReflow(this, IntrinsicDirty::None,
+ NS_FRAME_IS_DIRTY);
+ }
+#ifdef DEBUG
+ int32_t rowCount = mRowCount;
+ mView->GetRowCount(&rowCount);
+ NS_WARNING_ASSERTION(mRowCount == rowCount, "row count changed unexpectedly");
+#endif
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ // Loop through our columns and paint them (e.g., for sorting). This is only
+ // relevant when painting backgrounds, since columns contain no content.
+ // Content is contained in the rows.
+ for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol;
+ currCol = currCol->GetNext()) {
+ nsRect colRect;
+ nsresult rv =
+ currCol->GetRect(this, mInnerBox.y, mInnerBox.height, &colRect);
+ // Don't paint hidden columns.
+ if (NS_FAILED(rv) || colRect.width == 0) continue;
+
+ if (OffsetForHorzScroll(colRect, false)) {
+ nsRect dirtyRect;
+ colRect += aPt;
+ if (dirtyRect.IntersectRect(aDirtyRect, colRect)) {
+ result &= PaintColumn(currCol, colRect, PresContext(),
+ aRenderingContext, aDirtyRect);
+ }
+ }
+ }
+ // Loop through our on-screen rows.
+ for (int32_t i = mTopRowIndex;
+ i < mRowCount && i <= mTopRowIndex + mPageLength; i++) {
+ nsRect rowRect(mInnerBox.x, mInnerBox.y + mRowHeight * (i - mTopRowIndex),
+ mInnerBox.width, mRowHeight);
+ nsRect dirtyRect;
+ if (dirtyRect.IntersectRect(aDirtyRect, rowRect + aPt) &&
+ rowRect.y < (mInnerBox.y + mInnerBox.height)) {
+ result &= PaintRow(i, rowRect + aPt, PresContext(), aRenderingContext,
+ aDirtyRect, aPt, aBuilder);
+ }
+ }
+
+ if (mSlots && mSlots->mDropAllowed &&
+ (mSlots->mDropOrient == nsITreeView::DROP_BEFORE ||
+ mSlots->mDropOrient == nsITreeView::DROP_AFTER)) {
+ nscoord yPos = mInnerBox.y +
+ mRowHeight * (mSlots->mDropRow - mTopRowIndex) -
+ mRowHeight / 2;
+ nsRect feedbackRect(mInnerBox.x, yPos, mInnerBox.width, mRowHeight);
+ if (mSlots->mDropOrient == nsITreeView::DROP_AFTER)
+ feedbackRect.y += mRowHeight;
+
+ nsRect dirtyRect;
+ feedbackRect += aPt;
+ if (dirtyRect.IntersectRect(aDirtyRect, feedbackRect)) {
+ result &= PaintDropFeedback(feedbackRect, PresContext(),
+ aRenderingContext, aDirtyRect, aPt);
+ }
+ }
+ aRenderingContext.Restore();
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintColumn(nsTreeColumn* aColumn,
+ const nsRect& aColumnRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ // Now obtain the properties for our cell.
+ PrefillPropertyArray(-1, aColumn);
+ nsAutoString properties;
+
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ view->GetColumnProperties(aColumn, properties);
+ nsTreeUtils::TokenizeProperties(properties, mScratchArray);
+
+ // Resolve style for the column. It contains all the info we need to lay
+ // ourselves out and to paint.
+ ComputedStyle* colContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeColumn());
+
+ // Obtain the margins for the cell and then deflate our rect by that
+ // amount. The cell is assumed to be contained within the deflated rect.
+ nsRect colRect(aColumnRect);
+ nsMargin colMargin;
+ colContext->StyleMargin()->GetMargin(colMargin);
+ colRect.Deflate(colMargin);
+
+ return PaintBackgroundLayer(colContext, aPresContext, aRenderingContext,
+ colRect, aDirtyRect);
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintRow(int32_t aRowIndex,
+ const nsRect& aRowRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nsPoint aPt,
+ nsDisplayListBuilder* aBuilder) {
+ // We have been given a rect for our row. We treat this row like a full-blown
+ // frame, meaning that it can have borders, margins, padding, and a
+ // background.
+
+ // Without a view, we have no data. Check for this up front.
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ if (!view) {
+ return ImgDrawResult::SUCCESS;
+ }
+
+ nsresult rv;
+
+ // Now obtain the properties for our row.
+ // XXX Automatically fill in the following props: open, closed, container,
+ // leaf, selected, focused
+ PrefillPropertyArray(aRowIndex, nullptr);
+
+ nsAutoString properties;
+ view->GetRowProperties(aRowIndex, properties);
+ nsTreeUtils::TokenizeProperties(properties, mScratchArray);
+
+ // Resolve style for the row. It contains all the info we need to lay
+ // ourselves out and to paint.
+ ComputedStyle* rowContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow());
+
+ // Obtain the margins for the row and then deflate our rect by that
+ // amount. The row is assumed to be contained within the deflated rect.
+ nsRect rowRect(aRowRect);
+ nsMargin rowMargin;
+ rowContext->StyleMargin()->GetMargin(rowMargin);
+ rowRect.Deflate(rowMargin);
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ // Paint our borders and background for our row rect.
+ nsITheme* theme = nullptr;
+ auto appearance = rowContext->StyleDisplay()->EffectiveAppearance();
+ if (appearance != StyleAppearance::None) {
+ theme = aPresContext->Theme();
+ }
+
+ if (theme && theme->ThemeSupportsWidget(aPresContext, nullptr, appearance)) {
+ nsRect dirty;
+ dirty.IntersectRect(rowRect, aDirtyRect);
+ theme->DrawWidgetBackground(&aRenderingContext, this, appearance, rowRect,
+ dirty);
+ } else {
+ result &= PaintBackgroundLayer(rowContext, aPresContext, aRenderingContext,
+ rowRect, aDirtyRect);
+ }
+
+ // Adjust the rect for its border and padding.
+ nsRect originalRowRect = rowRect;
+ AdjustForBorderPadding(rowContext, rowRect);
+
+ bool isSeparator = false;
+ view->IsSeparator(aRowIndex, &isSeparator);
+ if (isSeparator) {
+ // The row is a separator.
+
+ nscoord primaryX = rowRect.x;
+ nsTreeColumn* primaryCol = mColumns->GetPrimaryColumn();
+ if (primaryCol) {
+ // Paint the primary cell.
+ nsRect cellRect;
+ rv = primaryCol->GetRect(this, rowRect.y, rowRect.height, &cellRect);
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT_UNREACHABLE("primary column is invalid");
+ return result;
+ }
+
+ if (OffsetForHorzScroll(cellRect, false)) {
+ cellRect.x += aPt.x;
+ nsRect dirtyRect;
+ nsRect checkRect(cellRect.x, originalRowRect.y, cellRect.width,
+ originalRowRect.height);
+ if (dirtyRect.IntersectRect(aDirtyRect, checkRect)) {
+ result &=
+ PaintCell(aRowIndex, primaryCol, cellRect, aPresContext,
+ aRenderingContext, aDirtyRect, primaryX, aPt, aBuilder);
+ }
+ }
+
+ // Paint the left side of the separator.
+ nscoord currX;
+ nsTreeColumn* previousCol = primaryCol->GetPrevious();
+ if (previousCol) {
+ nsRect prevColRect;
+ rv = previousCol->GetRect(this, 0, 0, &prevColRect);
+ if (NS_SUCCEEDED(rv)) {
+ currX = (prevColRect.x - mHorzPosition) + prevColRect.width + aPt.x;
+ } else {
+ MOZ_ASSERT_UNREACHABLE(
+ "The column before the primary column is "
+ "invalid");
+ currX = rowRect.x;
+ }
+ } else {
+ currX = rowRect.x;
+ }
+
+ int32_t level;
+ view->GetLevel(aRowIndex, &level);
+ if (level == 0) currX += mIndentation;
+
+ if (currX > rowRect.x) {
+ nsRect separatorRect(rowRect);
+ separatorRect.width -= rowRect.x + rowRect.width - currX;
+ result &= PaintSeparator(aRowIndex, separatorRect, aPresContext,
+ aRenderingContext, aDirtyRect);
+ }
+ }
+
+ // Paint the right side (whole) separator.
+ nsRect separatorRect(rowRect);
+ if (primaryX > rowRect.x) {
+ separatorRect.width -= primaryX - rowRect.x;
+ separatorRect.x += primaryX - rowRect.x;
+ }
+ result &= PaintSeparator(aRowIndex, separatorRect, aPresContext,
+ aRenderingContext, aDirtyRect);
+ } else {
+ // Now loop over our cells. Only paint a cell if it intersects with our
+ // dirty rect.
+ for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol;
+ currCol = currCol->GetNext()) {
+ nsRect cellRect;
+ rv = currCol->GetRect(this, rowRect.y, rowRect.height, &cellRect);
+ // Don't paint cells in hidden columns.
+ if (NS_FAILED(rv) || cellRect.width == 0) continue;
+
+ if (OffsetForHorzScroll(cellRect, false)) {
+ cellRect.x += aPt.x;
+
+ // for primary columns, use the row's vertical size so that the
+ // lines get drawn properly
+ nsRect checkRect = cellRect;
+ if (currCol->IsPrimary())
+ checkRect = nsRect(cellRect.x, originalRowRect.y, cellRect.width,
+ originalRowRect.height);
+
+ nsRect dirtyRect;
+ nscoord dummy;
+ if (dirtyRect.IntersectRect(aDirtyRect, checkRect))
+ result &=
+ PaintCell(aRowIndex, currCol, cellRect, aPresContext,
+ aRenderingContext, aDirtyRect, dummy, aPt, aBuilder);
+ }
+ }
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintSeparator(int32_t aRowIndex,
+ const nsRect& aSeparatorRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect) {
+ // Resolve style for the separator.
+ ComputedStyle* separatorContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeSeparator());
+ bool useTheme = false;
+ nsITheme* theme = nullptr;
+ StyleAppearance appearance =
+ separatorContext->StyleDisplay()->EffectiveAppearance();
+ if (appearance != StyleAppearance::None) {
+ theme = aPresContext->Theme();
+ if (theme->ThemeSupportsWidget(aPresContext, nullptr, appearance))
+ useTheme = true;
+ }
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ // use -moz-appearance if provided.
+ if (useTheme) {
+ nsRect dirty;
+ dirty.IntersectRect(aSeparatorRect, aDirtyRect);
+ theme->DrawWidgetBackground(&aRenderingContext, this, appearance,
+ aSeparatorRect, dirty);
+ } else {
+ const nsStylePosition* stylePosition = separatorContext->StylePosition();
+
+ // Obtain the height for the separator or use the default value.
+ nscoord height;
+ if (stylePosition->mHeight.ConvertsToLength()) {
+ height = stylePosition->mHeight.ToLength();
+ } else {
+ // Use default height 2px.
+ height = nsPresContext::CSSPixelsToAppUnits(2);
+ }
+
+ // Obtain the margins for the separator and then deflate our rect by that
+ // amount. The separator is assumed to be contained within the deflated
+ // rect.
+ nsRect separatorRect(aSeparatorRect.x, aSeparatorRect.y,
+ aSeparatorRect.width, height);
+ nsMargin separatorMargin;
+ separatorContext->StyleMargin()->GetMargin(separatorMargin);
+ separatorRect.Deflate(separatorMargin);
+
+ // Center the separator.
+ separatorRect.y += (aSeparatorRect.height - height) / 2;
+
+ result &=
+ PaintBackgroundLayer(separatorContext, aPresContext, aRenderingContext,
+ separatorRect, aDirtyRect);
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintCell(
+ int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aCellRect,
+ nsPresContext* aPresContext, gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aCurrX, nsPoint aPt,
+ nsDisplayListBuilder* aBuilder) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ // Now obtain the properties for our cell.
+ // XXX Automatically fill in the following props: open, closed, container,
+ // leaf, selected, focused, and the col ID.
+ PrefillPropertyArray(aRowIndex, aColumn);
+ nsAutoString properties;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ view->GetCellProperties(aRowIndex, aColumn, properties);
+ nsTreeUtils::TokenizeProperties(properties, mScratchArray);
+
+ // Resolve style for the cell. It contains all the info we need to lay
+ // ourselves out and to paint.
+ ComputedStyle* cellContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell());
+
+ bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
+
+ // Obtain the margins for the cell and then deflate our rect by that
+ // amount. The cell is assumed to be contained within the deflated rect.
+ nsRect cellRect(aCellRect);
+ nsMargin cellMargin;
+ cellContext->StyleMargin()->GetMargin(cellMargin);
+ cellRect.Deflate(cellMargin);
+
+ // Paint our borders and background for our row rect.
+ ImgDrawResult result = PaintBackgroundLayer(
+ cellContext, aPresContext, aRenderingContext, cellRect, aDirtyRect);
+
+ // Adjust the rect for its border and padding.
+ AdjustForBorderPadding(cellContext, cellRect);
+
+ nscoord currX = cellRect.x;
+ nscoord remainingWidth = cellRect.width;
+
+ // Now we paint the contents of the cells.
+ // Directionality of the tree determines the order in which we paint.
+ // StyleDirection::Ltr means paint from left to right.
+ // StyleDirection::Rtl means paint from right to left.
+
+ if (aColumn->IsPrimary()) {
+ // If we're the primary column, we need to indent and paint the twisty and
+ // any connecting lines between siblings.
+
+ int32_t level;
+ view->GetLevel(aRowIndex, &level);
+
+ if (!isRTL) currX += mIndentation * level;
+ remainingWidth -= mIndentation * level;
+
+ // Resolve the style to use for the connecting lines.
+ ComputedStyle* lineContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeLine());
+
+ if (mIndentation && level &&
+ lineContext->StyleVisibility()->IsVisibleOrCollapsed()) {
+ // Paint the thread lines.
+
+ // Get the size of the twisty. We don't want to paint the twisty
+ // before painting of connecting lines since it would paint lines over
+ // the twisty. But we need to leave a place for it.
+ ComputedStyle* twistyContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty());
+
+ nsRect imageSize;
+ nsRect twistyRect(aCellRect);
+ GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, aPresContext,
+ twistyContext);
+
+ nsMargin twistyMargin;
+ twistyContext->StyleMargin()->GetMargin(twistyMargin);
+ twistyRect.Inflate(twistyMargin);
+
+ const nsStyleBorder* borderStyle = lineContext->StyleBorder();
+ // Resolve currentcolor values against the treeline context
+ nscolor color = borderStyle->mBorderLeftColor.CalcColor(*lineContext);
+ ColorPattern colorPatt(ToDeviceColor(color));
+
+ StyleBorderStyle style = borderStyle->GetBorderStyle(eSideLeft);
+ StrokeOptions strokeOptions;
+ nsLayoutUtils::InitDashPattern(strokeOptions, style);
+
+ nscoord srcX = currX + twistyRect.width - mIndentation / 2;
+ nscoord lineY = (aRowIndex - mTopRowIndex) * mRowHeight + aPt.y;
+
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+ nsPresContext* pc = PresContext();
+
+ // Don't paint off our cell.
+ if (srcX <= cellRect.x + cellRect.width) {
+ nscoord destX = currX + twistyRect.width;
+ if (destX > cellRect.x + cellRect.width)
+ destX = cellRect.x + cellRect.width;
+ if (isRTL) {
+ srcX = currX + remainingWidth - (srcX - cellRect.x);
+ destX = currX + remainingWidth - (destX - cellRect.x);
+ }
+ Point p1(pc->AppUnitsToGfxUnits(srcX),
+ pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2));
+ Point p2(pc->AppUnitsToGfxUnits(destX),
+ pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2));
+ SnapLineToDevicePixelsForStroking(p1, p2, *drawTarget,
+ strokeOptions.mLineWidth);
+ drawTarget->StrokeLine(p1, p2, colorPatt, strokeOptions);
+ }
+
+ int32_t currentParent = aRowIndex;
+ for (int32_t i = level; i > 0; i--) {
+ if (srcX <= cellRect.x + cellRect.width) {
+ // Paint full vertical line only if we have next sibling.
+ bool hasNextSibling;
+ view->HasNextSibling(currentParent, aRowIndex, &hasNextSibling);
+ if (hasNextSibling || i == level) {
+ Point p1(pc->AppUnitsToGfxUnits(srcX),
+ pc->AppUnitsToGfxUnits(lineY));
+ Point p2;
+ p2.x = pc->AppUnitsToGfxUnits(srcX);
+
+ if (hasNextSibling)
+ p2.y = pc->AppUnitsToGfxUnits(lineY + mRowHeight);
+ else if (i == level)
+ p2.y = pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2);
+
+ SnapLineToDevicePixelsForStroking(p1, p2, *drawTarget,
+ strokeOptions.mLineWidth);
+ drawTarget->StrokeLine(p1, p2, colorPatt, strokeOptions);
+ }
+ }
+
+ int32_t parent;
+ if (NS_FAILED(view->GetParentIndex(currentParent, &parent)) ||
+ parent < 0)
+ break;
+ currentParent = parent;
+ srcX -= mIndentation;
+ }
+ }
+
+ // Always leave space for the twisty.
+ nsRect twistyRect(currX, cellRect.y, remainingWidth, cellRect.height);
+ result &= PaintTwisty(aRowIndex, aColumn, twistyRect, aPresContext,
+ aRenderingContext, aDirtyRect, remainingWidth, currX);
+ }
+
+ // Now paint the icon for our cell.
+ nsRect iconRect(currX, cellRect.y, remainingWidth, cellRect.height);
+ nsRect dirtyRect;
+ if (dirtyRect.IntersectRect(aDirtyRect, iconRect)) {
+ result &= PaintImage(aRowIndex, aColumn, iconRect, aPresContext,
+ aRenderingContext, aDirtyRect, remainingWidth, currX,
+ aBuilder);
+ }
+
+ // Now paint our element, but only if we aren't a cycler column.
+ // XXX until we have the ability to load images, allow the view to
+ // insert text into cycler columns...
+ if (!aColumn->IsCycler()) {
+ nsRect elementRect(currX, cellRect.y, remainingWidth, cellRect.height);
+ nsRect dirtyRect;
+ if (dirtyRect.IntersectRect(aDirtyRect, elementRect)) {
+ switch (aColumn->GetType()) {
+ case TreeColumn_Binding::TYPE_TEXT:
+ result &= PaintText(aRowIndex, aColumn, elementRect, aPresContext,
+ aRenderingContext, aDirtyRect, currX);
+ break;
+ case TreeColumn_Binding::TYPE_CHECKBOX:
+ result &= PaintCheckbox(aRowIndex, aColumn, elementRect, aPresContext,
+ aRenderingContext, aDirtyRect);
+ break;
+ }
+ }
+ }
+
+ aCurrX = currX;
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintTwisty(
+ int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aTwistyRect,
+ nsPresContext* aPresContext, gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aRemainingWidth, nscoord& aCurrX) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
+ nscoord rightEdge = aCurrX + aRemainingWidth;
+ // Paint the twisty, but only if we are a non-empty container.
+ bool shouldPaint = false;
+ bool isContainer = false;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ view->IsContainer(aRowIndex, &isContainer);
+ if (isContainer) {
+ bool isContainerEmpty = false;
+ view->IsContainerEmpty(aRowIndex, &isContainerEmpty);
+ if (!isContainerEmpty) shouldPaint = true;
+ }
+
+ // Resolve style for the twisty.
+ ComputedStyle* twistyContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty());
+
+ // Obtain the margins for the twisty and then deflate our rect by that
+ // amount. The twisty is assumed to be contained within the deflated rect.
+ nsRect twistyRect(aTwistyRect);
+ nsMargin twistyMargin;
+ twistyContext->StyleMargin()->GetMargin(twistyMargin);
+ twistyRect.Deflate(twistyMargin);
+
+ nsRect imageSize;
+ nsITheme* theme = GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect,
+ aPresContext, twistyContext);
+
+ // Subtract out the remaining width. This is done even when we don't actually
+ // paint a twisty in this cell, so that cells in different rows still line up.
+ nsRect copyRect(twistyRect);
+ copyRect.Inflate(twistyMargin);
+ aRemainingWidth -= copyRect.width;
+ if (!isRTL) aCurrX += copyRect.width;
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ if (shouldPaint) {
+ // Paint our borders and background for our image rect.
+ result &= PaintBackgroundLayer(twistyContext, aPresContext,
+ aRenderingContext, twistyRect, aDirtyRect);
+
+ if (theme) {
+ if (isRTL) twistyRect.x = rightEdge - twistyRect.width;
+ // yeah, I know it says we're drawing a background, but a twisty is really
+ // a fg object since it doesn't have anything that gecko would want to
+ // draw over it. Besides, we have to prevent imagelib from drawing it.
+ nsRect dirty;
+ dirty.IntersectRect(twistyRect, aDirtyRect);
+ theme->DrawWidgetBackground(
+ &aRenderingContext, this,
+ twistyContext->StyleDisplay()->EffectiveAppearance(), twistyRect,
+ dirty);
+ } else {
+ // Time to paint the twisty.
+ // Adjust the rect for its border and padding.
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(twistyContext, bp);
+ twistyRect.Deflate(bp);
+ if (isRTL) twistyRect.x = rightEdge - twistyRect.width;
+ imageSize.Deflate(bp);
+
+ // Get the image for drawing.
+ nsCOMPtr<imgIContainer> image;
+ bool useImageRegion = true;
+ GetImage(aRowIndex, aColumn, true, twistyContext, useImageRegion,
+ getter_AddRefs(image));
+ if (image) {
+ nsPoint anchorPoint = twistyRect.TopLeft();
+
+ // Center the image. XXX Obey vertical-align style prop?
+ if (imageSize.height < twistyRect.height) {
+ anchorPoint.y += (twistyRect.height - imageSize.height) / 2;
+ }
+
+ // Apply context paint if applicable
+ SVGImageContext svgContext;
+ SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext,
+ *twistyContext, image);
+
+ // Paint the image.
+ result &= nsLayoutUtils::DrawSingleUnscaledImage(
+ aRenderingContext, aPresContext, image, SamplingFilter::POINT,
+ anchorPoint, &aDirtyRect, svgContext, imgIContainer::FLAG_NONE,
+ &imageSize);
+ }
+ }
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintImage(
+ int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aImageRect,
+ nsPresContext* aPresContext, gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aRemainingWidth, nscoord& aCurrX,
+ nsDisplayListBuilder* aBuilder) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
+ nscoord rightEdge = aCurrX + aRemainingWidth;
+ // Resolve style for the image.
+ ComputedStyle* imageContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage());
+
+ // Obtain opacity value for the image.
+ float opacity = imageContext->StyleEffects()->mOpacity;
+
+ // Obtain the margins for the image and then deflate our rect by that
+ // amount. The image is assumed to be contained within the deflated rect.
+ nsRect imageRect(aImageRect);
+ nsMargin imageMargin;
+ imageContext->StyleMargin()->GetMargin(imageMargin);
+ imageRect.Deflate(imageMargin);
+
+ // Get the image.
+ bool useImageRegion = true;
+ nsCOMPtr<imgIContainer> image;
+ GetImage(aRowIndex, aColumn, false, imageContext, useImageRegion,
+ getter_AddRefs(image));
+
+ // Get the image destination size.
+ nsSize imageDestSize = GetImageDestSize(imageContext, useImageRegion, image);
+ if (!imageDestSize.width || !imageDestSize.height) {
+ return ImgDrawResult::SUCCESS;
+ }
+
+ // Get the borders and padding.
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(imageContext, bp);
+
+ // destRect will be passed as the aDestRect argument in the DrawImage method.
+ // Start with the imageDestSize width and height.
+ nsRect destRect(0, 0, imageDestSize.width, imageDestSize.height);
+ // Inflate destRect for borders and padding so that we can compare/adjust
+ // with respect to imageRect.
+ destRect.Inflate(bp);
+
+ // The destRect width and height have not been adjusted to fit within the
+ // cell width and height.
+ // We must adjust the width even if image is null, because the width is used
+ // to update the aRemainingWidth and aCurrX values.
+ // Since the height isn't used unless the image is not null, we will adjust
+ // the height inside the if (image) block below.
+
+ if (destRect.width > imageRect.width) {
+ // The destRect is too wide to fit within the cell width.
+ // Adjust destRect width to fit within the cell width.
+ destRect.width = imageRect.width;
+ } else {
+ // The cell is wider than the destRect.
+ // In a cycler column, the image is centered horizontally.
+ if (!aColumn->IsCycler()) {
+ // If this column is not a cycler, we won't center the image horizontally.
+ // We adjust the imageRect width so that the image is placed at the start
+ // of the cell.
+ imageRect.width = destRect.width;
+ }
+ }
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ if (image) {
+ if (isRTL) imageRect.x = rightEdge - imageRect.width;
+ // Paint our borders and background for our image rect
+ result &= PaintBackgroundLayer(imageContext, aPresContext,
+ aRenderingContext, imageRect, aDirtyRect);
+
+ // The destRect x and y have not been set yet. Let's do that now.
+ // Initially, we use the imageRect x and y.
+ destRect.x = imageRect.x;
+ destRect.y = imageRect.y;
+
+ if (destRect.width < imageRect.width) {
+ // The destRect width is smaller than the cell width.
+ // Center the image horizontally in the cell.
+ // Adjust the destRect x accordingly.
+ destRect.x += (imageRect.width - destRect.width) / 2;
+ }
+
+ // Now it's time to adjust the destRect height to fit within the cell
+ // height.
+ if (destRect.height > imageRect.height) {
+ // The destRect height is larger than the cell height.
+ // Adjust destRect height to fit within the cell height.
+ destRect.height = imageRect.height;
+ } else if (destRect.height < imageRect.height) {
+ // The destRect height is smaller than the cell height.
+ // Center the image vertically in the cell.
+ // Adjust the destRect y accordingly.
+ destRect.y += (imageRect.height - destRect.height) / 2;
+ }
+
+ // It's almost time to paint the image.
+ // Deflate destRect for the border and padding.
+ destRect.Deflate(bp);
+
+ // Compute the area where our whole image would be mapped, to get the
+ // desired subregion onto our actual destRect:
+ nsRect wholeImageDest;
+ CSSIntSize rawImageCSSIntSize;
+ if (NS_SUCCEEDED(image->GetWidth(&rawImageCSSIntSize.width)) &&
+ NS_SUCCEEDED(image->GetHeight(&rawImageCSSIntSize.height))) {
+ // Get the image source rectangle - the rectangle containing the part of
+ // the image that we are going to display. sourceRect will be passed as
+ // the aSrcRect argument in the DrawImage method.
+ nsRect sourceRect =
+ GetImageSourceRect(imageContext, useImageRegion, image);
+
+ // Let's say that the image is 100 pixels tall and that the CSS has
+ // specified that the destination height should be 50 pixels tall. Let's
+ // say that the cell height is only 20 pixels. So, in those 20 visible
+ // pixels, we want to see the top 20/50ths of the image. So, the
+ // sourceRect.height should be 100 * 20 / 50, which is 40 pixels.
+ // Essentially, we are scaling the image as dictated by the CSS
+ // destination height and width, and we are then clipping the scaled
+ // image by the cell width and height.
+ nsSize rawImageSize(CSSPixel::ToAppUnits(rawImageCSSIntSize));
+ wholeImageDest = nsLayoutUtils::GetWholeImageDestination(
+ rawImageSize, sourceRect, nsRect(destRect.TopLeft(), imageDestSize));
+ } else {
+ // GetWidth/GetHeight failed, so we can't easily map a subregion of the
+ // source image onto the destination area.
+ // * If this happens with a RasterImage, it probably means the image is
+ // in an error state, and we shouldn't draw anything. Hence, we leave
+ // wholeImageDest as an empty rect (its initial state).
+ // * If this happens with a VectorImage, it probably means the image has
+ // no explicit width or height attribute -- but we can still proceed and
+ // just treat the destination area as our whole SVG image area. Hence, we
+ // set wholeImageDest to the full destRect.
+ if (image->GetType() == imgIContainer::TYPE_VECTOR) {
+ wholeImageDest = destRect;
+ }
+ }
+
+ if (opacity != 1.0f) {
+ aRenderingContext.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA,
+ opacity);
+ }
+
+ uint32_t drawFlags = aBuilder && aBuilder->UseHighQualityScaling()
+ ? imgIContainer::FLAG_HIGH_QUALITY_SCALING
+ : imgIContainer::FLAG_NONE;
+ result &= nsLayoutUtils::DrawImage(
+ aRenderingContext, imageContext, aPresContext, image,
+ nsLayoutUtils::GetSamplingFilterForFrame(this), wholeImageDest,
+ destRect, destRect.TopLeft(), aDirtyRect, drawFlags);
+
+ if (opacity != 1.0f) {
+ aRenderingContext.PopGroupAndBlend();
+ }
+ }
+
+ // Update the aRemainingWidth and aCurrX values.
+ imageRect.Inflate(imageMargin);
+ aRemainingWidth -= imageRect.width;
+ if (!isRTL) {
+ aCurrX += imageRect.width;
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintText(
+ int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aTextRect,
+ nsPresContext* aPresContext, gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aCurrX) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl;
+
+ // Now obtain the text for our cell.
+ nsAutoString text;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ view->GetCellText(aRowIndex, aColumn, text);
+
+ // We're going to paint this text so we need to ensure bidi is enabled if
+ // necessary
+ CheckTextForBidi(text);
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ if (text.Length() == 0) {
+ // Don't paint an empty string. XXX What about background/borders? Still
+ // paint?
+ return result;
+ }
+
+ int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel();
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+
+ // Resolve style for the text. It contains all the info we need to lay
+ // ourselves out and to paint.
+ ComputedStyle* textContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText());
+
+ // Obtain opacity value for the image.
+ float opacity = textContext->StyleEffects()->mOpacity;
+
+ // Obtain the margins for the text and then deflate our rect by that
+ // amount. The text is assumed to be contained within the deflated rect.
+ nsRect textRect(aTextRect);
+ nsMargin textMargin;
+ textContext->StyleMargin()->GetMargin(textMargin);
+ textRect.Deflate(textMargin);
+
+ // Adjust the rect for its border and padding.
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(textContext, bp);
+ textRect.Deflate(bp);
+
+ // Compute our text size.
+ RefPtr<nsFontMetrics> fontMet =
+ nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, PresContext());
+
+ nscoord height = fontMet->MaxHeight();
+ nscoord baseline = fontMet->MaxAscent();
+
+ // Center the text. XXX Obey vertical-align style prop?
+ if (height < textRect.height) {
+ textRect.y += (textRect.height - height) / 2;
+ textRect.height = height;
+ }
+
+ // Set our font.
+ AdjustForCellText(text, aRowIndex, aColumn, aRenderingContext, *fontMet,
+ textRect);
+ textRect.Inflate(bp);
+
+ // Subtract out the remaining width.
+ if (!isRTL) aCurrX += textRect.width + textMargin.LeftRight();
+
+ result &= PaintBackgroundLayer(textContext, aPresContext, aRenderingContext,
+ textRect, aDirtyRect);
+
+ // Time to paint our text.
+ textRect.Deflate(bp);
+
+ // Set our color.
+ ColorPattern color(ToDeviceColor(textContext->StyleText()->mColor));
+
+ // Draw decorations.
+ StyleTextDecorationLine decorations =
+ textContext->StyleTextReset()->mTextDecorationLine;
+
+ nscoord offset;
+ nscoord size;
+ if (decorations & (StyleTextDecorationLine::OVERLINE |
+ StyleTextDecorationLine::UNDERLINE)) {
+ fontMet->GetUnderline(offset, size);
+ if (decorations & StyleTextDecorationLine::OVERLINE) {
+ nsRect r(textRect.x, textRect.y, textRect.width, size);
+ Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget);
+ drawTarget->FillRect(devPxRect, color);
+ }
+ if (decorations & StyleTextDecorationLine::UNDERLINE) {
+ nsRect r(textRect.x, textRect.y + baseline - offset, textRect.width,
+ size);
+ Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget);
+ drawTarget->FillRect(devPxRect, color);
+ }
+ }
+ if (decorations & StyleTextDecorationLine::LINE_THROUGH) {
+ fontMet->GetStrikeout(offset, size);
+ nsRect r(textRect.x, textRect.y + baseline - offset, textRect.width, size);
+ Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget);
+ drawTarget->FillRect(devPxRect, color);
+ }
+ ComputedStyle* cellContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell());
+
+ if (opacity != 1.0f) {
+ aRenderingContext.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA,
+ opacity);
+ }
+
+ aRenderingContext.SetColor(
+ sRGBColor::FromABGR(textContext->StyleText()->mColor.ToColor()));
+ nsLayoutUtils::DrawString(
+ this, *fontMet, &aRenderingContext, text.get(), text.Length(),
+ textRect.TopLeft() + nsPoint(0, baseline), cellContext);
+
+ if (opacity != 1.0f) {
+ aRenderingContext.PopGroupAndBlend();
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintCheckbox(int32_t aRowIndex,
+ nsTreeColumn* aColumn,
+ const nsRect& aCheckboxRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect) {
+ MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed");
+
+ // Resolve style for the checkbox.
+ ComputedStyle* checkboxContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCheckbox());
+
+ nscoord rightEdge = aCheckboxRect.XMost();
+
+ // Obtain the margins for the checkbox and then deflate our rect by that
+ // amount. The checkbox is assumed to be contained within the deflated rect.
+ nsRect checkboxRect(aCheckboxRect);
+ nsMargin checkboxMargin;
+ checkboxContext->StyleMargin()->GetMargin(checkboxMargin);
+ checkboxRect.Deflate(checkboxMargin);
+
+ nsRect imageSize = GetImageSize(aRowIndex, aColumn, true, checkboxContext);
+
+ if (imageSize.height > checkboxRect.height) {
+ imageSize.height = checkboxRect.height;
+ }
+ if (imageSize.width > checkboxRect.width) {
+ imageSize.width = checkboxRect.width;
+ }
+
+ if (StyleVisibility()->mDirection == StyleDirection::Rtl) {
+ checkboxRect.x = rightEdge - checkboxRect.width;
+ }
+
+ // Paint our borders and background for our image rect.
+ ImgDrawResult result =
+ PaintBackgroundLayer(checkboxContext, aPresContext, aRenderingContext,
+ checkboxRect, aDirtyRect);
+
+ // Time to paint the checkbox.
+ // Adjust the rect for its border and padding.
+ nsMargin bp(0, 0, 0, 0);
+ GetBorderPadding(checkboxContext, bp);
+ checkboxRect.Deflate(bp);
+
+ // Get the image for drawing.
+ nsCOMPtr<imgIContainer> image;
+ bool useImageRegion = true;
+ GetImage(aRowIndex, aColumn, true, checkboxContext, useImageRegion,
+ getter_AddRefs(image));
+ if (image) {
+ nsPoint pt = checkboxRect.TopLeft();
+
+ if (imageSize.height < checkboxRect.height) {
+ pt.y += (checkboxRect.height - imageSize.height) / 2;
+ }
+
+ if (imageSize.width < checkboxRect.width) {
+ pt.x += (checkboxRect.width - imageSize.width) / 2;
+ }
+
+ // Apply context paint if applicable
+ SVGImageContext svgContext;
+ SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext,
+ *checkboxContext, image);
+ // Paint the image.
+ result &= nsLayoutUtils::DrawSingleUnscaledImage(
+ aRenderingContext, aPresContext, image, SamplingFilter::POINT, pt,
+ &aDirtyRect, svgContext, imgIContainer::FLAG_NONE, &imageSize);
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintDropFeedback(
+ const nsRect& aDropFeedbackRect, nsPresContext* aPresContext,
+ gfxContext& aRenderingContext, const nsRect& aDirtyRect, nsPoint aPt) {
+ // Paint the drop feedback in between rows.
+
+ nscoord currX;
+
+ // Adjust for the primary cell.
+ nsTreeColumn* primaryCol = mColumns->GetPrimaryColumn();
+
+ if (primaryCol) {
+#ifdef DEBUG
+ nsresult rv =
+#endif
+ primaryCol->GetXInTwips(this, &currX);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "primary column is invalid?");
+
+ currX += aPt.x - mHorzPosition;
+ } else {
+ currX = aDropFeedbackRect.x;
+ }
+
+ PrefillPropertyArray(mSlots->mDropRow, primaryCol);
+
+ // Resolve the style to use for the drop feedback.
+ ComputedStyle* feedbackContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeDropFeedback());
+
+ ImgDrawResult result = ImgDrawResult::SUCCESS;
+
+ // Paint only if it is visible.
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ if (feedbackContext->StyleVisibility()->IsVisibleOrCollapsed()) {
+ int32_t level;
+ view->GetLevel(mSlots->mDropRow, &level);
+
+ // If our previous or next row has greater level use that for
+ // correct visual indentation.
+ if (mSlots->mDropOrient == nsITreeView::DROP_BEFORE) {
+ if (mSlots->mDropRow > 0) {
+ int32_t previousLevel;
+ view->GetLevel(mSlots->mDropRow - 1, &previousLevel);
+ if (previousLevel > level) level = previousLevel;
+ }
+ } else {
+ if (mSlots->mDropRow < mRowCount - 1) {
+ int32_t nextLevel;
+ view->GetLevel(mSlots->mDropRow + 1, &nextLevel);
+ if (nextLevel > level) level = nextLevel;
+ }
+ }
+
+ currX += mIndentation * level;
+
+ if (primaryCol) {
+ ComputedStyle* twistyContext =
+ GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty());
+ nsRect imageSize;
+ nsRect twistyRect;
+ GetTwistyRect(mSlots->mDropRow, primaryCol, imageSize, twistyRect,
+ aPresContext, twistyContext);
+ nsMargin twistyMargin;
+ twistyContext->StyleMargin()->GetMargin(twistyMargin);
+ twistyRect.Inflate(twistyMargin);
+ currX += twistyRect.width;
+ }
+
+ const nsStylePosition* stylePosition = feedbackContext->StylePosition();
+
+ // Obtain the width for the drop feedback or use default value.
+ nscoord width;
+ if (stylePosition->mWidth.ConvertsToLength()) {
+ width = stylePosition->mWidth.ToLength();
+ } else {
+ // Use default width 50px.
+ width = nsPresContext::CSSPixelsToAppUnits(50);
+ }
+
+ // Obtain the height for the drop feedback or use default value.
+ nscoord height;
+ if (stylePosition->mHeight.ConvertsToLength()) {
+ height = stylePosition->mHeight.ToLength();
+ } else {
+ // Use default height 2px.
+ height = nsPresContext::CSSPixelsToAppUnits(2);
+ }
+
+ // Obtain the margins for the drop feedback and then deflate our rect
+ // by that amount.
+ nsRect feedbackRect(currX, aDropFeedbackRect.y, width, height);
+ nsMargin margin;
+ feedbackContext->StyleMargin()->GetMargin(margin);
+ feedbackRect.Deflate(margin);
+
+ feedbackRect.y += (aDropFeedbackRect.height - height) / 2;
+
+ // Finally paint the drop feedback.
+ result &= PaintBackgroundLayer(feedbackContext, aPresContext,
+ aRenderingContext, feedbackRect, aDirtyRect);
+ }
+
+ return result;
+}
+
+ImgDrawResult nsTreeBodyFrame::PaintBackgroundLayer(
+ ComputedStyle* aComputedStyle, nsPresContext* aPresContext,
+ gfxContext& aRenderingContext, const nsRect& aRect,
+ const nsRect& aDirtyRect) {
+ const nsStyleBorder* myBorder = aComputedStyle->StyleBorder();
+ nsCSSRendering::PaintBGParams params =
+ nsCSSRendering::PaintBGParams::ForAllLayers(
+ *aPresContext, aDirtyRect, aRect, this,
+ nsCSSRendering::PAINTBG_SYNC_DECODE_IMAGES);
+ ImgDrawResult result = nsCSSRendering::PaintStyleImageLayerWithSC(
+ params, aRenderingContext, aComputedStyle, *myBorder);
+
+ result &= nsCSSRendering::PaintBorderWithStyleBorder(
+ aPresContext, aRenderingContext, this, aDirtyRect, aRect, *myBorder,
+ mComputedStyle, PaintBorderFlags::SyncDecodeImages);
+
+ nsCSSRendering::PaintNonThemedOutline(aPresContext, aRenderingContext, this,
+ aDirtyRect, aRect, aComputedStyle);
+
+ return result;
+}
+
+// Scrolling
+nsresult nsTreeBodyFrame::EnsureRowIsVisible(int32_t aRow) {
+ ScrollParts parts = GetScrollParts();
+ nsresult rv = EnsureRowIsVisibleInternal(parts, aRow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ UpdateScrollbars(parts);
+ return rv;
+}
+
+nsresult nsTreeBodyFrame::EnsureRowIsVisibleInternal(const ScrollParts& aParts,
+ int32_t aRow) {
+ if (!mView || !mPageLength) return NS_OK;
+
+ if (mTopRowIndex <= aRow && mTopRowIndex + mPageLength > aRow) return NS_OK;
+
+ if (aRow < mTopRowIndex)
+ ScrollToRowInternal(aParts, aRow);
+ else {
+ // Bring it just on-screen.
+ int32_t distance = aRow - (mTopRowIndex + mPageLength) + 1;
+ ScrollToRowInternal(aParts, mTopRowIndex + distance);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::EnsureCellIsVisible(int32_t aRow,
+ nsTreeColumn* aCol) {
+ if (!aCol) return NS_ERROR_INVALID_ARG;
+
+ ScrollParts parts = GetScrollParts();
+
+ nscoord result = -1;
+ nsresult rv;
+
+ nscoord columnPos;
+ rv = aCol->GetXInTwips(this, &columnPos);
+ if (NS_FAILED(rv)) return rv;
+
+ nscoord columnWidth;
+ rv = aCol->GetWidthInTwips(this, &columnWidth);
+ if (NS_FAILED(rv)) return rv;
+
+ // If the start of the column is before the
+ // start of the horizontal view, then scroll
+ if (columnPos < mHorzPosition) result = columnPos;
+ // If the end of the column is past the end of
+ // the horizontal view, then scroll
+ else if ((columnPos + columnWidth) > (mHorzPosition + mInnerBox.width))
+ result = ((columnPos + columnWidth) - (mHorzPosition + mInnerBox.width)) +
+ mHorzPosition;
+
+ if (result != -1) {
+ rv = ScrollHorzInternal(parts, result);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ rv = EnsureRowIsVisibleInternal(parts, aRow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ UpdateScrollbars(parts);
+ return rv;
+}
+
+void nsTreeBodyFrame::ScrollToRow(int32_t aRow) {
+ ScrollParts parts = GetScrollParts();
+ ScrollToRowInternal(parts, aRow);
+ UpdateScrollbars(parts);
+}
+
+nsresult nsTreeBodyFrame::ScrollToRowInternal(const ScrollParts& aParts,
+ int32_t aRow) {
+ ScrollInternal(aParts, aRow);
+
+ return NS_OK;
+}
+
+void nsTreeBodyFrame::ScrollByLines(int32_t aNumLines) {
+ if (!mView) {
+ return;
+ }
+ int32_t newIndex = mTopRowIndex + aNumLines;
+ ScrollToRow(newIndex);
+}
+
+void nsTreeBodyFrame::ScrollByPages(int32_t aNumPages) {
+ if (!mView) {
+ return;
+ }
+ int32_t newIndex = mTopRowIndex + aNumPages * mPageLength;
+ ScrollToRow(newIndex);
+}
+
+nsresult nsTreeBodyFrame::ScrollInternal(const ScrollParts& aParts,
+ int32_t aRow) {
+ if (!mView) {
+ return NS_OK;
+ }
+
+ // Note that we may be "over scrolled" at this point; that is the
+ // current mTopRowIndex may be larger than mRowCount - mPageLength.
+ // This can happen when items are removed for example. (bug 1085050)
+
+ int32_t maxTopRowIndex = std::max(0, mRowCount - mPageLength);
+ aRow = mozilla::clamped(aRow, 0, maxTopRowIndex);
+ if (aRow == mTopRowIndex) {
+ return NS_OK;
+ }
+ mTopRowIndex = aRow;
+ Invalidate();
+ PostScrollEvent();
+ return NS_OK;
+}
+
+nsresult nsTreeBodyFrame::ScrollHorzInternal(const ScrollParts& aParts,
+ int32_t aPosition) {
+ if (!mView || !aParts.mColumnsScrollFrame || !aParts.mHScrollbar)
+ return NS_OK;
+
+ if (aPosition == mHorzPosition) return NS_OK;
+
+ if (aPosition < 0 || aPosition > mHorzWidth) return NS_OK;
+
+ nsRect bounds = aParts.mColumnsFrame->GetRect();
+ if (aPosition > (mHorzWidth - bounds.width))
+ aPosition = mHorzWidth - bounds.width;
+
+ mHorzPosition = aPosition;
+
+ Invalidate();
+
+ // Update the column scroll view
+ AutoWeakFrame weakFrame(this);
+ aParts.mColumnsScrollFrame->ScrollTo(nsPoint(mHorzPosition, 0),
+ ScrollMode::Instant);
+ if (!weakFrame.IsAlive()) {
+ return NS_ERROR_FAILURE;
+ }
+ // And fire off an event about it all
+ PostScrollEvent();
+ return NS_OK;
+}
+
+void nsTreeBodyFrame::ScrollByPage(nsScrollbarFrame* aScrollbar,
+ int32_t aDirection,
+ ScrollSnapFlags aSnapFlags) {
+ // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored
+ MOZ_ASSERT(aScrollbar != nullptr);
+ ScrollByPages(aDirection);
+}
+
+void nsTreeBodyFrame::ScrollByWhole(nsScrollbarFrame* aScrollbar,
+ int32_t aDirection,
+ ScrollSnapFlags aSnapFlags) {
+ // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored
+ MOZ_ASSERT(aScrollbar != nullptr);
+ int32_t newIndex = aDirection < 0 ? 0 : mTopRowIndex;
+ ScrollToRow(newIndex);
+}
+
+void nsTreeBodyFrame::ScrollByLine(nsScrollbarFrame* aScrollbar,
+ int32_t aDirection,
+ ScrollSnapFlags aSnapFlags) {
+ // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored
+ MOZ_ASSERT(aScrollbar != nullptr);
+ ScrollByLines(aDirection);
+}
+
+void nsTreeBodyFrame::ScrollByUnit(
+ nsScrollbarFrame* aScrollbar, ScrollMode aMode, int32_t aDirection,
+ ScrollUnit aUnit, ScrollSnapFlags aSnapFlags /* = Disabled */) {
+ MOZ_ASSERT_UNREACHABLE("Can't get here, we pass false to MoveToNewPosition");
+}
+
+void nsTreeBodyFrame::RepeatButtonScroll(nsScrollbarFrame* aScrollbar) {
+ ScrollParts parts = GetScrollParts();
+ int32_t increment = aScrollbar->GetIncrement();
+ int32_t direction = 0;
+ if (increment < 0) {
+ direction = -1;
+ } else if (increment > 0) {
+ direction = 1;
+ }
+ bool isHorizontal = aScrollbar->IsXULHorizontal();
+
+ AutoWeakFrame weakFrame(this);
+ if (isHorizontal) {
+ int32_t curpos = aScrollbar->MoveToNewPosition(
+ nsScrollbarFrame::ImplementsScrollByUnit::No);
+ if (weakFrame.IsAlive()) {
+ ScrollHorzInternal(parts, curpos);
+ }
+ } else {
+ ScrollToRowInternal(parts, mTopRowIndex + direction);
+ }
+
+ if (weakFrame.IsAlive() && mScrollbarActivity) {
+ mScrollbarActivity->ActivityOccurred();
+ }
+ if (weakFrame.IsAlive()) {
+ UpdateScrollbars(parts);
+ }
+}
+
+void nsTreeBodyFrame::ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos,
+ nscoord aNewPos) {
+ ScrollParts parts = GetScrollParts();
+
+ if (aOldPos == aNewPos) return;
+
+ AutoWeakFrame weakFrame(this);
+
+ // Vertical Scrollbar
+ if (parts.mVScrollbar == aScrollbar) {
+ nscoord rh = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight);
+ nscoord newIndex = nsPresContext::AppUnitsToIntCSSPixels(aNewPos);
+ nscoord newrow = (rh > 0) ? (newIndex / rh) : 0;
+ ScrollInternal(parts, newrow);
+ // Horizontal Scrollbar
+ } else if (parts.mHScrollbar == aScrollbar) {
+ int32_t newIndex = nsPresContext::AppUnitsToIntCSSPixels(aNewPos);
+ ScrollHorzInternal(parts, newIndex);
+ }
+ if (weakFrame.IsAlive()) {
+ UpdateScrollbars(parts);
+ }
+}
+
+// The style cache.
+ComputedStyle* nsTreeBodyFrame::GetPseudoComputedStyle(
+ nsCSSAnonBoxPseudoStaticAtom* aPseudoElement) {
+ return mStyleCache.GetComputedStyle(PresContext(), mContent, mComputedStyle,
+ aPseudoElement, mScratchArray);
+}
+
+XULTreeElement* nsTreeBodyFrame::GetBaseElement() {
+ if (!mTree) {
+ nsIFrame* parent = GetParent();
+ while (parent) {
+ nsIContent* content = parent->GetContent();
+ if (content && content->IsXULElement(nsGkAtoms::tree)) {
+ mTree = XULTreeElement::FromNodeOrNull(content->AsElement());
+ break;
+ }
+
+ parent = parent->GetInFlowParent();
+ }
+ }
+
+ return mTree;
+}
+
+nsresult nsTreeBodyFrame::ClearStyleAndImageCaches() {
+ mStyleCache.Clear();
+ CancelImageRequests();
+ mImageCache.Clear();
+ return NS_OK;
+}
+
+void nsTreeBodyFrame::RemoveImageCacheEntry(int32_t aRowIndex,
+ nsTreeColumn* aCol) {
+ nsAutoString imageSrc;
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ if (NS_FAILED(view->GetImageSrc(aRowIndex, aCol, imageSrc))) {
+ return;
+ }
+ nsTreeImageCacheEntry entry;
+ if (!mImageCache.Get(imageSrc, &entry)) {
+ return;
+ }
+ nsLayoutUtils::DeregisterImageRequest(PresContext(), entry.request, nullptr);
+ entry.request->UnlockImage();
+ entry.request->CancelAndForgetObserver(NS_BINDING_ABORTED);
+ mImageCache.Remove(imageSrc);
+}
+
+/* virtual */
+void nsTreeBodyFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) {
+ nsLeafBoxFrame::DidSetComputedStyle(aOldComputedStyle);
+
+ // Clear the style cache; the pointers are no longer even valid
+ mStyleCache.Clear();
+ // XXX The following is hacky, but it's not incorrect,
+ // and appears to fix a few bugs with style changes, like text zoom and
+ // dpi changes
+ mIndentation = GetIndentation();
+ mRowHeight = GetRowHeight();
+ mStringWidth = -1;
+}
+
+bool nsTreeBodyFrame::OffsetForHorzScroll(nsRect& rect, bool clip) {
+ rect.x -= mHorzPosition;
+
+ // Scrolled out before
+ if (rect.XMost() <= mInnerBox.x) return false;
+
+ // Scrolled out after
+ if (rect.x > mInnerBox.XMost()) return false;
+
+ if (clip) {
+ nscoord leftEdge = std::max(rect.x, mInnerBox.x);
+ nscoord rightEdge = std::min(rect.XMost(), mInnerBox.XMost());
+ rect.x = leftEdge;
+ rect.width = rightEdge - leftEdge;
+
+ // Should have returned false above
+ NS_ASSERTION(rect.width >= 0, "horz scroll code out of sync");
+ }
+
+ return true;
+}
+
+bool nsTreeBodyFrame::CanAutoScroll(int32_t aRowIndex) {
+ // Check first for partially visible last row.
+ if (aRowIndex == mRowCount - 1) {
+ nscoord y = mInnerBox.y + (aRowIndex - mTopRowIndex) * mRowHeight;
+ if (y < mInnerBox.height && y + mRowHeight > mInnerBox.height) return true;
+ }
+
+ if (aRowIndex > 0 && aRowIndex < mRowCount - 1) return true;
+
+ return false;
+}
+
+// Given a dom event, figure out which row in the tree the mouse is over,
+// if we should drop before/after/on that row or we should auto-scroll.
+// Doesn't query the content about if the drag is allowable, that's done
+// elsewhere.
+//
+// For containers, we break up the vertical space of the row as follows: if in
+// the topmost 25%, the drop is _before_ the row the mouse is over; if in the
+// last 25%, _after_; in the middle 50%, we consider it a drop _on_ the
+// container.
+//
+// For non-containers, if the mouse is in the top 50% of the row, the drop is
+// _before_ and the bottom 50% _after_
+void nsTreeBodyFrame::ComputeDropPosition(WidgetGUIEvent* aEvent, int32_t* aRow,
+ int16_t* aOrient,
+ int16_t* aScrollLines) {
+ *aOrient = -1;
+ *aScrollLines = 0;
+
+ // Convert the event's point to our coordinates. We want it in
+ // the coordinates of our inner box's coordinates.
+ nsPoint pt =
+ nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this});
+ int32_t xTwips = pt.x - mInnerBox.x;
+ int32_t yTwips = pt.y - mInnerBox.y;
+
+ nsCOMPtr<nsITreeView> view = GetExistingView();
+ *aRow = GetRowAtInternal(xTwips, yTwips);
+ if (*aRow >= 0) {
+ // Compute the top/bottom of the row in question.
+ int32_t yOffset = yTwips - mRowHeight * (*aRow - mTopRowIndex);
+
+ bool isContainer = false;
+ view->IsContainer(*aRow, &isContainer);
+ if (isContainer) {
+ // for a container, use a 25%/50%/25% breakdown
+ if (yOffset < mRowHeight / 4)
+ *aOrient = nsITreeView::DROP_BEFORE;
+ else if (yOffset > mRowHeight - (mRowHeight / 4))
+ *aOrient = nsITreeView::DROP_AFTER;
+ else
+ *aOrient = nsITreeView::DROP_ON;
+ } else {
+ // for a non-container use a 50%/50% breakdown
+ if (yOffset < mRowHeight / 2)
+ *aOrient = nsITreeView::DROP_BEFORE;
+ else
+ *aOrient = nsITreeView::DROP_AFTER;
+ }
+ }
+
+ if (CanAutoScroll(*aRow)) {
+ // Get the max value from the look and feel service.
+ int32_t scrollLinesMax =
+ LookAndFeel::GetInt(LookAndFeel::IntID::TreeScrollLinesMax, 0);
+ scrollLinesMax--;
+ if (scrollLinesMax < 0) scrollLinesMax = 0;
+
+ // Determine if we're w/in a margin of the top/bottom of the tree during a
+ // drag. This will ultimately cause us to scroll, but that's done elsewhere.
+ nscoord height = (3 * mRowHeight) / 4;
+ if (yTwips < height) {
+ // scroll up
+ *aScrollLines =
+ NSToIntRound(-scrollLinesMax * (1 - (float)yTwips / height) - 1);
+ } else if (yTwips > mRect.height - height) {
+ // scroll down
+ *aScrollLines = NSToIntRound(
+ scrollLinesMax * (1 - (float)(mRect.height - yTwips) / height) + 1);
+ }
+ }
+} // ComputeDropPosition
+
+void nsTreeBodyFrame::OpenCallback(nsITimer* aTimer, void* aClosure) {
+ auto* self = static_cast<nsTreeBodyFrame*>(aClosure);
+ if (!self) {
+ return;
+ }
+
+ aTimer->Cancel();
+ self->mSlots->mTimer = nullptr;
+
+ nsCOMPtr<nsITreeView> view = self->GetExistingView();
+ if (self->mSlots->mDropRow >= 0) {
+ self->mSlots->mArray.AppendElement(self->mSlots->mDropRow);
+ view->ToggleOpenState(self->mSlots->mDropRow);
+ }
+}
+
+void nsTreeBodyFrame::CloseCallback(nsITimer* aTimer, void* aClosure) {
+ auto* self = static_cast<nsTreeBodyFrame*>(aClosure);
+ if (!self) {
+ return;
+ }
+
+ aTimer->Cancel();
+ self->mSlots->mTimer = nullptr;
+
+ nsCOMPtr<nsITreeView> view = self->GetExistingView();
+ auto array = std::move(self->mSlots->mArray);
+ if (!view) {
+ return;
+ }
+ for (auto elem : Reversed(array)) {
+ view->ToggleOpenState(elem);
+ }
+}
+
+void nsTreeBodyFrame::LazyScrollCallback(nsITimer* aTimer, void* aClosure) {
+ nsTreeBodyFrame* self = static_cast<nsTreeBodyFrame*>(aClosure);
+ if (self) {
+ aTimer->Cancel();
+ self->mSlots->mTimer = nullptr;
+
+ if (self->mView) {
+ // Set a new timer to scroll the tree repeatedly.
+ self->CreateTimer(LookAndFeel::IntID::TreeScrollDelay, ScrollCallback,
+ nsITimer::TYPE_REPEATING_SLACK,
+ getter_AddRefs(self->mSlots->mTimer),
+ "nsTreeBodyFrame::ScrollCallback");
+ self->ScrollByLines(self->mSlots->mScrollLines);
+ // ScrollByLines may have deleted |self|.
+ }
+ }
+}
+
+void nsTreeBodyFrame::ScrollCallback(nsITimer* aTimer, void* aClosure) {
+ nsTreeBodyFrame* self = static_cast<nsTreeBodyFrame*>(aClosure);
+ if (self) {
+ // Don't scroll if we are already at the top or bottom of the view.
+ if (self->mView && self->CanAutoScroll(self->mSlots->mDropRow)) {
+ self->ScrollByLines(self->mSlots->mScrollLines);
+ } else {
+ aTimer->Cancel();
+ self->mSlots->mTimer = nullptr;
+ }
+ }
+}
+
+// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398)
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsTreeBodyFrame::ScrollEvent::Run() {
+ if (mInner) {
+ mInner->FireScrollEvent();
+ }
+ return NS_OK;
+}
+
+void nsTreeBodyFrame::FireScrollEvent() {
+ mScrollEvent.Forget();
+ WidgetGUIEvent event(true, eScroll, nullptr);
+ // scroll events fired at elements don't bubble
+ event.mFlags.mBubbles = false;
+ RefPtr<nsIContent> content = GetContent();
+ RefPtr<nsPresContext> presContext = PresContext();
+ EventDispatcher::Dispatch(content, presContext, &event);
+}
+
+void nsTreeBodyFrame::PostScrollEvent() {
+ if (mScrollEvent.IsPending()) return;
+
+ RefPtr<ScrollEvent> event = new ScrollEvent(this);
+ nsresult rv =
+ mContent->OwnerDoc()->Dispatch(TaskCategory::Other, do_AddRef(event));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("failed to dispatch ScrollEvent");
+ } else {
+ mScrollEvent = std::move(event);
+ }
+}
+
+void nsTreeBodyFrame::ScrollbarActivityStarted() const {
+ if (mScrollbarActivity) {
+ mScrollbarActivity->ActivityStarted();
+ }
+}
+
+void nsTreeBodyFrame::ScrollbarActivityStopped() const {
+ if (mScrollbarActivity) {
+ mScrollbarActivity->ActivityStopped();
+ }
+}
+
+void nsTreeBodyFrame::DetachImageListeners() { mCreatedListeners.Clear(); }
+
+void nsTreeBodyFrame::RemoveTreeImageListener(nsTreeImageListener* aListener) {
+ if (aListener) {
+ mCreatedListeners.Remove(aListener);
+ }
+}
+
+#ifdef ACCESSIBILITY
+static void InitCustomEvent(CustomEvent* aEvent, const nsAString& aType,
+ nsIWritablePropertyBag2* aDetail) {
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(aEvent->GetParentObject())) {
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+ JS::Rooted<JS::Value> detail(cx);
+ if (!ToJSValue(cx, aDetail, &detail)) {
+ jsapi.ClearException();
+ return;
+ }
+
+ aEvent->InitCustomEvent(cx, aType, /* aCanBubble = */ true,
+ /* aCancelable = */ false, detail);
+}
+
+void nsTreeBodyFrame::FireRowCountChangedEvent(int32_t aIndex, int32_t aCount) {
+ RefPtr<XULTreeElement> tree(GetBaseElement());
+ if (!tree) return;
+
+ RefPtr<Document> doc = tree->OwnerDoc();
+ MOZ_ASSERT(doc);
+
+ RefPtr<Event> event =
+ doc->CreateEvent(u"customevent"_ns, CallerType::System, IgnoreErrors());
+
+ CustomEvent* treeEvent = event->AsCustomEvent();
+ if (!treeEvent) {
+ return;
+ }
+
+ nsCOMPtr<nsIWritablePropertyBag2> propBag(
+ do_CreateInstance("@mozilla.org/hash-property-bag;1"));
+ if (!propBag) {
+ return;
+ }
+
+ // Set 'index' data - the row index rows are changed from.
+ propBag->SetPropertyAsInt32(u"index"_ns, aIndex);
+
+ // Set 'count' data - the number of changed rows.
+ propBag->SetPropertyAsInt32(u"count"_ns, aCount);
+
+ InitCustomEvent(treeEvent, u"TreeRowCountChanged"_ns, propBag);
+
+ event->SetTrusted(true);
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(tree, event);
+ asyncDispatcher->PostDOMEvent();
+}
+
+void nsTreeBodyFrame::FireInvalidateEvent(int32_t aStartRowIdx,
+ int32_t aEndRowIdx,
+ nsTreeColumn* aStartCol,
+ nsTreeColumn* aEndCol) {
+ RefPtr<XULTreeElement> tree(GetBaseElement());
+ if (!tree) return;
+
+ RefPtr<Document> doc = tree->OwnerDoc();
+
+ RefPtr<Event> event =
+ doc->CreateEvent(u"customevent"_ns, CallerType::System, IgnoreErrors());
+
+ CustomEvent* treeEvent = event->AsCustomEvent();
+ if (!treeEvent) {
+ return;
+ }
+
+ nsCOMPtr<nsIWritablePropertyBag2> propBag(
+ do_CreateInstance("@mozilla.org/hash-property-bag;1"));
+ if (!propBag) {
+ return;
+ }
+
+ if (aStartRowIdx != -1 && aEndRowIdx != -1) {
+ // Set 'startrow' data - the start index of invalidated rows.
+ propBag->SetPropertyAsInt32(u"startrow"_ns, aStartRowIdx);
+
+ // Set 'endrow' data - the end index of invalidated rows.
+ propBag->SetPropertyAsInt32(u"endrow"_ns, aEndRowIdx);
+ }
+
+ if (aStartCol && aEndCol) {
+ // Set 'startcolumn' data - the start index of invalidated rows.
+ int32_t startColIdx = aStartCol->GetIndex();
+
+ propBag->SetPropertyAsInt32(u"startcolumn"_ns, startColIdx);
+
+ // Set 'endcolumn' data - the start index of invalidated rows.
+ int32_t endColIdx = aEndCol->GetIndex();
+ propBag->SetPropertyAsInt32(u"endcolumn"_ns, endColIdx);
+ }
+
+ InitCustomEvent(treeEvent, u"TreeInvalidated"_ns, propBag);
+
+ event->SetTrusted(true);
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(tree, event);
+ asyncDispatcher->PostDOMEvent();
+}
+#endif
+
+class nsOverflowChecker : public Runnable {
+ public:
+ explicit nsOverflowChecker(nsTreeBodyFrame* aFrame)
+ : mozilla::Runnable("nsOverflowChecker"), mFrame(aFrame) {}
+ NS_IMETHOD Run() override {
+ if (mFrame.IsAlive()) {
+ nsTreeBodyFrame* tree = static_cast<nsTreeBodyFrame*>(mFrame.GetFrame());
+ nsTreeBodyFrame::ScrollParts parts = tree->GetScrollParts();
+ tree->CheckOverflow(parts);
+ }
+ return NS_OK;
+ }
+
+ private:
+ WeakFrame mFrame;
+};
+
+bool nsTreeBodyFrame::FullScrollbarsUpdate(bool aNeedsFullInvalidation) {
+ ScrollParts parts = GetScrollParts();
+ AutoWeakFrame weakFrame(this);
+ AutoWeakFrame weakColumnsFrame(parts.mColumnsFrame);
+ UpdateScrollbars(parts);
+ NS_ENSURE_TRUE(weakFrame.IsAlive(), false);
+ if (aNeedsFullInvalidation) {
+ Invalidate();
+ }
+ InvalidateScrollbars(parts, weakColumnsFrame);
+ NS_ENSURE_TRUE(weakFrame.IsAlive(), false);
+
+ // Overflow checking dispatches synchronous events, which can cause infinite
+ // recursion during reflow. Do the first overflow check synchronously, but
+ // force any nested checks to round-trip through the event loop. See bug
+ // 905909.
+ RefPtr<nsOverflowChecker> checker = new nsOverflowChecker(this);
+ if (!mCheckingOverflow) {
+ nsContentUtils::AddScriptRunner(checker);
+ } else {
+ mContent->OwnerDoc()->Dispatch(TaskCategory::Other, checker.forget());
+ }
+ return weakFrame.IsAlive();
+}
+
+void nsTreeBodyFrame::OnImageIsAnimated(imgIRequest* aRequest) {
+ nsLayoutUtils::RegisterImageRequest(PresContext(), aRequest, nullptr);
+}
diff --git a/layout/xul/tree/nsTreeBodyFrame.h b/layout/xul/tree/nsTreeBodyFrame.h
new file mode 100644
index 0000000000..e2ddacc539
--- /dev/null
+++ b/layout/xul/tree/nsTreeBodyFrame.h
@@ -0,0 +1,614 @@
+/* -*- 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 nsTreeBodyFrame_h
+#define nsTreeBodyFrame_h
+
+#include "mozilla/AtomArray.h"
+#include "mozilla/Attributes.h"
+
+#include "nsLeafBoxFrame.h"
+#include "nsITreeView.h"
+#include "nsIScrollbarMediator.h"
+#include "nsITimer.h"
+#include "nsIReflowCallback.h"
+#include "nsTArray.h"
+#include "nsTreeStyleCache.h"
+#include "nsTreeColumns.h"
+#include "nsTHashMap.h"
+#include "nsTHashSet.h"
+#include "imgIRequest.h"
+#include "imgINotificationObserver.h"
+#include "nsScrollbarFrame.h"
+#include "nsThreadUtils.h"
+#include "mozilla/LookAndFeel.h"
+
+class nsFontMetrics;
+class nsOverflowChecker;
+class nsTreeImageListener;
+
+namespace mozilla {
+class PresShell;
+namespace layout {
+class ScrollbarActivity;
+} // namespace layout
+} // namespace mozilla
+
+// An entry in the tree's image cache
+struct nsTreeImageCacheEntry {
+ nsTreeImageCacheEntry() = default;
+ nsTreeImageCacheEntry(imgIRequest* aRequest,
+ imgINotificationObserver* aListener)
+ : request(aRequest), listener(aListener) {}
+
+ nsCOMPtr<imgIRequest> request;
+ nsCOMPtr<imgINotificationObserver> listener;
+};
+
+// The actual frame that paints the cells and rows.
+class nsTreeBodyFrame final : public nsLeafBoxFrame,
+ public nsIScrollbarMediator,
+ public nsIReflowCallback {
+ typedef mozilla::layout::ScrollbarActivity ScrollbarActivity;
+ typedef mozilla::image::ImgDrawResult ImgDrawResult;
+
+ public:
+ explicit nsTreeBodyFrame(ComputedStyle* aStyle, nsPresContext* aPresContext);
+ ~nsTreeBodyFrame();
+
+ NS_DECL_QUERYFRAME
+ NS_DECL_FRAMEARENA_HELPERS(nsTreeBodyFrame)
+
+ // Callback handler methods for refresh driver based animations.
+ // Calls to these functions are forwarded from nsTreeImageListener. These
+ // mirror how nsImageFrame works.
+ void OnImageIsAnimated(imgIRequest* aRequest);
+
+ // non-virtual signatures like nsITreeBodyFrame
+ already_AddRefed<nsTreeColumns> Columns() const {
+ RefPtr<nsTreeColumns> cols = mColumns;
+ return cols.forget();
+ }
+ already_AddRefed<nsITreeView> GetExistingView() const {
+ nsCOMPtr<nsITreeView> view = mView;
+ return view.forget();
+ }
+ already_AddRefed<nsITreeSelection> GetSelection() const;
+ nsresult GetView(nsITreeView** aView);
+ nsresult SetView(nsITreeView* aView);
+ bool GetFocused() const { return mFocused; }
+ nsresult SetFocused(bool aFocused);
+ nsresult GetTreeBody(mozilla::dom::Element** aElement);
+ int32_t RowHeight() const;
+ int32_t RowWidth();
+ int32_t GetHorizontalPosition() const;
+ mozilla::Maybe<mozilla::CSSIntRegion> GetSelectionRegion();
+ int32_t FirstVisibleRow() const { return mTopRowIndex; }
+ int32_t LastVisibleRow() const { return mTopRowIndex + mPageLength; }
+ int32_t PageLength() const { return mPageLength; }
+ nsresult EnsureRowIsVisible(int32_t aRow);
+ nsresult EnsureCellIsVisible(int32_t aRow, nsTreeColumn* aCol);
+ void ScrollToRow(int32_t aRow);
+ void ScrollByLines(int32_t aNumLines);
+ void ScrollByPages(int32_t aNumPages);
+ nsresult Invalidate();
+ nsresult InvalidateColumn(nsTreeColumn* aCol);
+ nsresult InvalidateRow(int32_t aRow);
+ nsresult InvalidateCell(int32_t aRow, nsTreeColumn* aCol);
+ nsresult InvalidateRange(int32_t aStart, int32_t aEnd);
+ int32_t GetRowAt(int32_t aX, int32_t aY);
+ nsresult GetCellAt(int32_t aX, int32_t aY, int32_t* aRow, nsTreeColumn** aCol,
+ nsACString& aChildElt);
+ nsresult GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol,
+ const nsACString& aElt, int32_t* aX,
+ int32_t* aY, int32_t* aWidth, int32_t* aHeight);
+ nsresult IsCellCropped(int32_t aRow, nsTreeColumn* aCol, bool* aResult);
+ nsresult RowCountChanged(int32_t aIndex, int32_t aCount);
+ nsresult BeginUpdateBatch();
+ nsresult EndUpdateBatch();
+ nsresult ClearStyleAndImageCaches();
+ void RemoveImageCacheEntry(int32_t aRowIndex, nsTreeColumn* aCol);
+
+ void CancelImageRequests();
+
+ void ManageReflowCallback(const nsRect& aRect, nscoord aHorzWidth);
+
+ virtual nsSize GetXULMinSize(nsBoxLayoutState& aBoxLayoutState) override;
+ virtual void SetXULBounds(nsBoxLayoutState& aBoxLayoutState,
+ const nsRect& aRect,
+ bool aRemoveOverflowArea = false) override;
+
+ // nsIReflowCallback
+ virtual bool ReflowFinished() override;
+ virtual void ReflowCallbackCanceled() override;
+
+ // nsIScrollbarMediator
+ virtual void ScrollByPage(nsScrollbarFrame* aScrollbar, int32_t aDirection,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) override;
+ virtual void ScrollByWhole(nsScrollbarFrame* aScrollbar, int32_t aDirection,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) override;
+ virtual void ScrollByLine(nsScrollbarFrame* aScrollbar, int32_t aDirection,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) override;
+ virtual void ScrollByUnit(nsScrollbarFrame* aScrollbar,
+ mozilla::ScrollMode aMode, int32_t aDirection,
+ mozilla::ScrollUnit aUnit,
+ mozilla::ScrollSnapFlags aSnapFlags =
+ mozilla::ScrollSnapFlags::Disabled) override;
+ virtual void RepeatButtonScroll(nsScrollbarFrame* aScrollbar) override;
+ virtual void ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos,
+ nscoord aNewPos) override;
+ virtual void ScrollbarReleased(nsScrollbarFrame* aScrollbar) override {}
+ virtual void VisibilityChanged(bool aVisible) override { Invalidate(); }
+ virtual nsIFrame* GetScrollbarBox(bool aVertical) override {
+ ScrollParts parts = GetScrollParts();
+ return aVertical ? parts.mVScrollbar : parts.mHScrollbar;
+ }
+ virtual void ScrollbarActivityStarted() const override;
+ virtual void ScrollbarActivityStopped() const override;
+ virtual bool IsScrollbarOnRight() const override {
+ return StyleVisibility()->mDirection == mozilla::StyleDirection::Ltr;
+ }
+ virtual bool ShouldSuppressScrollbarRepaints() const override {
+ return false;
+ }
+
+ // Overridden from nsIFrame to cache our pres context.
+ virtual void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+ virtual void DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) override;
+
+ mozilla::Maybe<Cursor> GetCursor(const nsPoint&) override;
+
+ virtual nsresult HandleEvent(nsPresContext* aPresContext,
+ mozilla::WidgetGUIEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+ virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) override;
+
+ virtual void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override;
+
+ friend nsIFrame* NS_NewTreeBodyFrame(mozilla::PresShell* aPresShell);
+ friend class nsTreeColumn;
+
+ struct ScrollParts {
+ nsScrollbarFrame* mVScrollbar;
+ RefPtr<mozilla::dom::Element> mVScrollbarContent;
+ nsScrollbarFrame* mHScrollbar;
+ RefPtr<mozilla::dom::Element> mHScrollbarContent;
+ nsIFrame* mColumnsFrame;
+ nsIScrollableFrame* mColumnsScrollFrame;
+ };
+
+ ImgDrawResult PaintTreeBody(gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nsPoint aPt,
+ nsDisplayListBuilder* aBuilder);
+
+ // Get the base element, <tree>
+ mozilla::dom::XULTreeElement* GetBaseElement();
+
+ bool GetVerticalOverflow() const { return mVerticalOverflow; }
+ bool GetHorizontalOverflow() const { return mHorizontalOverflow; }
+
+ // This returns the property array where atoms are stored for style during
+ // draw, whether the row currently being drawn is selected, hovered, etc.
+ const mozilla::AtomArray& GetPropertyArrayForCurrentDrawingItem() {
+ return mScratchArray;
+ }
+
+ protected:
+ friend class nsOverflowChecker;
+
+ // This method paints a specific column background of the tree.
+ ImgDrawResult PaintColumn(nsTreeColumn* aColumn, const nsRect& aColumnRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect);
+
+ // This method paints a single row in the tree.
+ ImgDrawResult PaintRow(int32_t aRowIndex, const nsRect& aRowRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nsPoint aPt,
+ nsDisplayListBuilder* aBuilder);
+
+ // This method paints a single separator in the tree.
+ ImgDrawResult PaintSeparator(int32_t aRowIndex, const nsRect& aSeparatorRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect);
+
+ // This method paints a specific cell in a given row of the tree.
+ ImgDrawResult PaintCell(int32_t aRowIndex, nsTreeColumn* aColumn,
+ const nsRect& aCellRect, nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aCurrX,
+ nsPoint aPt, nsDisplayListBuilder* aBuilder);
+
+ // This method paints the twisty inside a cell in the primary column of an
+ // tree.
+ ImgDrawResult PaintTwisty(int32_t aRowIndex, nsTreeColumn* aColumn,
+ const nsRect& aTwistyRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aRemainingWidth,
+ nscoord& aCurrX);
+
+ // This method paints the image inside the cell of an tree.
+ ImgDrawResult PaintImage(int32_t aRowIndex, nsTreeColumn* aColumn,
+ const nsRect& aImageRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aRemainingWidth,
+ nscoord& aCurrX, nsDisplayListBuilder* aBuilder);
+
+ // This method paints the text string inside a particular cell of the tree.
+ ImgDrawResult PaintText(int32_t aRowIndex, nsTreeColumn* aColumn,
+ const nsRect& aTextRect, nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nscoord& aCurrX);
+
+ // This method paints the checkbox inside a particular cell of the tree.
+ ImgDrawResult PaintCheckbox(int32_t aRowIndex, nsTreeColumn* aColumn,
+ const nsRect& aCheckboxRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect);
+
+ // This method paints a drop feedback of the tree.
+ ImgDrawResult PaintDropFeedback(const nsRect& aDropFeedbackRect,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aDirtyRect, nsPoint aPt);
+
+ // This method is called with a specific ComputedStyle and rect to
+ // paint the background rect as if it were a full-blown frame.
+ ImgDrawResult PaintBackgroundLayer(ComputedStyle* aComputedStyle,
+ nsPresContext* aPresContext,
+ gfxContext& aRenderingContext,
+ const nsRect& aRect,
+ const nsRect& aDirtyRect);
+
+ // An internal hit test. aX and aY are expected to be in twips in the
+ // coordinate system of this frame.
+ int32_t GetRowAtInternal(nscoord aX, nscoord aY);
+
+ // Check for bidi characters in the text, and if there are any, ensure
+ // that the prescontext is in bidi mode.
+ void CheckTextForBidi(nsAutoString& aText);
+
+ void AdjustForCellText(nsAutoString& aText, int32_t aRowIndex,
+ nsTreeColumn* aColumn, gfxContext& aRenderingContext,
+ nsFontMetrics& aFontMetrics, nsRect& aTextRect);
+
+ // A helper used when hit testing.
+ nsCSSAnonBoxPseudoStaticAtom* GetItemWithinCellAt(nscoord aX,
+ const nsRect& aCellRect,
+ int32_t aRowIndex,
+ nsTreeColumn* aColumn);
+
+ // An internal hit test. aX and aY are expected to be in twips in the
+ // coordinate system of this frame.
+ void GetCellAt(nscoord aX, nscoord aY, int32_t* aRow, nsTreeColumn** aCol,
+ nsCSSAnonBoxPseudoStaticAtom** aChildElt);
+
+ // Retrieve the area for the twisty for a cell.
+ nsITheme* GetTwistyRect(int32_t aRowIndex, nsTreeColumn* aColumn,
+ nsRect& aImageRect, nsRect& aTwistyRect,
+ nsPresContext* aPresContext,
+ ComputedStyle* aTwistyContext);
+
+ // Fetch an image from the image cache.
+ nsresult GetImage(int32_t aRowIndex, nsTreeColumn* aCol, bool aUseContext,
+ ComputedStyle* aComputedStyle, bool& aAllowImageRegions,
+ imgIContainer** aResult);
+
+ // Returns the size of a given image. This size *includes* border and
+ // padding. It does not include margins.
+ nsRect GetImageSize(int32_t aRowIndex, nsTreeColumn* aCol, bool aUseContext,
+ ComputedStyle* aComputedStyle);
+
+ // Returns the destination size of the image, not including borders and
+ // padding.
+ nsSize GetImageDestSize(ComputedStyle* aComputedStyle, bool useImageRegion,
+ imgIContainer* image);
+
+ // Returns the source rectangle of the image to be displayed.
+ nsRect GetImageSourceRect(ComputedStyle* aComputedStyle, bool useImageRegion,
+ imgIContainer* image);
+
+ // Returns the height of rows in the tree.
+ int32_t GetRowHeight();
+
+ // Returns our indentation width.
+ int32_t GetIndentation();
+
+ // Calculates our width/height once border and padding have been removed.
+ void CalcInnerBox();
+
+ // Calculate the total width of our scrollable portion
+ nscoord CalcHorzWidth(const ScrollParts& aParts);
+
+ // Looks up a ComputedStyle in the style cache. On a cache miss we resolve
+ // the pseudo-styles passed in and place them into the cache.
+ ComputedStyle* GetPseudoComputedStyle(
+ nsCSSAnonBoxPseudoStaticAtom* aPseudoElement);
+
+ // Retrieves the scrollbars and scrollview relevant to this treebody. We
+ // traverse the frame tree under our base element, in frame order, looking
+ // for the first relevant vertical scrollbar, horizontal scrollbar, and
+ // scrollable frame (with associated content and scrollable view). These
+ // are all volatile and should not be retained.
+ ScrollParts GetScrollParts();
+
+ // Update the curpos of the scrollbar.
+ void UpdateScrollbars(const ScrollParts& aParts);
+
+ // Update the maxpos of the scrollbar.
+ void InvalidateScrollbars(const ScrollParts& aParts,
+ AutoWeakFrame& aWeakColumnsFrame);
+
+ // Check overflow and generate events.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void CheckOverflow(const ScrollParts& aParts);
+
+ // Calls UpdateScrollbars, Invalidate aNeedsFullInvalidation if true,
+ // InvalidateScrollbars and finally CheckOverflow.
+ // returns true if the frame is still alive after the method call.
+ bool FullScrollbarsUpdate(bool aNeedsFullInvalidation);
+
+ // Use to auto-fill some of the common properties without the view having to
+ // do it. Examples include container, open, selected, and focus.
+ void PrefillPropertyArray(int32_t aRowIndex, nsTreeColumn* aCol);
+
+ // Our internal scroll method, used by all the public scroll methods.
+ nsresult ScrollInternal(const ScrollParts& aParts, int32_t aRow);
+ nsresult ScrollToRowInternal(const ScrollParts& aParts, int32_t aRow);
+ nsresult ScrollHorzInternal(const ScrollParts& aParts, int32_t aPosition);
+ nsresult EnsureRowIsVisibleInternal(const ScrollParts& aParts, int32_t aRow);
+
+ // Convert client pixels into appunits in our coordinate space.
+ nsPoint AdjustClientCoordsToBoxCoordSpace(int32_t aX, int32_t aY);
+
+ void EnsureView();
+
+ nsresult GetCellWidth(int32_t aRow, nsTreeColumn* aCol,
+ gfxContext* aRenderingContext, nscoord& aDesiredSize,
+ nscoord& aCurrentSize);
+ nscoord CalcMaxRowWidth();
+
+ // Translate the given rect horizontally from tree coordinates into the
+ // coordinate system of our nsTreeBodyFrame. If clip is true, then clip the
+ // rect to its intersection with mInnerBox in the horizontal direction.
+ // Return whether the result has a nonempty intersection with mInnerBox
+ // after projecting both onto the horizontal coordinate axis.
+ bool OffsetForHorzScroll(nsRect& rect, bool clip);
+
+ bool CanAutoScroll(int32_t aRowIndex);
+
+ // Calc the row and above/below/on status given where the mouse currently is
+ // hovering. Also calc if we're in the region in which we want to auto-scroll
+ // the tree. A positive value of |aScrollLines| means scroll down, a negative
+ // value means scroll up, a zero value means that we aren't in drag scroll
+ // region.
+ void ComputeDropPosition(mozilla::WidgetGUIEvent* aEvent, int32_t* aRow,
+ int16_t* aOrient, int16_t* aScrollLines);
+
+ void InvalidateDropFeedback(int32_t aRow, int16_t aOrientation) {
+ InvalidateRow(aRow);
+ if (aOrientation != nsITreeView::DROP_ON)
+ InvalidateRow(aRow + aOrientation);
+ }
+
+ public:
+ /**
+ * Remove an nsITreeImageListener from being tracked by this frame. Only tree
+ * image listeners that are created by this frame are tracked.
+ *
+ * @param aListener A pointer to an nsTreeImageListener to no longer
+ * track.
+ */
+ void RemoveTreeImageListener(nsTreeImageListener* aListener);
+
+ protected:
+ // Create a new timer. This method is used to delay various actions like
+ // opening/closing folders or tree scrolling.
+ // aID is type of the action, aFunc is the function to be called when
+ // the timer fires and aType is type of timer - one shot or repeating.
+ nsresult CreateTimer(const mozilla::LookAndFeel::IntID aID,
+ nsTimerCallbackFunc aFunc, int32_t aType,
+ nsITimer** aTimer, const char* aName);
+
+ static void OpenCallback(nsITimer* aTimer, void* aClosure);
+
+ static void CloseCallback(nsITimer* aTimer, void* aClosure);
+
+ static void LazyScrollCallback(nsITimer* aTimer, void* aClosure);
+
+ static void ScrollCallback(nsITimer* aTimer, void* aClosure);
+
+ class ScrollEvent : public mozilla::Runnable {
+ public:
+ NS_DECL_NSIRUNNABLE
+ explicit ScrollEvent(nsTreeBodyFrame* aInner)
+ : mozilla::Runnable("nsTreeBodyFrame::ScrollEvent"), mInner(aInner) {}
+ void Revoke() { mInner = nullptr; }
+
+ private:
+ nsTreeBodyFrame* mInner;
+ };
+
+ void PostScrollEvent();
+ MOZ_CAN_RUN_SCRIPT void FireScrollEvent();
+
+ /**
+ * Clear the pointer to this frame for all nsTreeImageListeners that were
+ * created by this frame.
+ */
+ void DetachImageListeners();
+
+#ifdef ACCESSIBILITY
+ /**
+ * Fires 'treeRowCountChanged' event asynchronously. The event is a
+ * CustomEvent that is used to expose the following information structures
+ * via a property bag.
+ *
+ * @param aIndex the row index rows are added/removed from
+ * @param aCount the number of added/removed rows (the sign points to
+ * an operation, plus - addition, minus - removing)
+ */
+ void FireRowCountChangedEvent(int32_t aIndex, int32_t aCount);
+
+ /**
+ * Fires 'treeInvalidated' event asynchronously. The event is a CustomEvent
+ * that is used to expose the information structures described by method
+ * arguments via a property bag.
+ *
+ * @param aStartRow the start index of invalidated rows, -1 means that
+ * columns have been invalidated only
+ * @param aEndRow the end index of invalidated rows, -1 means that columns
+ * have been invalidated only
+ * @param aStartCol the start invalidated column, nullptr means that only
+ * rows have been invalidated
+ * @param aEndCol the end invalidated column, nullptr means that rows have
+ * been invalidated only
+ */
+ void FireInvalidateEvent(int32_t aStartRow, int32_t aEndRow,
+ nsTreeColumn* aStartCol, nsTreeColumn* aEndCol);
+#endif
+
+ protected: // Data Members
+ class Slots {
+ public:
+ Slots() = default;
+
+ ~Slots() {
+ if (mTimer) {
+ mTimer->Cancel();
+ }
+ }
+
+ friend class nsTreeBodyFrame;
+
+ protected:
+ // If the drop is actually allowed here or not.
+ bool mDropAllowed = false;
+
+ // True while dragging over the tree.
+ bool mIsDragging = false;
+
+ // The row the mouse is hovering over during a drop.
+ int32_t mDropRow = -1;
+
+ // Where we want to draw feedback (above/on this row/below) if allowed.
+ int16_t mDropOrient = -1;
+
+ // Number of lines to be scrolled.
+ int16_t mScrollLines = 0;
+
+ // The drag action that was received for this slot
+ uint32_t mDragAction = 0;
+
+ // Timer for opening/closing spring loaded folders or scrolling the tree.
+ nsCOMPtr<nsITimer> mTimer;
+
+ // An array used to keep track of all spring loaded folders.
+ nsTArray<int32_t> mArray;
+ };
+
+ mozilla::UniquePtr<Slots> mSlots;
+
+ nsRevocableEventPtr<ScrollEvent> mScrollEvent;
+
+ RefPtr<ScrollbarActivity> mScrollbarActivity;
+
+ // The <tree> element containing this treebody.
+ RefPtr<mozilla::dom::XULTreeElement> mTree;
+
+ // Cached column information.
+ RefPtr<nsTreeColumns> mColumns;
+
+ // The current view for this tree widget. We get all of our row and cell data
+ // from the view.
+ nsCOMPtr<nsITreeView> mView;
+
+ // A cache of all the ComputedStyles we have seen for rows and cells of the
+ // tree. This is a mapping from a list of atoms to a corresponding
+ // ComputedStyle. This cache stores every combination that occurs in the
+ // tree, so for n distinct properties, this cache could have 2 to the n
+ // entries (the power set of all row properties).
+ nsTreeStyleCache mStyleCache;
+
+ // A hashtable that maps from URLs to image request/listener pairs. The URL
+ // is provided by the view or by the ComputedStyle. The ComputedStyle
+ // represents a resolved :-moz-tree-cell-image (or twisty) pseudo-element.
+ // It maps directly to an imgIRequest.
+ nsTHashMap<nsStringHashKey, nsTreeImageCacheEntry> mImageCache;
+
+ // A scratch array used when looking up cached ComputedStyles.
+ mozilla::AtomArray mScratchArray;
+
+ // The index of the first visible row and the # of rows visible onscreen.
+ // The tree only examines onscreen rows, starting from
+ // this index and going up to index+pageLength.
+ int32_t mTopRowIndex;
+ int32_t mPageLength;
+
+ // The horizontal scroll position
+ nscoord mHorzPosition;
+
+ // The original desired horizontal width before changing it and posting a
+ // reflow callback. In some cases, the desired horizontal width can first be
+ // different from the current desired horizontal width, only to return to
+ // the same value later during the same reflow. In this case, we can cancel
+ // the posted reflow callback and prevent an unnecessary reflow.
+ nscoord mOriginalHorzWidth;
+ // Our desired horizontal width (the width for which we actually have tree
+ // columns).
+ nscoord mHorzWidth;
+ // The amount by which to adjust the width of the last cell.
+ // This depends on whether or not the columnpicker and scrollbars are present.
+ nscoord mAdjustWidth;
+
+ // Cached heights and indent info.
+ nsRect mInnerBox; // 4-byte aligned
+ int32_t mRowHeight;
+ int32_t mIndentation;
+ nscoord mStringWidth;
+
+ int32_t mUpdateBatchNest;
+
+ // Cached row count.
+ int32_t mRowCount;
+
+ // The row the mouse is hovering over.
+ int32_t mMouseOverRow;
+
+ // Whether or not we're currently focused.
+ bool mFocused;
+
+ // Do we have a fixed number of onscreen rows?
+ bool mHasFixedRowCount;
+
+ bool mVerticalOverflow;
+ bool mHorizontalOverflow;
+
+ bool mReflowCallbackPosted;
+
+ // Set while we flush layout to take account of effects of
+ // overflow/underflow event handlers
+ bool mCheckingOverflow;
+
+ // Hash set to keep track of which listeners we created and thus
+ // have pointers to us.
+ nsTHashSet<nsTreeImageListener*> mCreatedListeners;
+
+}; // class nsTreeBodyFrame
+
+#endif
diff --git a/layout/xul/tree/nsTreeColFrame.cpp b/layout/xul/tree/nsTreeColFrame.cpp
new file mode 100644
index 0000000000..d075da8a72
--- /dev/null
+++ b/layout/xul/tree/nsTreeColFrame.cpp
@@ -0,0 +1,169 @@
+/* -*- 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 "nsTreeColFrame.h"
+
+#include "mozilla/ComputedStyle.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/XULTreeElement.h"
+#include "mozilla/CSSOrderAwareFrameIterator.h"
+#include "nsCOMPtr.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsNameSpaceManager.h"
+#include "nsTreeColumns.h"
+#include "nsDisplayList.h"
+#include "nsTreeBodyFrame.h"
+#include "nsXULElement.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+//
+// NS_NewTreeColFrame
+//
+// Creates a new col frame
+//
+nsIFrame* NS_NewTreeColFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsTreeColFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsTreeColFrame)
+
+// Destructor
+nsTreeColFrame::~nsTreeColFrame() = default;
+
+void nsTreeColFrame::Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) {
+ nsBoxFrame::Init(aContent, aParent, aPrevInFlow);
+ InvalidateColumns();
+}
+
+void nsTreeColFrame::DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) {
+ InvalidateColumns(false);
+ nsBoxFrame::DestroyFrom(aDestructRoot, aPostDestroyData);
+}
+
+nsresult nsTreeColFrame::AttributeChanged(int32_t aNameSpaceID,
+ nsAtom* aAttribute,
+ int32_t aModType) {
+ nsresult rv =
+ nsBoxFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType);
+
+ if (aAttribute == nsGkAtoms::primary) {
+ InvalidateColumns();
+ }
+
+ return rv;
+}
+
+void nsTreeColFrame::SetXULBounds(nsBoxLayoutState& aBoxLayoutState,
+ const nsRect& aRect,
+ bool aRemoveOverflowArea) {
+ nscoord oldWidth = mRect.width;
+
+ nsBoxFrame::SetXULBounds(aBoxLayoutState, aRect, aRemoveOverflowArea);
+ if (mRect.width != oldWidth) {
+ RefPtr<XULTreeElement> tree = GetTree();
+ if (tree) {
+ tree->Invalidate();
+ }
+ }
+}
+
+XULTreeElement* nsTreeColFrame::GetTree() {
+ nsIContent* parent = mContent->GetParent();
+ return parent ? XULTreeElement::FromNodeOrNull(parent->GetParent()) : nullptr;
+}
+
+void nsTreeColFrame::InvalidateColumns(bool aCanWalkFrameTree) {
+ RefPtr<XULTreeElement> tree = GetTree();
+ if (!tree) {
+ return;
+ }
+
+ nsTreeBodyFrame* body = aCanWalkFrameTree
+ ? tree->GetTreeBodyFrame(FlushType::None)
+ : tree->GetCachedTreeBodyFrame();
+
+ if (!body) {
+ return;
+ }
+
+ RefPtr<nsTreeColumns> columns = body->Columns();
+ if (!columns) {
+ return;
+ }
+
+ columns->InvalidateColumns();
+}
+
+namespace mozilla {
+
+class nsDisplayXULTreeColSplitterTarget final : public nsDisplayItem {
+ public:
+ nsDisplayXULTreeColSplitterTarget(nsDisplayListBuilder* aBuilder,
+ nsIFrame* aFrame)
+ : nsDisplayItem(aBuilder, aFrame) {
+ MOZ_COUNT_CTOR(nsDisplayXULTreeColSplitterTarget);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayXULTreeColSplitterTarget)
+
+ virtual void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect,
+ HitTestState* aState,
+ nsTArray<nsIFrame*>* aOutFrames) override;
+ NS_DISPLAY_DECL_NAME("XULTreeColSplitterTarget",
+ TYPE_XUL_TREE_COL_SPLITTER_TARGET)
+};
+
+void nsDisplayXULTreeColSplitterTarget::HitTest(
+ nsDisplayListBuilder* aBuilder, const nsRect& aRect, HitTestState* aState,
+ nsTArray<nsIFrame*>* aOutFrames) {
+ nsRect rect = aRect - ToReferenceFrame();
+ // If we are in either in the first 4 pixels or the last 4 pixels, we're going
+ // to do something really strange. Check for an adjacent splitter.
+ bool left = false;
+ bool right = false;
+ if (mFrame->GetSize().width - nsPresContext::CSSPixelsToAppUnits(4) <=
+ rect.XMost()) {
+ right = true;
+ } else if (nsPresContext::CSSPixelsToAppUnits(4) > rect.x) {
+ left = true;
+ }
+
+ // Swap left and right for RTL trees in order to find the correct splitter
+ if (mFrame->StyleVisibility()->mDirection == StyleDirection::Rtl) {
+ std::swap(left, right);
+ }
+
+ if (left || right) {
+ nsIFrame* child = nsBoxFrame::SlowOrdinalGroupAwareSibling(mFrame, right);
+ // We are a header. Look for the correct splitter.
+ if (child && child->GetContent()->IsXULElement(nsGkAtoms::splitter)) {
+ aOutFrames->AppendElement(child);
+ }
+ }
+}
+
+} // namespace mozilla
+
+void nsTreeColFrame::BuildDisplayListForChildren(
+ nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) {
+ if (!aBuilder->IsForEventDelivery()) {
+ nsBoxFrame::BuildDisplayListForChildren(aBuilder, aLists);
+ return;
+ }
+
+ nsDisplayListCollection set(aBuilder);
+ nsBoxFrame::BuildDisplayListForChildren(aBuilder, set);
+
+ WrapListsInRedirector(aBuilder, set, aLists);
+
+ aLists.Content()->AppendNewToTop<nsDisplayXULTreeColSplitterTarget>(aBuilder,
+ this);
+}
diff --git a/layout/xul/tree/nsTreeColFrame.h b/layout/xul/tree/nsTreeColFrame.h
new file mode 100644
index 0000000000..f2525f7599
--- /dev/null
+++ b/layout/xul/tree/nsTreeColFrame.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/. */
+
+#include "mozilla/Attributes.h"
+#include "mozilla/ComputedStyle.h"
+#include "nsBoxFrame.h"
+
+namespace mozilla {
+class PresShell;
+namespace dom {
+class XULTreeElement;
+}
+} // namespace mozilla
+
+nsIFrame* NS_NewTreeColFrame(mozilla::PresShell* aPresShell,
+ mozilla::ComputedStyle* aStyle);
+
+class nsTreeColFrame final : public nsBoxFrame {
+ public:
+ NS_DECL_FRAMEARENA_HELPERS(nsTreeColFrame)
+
+ explicit nsTreeColFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
+ : nsBoxFrame(aStyle, aPresContext, kClassID) {}
+
+ virtual void Init(nsIContent* aContent, nsContainerFrame* aParent,
+ nsIFrame* aPrevInFlow) override;
+
+ virtual void DestroyFrom(nsIFrame* aDestructRoot,
+ PostDestroyData& aPostDestroyData) override;
+
+ virtual void BuildDisplayListForChildren(
+ nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) override;
+
+ virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute,
+ int32_t aModType) override;
+
+ virtual void SetXULBounds(nsBoxLayoutState& aBoxLayoutState,
+ const nsRect& aRect,
+ bool aRemoveOverflowArea = false) override;
+
+ friend nsIFrame* NS_NewTreeColFrame(mozilla::PresShell* aPresShell,
+ ComputedStyle* aStyle);
+
+ protected:
+ virtual ~nsTreeColFrame();
+
+ /**
+ * @return the tree that this column belongs to, or nullptr.
+ */
+ mozilla::dom::XULTreeElement* GetTree();
+
+ /**
+ * Helper method that gets the TreeColumns object this column belongs to
+ * and calls InvalidateColumns() on it.
+ */
+ void InvalidateColumns(bool aCanWalkFrameTree = true);
+};
diff --git a/layout/xul/tree/nsTreeColumns.cpp b/layout/xul/tree/nsTreeColumns.cpp
new file mode 100644
index 0000000000..209a2582b0
--- /dev/null
+++ b/layout/xul/tree/nsTreeColumns.cpp
@@ -0,0 +1,464 @@
+/* -*- 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 "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsTreeColumns.h"
+#include "nsTreeUtils.h"
+#include "mozilla/ComputedStyle.h"
+#include "nsContentUtils.h"
+#include "nsTreeBodyFrame.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/CSSOrderAwareFrameIterator.h"
+#include "mozilla/dom/TreeColumnBinding.h"
+#include "mozilla/dom/TreeColumnsBinding.h"
+#include "mozilla/dom/XULTreeElement.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+// Column class that caches all the info about our column.
+nsTreeColumn::nsTreeColumn(nsTreeColumns* aColumns, dom::Element* aElement)
+ : mContent(aElement), mColumns(aColumns), mIndex(0), mPrevious(nullptr) {
+ NS_ASSERTION(aElement && aElement->NodeInfo()->Equals(nsGkAtoms::treecol,
+ kNameSpaceID_XUL),
+ "nsTreeColumn's content must be a <xul:treecol>");
+
+ Invalidate(IgnoreErrors());
+}
+
+nsTreeColumn::~nsTreeColumn() {
+ if (mNext) {
+ mNext->SetPrevious(nullptr);
+ }
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsTreeColumn)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsTreeColumn)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mContent)
+ if (tmp->mNext) {
+ tmp->mNext->SetPrevious(nullptr);
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mNext)
+ }
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsTreeColumn)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContent)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNext)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeColumn)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeColumn)
+
+// QueryInterface implementation for nsTreeColumn
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeColumn)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+ NS_INTERFACE_MAP_ENTRY_CONCRETE(nsTreeColumn)
+NS_INTERFACE_MAP_END
+
+nsIFrame* nsTreeColumn::GetFrame() { return mContent->GetPrimaryFrame(); }
+
+bool nsTreeColumn::IsLastVisible(nsTreeBodyFrame* aBodyFrame) {
+ NS_ASSERTION(GetFrame(), "should have checked for this already");
+
+ // cyclers are fixed width, don't adjust them
+ if (IsCycler()) return false;
+
+ // we're certainly not the last visible if we're not visible
+ if (GetFrame()->GetRect().width == 0) return false;
+
+ // try to find a visible successor
+ for (nsTreeColumn* next = GetNext(); next; next = next->GetNext()) {
+ nsIFrame* frame = next->GetFrame();
+ if (frame && frame->GetRect().width > 0) return false;
+ }
+ return true;
+}
+
+nsresult nsTreeColumn::GetRect(nsTreeBodyFrame* aBodyFrame, nscoord aY,
+ nscoord aHeight, nsRect* aResult) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) {
+ *aResult = nsRect();
+ return NS_ERROR_FAILURE;
+ }
+
+ const bool isRTL =
+ aBodyFrame->StyleVisibility()->mDirection == StyleDirection::Rtl;
+ *aResult = frame->GetRect();
+ if (frame->StyleVisibility()->IsCollapse()) {
+ aResult->SizeTo(nsSize());
+ }
+ aResult->y = aY;
+ aResult->height = aHeight;
+ if (isRTL)
+ aResult->x += aBodyFrame->mAdjustWidth;
+ else if (IsLastVisible(aBodyFrame))
+ aResult->width += aBodyFrame->mAdjustWidth;
+ return NS_OK;
+}
+
+nsresult nsTreeColumn::GetXInTwips(nsTreeBodyFrame* aBodyFrame,
+ nscoord* aResult) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) {
+ *aResult = 0;
+ return NS_ERROR_FAILURE;
+ }
+ *aResult = frame->GetRect().x;
+ return NS_OK;
+}
+
+nsresult nsTreeColumn::GetWidthInTwips(nsTreeBodyFrame* aBodyFrame,
+ nscoord* aResult) {
+ nsIFrame* frame = GetFrame();
+ if (!frame) {
+ *aResult = 0;
+ return NS_ERROR_FAILURE;
+ }
+ *aResult = frame->GetRect().width;
+ if (IsLastVisible(aBodyFrame)) *aResult += aBodyFrame->mAdjustWidth;
+ return NS_OK;
+}
+
+void nsTreeColumn::GetId(nsAString& aId) const { aId = GetId(); }
+
+void nsTreeColumn::Invalidate(ErrorResult& aRv) {
+ nsIFrame* frame = GetFrame();
+ if (NS_WARN_IF(!frame)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // Fetch the Id.
+ mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::id, mId);
+
+ // If we have an Id, cache the Id as an atom.
+ if (!mId.IsEmpty()) {
+ mAtom = NS_Atomize(mId);
+ }
+
+ // Cache our index.
+ nsTreeUtils::GetColumnIndex(mContent, &mIndex);
+
+ const nsStyleVisibility* vis = frame->StyleVisibility();
+
+ // Cache our text alignment policy.
+ const nsStyleText* textStyle = frame->StyleText();
+
+ mTextAlignment = textStyle->mTextAlign;
+ // START or END alignment sometimes means RIGHT
+ if ((mTextAlignment == StyleTextAlign::Start &&
+ vis->mDirection == StyleDirection::Rtl) ||
+ (mTextAlignment == StyleTextAlign::End &&
+ vis->mDirection == StyleDirection::Ltr)) {
+ mTextAlignment = StyleTextAlign::Right;
+ } else if (mTextAlignment == StyleTextAlign::Start ||
+ mTextAlignment == StyleTextAlign::End) {
+ mTextAlignment = StyleTextAlign::Left;
+ }
+
+ // Figure out if we're the primary column (that has to have indentation
+ // and twisties drawn.
+ mIsPrimary = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::primary,
+ nsGkAtoms::_true, eCaseMatters);
+
+ // Figure out if we're a cycling column (one that doesn't cause a selection
+ // to happen).
+ mIsCycler = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::cycler,
+ nsGkAtoms::_true, eCaseMatters);
+
+ mIsEditable = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::editable,
+ nsGkAtoms::_true, eCaseMatters);
+
+ mOverflow = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::overflow,
+ nsGkAtoms::_true, eCaseMatters);
+
+ // Figure out our column type. Default type is text.
+ mType = TreeColumn_Binding::TYPE_TEXT;
+ static Element::AttrValuesArray typestrings[] = {nsGkAtoms::checkbox,
+ nullptr};
+ switch (mContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type,
+ typestrings, eCaseMatters)) {
+ case 0:
+ mType = TreeColumn_Binding::TYPE_CHECKBOX;
+ break;
+ }
+
+ // Fetch the crop style.
+ mCropStyle = 0;
+ static Element::AttrValuesArray cropstrings[] = {
+ nsGkAtoms::center, nsGkAtoms::left, nsGkAtoms::start, nullptr};
+ switch (mContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::crop,
+ cropstrings, eCaseMatters)) {
+ case 0:
+ mCropStyle = 1;
+ break;
+ case 1:
+ case 2:
+ mCropStyle = 2;
+ break;
+ }
+}
+
+nsIContent* nsTreeColumn::GetParentObject() const { return mContent; }
+
+/* virtual */
+JSObject* nsTreeColumn::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::TreeColumn_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+Element* nsTreeColumn::Element() { return mContent; }
+
+int32_t nsTreeColumn::GetX(mozilla::ErrorResult& aRv) {
+ nsIFrame* frame = GetFrame();
+ if (NS_WARN_IF(!frame)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return 0;
+ }
+
+ return nsPresContext::AppUnitsToIntCSSPixels(frame->GetRect().x);
+}
+
+int32_t nsTreeColumn::GetWidth(mozilla::ErrorResult& aRv) {
+ nsIFrame* frame = GetFrame();
+ if (NS_WARN_IF(!frame)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return 0;
+ }
+
+ return nsPresContext::AppUnitsToIntCSSPixels(frame->GetRect().width);
+}
+
+already_AddRefed<nsTreeColumn> nsTreeColumn::GetPreviousColumn() {
+ return do_AddRef(mPrevious);
+}
+
+nsTreeColumns::nsTreeColumns(nsTreeBodyFrame* aTree) : mTree(aTree) {}
+
+nsTreeColumns::~nsTreeColumns() { nsTreeColumns::InvalidateColumns(); }
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(nsTreeColumns)
+
+// QueryInterface implementation for nsTreeColumns
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeColumns)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeColumns)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeColumns)
+
+nsIContent* nsTreeColumns::GetParentObject() const {
+ return mTree ? mTree->GetBaseElement() : nullptr;
+}
+
+/* virtual */
+JSObject* nsTreeColumns::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::TreeColumns_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+XULTreeElement* nsTreeColumns::GetTree() const {
+ if (!mTree) {
+ return nullptr;
+ }
+
+ return XULTreeElement::FromNodeOrNull(mTree->GetBaseElement());
+}
+
+uint32_t nsTreeColumns::Count() {
+ EnsureColumns();
+ uint32_t count = 0;
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ ++count;
+ }
+ return count;
+}
+
+nsTreeColumn* nsTreeColumns::GetLastColumn() {
+ EnsureColumns();
+ nsTreeColumn* currCol = mFirstColumn;
+ while (currCol) {
+ nsTreeColumn* next = currCol->GetNext();
+ if (!next) {
+ return currCol;
+ }
+ currCol = next;
+ }
+ return nullptr;
+}
+
+nsTreeColumn* nsTreeColumns::GetSortedColumn() {
+ EnsureColumns();
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ if (nsContentUtils::HasNonEmptyAttr(currCol->mContent, kNameSpaceID_None,
+ nsGkAtoms::sortDirection)) {
+ return currCol;
+ }
+ }
+ return nullptr;
+}
+
+nsTreeColumn* nsTreeColumns::GetKeyColumn() {
+ EnsureColumns();
+
+ nsTreeColumn* first = nullptr;
+ nsTreeColumn* primary = nullptr;
+ nsTreeColumn* sorted = nullptr;
+
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ // Skip hidden columns.
+ if (currCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters))
+ continue;
+
+ // Skip non-text column
+ if (currCol->GetType() != TreeColumn_Binding::TYPE_TEXT) continue;
+
+ if (!first) first = currCol;
+
+ if (nsContentUtils::HasNonEmptyAttr(currCol->mContent, kNameSpaceID_None,
+ nsGkAtoms::sortDirection)) {
+ // Use sorted column as the key.
+ sorted = currCol;
+ break;
+ }
+
+ if (currCol->IsPrimary())
+ if (!primary) primary = currCol;
+ }
+
+ if (sorted) return sorted;
+ if (primary) return primary;
+ return first;
+}
+
+nsTreeColumn* nsTreeColumns::GetColumnFor(dom::Element* aElement) {
+ EnsureColumns();
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ if (currCol->mContent == aElement) {
+ return currCol;
+ }
+ }
+ return nullptr;
+}
+
+nsTreeColumn* nsTreeColumns::NamedGetter(const nsAString& aId, bool& aFound) {
+ EnsureColumns();
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ if (currCol->GetId().Equals(aId)) {
+ aFound = true;
+ return currCol;
+ }
+ }
+ aFound = false;
+ return nullptr;
+}
+
+nsTreeColumn* nsTreeColumns::GetNamedColumn(const nsAString& aId) {
+ bool dummy;
+ return NamedGetter(aId, dummy);
+}
+
+void nsTreeColumns::GetSupportedNames(nsTArray<nsString>& aNames) {
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ aNames.AppendElement(currCol->GetId());
+ }
+}
+
+nsTreeColumn* nsTreeColumns::IndexedGetter(uint32_t aIndex, bool& aFound) {
+ EnsureColumns();
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ if (currCol->GetIndex() == static_cast<int32_t>(aIndex)) {
+ aFound = true;
+ return currCol;
+ }
+ }
+ aFound = false;
+ return nullptr;
+}
+
+nsTreeColumn* nsTreeColumns::GetColumnAt(uint32_t aIndex) {
+ bool dummy;
+ return IndexedGetter(aIndex, dummy);
+}
+
+void nsTreeColumns::InvalidateColumns() {
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ currCol->SetColumns(nullptr);
+ }
+ mFirstColumn = nullptr;
+}
+
+nsTreeColumn* nsTreeColumns::GetPrimaryColumn() {
+ EnsureColumns();
+ for (nsTreeColumn* currCol = mFirstColumn; currCol;
+ currCol = currCol->GetNext()) {
+ if (currCol->IsPrimary()) {
+ return currCol;
+ }
+ }
+ return nullptr;
+}
+
+void nsTreeColumns::EnsureColumns() {
+ if (mTree && !mFirstColumn) {
+ nsIContent* treeContent = mTree->GetBaseElement();
+ if (!treeContent) return;
+
+ nsIContent* colsContent =
+ nsTreeUtils::GetDescendantChild(treeContent, nsGkAtoms::treecols);
+ if (!colsContent) return;
+
+ nsIContent* colContent =
+ nsTreeUtils::GetDescendantChild(colsContent, nsGkAtoms::treecol);
+ if (!colContent) return;
+
+ nsIFrame* colFrame = colContent->GetPrimaryFrame();
+ if (!colFrame) return;
+
+ colFrame = colFrame->GetParent();
+ if (!colFrame) return;
+
+ nsTreeColumn* currCol = nullptr;
+
+ // Enumerate the columns in visible order
+ CSSOrderAwareFrameIterator iter(
+ colFrame, FrameChildListID::Principal,
+ CSSOrderAwareFrameIterator::ChildFilter::IncludeAll,
+ CSSOrderAwareFrameIterator::OrderState::Unknown,
+ CSSOrderAwareFrameIterator::OrderingProperty::BoxOrdinalGroup);
+ for (; !iter.AtEnd(); iter.Next()) {
+ nsIFrame* colFrame = iter.get();
+ nsIContent* colContent = colFrame->GetContent();
+ if (!colContent->IsXULElement(nsGkAtoms::treecol)) {
+ continue;
+ }
+ // Create a new column structure.
+ nsTreeColumn* col = new nsTreeColumn(this, colContent->AsElement());
+
+ if (currCol) {
+ currCol->SetNext(col);
+ col->SetPrevious(currCol);
+ } else {
+ mFirstColumn = col;
+ }
+ currCol = col;
+ }
+ }
+}
diff --git a/layout/xul/tree/nsTreeColumns.h b/layout/xul/tree/nsTreeColumns.h
new file mode 100644
index 0000000000..9f2d7989db
--- /dev/null
+++ b/layout/xul/tree/nsTreeColumns.h
@@ -0,0 +1,214 @@
+/* -*- 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 nsTreeColumns_h__
+#define nsTreeColumns_h__
+
+#include "mozilla/Attributes.h"
+#include "mozilla/RefPtr.h"
+#include "nsCoord.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsQueryObject.h"
+#include "nsWrapperCache.h"
+#include "nsString.h"
+
+class nsAtom;
+class nsTreeBodyFrame;
+class nsTreeColumns;
+class nsIFrame;
+class nsIContent;
+struct nsRect;
+
+namespace mozilla {
+enum class StyleTextAlignKeyword : uint8_t;
+using StyleTextAlign = StyleTextAlignKeyword;
+class ErrorResult;
+namespace dom {
+class Element;
+class XULTreeElement;
+} // namespace dom
+} // namespace mozilla
+
+#define NS_TREECOLUMN_IMPL_CID \
+ { /* 02cd1963-4b5d-4a6c-9223-814d3ade93a3 */ \
+ 0x02cd1963, 0x4b5d, 0x4a6c, { \
+ 0x92, 0x23, 0x81, 0x4d, 0x3a, 0xde, 0x93, 0xa3 \
+ } \
+ }
+
+// This class is our column info. We use it to iterate our columns and to
+// obtain information about each column.
+class nsTreeColumn final : public nsISupports, public nsWrapperCache {
+ public:
+ nsTreeColumn(nsTreeColumns* aColumns, mozilla::dom::Element* aElement);
+
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_TREECOLUMN_IMPL_CID)
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsTreeColumn)
+
+ // WebIDL
+ nsIContent* GetParentObject() const;
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ mozilla::dom::Element* Element();
+
+ nsTreeColumns* GetColumns() const { return mColumns; }
+
+ int32_t GetX(mozilla::ErrorResult& aRv);
+ int32_t GetWidth(mozilla::ErrorResult& aRv);
+
+ void GetId(nsAString& aId) const;
+ int32_t Index() const { return mIndex; }
+
+ bool Primary() const { return mIsPrimary; }
+ bool Cycler() const { return mIsCycler; }
+ bool Editable() const { return mIsEditable; }
+ int16_t Type() const { return mType; }
+
+ nsTreeColumn* GetNext() const { return mNext; }
+ nsTreeColumn* GetPrevious() const { return mPrevious; }
+
+ already_AddRefed<nsTreeColumn> GetPreviousColumn();
+
+ void Invalidate(mozilla::ErrorResult& aRv);
+
+ friend class nsTreeBodyFrame;
+ friend class nsTreeColumns;
+
+ protected:
+ ~nsTreeColumn();
+ nsIFrame* GetFrame();
+ nsIFrame* GetFrame(nsTreeBodyFrame* aBodyFrame);
+ // Don't call this if GetWidthInTwips or GetRect fails
+ bool IsLastVisible(nsTreeBodyFrame* aBodyFrame);
+
+ /**
+ * Returns a rect with x and width taken from the frame's rect and specified
+ * y and height. May fail in case there's no frame for the column.
+ */
+ nsresult GetRect(nsTreeBodyFrame* aBodyFrame, nscoord aY, nscoord aHeight,
+ nsRect* aResult);
+
+ nsresult GetXInTwips(nsTreeBodyFrame* aBodyFrame, nscoord* aResult);
+ nsresult GetWidthInTwips(nsTreeBodyFrame* aBodyFrame, nscoord* aResult);
+
+ void SetColumns(nsTreeColumns* aColumns) { mColumns = aColumns; }
+
+ public:
+ const nsAString& GetId() const { return mId; }
+ nsAtom* GetAtom() { return mAtom; }
+ int32_t GetIndex() { return mIndex; }
+
+ protected:
+ bool IsPrimary() { return mIsPrimary; }
+ bool IsCycler() { return mIsCycler; }
+ bool IsEditable() { return mIsEditable; }
+ bool Overflow() { return mOverflow; }
+
+ int16_t GetType() { return mType; }
+
+ int8_t GetCropStyle() { return mCropStyle; }
+ mozilla::StyleTextAlign GetTextAlignment() { return mTextAlignment; }
+
+ void SetNext(nsTreeColumn* aNext) {
+ NS_ASSERTION(!mNext, "already have a next sibling");
+ mNext = aNext;
+ }
+ void SetPrevious(nsTreeColumn* aPrevious) { mPrevious = aPrevious; }
+
+ private:
+ /**
+ * Non-null nsIContent for the associated <treecol> element.
+ */
+ RefPtr<mozilla::dom::Element> mContent;
+
+ nsTreeColumns* mColumns;
+
+ nsString mId;
+ RefPtr<nsAtom> mAtom;
+
+ int32_t mIndex;
+
+ bool mIsPrimary;
+ bool mIsCycler;
+ bool mIsEditable;
+ bool mOverflow;
+
+ int16_t mType;
+
+ int8_t mCropStyle;
+ mozilla::StyleTextAlign mTextAlignment;
+
+ RefPtr<nsTreeColumn> mNext;
+ nsTreeColumn* mPrevious;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsTreeColumn, NS_TREECOLUMN_IMPL_CID)
+
+class nsTreeColumns final : public nsISupports, public nsWrapperCache {
+ private:
+ ~nsTreeColumns();
+
+ public:
+ explicit nsTreeColumns(nsTreeBodyFrame* aTree);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsTreeColumns)
+
+ nsIContent* GetParentObject() const;
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL
+ mozilla::dom::XULTreeElement* GetTree() const;
+ uint32_t Count();
+ uint32_t Length() { return Count(); }
+
+ nsTreeColumn* GetFirstColumn() {
+ EnsureColumns();
+ return mFirstColumn;
+ }
+ nsTreeColumn* GetLastColumn();
+
+ nsTreeColumn* GetPrimaryColumn();
+ nsTreeColumn* GetSortedColumn();
+ nsTreeColumn* GetKeyColumn();
+
+ nsTreeColumn* GetColumnFor(mozilla::dom::Element* aElement);
+
+ nsTreeColumn* IndexedGetter(uint32_t aIndex, bool& aFound);
+ nsTreeColumn* GetColumnAt(uint32_t aIndex);
+ nsTreeColumn* NamedGetter(const nsAString& aId, bool& aFound);
+ nsTreeColumn* GetNamedColumn(const nsAString& aId);
+ void GetSupportedNames(nsTArray<nsString>& aNames);
+
+ void InvalidateColumns();
+
+ friend class nsTreeBodyFrame;
+
+ protected:
+ void SetTree(nsTreeBodyFrame* aTree) { mTree = aTree; }
+
+ // Builds our cache of column info.
+ void EnsureColumns();
+
+ private:
+ nsTreeBodyFrame* mTree;
+
+ /**
+ * The first column in the list of columns. All of the columns are supposed
+ * to be "alive", i.e. have a frame. This is achieved by clearing the columns
+ * list each time an nsTreeColFrame is destroyed.
+ *
+ * XXX this means that new nsTreeColumn objects are unnecessarily created
+ * for untouched columns.
+ */
+ RefPtr<nsTreeColumn> mFirstColumn;
+};
+
+#endif // nsTreeColumns_h__
diff --git a/layout/xul/tree/nsTreeContentView.cpp b/layout/xul/tree/nsTreeContentView.cpp
new file mode 100644
index 0000000000..edb5368e2a
--- /dev/null
+++ b/layout/xul/tree/nsTreeContentView.cpp
@@ -0,0 +1,1269 @@
+/* -*- 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 "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsTreeUtils.h"
+#include "nsTreeContentView.h"
+#include "ChildIterator.h"
+#include "nsError.h"
+#include "nsXULSortService.h"
+#include "nsTreeBodyFrame.h"
+#include "nsTreeColumns.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/TreeContentViewBinding.h"
+#include "mozilla/dom/XULTreeElement.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/dom/Document.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+// A content model view implementation for the tree.
+
+#define ROW_FLAG_CONTAINER 0x01
+#define ROW_FLAG_OPEN 0x02
+#define ROW_FLAG_EMPTY 0x04
+#define ROW_FLAG_SEPARATOR 0x08
+
+class Row {
+ public:
+ Row(Element* aContent, int32_t aParentIndex)
+ : mContent(aContent),
+ mParentIndex(aParentIndex),
+ mSubtreeSize(0),
+ mFlags(0) {}
+
+ ~Row() = default;
+
+ void SetContainer(bool aContainer) {
+ aContainer ? mFlags |= ROW_FLAG_CONTAINER : mFlags &= ~ROW_FLAG_CONTAINER;
+ }
+ bool IsContainer() { return mFlags & ROW_FLAG_CONTAINER; }
+
+ void SetOpen(bool aOpen) {
+ aOpen ? mFlags |= ROW_FLAG_OPEN : mFlags &= ~ROW_FLAG_OPEN;
+ }
+ bool IsOpen() { return !!(mFlags & ROW_FLAG_OPEN); }
+
+ void SetEmpty(bool aEmpty) {
+ aEmpty ? mFlags |= ROW_FLAG_EMPTY : mFlags &= ~ROW_FLAG_EMPTY;
+ }
+ bool IsEmpty() { return !!(mFlags & ROW_FLAG_EMPTY); }
+
+ void SetSeparator(bool aSeparator) {
+ aSeparator ? mFlags |= ROW_FLAG_SEPARATOR : mFlags &= ~ROW_FLAG_SEPARATOR;
+ }
+ bool IsSeparator() { return !!(mFlags & ROW_FLAG_SEPARATOR); }
+
+ // Weak reference to a content item.
+ Element* mContent;
+
+ // The parent index of the item, set to -1 for the top level items.
+ int32_t mParentIndex;
+
+ // Subtree size for this item.
+ int32_t mSubtreeSize;
+
+ private:
+ // State flags
+ int8_t mFlags;
+};
+
+// We don't reference count the reference to the document
+// If the document goes away first, we'll be informed and we
+// can drop our reference.
+// If we go away first, we'll get rid of ourselves from the
+// document's observer list.
+
+nsTreeContentView::nsTreeContentView(void)
+ : mTree(nullptr), mSelection(nullptr), mDocument(nullptr) {}
+
+nsTreeContentView::~nsTreeContentView(void) {
+ // Remove ourselves from mDocument's observers.
+ if (mDocument) mDocument->RemoveObserver(this);
+}
+
+nsresult NS_NewTreeContentView(nsITreeView** aResult) {
+ *aResult = new nsTreeContentView;
+ if (!*aResult) return NS_ERROR_OUT_OF_MEMORY;
+ NS_ADDREF(*aResult);
+ return NS_OK;
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(nsTreeContentView, mTree, mSelection,
+ mBody)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeContentView)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeContentView)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeContentView)
+ NS_INTERFACE_MAP_ENTRY(nsITreeView)
+ NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITreeView)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+NS_INTERFACE_MAP_END
+
+JSObject* nsTreeContentView::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return TreeContentView_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsISupports* nsTreeContentView::GetParentObject() { return mTree; }
+
+NS_IMETHODIMP
+nsTreeContentView::GetRowCount(int32_t* aRowCount) {
+ *aRowCount = mRows.Length();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetSelection(nsITreeSelection** aSelection) {
+ NS_IF_ADDREF(*aSelection = GetSelection());
+
+ return NS_OK;
+}
+
+bool nsTreeContentView::CanTrustTreeSelection(nsISupports* aValue) {
+ // Untrusted content is only allowed to specify known-good views
+ if (nsContentUtils::LegacyIsCallerChromeOrNativeCode()) return true;
+ nsCOMPtr<nsINativeTreeSelection> nativeTreeSel = do_QueryInterface(aValue);
+ return nativeTreeSel && NS_SUCCEEDED(nativeTreeSel->EnsureNative());
+}
+
+NS_IMETHODIMP
+nsTreeContentView::SetSelection(nsITreeSelection* aSelection) {
+ ErrorResult rv;
+ SetSelection(aSelection, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::SetSelection(nsITreeSelection* aSelection,
+ ErrorResult& aError) {
+ if (aSelection && !CanTrustTreeSelection(aSelection)) {
+ aError.ThrowSecurityError("Not allowed to set tree selection");
+ return;
+ }
+
+ mSelection = aSelection;
+}
+
+void nsTreeContentView::GetRowProperties(int32_t aRow, nsAString& aProperties,
+ ErrorResult& aError) {
+ aProperties.Truncate();
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+ nsIContent* realRow;
+ if (row->IsSeparator())
+ realRow = row->mContent;
+ else
+ realRow = nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+
+ if (realRow && realRow->IsElement()) {
+ realRow->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::properties,
+ aProperties);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetRowProperties(int32_t aIndex, nsAString& aProps) {
+ ErrorResult rv;
+ GetRowProperties(aIndex, aProps, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::GetCellProperties(int32_t aRow, nsTreeColumn& aColumn,
+ nsAString& aProperties,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell) {
+ cell->GetAttr(kNameSpaceID_None, nsGkAtoms::properties, aProperties);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetCellProperties(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& aProps) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ GetCellProperties(aRow, *aCol, aProps, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::GetColumnProperties(nsTreeColumn& aColumn,
+ nsAString& aProperties) {
+ RefPtr<Element> element = aColumn.Element();
+
+ if (element) {
+ element->GetAttr(nsGkAtoms::properties, aProperties);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetColumnProperties(nsTreeColumn* aCol, nsAString& aProps) {
+ NS_ENSURE_ARG(aCol);
+
+ GetColumnProperties(*aCol, aProps);
+ return NS_OK;
+}
+
+bool nsTreeContentView::IsContainer(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ return mRows[aRow]->IsContainer();
+}
+
+NS_IMETHODIMP
+nsTreeContentView::IsContainer(int32_t aIndex, bool* _retval) {
+ ErrorResult rv;
+ *_retval = IsContainer(aIndex, rv);
+ return rv.StealNSResult();
+}
+
+bool nsTreeContentView::IsContainerOpen(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ return mRows[aRow]->IsOpen();
+}
+
+NS_IMETHODIMP
+nsTreeContentView::IsContainerOpen(int32_t aIndex, bool* _retval) {
+ ErrorResult rv;
+ *_retval = IsContainerOpen(aIndex, rv);
+ return rv.StealNSResult();
+}
+
+bool nsTreeContentView::IsContainerEmpty(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ return mRows[aRow]->IsEmpty();
+}
+
+NS_IMETHODIMP
+nsTreeContentView::IsContainerEmpty(int32_t aIndex, bool* _retval) {
+ ErrorResult rv;
+ *_retval = IsContainerEmpty(aIndex, rv);
+ return rv.StealNSResult();
+}
+
+bool nsTreeContentView::IsSeparator(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ return mRows[aRow]->IsSeparator();
+}
+
+NS_IMETHODIMP
+nsTreeContentView::IsSeparator(int32_t aIndex, bool* _retval) {
+ ErrorResult rv;
+ *_retval = IsSeparator(aIndex, rv);
+ return rv.StealNSResult();
+}
+
+NS_IMETHODIMP
+nsTreeContentView::IsSorted(bool* _retval) {
+ *_retval = IsSorted();
+
+ return NS_OK;
+}
+
+bool nsTreeContentView::CanDrop(int32_t aRow, int32_t aOrientation,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ }
+ return false;
+}
+
+bool nsTreeContentView::CanDrop(int32_t aRow, int32_t aOrientation,
+ DataTransfer* aDataTransfer,
+ ErrorResult& aError) {
+ return CanDrop(aRow, aOrientation, aError);
+}
+
+NS_IMETHODIMP
+nsTreeContentView::CanDrop(int32_t aIndex, int32_t aOrientation,
+ DataTransfer* aDataTransfer, bool* _retval) {
+ ErrorResult rv;
+ *_retval = CanDrop(aIndex, aOrientation, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ }
+}
+
+void nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation,
+ DataTransfer* aDataTransfer, ErrorResult& aError) {
+ Drop(aRow, aOrientation, aError);
+}
+
+NS_IMETHODIMP
+nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation,
+ DataTransfer* aDataTransfer) {
+ ErrorResult rv;
+ Drop(aRow, aOrientation, rv);
+ return rv.StealNSResult();
+}
+
+int32_t nsTreeContentView::GetParentIndex(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return 0;
+ }
+
+ return mRows[aRow]->mParentIndex;
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetParentIndex(int32_t aRowIndex, int32_t* _retval) {
+ ErrorResult rv;
+ *_retval = GetParentIndex(aRowIndex, rv);
+ return rv.StealNSResult();
+}
+
+bool nsTreeContentView::HasNextSibling(int32_t aRow, int32_t aAfterIndex,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ // We have a next sibling if the row is not the last in the subtree.
+ int32_t parentIndex = mRows[aRow]->mParentIndex;
+ if (parentIndex < 0) {
+ return uint32_t(aRow) < mRows.Length() - 1;
+ }
+
+ // Compute the last index in this subtree.
+ int32_t lastIndex = parentIndex + (mRows[parentIndex])->mSubtreeSize;
+ Row* row = mRows[lastIndex].get();
+ while (row->mParentIndex != parentIndex) {
+ lastIndex = row->mParentIndex;
+ row = mRows[lastIndex].get();
+ }
+
+ return aRow < lastIndex;
+}
+
+NS_IMETHODIMP
+nsTreeContentView::HasNextSibling(int32_t aRowIndex, int32_t aAfterIndex,
+ bool* _retval) {
+ ErrorResult rv;
+ *_retval = HasNextSibling(aRowIndex, aAfterIndex, rv);
+ return rv.StealNSResult();
+}
+
+int32_t nsTreeContentView::GetLevel(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return 0;
+ }
+
+ int32_t level = 0;
+ Row* row = mRows[aRow].get();
+ while (row->mParentIndex >= 0) {
+ level++;
+ row = mRows[row->mParentIndex].get();
+ }
+ return level;
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetLevel(int32_t aIndex, int32_t* _retval) {
+ ErrorResult rv;
+ *_retval = GetLevel(aIndex, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::GetImageSrc(int32_t aRow, nsTreeColumn& aColumn,
+ nsAString& aSrc, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell) cell->GetAttr(kNameSpaceID_None, nsGkAtoms::src, aSrc);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetImageSrc(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& _retval) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ GetImageSrc(aRow, *aCol, _retval, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::GetCellValue(int32_t aRow, nsTreeColumn& aColumn,
+ nsAString& aValue, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell) cell->GetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetCellValue(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& _retval) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ GetCellValue(aRow, *aCol, _retval, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::GetCellText(int32_t aRow, nsTreeColumn& aColumn,
+ nsAString& aText, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+
+ // Check for a "label" attribute - this is valid on an <treeitem>
+ // with a single implied column.
+ if (row->mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aText) &&
+ !aText.IsEmpty()) {
+ return;
+ }
+
+ if (row->mContent->IsXULElement(nsGkAtoms::treeitem)) {
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell) cell->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aText);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::GetCellText(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& _retval) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ GetCellText(aRow, *aCol, _retval, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::SetTree(XULTreeElement* aTree, ErrorResult& aError) {
+ aError = SetTree(aTree);
+}
+
+NS_IMETHODIMP
+nsTreeContentView::SetTree(XULTreeElement* aTree) {
+ ClearRows();
+
+ mTree = aTree;
+
+ if (aTree) {
+ // Add ourselves to document's observers.
+ Document* document = mTree->GetComposedDoc();
+ if (document) {
+ document->AddObserver(this);
+ mDocument = document;
+ }
+
+ RefPtr<dom::Element> bodyElement = mTree->GetTreeBody();
+ if (bodyElement) {
+ mBody = std::move(bodyElement);
+ int32_t index = 0;
+ Serialize(mBody, -1, &index, mRows);
+ }
+ }
+
+ return NS_OK;
+}
+
+void nsTreeContentView::ToggleOpenState(int32_t aRow, ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ // We don't serialize content right here, since content might be generated
+ // lazily.
+ Row* row = mRows[aRow].get();
+
+ if (row->IsOpen())
+ row->mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"false"_ns,
+ true);
+ else
+ row->mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns,
+ true);
+}
+
+NS_IMETHODIMP
+nsTreeContentView::ToggleOpenState(int32_t aIndex) {
+ ErrorResult rv;
+ ToggleOpenState(aIndex, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::CycleHeader(nsTreeColumn& aColumn,
+ ErrorResult& aError) {
+ if (!mTree) return;
+
+ RefPtr<Element> column = aColumn.Element();
+ nsAutoString sort;
+ column->GetAttr(kNameSpaceID_None, nsGkAtoms::sort, sort);
+ if (!sort.IsEmpty()) {
+ nsAutoString sortdirection;
+ static Element::AttrValuesArray strings[] = {
+ nsGkAtoms::ascending, nsGkAtoms::descending, nullptr};
+ switch (column->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::sortDirection,
+ strings, eCaseMatters)) {
+ case 0:
+ sortdirection.AssignLiteral("descending");
+ break;
+ case 1:
+ sortdirection.AssignLiteral("natural");
+ break;
+ default:
+ sortdirection.AssignLiteral("ascending");
+ break;
+ }
+
+ nsAutoString hints;
+ column->GetAttr(kNameSpaceID_None, nsGkAtoms::sorthints, hints);
+ sortdirection.Append(' ');
+ sortdirection += hints;
+
+ XULWidgetSort(mTree, sort, sortdirection);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::CycleHeader(nsTreeColumn* aCol) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ CycleHeader(*aCol, rv);
+ return rv.StealNSResult();
+}
+
+NS_IMETHODIMP
+nsTreeContentView::SelectionChangedXPCOM() { return NS_OK; }
+
+NS_IMETHODIMP
+nsTreeContentView::CycleCell(int32_t aRow, nsTreeColumn* aCol) { return NS_OK; }
+
+bool nsTreeContentView::IsEditable(int32_t aRow, nsTreeColumn& aColumn,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return false;
+ }
+
+ Row* row = mRows[aRow].get();
+
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell && cell->AttrValueIs(kNameSpaceID_None, nsGkAtoms::editable,
+ nsGkAtoms::_false, eCaseMatters)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+NS_IMETHODIMP
+nsTreeContentView::IsEditable(int32_t aRow, nsTreeColumn* aCol, bool* _retval) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ *_retval = IsEditable(aRow, *aCol, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::SetCellValue(int32_t aRow, nsTreeColumn& aColumn,
+ const nsAString& aValue,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell) cell->SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue, true);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::SetCellValue(int32_t aRow, nsTreeColumn* aCol,
+ const nsAString& aValue) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ SetCellValue(aRow, *aCol, aValue, rv);
+ return rv.StealNSResult();
+}
+
+void nsTreeContentView::SetCellText(int32_t aRow, nsTreeColumn& aColumn,
+ const nsAString& aValue,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aRow)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return;
+ }
+
+ Row* row = mRows[aRow].get();
+
+ nsIContent* realRow =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow);
+ if (realRow) {
+ Element* cell = GetCell(realRow, aColumn);
+ if (cell) cell->SetAttr(kNameSpaceID_None, nsGkAtoms::label, aValue, true);
+ }
+}
+
+NS_IMETHODIMP
+nsTreeContentView::SetCellText(int32_t aRow, nsTreeColumn* aCol,
+ const nsAString& aValue) {
+ NS_ENSURE_ARG(aCol);
+
+ ErrorResult rv;
+ SetCellText(aRow, *aCol, aValue, rv);
+ return rv.StealNSResult();
+}
+
+Element* nsTreeContentView::GetItemAtIndex(int32_t aIndex,
+ ErrorResult& aError) {
+ if (!IsValidRowIndex(aIndex)) {
+ aError.Throw(NS_ERROR_INVALID_ARG);
+ return nullptr;
+ }
+
+ return mRows[aIndex]->mContent;
+}
+
+int32_t nsTreeContentView::GetIndexOfItem(Element* aItem) {
+ return FindContent(aItem);
+}
+
+void nsTreeContentView::AttributeChanged(dom::Element* aElement,
+ int32_t aNameSpaceID,
+ nsAtom* aAttribute, int32_t aModType,
+ const nsAttrValue* aOldValue) {
+ // Lots of codepaths under here that do all sorts of stuff, so be safe.
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+
+ // Make sure this notification concerns us.
+ // First check the tag to see if it's one that we care about.
+ if (aElement == mTree || aElement == mBody) {
+ mTree->ClearStyleAndImageCaches();
+ mTree->Invalidate();
+ }
+
+ // We don't consider non-XUL nodes.
+ nsIContent* parent = nullptr;
+ if (!aElement->IsXULElement() ||
+ ((parent = aElement->GetParent()) && !parent->IsXULElement())) {
+ return;
+ }
+ if (!aElement->IsAnyOfXULElements(nsGkAtoms::treecol, nsGkAtoms::treeitem,
+ nsGkAtoms::treeseparator,
+ nsGkAtoms::treerow, nsGkAtoms::treecell)) {
+ return;
+ }
+
+ // If we have a legal tag, go up to the tree/select and make sure
+ // that it's ours.
+
+ for (nsIContent* element = aElement; element != mBody;
+ element = element->GetParent()) {
+ if (!element) return; // this is not for us
+ if (element->IsXULElement(nsGkAtoms::tree)) return; // this is not for us
+ }
+
+ // Handle changes of the hidden attribute.
+ if (aAttribute == nsGkAtoms::hidden &&
+ aElement->IsAnyOfXULElements(nsGkAtoms::treeitem,
+ nsGkAtoms::treeseparator)) {
+ bool hidden = aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters);
+
+ int32_t index = FindContent(aElement);
+ if (hidden && index >= 0) {
+ // Hide this row along with its children.
+ int32_t count = RemoveRow(index);
+ if (mTree) mTree->RowCountChanged(index, -count);
+ } else if (!hidden && index < 0) {
+ // Show this row along with its children.
+ nsCOMPtr<nsIContent> parent = aElement->GetParent();
+ if (parent) {
+ InsertRowFor(parent, aElement);
+ }
+ }
+
+ return;
+ }
+
+ if (aElement->IsXULElement(nsGkAtoms::treecol)) {
+ if (aAttribute == nsGkAtoms::properties) {
+ if (mTree) {
+ RefPtr<nsTreeColumns> cols = mTree->GetColumns();
+ if (cols) {
+ RefPtr<nsTreeColumn> col = cols->GetColumnFor(aElement);
+ mTree->InvalidateColumn(col);
+ }
+ }
+ }
+ } else if (aElement->IsXULElement(nsGkAtoms::treeitem)) {
+ int32_t index = FindContent(aElement);
+ if (index >= 0) {
+ Row* row = mRows[index].get();
+ if (aAttribute == nsGkAtoms::container) {
+ bool isContainer =
+ aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::container,
+ nsGkAtoms::_true, eCaseMatters);
+ row->SetContainer(isContainer);
+ if (mTree) mTree->InvalidateRow(index);
+ } else if (aAttribute == nsGkAtoms::open) {
+ bool isOpen = aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::open,
+ nsGkAtoms::_true, eCaseMatters);
+ bool wasOpen = row->IsOpen();
+ if (!isOpen && wasOpen)
+ CloseContainer(index);
+ else if (isOpen && !wasOpen)
+ OpenContainer(index);
+ } else if (aAttribute == nsGkAtoms::empty) {
+ bool isEmpty =
+ aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::empty,
+ nsGkAtoms::_true, eCaseMatters);
+ row->SetEmpty(isEmpty);
+ if (mTree) mTree->InvalidateRow(index);
+ }
+ }
+ } else if (aElement->IsXULElement(nsGkAtoms::treeseparator)) {
+ int32_t index = FindContent(aElement);
+ if (index >= 0) {
+ if (aAttribute == nsGkAtoms::properties && mTree) {
+ mTree->InvalidateRow(index);
+ }
+ }
+ } else if (aElement->IsXULElement(nsGkAtoms::treerow)) {
+ if (aAttribute == nsGkAtoms::properties) {
+ nsCOMPtr<nsIContent> parent = aElement->GetParent();
+ if (parent) {
+ int32_t index = FindContent(parent);
+ if (index >= 0 && mTree) {
+ mTree->InvalidateRow(index);
+ }
+ }
+ }
+ } else if (aElement->IsXULElement(nsGkAtoms::treecell)) {
+ if (aAttribute == nsGkAtoms::properties || aAttribute == nsGkAtoms::mode ||
+ aAttribute == nsGkAtoms::src || aAttribute == nsGkAtoms::value ||
+ aAttribute == nsGkAtoms::label) {
+ nsIContent* parent = aElement->GetParent();
+ if (parent) {
+ nsCOMPtr<nsIContent> grandParent = parent->GetParent();
+ if (grandParent && grandParent->IsXULElement()) {
+ int32_t index = FindContent(grandParent);
+ if (index >= 0 && mTree) {
+ // XXX Should we make an effort to invalidate only cell ?
+ mTree->InvalidateRow(index);
+ }
+ }
+ }
+ }
+ }
+}
+
+void nsTreeContentView::ContentAppended(nsIContent* aFirstNewContent) {
+ for (nsIContent* cur = aFirstNewContent; cur; cur = cur->GetNextSibling()) {
+ // Our contentinserted doesn't use the index
+ ContentInserted(cur);
+ }
+}
+
+void nsTreeContentView::ContentInserted(nsIContent* aChild) {
+ NS_ASSERTION(aChild, "null ptr");
+ nsIContent* container = aChild->GetParent();
+
+ // Make sure this notification concerns us.
+ // First check the tag to see if it's one that we care about.
+
+ // Don't allow non-XUL nodes.
+ if (!aChild->IsXULElement() || !container->IsXULElement()) return;
+
+ if (!aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, nsGkAtoms::treeseparator,
+ nsGkAtoms::treechildren, nsGkAtoms::treerow,
+ nsGkAtoms::treecell)) {
+ return;
+ }
+
+ // If we have a legal tag, go up to the tree/select and make sure
+ // that it's ours.
+
+ for (nsIContent* element = container; element != mBody;
+ element = element->GetParent()) {
+ if (!element) return; // this is not for us
+ if (element->IsXULElement(nsGkAtoms::tree)) return; // this is not for us
+ }
+
+ // Lots of codepaths under here that do all sorts of stuff, so be safe.
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+
+ if (aChild->IsXULElement(nsGkAtoms::treechildren)) {
+ int32_t index = FindContent(container);
+ if (index >= 0) {
+ Row* row = mRows[index].get();
+ row->SetEmpty(false);
+ if (mTree) mTree->InvalidateRow(index);
+ if (row->IsContainer() && row->IsOpen()) {
+ int32_t count = EnsureSubtree(index);
+ if (mTree) mTree->RowCountChanged(index + 1, count);
+ }
+ }
+ } else if (aChild->IsAnyOfXULElements(nsGkAtoms::treeitem,
+ nsGkAtoms::treeseparator)) {
+ InsertRowFor(container, aChild);
+ } else if (aChild->IsXULElement(nsGkAtoms::treerow)) {
+ int32_t index = FindContent(container);
+ if (index >= 0 && mTree) mTree->InvalidateRow(index);
+ } else if (aChild->IsXULElement(nsGkAtoms::treecell)) {
+ nsCOMPtr<nsIContent> parent = container->GetParent();
+ if (parent) {
+ int32_t index = FindContent(parent);
+ if (index >= 0 && mTree) mTree->InvalidateRow(index);
+ }
+ }
+}
+
+void nsTreeContentView::ContentRemoved(nsIContent* aChild,
+ nsIContent* aPreviousSibling) {
+ NS_ASSERTION(aChild, "null ptr");
+
+ nsIContent* container = aChild->GetParent();
+ // Make sure this notification concerns us.
+ // First check the tag to see if it's one that we care about.
+
+ // We don't consider non-XUL nodes.
+ if (!aChild->IsXULElement() || !container->IsXULElement()) return;
+
+ if (!aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, nsGkAtoms::treeseparator,
+ nsGkAtoms::treechildren, nsGkAtoms::treerow,
+ nsGkAtoms::treecell)) {
+ return;
+ }
+
+ // If we have a legal tag, go up to the tree/select and make sure
+ // that it's ours.
+
+ for (nsIContent* element = container; element != mBody;
+ element = element->GetParent()) {
+ if (!element) return; // this is not for us
+ if (element->IsXULElement(nsGkAtoms::tree)) return; // this is not for us
+ }
+
+ // Lots of codepaths under here that do all sorts of stuff, so be safe.
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+
+ if (aChild->IsXULElement(nsGkAtoms::treechildren)) {
+ int32_t index = FindContent(container);
+ if (index >= 0) {
+ Row* row = mRows[index].get();
+ row->SetEmpty(true);
+ int32_t count = RemoveSubtree(index);
+ // Invalidate also the row to update twisty.
+ if (mTree) {
+ mTree->InvalidateRow(index);
+ mTree->RowCountChanged(index + 1, -count);
+ }
+ }
+ } else if (aChild->IsAnyOfXULElements(nsGkAtoms::treeitem,
+ nsGkAtoms::treeseparator)) {
+ int32_t index = FindContent(aChild);
+ if (index >= 0) {
+ int32_t count = RemoveRow(index);
+ if (mTree) mTree->RowCountChanged(index, -count);
+ }
+ } else if (aChild->IsXULElement(nsGkAtoms::treerow)) {
+ int32_t index = FindContent(container);
+ if (index >= 0 && mTree) mTree->InvalidateRow(index);
+ } else if (aChild->IsXULElement(nsGkAtoms::treecell)) {
+ nsCOMPtr<nsIContent> parent = container->GetParent();
+ if (parent) {
+ int32_t index = FindContent(parent);
+ if (index >= 0 && mTree) mTree->InvalidateRow(index);
+ }
+ }
+}
+
+void nsTreeContentView::NodeWillBeDestroyed(nsINode* aNode) {
+ // XXXbz do we need this strong ref? Do we drop refs to self in ClearRows?
+ nsCOMPtr<nsIMutationObserver> kungFuDeathGrip(this);
+ ClearRows();
+}
+
+// Recursively serialize content, starting with aContent.
+void nsTreeContentView::Serialize(nsIContent* aContent, int32_t aParentIndex,
+ int32_t* aIndex,
+ nsTArray<UniquePtr<Row>>& aRows) {
+ // Don't allow non-XUL nodes.
+ if (!aContent->IsXULElement()) return;
+
+ dom::FlattenedChildIterator iter(aContent);
+ for (nsIContent* content = iter.GetNextChild(); content;
+ content = iter.GetNextChild()) {
+ int32_t count = aRows.Length();
+
+ if (content->IsXULElement(nsGkAtoms::treeitem)) {
+ SerializeItem(content->AsElement(), aParentIndex, aIndex, aRows);
+ } else if (content->IsXULElement(nsGkAtoms::treeseparator)) {
+ SerializeSeparator(content->AsElement(), aParentIndex, aIndex, aRows);
+ }
+
+ *aIndex += aRows.Length() - count;
+ }
+}
+
+void nsTreeContentView::SerializeItem(Element* aContent, int32_t aParentIndex,
+ int32_t* aIndex,
+ nsTArray<UniquePtr<Row>>& aRows) {
+ if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters))
+ return;
+
+ aRows.AppendElement(MakeUnique<Row>(aContent, aParentIndex));
+ Row* row = aRows.LastElement().get();
+
+ if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::container,
+ nsGkAtoms::_true, eCaseMatters)) {
+ row->SetContainer(true);
+ if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::open,
+ nsGkAtoms::_true, eCaseMatters)) {
+ row->SetOpen(true);
+ nsIContent* child =
+ nsTreeUtils::GetImmediateChild(aContent, nsGkAtoms::treechildren);
+ if (child && child->IsXULElement()) {
+ // Now, recursively serialize our child.
+ int32_t count = aRows.Length();
+ int32_t index = 0;
+ Serialize(child, aParentIndex + *aIndex + 1, &index, aRows);
+ row->mSubtreeSize += aRows.Length() - count;
+ } else
+ row->SetEmpty(true);
+ } else if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::empty,
+ nsGkAtoms::_true, eCaseMatters)) {
+ row->SetEmpty(true);
+ }
+ }
+}
+
+void nsTreeContentView::SerializeSeparator(Element* aContent,
+ int32_t aParentIndex,
+ int32_t* aIndex,
+ nsTArray<UniquePtr<Row>>& aRows) {
+ if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters))
+ return;
+
+ auto row = MakeUnique<Row>(aContent, aParentIndex);
+ row->SetSeparator(true);
+ aRows.AppendElement(std::move(row));
+}
+
+void nsTreeContentView::GetIndexInSubtree(nsIContent* aContainer,
+ nsIContent* aContent,
+ int32_t* aIndex) {
+ if (!aContainer->IsXULElement()) return;
+
+ for (nsIContent* content = aContainer->GetFirstChild(); content;
+ content = content->GetNextSibling()) {
+ if (content == aContent) break;
+
+ if (content->IsXULElement(nsGkAtoms::treeitem)) {
+ if (!content->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters)) {
+ (*aIndex)++;
+ if (content->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::container,
+ nsGkAtoms::_true, eCaseMatters) &&
+ content->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::open, nsGkAtoms::_true,
+ eCaseMatters)) {
+ nsIContent* child =
+ nsTreeUtils::GetImmediateChild(content, nsGkAtoms::treechildren);
+ if (child && child->IsXULElement())
+ GetIndexInSubtree(child, aContent, aIndex);
+ }
+ }
+ } else if (content->IsXULElement(nsGkAtoms::treeseparator)) {
+ if (!content->AsElement()->AttrValueIs(kNameSpaceID_None,
+ nsGkAtoms::hidden,
+ nsGkAtoms::_true, eCaseMatters))
+ (*aIndex)++;
+ }
+ }
+}
+
+int32_t nsTreeContentView::EnsureSubtree(int32_t aIndex) {
+ Row* row = mRows[aIndex].get();
+
+ nsIContent* child;
+ child =
+ nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treechildren);
+ if (!child || !child->IsXULElement()) {
+ return 0;
+ }
+
+ AutoTArray<UniquePtr<Row>, 8> rows;
+ int32_t index = 0;
+ Serialize(child, aIndex, &index, rows);
+ // Insert |rows| into |mRows| at position |aIndex|, by first creating empty
+ // UniquePtr entries and then Move'ing |rows|'s entries into them. (Note
+ // that we can't simply use InsertElementsAt with an array argument, since
+ // the destination can't steal ownership from its const source argument.)
+ UniquePtr<Row>* newRows = mRows.InsertElementsAt(aIndex + 1, rows.Length());
+ for (nsTArray<Row>::index_type i = 0; i < rows.Length(); i++) {
+ newRows[i] = std::move(rows[i]);
+ }
+ int32_t count = rows.Length();
+
+ row->mSubtreeSize += count;
+ UpdateSubtreeSizes(row->mParentIndex, count);
+
+ // Update parent indexes, but skip newly added rows.
+ // They already have correct values.
+ UpdateParentIndexes(aIndex, count + 1, count);
+
+ return count;
+}
+
+int32_t nsTreeContentView::RemoveSubtree(int32_t aIndex) {
+ Row* row = mRows[aIndex].get();
+ int32_t count = row->mSubtreeSize;
+
+ mRows.RemoveElementsAt(aIndex + 1, count);
+
+ row->mSubtreeSize -= count;
+ UpdateSubtreeSizes(row->mParentIndex, -count);
+
+ UpdateParentIndexes(aIndex, 0, -count);
+
+ return count;
+}
+
+void nsTreeContentView::InsertRowFor(nsIContent* aParent, nsIContent* aChild) {
+ int32_t grandParentIndex = -1;
+ bool insertRow = false;
+
+ nsCOMPtr<nsIContent> grandParent = aParent->GetParent();
+
+ if (grandParent->IsXULElement(nsGkAtoms::tree)) {
+ // Allow insertion to the outermost container.
+ insertRow = true;
+ } else {
+ // Test insertion to an inner container.
+
+ // First try to find this parent in our array of rows, if we find one
+ // we can be sure that all other parents are open too.
+ grandParentIndex = FindContent(grandParent);
+ if (grandParentIndex >= 0) {
+ // Got it, now test if it is open.
+ if (mRows[grandParentIndex]->IsOpen()) insertRow = true;
+ }
+ }
+
+ if (insertRow) {
+ int32_t index = 0;
+ GetIndexInSubtree(aParent, aChild, &index);
+
+ int32_t count = InsertRow(grandParentIndex, index, aChild);
+ if (mTree) mTree->RowCountChanged(grandParentIndex + index + 1, count);
+ }
+}
+
+int32_t nsTreeContentView::InsertRow(int32_t aParentIndex, int32_t aIndex,
+ nsIContent* aContent) {
+ AutoTArray<UniquePtr<Row>, 8> rows;
+ if (aContent->IsXULElement(nsGkAtoms::treeitem)) {
+ SerializeItem(aContent->AsElement(), aParentIndex, &aIndex, rows);
+ } else if (aContent->IsXULElement(nsGkAtoms::treeseparator)) {
+ SerializeSeparator(aContent->AsElement(), aParentIndex, &aIndex, rows);
+ }
+
+ // We can't use InsertElementsAt since the destination can't steal
+ // ownership from its const source argument.
+ int32_t count = rows.Length();
+ for (nsTArray<Row>::index_type i = 0; i < size_t(count); i++) {
+ mRows.InsertElementAt(aParentIndex + aIndex + i + 1, std::move(rows[i]));
+ }
+
+ UpdateSubtreeSizes(aParentIndex, count);
+
+ // Update parent indexes, but skip added rows.
+ // They already have correct values.
+ UpdateParentIndexes(aParentIndex + aIndex, count + 1, count);
+
+ return count;
+}
+
+int32_t nsTreeContentView::RemoveRow(int32_t aIndex) {
+ Row* row = mRows[aIndex].get();
+ int32_t count = row->mSubtreeSize + 1;
+ int32_t parentIndex = row->mParentIndex;
+
+ mRows.RemoveElementsAt(aIndex, count);
+
+ UpdateSubtreeSizes(parentIndex, -count);
+
+ UpdateParentIndexes(aIndex, 0, -count);
+
+ return count;
+}
+
+void nsTreeContentView::ClearRows() {
+ mRows.Clear();
+ mBody = nullptr;
+ // Remove ourselves from mDocument's observers.
+ if (mDocument) {
+ mDocument->RemoveObserver(this);
+ mDocument = nullptr;
+ }
+}
+
+void nsTreeContentView::OpenContainer(int32_t aIndex) {
+ Row* row = mRows[aIndex].get();
+ row->SetOpen(true);
+
+ int32_t count = EnsureSubtree(aIndex);
+ if (mTree) {
+ mTree->InvalidateRow(aIndex);
+ mTree->RowCountChanged(aIndex + 1, count);
+ }
+}
+
+void nsTreeContentView::CloseContainer(int32_t aIndex) {
+ Row* row = mRows[aIndex].get();
+ row->SetOpen(false);
+
+ int32_t count = RemoveSubtree(aIndex);
+ if (mTree) {
+ mTree->InvalidateRow(aIndex);
+ mTree->RowCountChanged(aIndex + 1, -count);
+ }
+}
+
+int32_t nsTreeContentView::FindContent(nsIContent* aContent) {
+ for (uint32_t i = 0; i < mRows.Length(); i++) {
+ if (mRows[i]->mContent == aContent) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+void nsTreeContentView::UpdateSubtreeSizes(int32_t aParentIndex,
+ int32_t count) {
+ while (aParentIndex >= 0) {
+ Row* row = mRows[aParentIndex].get();
+ row->mSubtreeSize += count;
+ aParentIndex = row->mParentIndex;
+ }
+}
+
+void nsTreeContentView::UpdateParentIndexes(int32_t aIndex, int32_t aSkip,
+ int32_t aCount) {
+ int32_t count = mRows.Length();
+ for (int32_t i = aIndex + aSkip; i < count; i++) {
+ Row* row = mRows[i].get();
+ if (row->mParentIndex > aIndex) {
+ row->mParentIndex += aCount;
+ }
+ }
+}
+
+Element* nsTreeContentView::GetCell(nsIContent* aContainer,
+ nsTreeColumn& aCol) {
+ int32_t colIndex(aCol.GetIndex());
+
+ // Traverse through cells, try to find the cell by index in a row.
+ Element* result = nullptr;
+ int32_t j = 0;
+ dom::FlattenedChildIterator iter(aContainer);
+ for (nsIContent* cell = iter.GetNextChild(); cell;
+ cell = iter.GetNextChild()) {
+ if (cell->IsXULElement(nsGkAtoms::treecell)) {
+ if (j == colIndex) {
+ result = cell->AsElement();
+ break;
+ }
+ j++;
+ }
+ }
+
+ return result;
+}
+
+bool nsTreeContentView::IsValidRowIndex(int32_t aRowIndex) {
+ return aRowIndex >= 0 && aRowIndex < int32_t(mRows.Length());
+}
diff --git a/layout/xul/tree/nsTreeContentView.h b/layout/xul/tree/nsTreeContentView.h
new file mode 100644
index 0000000000..8138ab44fc
--- /dev/null
+++ b/layout/xul/tree/nsTreeContentView.h
@@ -0,0 +1,164 @@
+/* -*- 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 nsTreeContentView_h__
+#define nsTreeContentView_h__
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsTArray.h"
+#include "nsStubDocumentObserver.h"
+#include "nsITreeView.h"
+#include "nsITreeSelection.h"
+#include "nsWrapperCache.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+
+class nsSelection;
+class nsTreeColumn;
+class Row;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+class DataTransfer;
+class Document;
+class Element;
+class XULTreeElement;
+} // namespace dom
+} // namespace mozilla
+
+nsresult NS_NewTreeContentView(nsITreeView** aResult);
+
+class nsTreeContentView final : public nsITreeView,
+ public nsStubDocumentObserver,
+ public nsWrapperCache {
+ typedef mozilla::dom::Element Element;
+
+ public:
+ nsTreeContentView(void);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(nsTreeContentView,
+ nsITreeView)
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ nsISupports* GetParentObject();
+
+ int32_t RowCount() { return mRows.Length(); }
+ nsITreeSelection* GetSelection() { return mSelection; }
+ void SetSelection(nsITreeSelection* aSelection, mozilla::ErrorResult& aError);
+ void GetRowProperties(int32_t aRow, nsAString& aProperties,
+ mozilla::ErrorResult& aError);
+ void GetCellProperties(int32_t aRow, nsTreeColumn& aColumn,
+ nsAString& aProperies, mozilla::ErrorResult& aError);
+ void GetColumnProperties(nsTreeColumn& aColumn, nsAString& aProperies);
+ bool IsContainer(int32_t aRow, mozilla::ErrorResult& aError);
+ bool IsContainerOpen(int32_t aRow, mozilla::ErrorResult& aError);
+ bool IsContainerEmpty(int32_t aRow, mozilla::ErrorResult& aError);
+ bool IsSeparator(int32_t aRow, mozilla::ErrorResult& aError);
+ bool IsSorted() { return false; }
+ bool CanDrop(int32_t aRow, int32_t aOrientation,
+ mozilla::dom::DataTransfer* aDataTransfer,
+ mozilla::ErrorResult& aError);
+ void Drop(int32_t aRow, int32_t aOrientation,
+ mozilla::dom::DataTransfer* aDataTransfer,
+ mozilla::ErrorResult& aError);
+ int32_t GetParentIndex(int32_t aRow, mozilla::ErrorResult& aError);
+ bool HasNextSibling(int32_t aRow, int32_t aAfterIndex,
+ mozilla::ErrorResult& aError);
+ int32_t GetLevel(int32_t aRow, mozilla::ErrorResult& aError);
+ void GetImageSrc(int32_t aRow, nsTreeColumn& aColumn, nsAString& aSrc,
+ mozilla::ErrorResult& aError);
+ void GetCellValue(int32_t aRow, nsTreeColumn& aColumn, nsAString& aValue,
+ mozilla::ErrorResult& aError);
+ void GetCellText(int32_t aRow, nsTreeColumn& aColumn, nsAString& aText,
+ mozilla::ErrorResult& aError);
+ void SetTree(mozilla::dom::XULTreeElement* aTree,
+ mozilla::ErrorResult& aError);
+ void ToggleOpenState(int32_t aRow, mozilla::ErrorResult& aError);
+ void CycleHeader(nsTreeColumn& aColumn, mozilla::ErrorResult& aError);
+ void SelectionChanged() {}
+ void CycleCell(int32_t aRow, nsTreeColumn& aColumn) {}
+ bool IsEditable(int32_t aRow, nsTreeColumn& aColumn,
+ mozilla::ErrorResult& aError);
+ void SetCellValue(int32_t aRow, nsTreeColumn& aColumn,
+ const nsAString& aValue, mozilla::ErrorResult& aError);
+ void SetCellText(int32_t aRow, nsTreeColumn& aColumn, const nsAString& aText,
+ mozilla::ErrorResult& aError);
+ Element* GetItemAtIndex(int32_t aRow, mozilla::ErrorResult& aError);
+ int32_t GetIndexOfItem(Element* aItem);
+
+ NS_DECL_NSITREEVIEW
+
+ // nsIDocumentObserver
+ NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
+ NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
+ NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED
+
+ static bool CanTrustTreeSelection(nsISupports* aValue);
+
+ protected:
+ ~nsTreeContentView(void);
+
+ // Recursive methods which deal with serializing of nested content.
+ void Serialize(nsIContent* aContent, int32_t aParentIndex, int32_t* aIndex,
+ nsTArray<mozilla::UniquePtr<Row>>& aRows);
+
+ void SerializeItem(Element* aContent, int32_t aParentIndex, int32_t* aIndex,
+ nsTArray<mozilla::UniquePtr<Row>>& aRows);
+
+ void SerializeSeparator(Element* aContent, int32_t aParentIndex,
+ int32_t* aIndex,
+ nsTArray<mozilla::UniquePtr<Row>>& aRows);
+
+ void GetIndexInSubtree(nsIContent* aContainer, nsIContent* aContent,
+ int32_t* aResult);
+
+ // Helper methods which we use to manage our plain array of rows.
+ int32_t EnsureSubtree(int32_t aIndex);
+
+ int32_t RemoveSubtree(int32_t aIndex);
+
+ int32_t InsertRow(int32_t aParentIndex, int32_t aIndex, nsIContent* aContent);
+
+ void InsertRowFor(nsIContent* aParent, nsIContent* aChild);
+
+ int32_t RemoveRow(int32_t aIndex);
+
+ void ClearRows();
+
+ void OpenContainer(int32_t aIndex);
+
+ void CloseContainer(int32_t aIndex);
+
+ int32_t FindContent(nsIContent* aContent);
+
+ void UpdateSubtreeSizes(int32_t aIndex, int32_t aCount);
+
+ void UpdateParentIndexes(int32_t aIndex, int32_t aSkip, int32_t aCount);
+
+ bool CanDrop(int32_t aRow, int32_t aOrientation,
+ mozilla::ErrorResult& aError);
+ void Drop(int32_t aRow, int32_t aOrientation, mozilla::ErrorResult& aError);
+
+ // Content helpers.
+ Element* GetCell(nsIContent* aContainer, nsTreeColumn& aCol);
+
+ private:
+ bool IsValidRowIndex(int32_t aRowIndex);
+
+ RefPtr<mozilla::dom::XULTreeElement> mTree;
+ nsCOMPtr<nsITreeSelection> mSelection;
+ nsCOMPtr<nsIContent> mBody;
+ mozilla::dom::Document* mDocument; // WEAK
+ nsTArray<mozilla::UniquePtr<Row>> mRows;
+};
+
+#endif // nsTreeContentView_h__
diff --git a/layout/xul/tree/nsTreeImageListener.cpp b/layout/xul/tree/nsTreeImageListener.cpp
new file mode 100644
index 0000000000..a560ada948
--- /dev/null
+++ b/layout/xul/tree/nsTreeImageListener.cpp
@@ -0,0 +1,115 @@
+/* -*- 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 "nsTreeImageListener.h"
+#include "XULTreeElement.h"
+#include "imgIRequest.h"
+#include "imgIContainer.h"
+#include "nsIContent.h"
+#include "nsTreeColumns.h"
+
+using mozilla::dom::XULTreeElement;
+
+NS_IMPL_ISUPPORTS(nsTreeImageListener, imgINotificationObserver)
+
+nsTreeImageListener::nsTreeImageListener(nsTreeBodyFrame* aTreeFrame)
+ : mTreeFrame(aTreeFrame),
+ mInvalidationSuppressed(true),
+ mInvalidationArea(nullptr) {}
+
+nsTreeImageListener::~nsTreeImageListener() { delete mInvalidationArea; }
+
+void nsTreeImageListener::Notify(imgIRequest* aRequest, int32_t aType,
+ const nsIntRect* aData) {
+ if (aType == imgINotificationObserver::IS_ANIMATED) {
+ if (mTreeFrame) {
+ mTreeFrame->OnImageIsAnimated(aRequest);
+ }
+ return;
+ }
+
+ if (aType == imgINotificationObserver::SIZE_AVAILABLE) {
+ // Ensure the animation (if any) is started. Note: There is no
+ // corresponding call to Decrement for this. This Increment will be
+ // 'cleaned up' by the Request when it is destroyed, but only then.
+ aRequest->IncrementAnimationConsumers();
+
+ if (mTreeFrame) {
+ nsCOMPtr<imgIContainer> image;
+ aRequest->GetImage(getter_AddRefs(image));
+ if (image) {
+ nsPresContext* presContext = mTreeFrame->PresContext();
+ image->SetAnimationMode(presContext->ImageAnimationMode());
+ }
+ }
+ }
+
+ if (aType == imgINotificationObserver::FRAME_UPDATE) {
+ Invalidate();
+ }
+}
+
+void nsTreeImageListener::AddCell(int32_t aIndex, nsTreeColumn* aCol) {
+ if (!mInvalidationArea) {
+ mInvalidationArea = new InvalidationArea(aCol);
+ mInvalidationArea->AddRow(aIndex);
+ } else {
+ InvalidationArea* currArea;
+ for (currArea = mInvalidationArea; currArea;
+ currArea = currArea->GetNext()) {
+ if (currArea->GetCol() == aCol) {
+ currArea->AddRow(aIndex);
+ break;
+ }
+ }
+ if (!currArea) {
+ currArea = new InvalidationArea(aCol);
+ currArea->SetNext(mInvalidationArea);
+ mInvalidationArea = currArea;
+ mInvalidationArea->AddRow(aIndex);
+ }
+ }
+}
+
+void nsTreeImageListener::Invalidate() {
+ if (!mInvalidationSuppressed) {
+ for (InvalidationArea* currArea = mInvalidationArea; currArea;
+ currArea = currArea->GetNext()) {
+ // Loop from min to max, invalidating each cell that was listening for
+ // this image.
+ for (int32_t i = currArea->GetMin(); i <= currArea->GetMax(); ++i) {
+ if (mTreeFrame) {
+ RefPtr<XULTreeElement> tree =
+ XULTreeElement::FromNodeOrNull(mTreeFrame->GetBaseElement());
+ if (tree) {
+ tree->InvalidateCell(i, currArea->GetCol());
+ }
+ }
+ }
+ }
+ }
+}
+
+nsTreeImageListener::InvalidationArea::InvalidationArea(nsTreeColumn* aCol)
+ : mCol(aCol),
+ mMin(-1), // min should start out "undefined"
+ mMax(0),
+ mNext(nullptr) {}
+
+void nsTreeImageListener::InvalidationArea::AddRow(int32_t aIndex) {
+ if (mMin == -1)
+ mMin = mMax = aIndex;
+ else if (aIndex < mMin)
+ mMin = aIndex;
+ else if (aIndex > mMax)
+ mMax = aIndex;
+}
+
+NS_IMETHODIMP
+nsTreeImageListener::ClearFrame() {
+ mTreeFrame = nullptr;
+ return NS_OK;
+}
diff --git a/layout/xul/tree/nsTreeImageListener.h b/layout/xul/tree/nsTreeImageListener.h
new file mode 100644
index 0000000000..f5e6e70512
--- /dev/null
+++ b/layout/xul/tree/nsTreeImageListener.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 nsTreeImageListener_h__
+#define nsTreeImageListener_h__
+
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsTreeBodyFrame.h"
+#include "mozilla/Attributes.h"
+
+class nsTreeColumn;
+
+// This class handles image load observation.
+class nsTreeImageListener final : public imgINotificationObserver {
+ public:
+ explicit nsTreeImageListener(nsTreeBodyFrame* aTreeFrame);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_IMGINOTIFICATIONOBSERVER
+
+ NS_IMETHOD ClearFrame();
+
+ friend class nsTreeBodyFrame;
+
+ protected:
+ ~nsTreeImageListener();
+
+ void UnsuppressInvalidation() { mInvalidationSuppressed = false; }
+ void Invalidate();
+ void AddCell(int32_t aIndex, nsTreeColumn* aCol);
+
+ private:
+ nsTreeBodyFrame* mTreeFrame;
+
+ // A guard that prevents us from recursive painting.
+ bool mInvalidationSuppressed;
+
+ class InvalidationArea {
+ public:
+ explicit InvalidationArea(nsTreeColumn* aCol);
+ ~InvalidationArea() { delete mNext; }
+
+ friend class nsTreeImageListener;
+
+ protected:
+ void AddRow(int32_t aIndex);
+ nsTreeColumn* GetCol() { return mCol.get(); }
+ int32_t GetMin() { return mMin; }
+ int32_t GetMax() { return mMax; }
+ InvalidationArea* GetNext() { return mNext; }
+ void SetNext(InvalidationArea* aNext) { mNext = aNext; }
+
+ private:
+ RefPtr<nsTreeColumn> mCol;
+ int32_t mMin;
+ int32_t mMax;
+ InvalidationArea* mNext;
+ };
+
+ InvalidationArea* mInvalidationArea;
+};
+
+#endif // nsTreeImageListener_h__
diff --git a/layout/xul/tree/nsTreeSelection.cpp b/layout/xul/tree/nsTreeSelection.cpp
new file mode 100644
index 0000000000..7915b3feb2
--- /dev/null
+++ b/layout/xul/tree/nsTreeSelection.cpp
@@ -0,0 +1,723 @@
+/* -*- 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/AsyncEventDispatcher.h"
+#include "mozilla/dom/Element.h"
+#include "nsCOMPtr.h"
+#include "nsTreeSelection.h"
+#include "XULTreeElement.h"
+#include "nsITreeView.h"
+#include "nsString.h"
+#include "nsIContent.h"
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsComponentManagerUtils.h"
+#include "nsTreeColumns.h"
+
+using namespace mozilla;
+using dom::XULTreeElement;
+
+// A helper class for managing our ranges of selection.
+struct nsTreeRange {
+ nsTreeSelection* mSelection;
+
+ nsTreeRange* mPrev;
+ nsTreeRange* mNext;
+
+ int32_t mMin;
+ int32_t mMax;
+
+ nsTreeRange(nsTreeSelection* aSel, int32_t aSingleVal)
+ : mSelection(aSel),
+ mPrev(nullptr),
+ mNext(nullptr),
+ mMin(aSingleVal),
+ mMax(aSingleVal) {}
+ nsTreeRange(nsTreeSelection* aSel, int32_t aMin, int32_t aMax)
+ : mSelection(aSel),
+ mPrev(nullptr),
+ mNext(nullptr),
+ mMin(aMin),
+ mMax(aMax) {}
+
+ ~nsTreeRange() { delete mNext; }
+
+ void Connect(nsTreeRange* aPrev = nullptr, nsTreeRange* aNext = nullptr) {
+ if (aPrev)
+ aPrev->mNext = this;
+ else
+ mSelection->mFirstRange = this;
+
+ if (aNext) aNext->mPrev = this;
+
+ mPrev = aPrev;
+ mNext = aNext;
+ }
+
+ nsresult RemoveRange(int32_t aStart, int32_t aEnd) {
+ // This should so be a loop... sigh...
+ // We start past the range to remove, so no more to remove
+ if (aEnd < mMin) return NS_OK;
+ // We are the last range to be affected
+ if (aEnd < mMax) {
+ if (aStart <= mMin) {
+ // Just chop the start of the range off
+ mMin = aEnd + 1;
+ } else {
+ // We need to split the range
+ nsTreeRange* range = new nsTreeRange(mSelection, aEnd + 1, mMax);
+ if (!range) return NS_ERROR_OUT_OF_MEMORY;
+
+ mMax = aStart - 1;
+ range->Connect(this, mNext);
+ }
+ return NS_OK;
+ }
+ nsTreeRange* next = mNext;
+ if (aStart <= mMin) {
+ // The remove includes us, remove ourselves from the list
+ if (mPrev)
+ mPrev->mNext = next;
+ else
+ mSelection->mFirstRange = next;
+
+ if (next) next->mPrev = mPrev;
+ mPrev = mNext = nullptr;
+ delete this;
+ } else if (aStart <= mMax) {
+ // Just chop the end of the range off
+ mMax = aStart - 1;
+ }
+ return next ? next->RemoveRange(aStart, aEnd) : NS_OK;
+ }
+
+ nsresult Remove(int32_t aIndex) {
+ if (aIndex >= mMin && aIndex <= mMax) {
+ // We have found the range that contains us.
+ if (mMin == mMax) {
+ // Delete the whole range.
+ if (mPrev) mPrev->mNext = mNext;
+ if (mNext) mNext->mPrev = mPrev;
+ nsTreeRange* first = mSelection->mFirstRange;
+ if (first == this) mSelection->mFirstRange = mNext;
+ mNext = mPrev = nullptr;
+ delete this;
+ } else if (aIndex == mMin)
+ mMin++;
+ else if (aIndex == mMax)
+ mMax--;
+ else {
+ // We have to break this range.
+ nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex + 1, mMax);
+ if (!newRange) return NS_ERROR_OUT_OF_MEMORY;
+
+ newRange->Connect(this, mNext);
+ mMax = aIndex - 1;
+ }
+ } else if (mNext)
+ return mNext->Remove(aIndex);
+
+ return NS_OK;
+ }
+
+ nsresult Add(int32_t aIndex) {
+ if (aIndex < mMin) {
+ // We have found a spot to insert.
+ if (aIndex + 1 == mMin)
+ mMin = aIndex;
+ else if (mPrev && mPrev->mMax + 1 == aIndex)
+ mPrev->mMax = aIndex;
+ else {
+ // We have to create a new range.
+ nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex);
+ if (!newRange) return NS_ERROR_OUT_OF_MEMORY;
+
+ newRange->Connect(mPrev, this);
+ }
+ } else if (mNext)
+ mNext->Add(aIndex);
+ else {
+ // Insert on to the end.
+ if (mMax + 1 == aIndex)
+ mMax = aIndex;
+ else {
+ // We have to create a new range.
+ nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex);
+ if (!newRange) return NS_ERROR_OUT_OF_MEMORY;
+
+ newRange->Connect(this, nullptr);
+ }
+ }
+ return NS_OK;
+ }
+
+ bool Contains(int32_t aIndex) {
+ if (aIndex >= mMin && aIndex <= mMax) return true;
+
+ if (mNext) return mNext->Contains(aIndex);
+
+ return false;
+ }
+
+ int32_t Count() {
+ int32_t total = mMax - mMin + 1;
+ if (mNext) total += mNext->Count();
+ return total;
+ }
+
+ static void CollectRanges(nsTreeRange* aRange, nsTArray<int32_t>& aRanges) {
+ nsTreeRange* cur = aRange;
+ while (cur) {
+ aRanges.AppendElement(cur->mMin);
+ aRanges.AppendElement(cur->mMax);
+ cur = cur->mNext;
+ }
+ }
+
+ static void InvalidateRanges(XULTreeElement* aTree,
+ nsTArray<int32_t>& aRanges) {
+ if (aTree) {
+ RefPtr<nsXULElement> tree = aTree;
+ for (uint32_t i = 0; i < aRanges.Length(); i += 2) {
+ aTree->InvalidateRange(aRanges[i], aRanges[i + 1]);
+ }
+ }
+ }
+
+ void Invalidate() {
+ nsTArray<int32_t> ranges;
+ CollectRanges(this, ranges);
+ InvalidateRanges(mSelection->mTree, ranges);
+ }
+
+ void RemoveAllBut(int32_t aIndex) {
+ if (aIndex >= mMin && aIndex <= mMax) {
+ // Invalidate everything in this list.
+ nsTArray<int32_t> ranges;
+ CollectRanges(mSelection->mFirstRange, ranges);
+
+ mMin = aIndex;
+ mMax = aIndex;
+
+ nsTreeRange* first = mSelection->mFirstRange;
+ if (mPrev) mPrev->mNext = mNext;
+ if (mNext) mNext->mPrev = mPrev;
+ mNext = mPrev = nullptr;
+
+ if (first != this) {
+ delete mSelection->mFirstRange;
+ mSelection->mFirstRange = this;
+ }
+ InvalidateRanges(mSelection->mTree, ranges);
+ } else if (mNext)
+ mNext->RemoveAllBut(aIndex);
+ }
+
+ void Insert(nsTreeRange* aRange) {
+ if (mMin >= aRange->mMax)
+ aRange->Connect(mPrev, this);
+ else if (mNext)
+ mNext->Insert(aRange);
+ else
+ aRange->Connect(this, nullptr);
+ }
+};
+
+nsTreeSelection::nsTreeSelection(XULTreeElement* aTree)
+ : mTree(aTree),
+ mSuppressed(false),
+ mCurrentIndex(-1),
+ mShiftSelectPivot(-1),
+ mFirstRange(nullptr) {}
+
+nsTreeSelection::~nsTreeSelection() {
+ delete mFirstRange;
+ if (mSelectTimer) mSelectTimer->Cancel();
+}
+
+NS_IMPL_CYCLE_COLLECTION(nsTreeSelection, mTree)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeSelection)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeSelection)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeSelection)
+ NS_INTERFACE_MAP_ENTRY(nsITreeSelection)
+ NS_INTERFACE_MAP_ENTRY(nsINativeTreeSelection)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMETHODIMP nsTreeSelection::GetTree(XULTreeElement** aTree) {
+ NS_IF_ADDREF(*aTree = mTree);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::SetTree(XULTreeElement* aTree) {
+ if (mSelectTimer) {
+ mSelectTimer->Cancel();
+ mSelectTimer = nullptr;
+ }
+
+ mTree = aTree;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::GetSingle(bool* aSingle) {
+ if (!mTree) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ *aSingle = mTree->AttrValueIs(kNameSpaceID_None, nsGkAtoms::seltype,
+ u"single"_ns, eCaseMatters);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::IsSelected(int32_t aIndex, bool* aResult) {
+ if (mFirstRange)
+ *aResult = mFirstRange->Contains(aIndex);
+ else
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::TimedSelect(int32_t aIndex, int32_t aMsec) {
+ bool suppressSelect = mSuppressed;
+
+ if (aMsec != -1) mSuppressed = true;
+
+ nsresult rv = Select(aIndex);
+ if (NS_FAILED(rv)) return rv;
+
+ if (aMsec != -1) {
+ mSuppressed = suppressSelect;
+ if (!mSuppressed) {
+ if (mSelectTimer) mSelectTimer->Cancel();
+
+ if (!mTree) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ nsIEventTarget* target =
+ mTree->OwnerDoc()->EventTargetFor(TaskCategory::Other);
+ NS_NewTimerWithFuncCallback(getter_AddRefs(mSelectTimer), SelectCallback,
+ this, aMsec, nsITimer::TYPE_ONE_SHOT,
+ "nsTreeSelection::SelectCallback", target);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::Select(int32_t aIndex) {
+ mShiftSelectPivot = -1;
+
+ nsresult rv = SetCurrentIndex(aIndex);
+ if (NS_FAILED(rv)) return rv;
+
+ if (mFirstRange) {
+ bool alreadySelected = mFirstRange->Contains(aIndex);
+
+ if (alreadySelected) {
+ int32_t count = mFirstRange->Count();
+ if (count > 1) {
+ // We need to deselect everything but our item.
+ mFirstRange->RemoveAllBut(aIndex);
+ FireOnSelectHandler();
+ }
+ return NS_OK;
+ } else {
+ // Clear out our selection.
+ mFirstRange->Invalidate();
+ delete mFirstRange;
+ }
+ }
+
+ // Create our new selection.
+ mFirstRange = new nsTreeRange(this, aIndex);
+ if (!mFirstRange) return NS_ERROR_OUT_OF_MEMORY;
+
+ mFirstRange->Invalidate();
+
+ // Fire the select event
+ FireOnSelectHandler();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::ToggleSelect(int32_t aIndex) {
+ // There are six cases that can occur on a ToggleSelect with our
+ // range code.
+ // (1) A new range should be made for a selection.
+ // (2) A single range is removed from the selection.
+ // (3) The item is added to an existing range.
+ // (4) The item is removed from an existing range.
+ // (5) The addition of the item causes two ranges to be merged.
+ // (6) The removal of the item causes two ranges to be split.
+ mShiftSelectPivot = -1;
+ nsresult rv = SetCurrentIndex(aIndex);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!mFirstRange)
+ Select(aIndex);
+ else {
+ if (!mFirstRange->Contains(aIndex)) {
+ bool single;
+ rv = GetSingle(&single);
+ if (NS_SUCCEEDED(rv) && !single) rv = mFirstRange->Add(aIndex);
+ } else
+ rv = mFirstRange->Remove(aIndex);
+ if (NS_SUCCEEDED(rv)) {
+ if (mTree) mTree->InvalidateRow(aIndex);
+
+ FireOnSelectHandler();
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsTreeSelection::RangedSelect(int32_t aStartIndex,
+ int32_t aEndIndex, bool aAugment) {
+ bool single;
+ nsresult rv = GetSingle(&single);
+ if (NS_FAILED(rv)) return rv;
+
+ if ((mFirstRange || (aStartIndex != aEndIndex)) && single) return NS_OK;
+
+ if (!aAugment) {
+ // Clear our selection.
+ if (mFirstRange) {
+ mFirstRange->Invalidate();
+ delete mFirstRange;
+ mFirstRange = nullptr;
+ }
+ }
+
+ if (aStartIndex == -1) {
+ if (mShiftSelectPivot != -1)
+ aStartIndex = mShiftSelectPivot;
+ else if (mCurrentIndex != -1)
+ aStartIndex = mCurrentIndex;
+ else
+ aStartIndex = aEndIndex;
+ }
+
+ mShiftSelectPivot = aStartIndex;
+ rv = SetCurrentIndex(aEndIndex);
+ if (NS_FAILED(rv)) return rv;
+
+ int32_t start = aStartIndex < aEndIndex ? aStartIndex : aEndIndex;
+ int32_t end = aStartIndex < aEndIndex ? aEndIndex : aStartIndex;
+
+ if (aAugment && mFirstRange) {
+ // We need to remove all the items within our selected range from the
+ // selection, and then we insert our new range into the list.
+ nsresult rv = mFirstRange->RemoveRange(start, end);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ nsTreeRange* range = new nsTreeRange(this, start, end);
+ if (!range) return NS_ERROR_OUT_OF_MEMORY;
+
+ range->Invalidate();
+
+ if (aAugment && mFirstRange)
+ mFirstRange->Insert(range);
+ else
+ mFirstRange = range;
+
+ FireOnSelectHandler();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::ClearRange(int32_t aStartIndex,
+ int32_t aEndIndex) {
+ nsresult rv = SetCurrentIndex(aEndIndex);
+ if (NS_FAILED(rv)) return rv;
+
+ if (mFirstRange) {
+ int32_t start = aStartIndex < aEndIndex ? aStartIndex : aEndIndex;
+ int32_t end = aStartIndex < aEndIndex ? aEndIndex : aStartIndex;
+
+ mFirstRange->RemoveRange(start, end);
+
+ if (mTree) mTree->InvalidateRange(start, end);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::ClearSelection() {
+ if (mFirstRange) {
+ mFirstRange->Invalidate();
+ delete mFirstRange;
+ mFirstRange = nullptr;
+ }
+ mShiftSelectPivot = -1;
+
+ FireOnSelectHandler();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::SelectAll() {
+ if (!mTree) return NS_OK;
+
+ nsCOMPtr<nsITreeView> view = mTree->GetView();
+ if (!view) return NS_OK;
+
+ int32_t rowCount;
+ view->GetRowCount(&rowCount);
+ bool single;
+ nsresult rv = GetSingle(&single);
+ if (NS_FAILED(rv)) return rv;
+
+ if (rowCount == 0 || (rowCount > 1 && single)) return NS_OK;
+
+ mShiftSelectPivot = -1;
+
+ // Invalidate not necessary when clearing selection, since
+ // we're going to invalidate the world on the SelectAll.
+ delete mFirstRange;
+
+ mFirstRange = new nsTreeRange(this, 0, rowCount - 1);
+ mFirstRange->Invalidate();
+
+ FireOnSelectHandler();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::GetRangeCount(int32_t* aResult) {
+ int32_t count = 0;
+ nsTreeRange* curr = mFirstRange;
+ while (curr) {
+ count++;
+ curr = curr->mNext;
+ }
+
+ *aResult = count;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::GetRangeAt(int32_t aIndex, int32_t* aMin,
+ int32_t* aMax) {
+ *aMin = *aMax = -1;
+ int32_t i = -1;
+ nsTreeRange* curr = mFirstRange;
+ while (curr) {
+ i++;
+ if (i == aIndex) {
+ *aMin = curr->mMin;
+ *aMax = curr->mMax;
+ break;
+ }
+ curr = curr->mNext;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::GetCount(int32_t* count) {
+ if (mFirstRange)
+ *count = mFirstRange->Count();
+ else // No range available, so there's no selected row.
+ *count = 0;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::GetSelectEventsSuppressed(
+ bool* aSelectEventsSuppressed) {
+ *aSelectEventsSuppressed = mSuppressed;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::SetSelectEventsSuppressed(
+ bool aSelectEventsSuppressed) {
+ mSuppressed = aSelectEventsSuppressed;
+ if (!mSuppressed) FireOnSelectHandler();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::GetCurrentIndex(int32_t* aCurrentIndex) {
+ *aCurrentIndex = mCurrentIndex;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsTreeSelection::SetCurrentIndex(int32_t aIndex) {
+ if (!mTree) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ if (mCurrentIndex == aIndex) {
+ return NS_OK;
+ }
+ if (mCurrentIndex != -1 && mTree) mTree->InvalidateRow(mCurrentIndex);
+
+ mCurrentIndex = aIndex;
+ if (!mTree) return NS_OK;
+
+ if (aIndex != -1) mTree->InvalidateRow(aIndex);
+
+ // Fire DOMMenuItemActive or DOMMenuItemInactive event for tree.
+ NS_ENSURE_STATE(mTree);
+
+ constexpr auto DOMMenuItemActive = u"DOMMenuItemActive"_ns;
+ constexpr auto DOMMenuItemInactive = u"DOMMenuItemInactive"_ns;
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher(
+ mTree, (aIndex != -1 ? DOMMenuItemActive : DOMMenuItemInactive),
+ CanBubble::eYes, ChromeOnlyDispatch::eNo);
+ return asyncDispatcher->PostDOMEvent();
+}
+
+#define ADD_NEW_RANGE(macro_range, macro_selection, macro_start, macro_end) \
+ { \
+ int32_t start = macro_start; \
+ int32_t end = macro_end; \
+ if (start > end) { \
+ end = start; \
+ } \
+ nsTreeRange* macro_new_range = \
+ new nsTreeRange(macro_selection, start, end); \
+ if (macro_range) \
+ macro_range->Insert(macro_new_range); \
+ else \
+ macro_range = macro_new_range; \
+ }
+
+NS_IMETHODIMP
+nsTreeSelection::AdjustSelection(int32_t aIndex, int32_t aCount) {
+ NS_ASSERTION(aCount != 0, "adjusting by zero");
+ if (!aCount) return NS_OK;
+
+ // adjust mShiftSelectPivot, if necessary
+ if ((mShiftSelectPivot != 1) && (aIndex <= mShiftSelectPivot)) {
+ // if we are deleting and the delete includes the shift select pivot, reset
+ // it
+ if (aCount < 0 && (mShiftSelectPivot <= (aIndex - aCount - 1))) {
+ mShiftSelectPivot = -1;
+ } else {
+ mShiftSelectPivot += aCount;
+ }
+ }
+
+ // adjust mCurrentIndex, if necessary
+ if ((mCurrentIndex != -1) && (aIndex <= mCurrentIndex)) {
+ // if we are deleting and the delete includes the current index, reset it
+ if (aCount < 0 && (mCurrentIndex <= (aIndex - aCount - 1))) {
+ mCurrentIndex = -1;
+ } else {
+ mCurrentIndex += aCount;
+ }
+ }
+
+ // no selection, so nothing to do.
+ if (!mFirstRange) return NS_OK;
+
+ bool selChanged = false;
+ nsTreeRange* oldFirstRange = mFirstRange;
+ nsTreeRange* curr = mFirstRange;
+ mFirstRange = nullptr;
+ while (curr) {
+ if (aCount > 0) {
+ // inserting
+ if (aIndex > curr->mMax) {
+ // adjustment happens after the range, so no change
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax);
+ } else if (aIndex <= curr->mMin) {
+ // adjustment happens before the start of the range, so shift down
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin + aCount,
+ curr->mMax + aCount);
+ selChanged = true;
+ } else {
+ // adjustment happen inside the range.
+ // break apart the range and create two ranges
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin, aIndex - 1);
+ ADD_NEW_RANGE(mFirstRange, this, aIndex + aCount, curr->mMax + aCount);
+ selChanged = true;
+ }
+ } else {
+ // deleting
+ if (aIndex > curr->mMax) {
+ // adjustment happens after the range, so no change
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax);
+ } else {
+ // remember, aCount is negative
+ selChanged = true;
+ int32_t lastIndexOfAdjustment = aIndex - aCount - 1;
+ if (aIndex <= curr->mMin) {
+ if (lastIndexOfAdjustment < curr->mMin) {
+ // adjustment happens before the start of the range, so shift up
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin + aCount,
+ curr->mMax + aCount);
+ } else if (lastIndexOfAdjustment >= curr->mMax) {
+ // adjustment contains the range. remove the range by not adding it
+ // to the newRange
+ } else {
+ // adjustment starts before the range, and ends in the middle of it,
+ // so trim the range
+ ADD_NEW_RANGE(mFirstRange, this, aIndex, curr->mMax + aCount)
+ }
+ } else if (lastIndexOfAdjustment >= curr->mMax) {
+ // adjustment starts in the middle of the current range, and contains
+ // the end of the range, so trim the range
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin, aIndex - 1)
+ } else {
+ // range contains the adjustment, so shorten the range
+ ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax + aCount)
+ }
+ }
+ }
+ curr = curr->mNext;
+ }
+
+ delete oldFirstRange;
+
+ // Fire the select event
+ if (selChanged) FireOnSelectHandler();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTreeSelection::InvalidateSelection() {
+ if (mFirstRange) mFirstRange->Invalidate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTreeSelection::GetShiftSelectPivot(int32_t* aIndex) {
+ *aIndex = mShiftSelectPivot;
+ return NS_OK;
+}
+
+nsresult nsTreeSelection::FireOnSelectHandler() {
+ if (mSuppressed || !mTree) return NS_OK;
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher(
+ mTree, u"select"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo);
+ asyncDispatcher->RunDOMEventWhenSafe();
+ return NS_OK;
+}
+
+void nsTreeSelection::SelectCallback(nsITimer* aTimer, void* aClosure) {
+ RefPtr<nsTreeSelection> self = static_cast<nsTreeSelection*>(aClosure);
+ if (self) {
+ self->FireOnSelectHandler();
+ aTimer->Cancel();
+ self->mSelectTimer = nullptr;
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////////
+
+nsresult NS_NewTreeSelection(XULTreeElement* aTree,
+ nsITreeSelection** aResult) {
+ *aResult = new nsTreeSelection(aTree);
+ if (!*aResult) return NS_ERROR_OUT_OF_MEMORY;
+ NS_ADDREF(*aResult);
+ return NS_OK;
+}
diff --git a/layout/xul/tree/nsTreeSelection.h b/layout/xul/tree/nsTreeSelection.h
new file mode 100644
index 0000000000..3b0eb6b21e
--- /dev/null
+++ b/layout/xul/tree/nsTreeSelection.h
@@ -0,0 +1,56 @@
+/* -*- 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 nsTreeSelection_h__
+#define nsTreeSelection_h__
+
+#include "nsITreeSelection.h"
+#include "nsITimer.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/Attributes.h"
+#include "XULTreeElement.h"
+
+class nsTreeColumn;
+struct nsTreeRange;
+
+class nsTreeSelection final : public nsINativeTreeSelection {
+ public:
+ explicit nsTreeSelection(mozilla::dom::XULTreeElement* aTree);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(nsTreeSelection)
+ NS_DECL_NSITREESELECTION
+
+ // nsINativeTreeSelection: Untrusted code can use us
+ NS_IMETHOD EnsureNative() override { return NS_OK; }
+
+ friend struct nsTreeRange;
+
+ protected:
+ ~nsTreeSelection();
+
+ nsresult FireOnSelectHandler();
+ static void SelectCallback(nsITimer* aTimer, void* aClosure);
+
+ protected:
+ // The tree will hold on to us through the view and let go when it dies.
+ RefPtr<mozilla::dom::XULTreeElement> mTree;
+
+ bool mSuppressed; // Whether or not we should be firing onselect events.
+ int32_t mCurrentIndex; // The item to draw the rect around. The last one
+ // clicked, etc.
+ int32_t mShiftSelectPivot; // Used when multiple SHIFT+selects are performed
+ // to pivot on.
+
+ nsTreeRange* mFirstRange; // Our list of ranges.
+
+ nsCOMPtr<nsITimer> mSelectTimer;
+};
+
+nsresult NS_NewTreeSelection(mozilla::dom::XULTreeElement* aTree,
+ nsITreeSelection** aResult);
+
+#endif
diff --git a/layout/xul/tree/nsTreeStyleCache.cpp b/layout/xul/tree/nsTreeStyleCache.cpp
new file mode 100644
index 0000000000..108d808aab
--- /dev/null
+++ b/layout/xul/tree/nsTreeStyleCache.cpp
@@ -0,0 +1,102 @@
+/* -*- 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 "nsTreeStyleCache.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/ServoStyleSet.h"
+#include "nsPresContextInlines.h"
+
+using namespace mozilla;
+
+nsTreeStyleCache::Transition::Transition(DFAState aState, nsAtom* aSymbol)
+ : mState(aState), mInputSymbol(aSymbol) {}
+
+bool nsTreeStyleCache::Transition::operator==(const Transition& aOther) const {
+ return aOther.mState == mState && aOther.mInputSymbol == mInputSymbol;
+}
+
+uint32_t nsTreeStyleCache::Transition::Hash() const {
+ // Make a 32-bit integer that combines the low-order 16 bits of the state and
+ // the input symbol.
+ uint32_t hb = mState << 16;
+ uint32_t lb = (NS_PTR_TO_UINT32(mInputSymbol.get()) << 16) >> 16;
+ return hb + lb;
+}
+
+// The ComputedStyle cache impl
+ComputedStyle* nsTreeStyleCache::GetComputedStyle(
+ nsPresContext* aPresContext, nsIContent* aContent, ComputedStyle* aStyle,
+ nsCSSAnonBoxPseudoStaticAtom* aPseudoElement, const AtomArray& aInputWord) {
+ MOZ_ASSERT(nsCSSAnonBoxes::IsTreePseudoElement(aPseudoElement));
+
+ uint32_t count = aInputWord.Length();
+
+ // Go ahead and init the transition table.
+ if (!mTransitionTable) {
+ // Automatic miss. Build the table
+ mTransitionTable = MakeUnique<TransitionTable>();
+ }
+
+ // The first transition is always made off the supplied pseudo-element.
+ Transition transition(0, aPseudoElement);
+ DFAState currState = mTransitionTable->Get(transition);
+
+ if (!currState) {
+ // We had a miss. Make a new state and add it to our hash.
+ currState = mNextState;
+ mNextState++;
+ mTransitionTable->InsertOrUpdate(transition, currState);
+ }
+
+ for (uint32_t i = 0; i < count; i++) {
+ Transition transition(currState, aInputWord[i]);
+ currState = mTransitionTable->Get(transition);
+
+ if (!currState) {
+ // We had a miss. Make a new state and add it to our hash.
+ currState = mNextState;
+ mNextState++;
+ mTransitionTable->InsertOrUpdate(transition, currState);
+ }
+ }
+
+ // We're in a final state.
+ // Look up our ComputedStyle for this state.
+ ComputedStyle* result = nullptr;
+ if (mCache) {
+ result = mCache->GetWeak(currState);
+ }
+ if (!result) {
+ // We missed the cache. Resolve this pseudo-style.
+ RefPtr<ComputedStyle> newResult =
+ aPresContext->StyleSet()->ResolveXULTreePseudoStyle(
+ aContent->AsElement(), aPseudoElement, aStyle, aInputWord);
+
+ // Normally we rely on nsIFrame::Init / RestyleManager to call this, but
+ // these are weird and don't use a frame, yet ::-moz-tree-twisty definitely
+ // pokes at list-style-image.
+ newResult->StartImageLoads(*aPresContext->Document());
+
+ // Even though xul-tree pseudos are defined in nsCSSAnonBoxList, nothing has
+ // ever treated them as an anon box, and they don't ever get boxes anyway.
+ //
+ // This is really weird, and probably nothing really relies on the result of
+ // these assert, but it's here just to avoid changing them accidentally.
+ MOZ_ASSERT(newResult->GetPseudoType() == PseudoStyleType::XULTree);
+ MOZ_ASSERT(!newResult->IsAnonBox());
+ MOZ_ASSERT(!newResult->IsPseudoElement());
+
+ // Put the ComputedStyle in our table, transferring the owning reference to
+ // the table.
+ if (!mCache) {
+ mCache = MakeUnique<ComputedStyleCache>();
+ }
+ result = newResult.get();
+ mCache->InsertOrUpdate(currState, std::move(newResult));
+ }
+
+ return result;
+}
diff --git a/layout/xul/tree/nsTreeStyleCache.h b/layout/xul/tree/nsTreeStyleCache.h
new file mode 100644
index 0000000000..b0c0e4d9dc
--- /dev/null
+++ b/layout/xul/tree/nsTreeStyleCache.h
@@ -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/. */
+
+#ifndef nsTreeStyleCache_h__
+#define nsTreeStyleCache_h__
+
+#include "mozilla/AtomArray.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMArray.h"
+#include "nsTHashMap.h"
+#include "nsRefPtrHashtable.h"
+#include "mozilla/ComputedStyle.h"
+
+class nsIContent;
+
+class nsTreeStyleCache {
+ public:
+ nsTreeStyleCache() : mNextState(0) {}
+
+ ~nsTreeStyleCache() { Clear(); }
+
+ void Clear() {
+ mTransitionTable = nullptr;
+ mCache = nullptr;
+ mNextState = 0;
+ }
+
+ mozilla::ComputedStyle* GetComputedStyle(
+ nsPresContext* aPresContext, nsIContent* aContent,
+ mozilla::ComputedStyle* aStyle,
+ nsCSSAnonBoxPseudoStaticAtom* aPseudoElement,
+ const mozilla::AtomArray& aInputWord);
+
+ protected:
+ typedef uint32_t DFAState;
+
+ class Transition final {
+ public:
+ Transition(DFAState aState, nsAtom* aSymbol);
+ bool operator==(const Transition& aOther) const;
+ uint32_t Hash() const;
+
+ private:
+ DFAState mState;
+ RefPtr<nsAtom> mInputSymbol;
+ };
+
+ typedef nsTHashMap<nsGenericHashKey<Transition>, DFAState> TransitionTable;
+
+ // A transition table for a deterministic finite automaton. The DFA
+ // takes as its input a single pseudoelement and an ordered set of properties.
+ // It transitions on an input word that is the concatenation of the
+ // pseudoelement supplied with the properties in the array.
+ //
+ // It transitions from state to state by looking up entries in the transition
+ // table (which is a mapping from (S,i)->S', where S is the current state, i
+ // is the next property in the input word, and S' is the state to transition
+ // to.
+ //
+ // If S' is not found, it is constructed and entered into the hashtable
+ // under the key (S,i).
+ //
+ // Once the entire word has been consumed, the final state is used
+ // to reference the cache table to locate the ComputedStyle.
+ mozilla::UniquePtr<TransitionTable> mTransitionTable;
+
+ // The cache of all active ComputedStyles. This is a hash from
+ // a final state in the DFA, Sf, to the resultant ComputedStyle.
+ typedef nsRefPtrHashtable<nsUint32HashKey, mozilla::ComputedStyle>
+ ComputedStyleCache;
+ mozilla::UniquePtr<ComputedStyleCache> mCache;
+
+ // An integer counter that is used when we need to make new states in the
+ // DFA.
+ DFAState mNextState;
+};
+
+#endif // nsTreeStyleCache_h__
diff --git a/layout/xul/tree/nsTreeUtils.cpp b/layout/xul/tree/nsTreeUtils.cpp
new file mode 100644
index 0000000000..10767e22d2
--- /dev/null
+++ b/layout/xul/tree/nsTreeUtils.cpp
@@ -0,0 +1,135 @@
+/* -*- 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 "nsReadableUtils.h"
+#include "nsTreeUtils.h"
+#include "ChildIterator.h"
+#include "nsCRT.h"
+#include "nsAtom.h"
+#include "nsNameSpaceManager.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+
+using namespace mozilla;
+
+nsresult nsTreeUtils::TokenizeProperties(const nsAString& aProperties,
+ AtomArray& aPropertiesArray) {
+ nsAString::const_iterator end;
+ aProperties.EndReading(end);
+
+ nsAString::const_iterator iter;
+ aProperties.BeginReading(iter);
+
+ do {
+ // Skip whitespace
+ while (iter != end && nsCRT::IsAsciiSpace(*iter)) ++iter;
+
+ // If only whitespace, we're done
+ if (iter == end) break;
+
+ // Note the first non-whitespace character
+ nsAString::const_iterator first = iter;
+
+ // Advance to the next whitespace character
+ while (iter != end && !nsCRT::IsAsciiSpace(*iter)) ++iter;
+
+ // XXX this would be nonsensical
+ NS_ASSERTION(iter != first, "eh? something's wrong here");
+ if (iter == first) break;
+
+ RefPtr<nsAtom> atom = NS_Atomize(Substring(first, iter));
+ aPropertiesArray.AppendElement(atom);
+ } while (iter != end);
+
+ return NS_OK;
+}
+
+nsIContent* nsTreeUtils::GetImmediateChild(nsIContent* aContainer,
+ nsAtom* aTag) {
+ dom::FlattenedChildIterator iter(aContainer);
+ for (nsIContent* child = iter.GetNextChild(); child;
+ child = iter.GetNextChild()) {
+ if (child->IsXULElement(aTag)) {
+ return child;
+ }
+ // <slot> is in the flattened tree, but <tree> code is used to work with
+ // <xbl:children> which is not, so recurse in <slot> here.
+ if (child->IsHTMLElement(nsGkAtoms::slot)) {
+ if (nsIContent* c = GetImmediateChild(child, aTag)) {
+ return c;
+ }
+ }
+ }
+
+ return nullptr;
+}
+
+nsIContent* nsTreeUtils::GetDescendantChild(nsIContent* aContainer,
+ nsAtom* aTag) {
+ dom::FlattenedChildIterator iter(aContainer);
+ for (nsIContent* child = iter.GetNextChild(); child;
+ child = iter.GetNextChild()) {
+ if (child->IsXULElement(aTag)) {
+ return child;
+ }
+
+ child = GetDescendantChild(child, aTag);
+ if (child) {
+ return child;
+ }
+ }
+
+ return nullptr;
+}
+
+nsresult nsTreeUtils::UpdateSortIndicators(dom::Element* aColumn,
+ const nsAString& aDirection) {
+ aColumn->SetAttr(kNameSpaceID_None, nsGkAtoms::sortDirection, aDirection,
+ true);
+ aColumn->SetAttr(kNameSpaceID_None, nsGkAtoms::sortActive, u"true"_ns, true);
+
+ // Unset sort attribute(s) on the other columns
+ nsCOMPtr<nsIContent> parentContent = aColumn->GetParent();
+ if (parentContent && parentContent->NodeInfo()->Equals(nsGkAtoms::treecols,
+ kNameSpaceID_XUL)) {
+ for (nsINode* childContent = parentContent->GetFirstChild(); childContent;
+ childContent = childContent->GetNextSibling()) {
+ if (childContent != aColumn &&
+ childContent->NodeInfo()->Equals(nsGkAtoms::treecol,
+ kNameSpaceID_XUL)) {
+ childContent->AsElement()->UnsetAttr(kNameSpaceID_None,
+ nsGkAtoms::sortDirection, true);
+ childContent->AsElement()->UnsetAttr(kNameSpaceID_None,
+ nsGkAtoms::sortActive, true);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsTreeUtils::GetColumnIndex(dom::Element* aColumn, int32_t* aResult) {
+ nsIContent* parentContent = aColumn->GetParent();
+ if (parentContent && parentContent->NodeInfo()->Equals(nsGkAtoms::treecols,
+ kNameSpaceID_XUL)) {
+ int32_t colIndex = 0;
+
+ for (nsINode* childContent = parentContent->GetFirstChild(); childContent;
+ childContent = childContent->GetNextSibling()) {
+ if (childContent->NodeInfo()->Equals(nsGkAtoms::treecol,
+ kNameSpaceID_XUL)) {
+ if (childContent == aColumn) {
+ *aResult = colIndex;
+ return NS_OK;
+ }
+ ++colIndex;
+ }
+ }
+ }
+
+ *aResult = -1;
+ return NS_OK;
+}
diff --git a/layout/xul/tree/nsTreeUtils.h b/layout/xul/tree/nsTreeUtils.h
new file mode 100644
index 0000000000..d0588f1273
--- /dev/null
+++ b/layout/xul/tree/nsTreeUtils.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsTreeUtils_h__
+#define nsTreeUtils_h__
+
+#include "mozilla/AtomArray.h"
+#include "nsError.h"
+#include "nsString.h"
+#include "nsTreeStyleCache.h"
+
+class nsAtom;
+class nsIContent;
+namespace mozilla {
+namespace dom {
+class Element;
+}
+} // namespace mozilla
+
+class nsTreeUtils {
+ public:
+ /**
+ * Parse a whitespace separated list of properties into an array
+ * of atoms.
+ */
+ static nsresult TokenizeProperties(const nsAString& aProperties,
+ mozilla::AtomArray& aPropertiesArray);
+
+ static nsIContent* GetImmediateChild(nsIContent* aContainer, nsAtom* aTag);
+
+ static nsIContent* GetDescendantChild(nsIContent* aContainer, nsAtom* aTag);
+
+ static nsresult UpdateSortIndicators(mozilla::dom::Element* aColumn,
+ const nsAString& aDirection);
+
+ static nsresult GetColumnIndex(mozilla::dom::Element* aColumn,
+ int32_t* aResult);
+};
+
+#endif // nsTreeUtils_h__