Merge inbound to mozilla-central. a=merge

--HG--
rename : testing/web-platform/tests/css/motion/offset-path-string.html => testing/web-platform/tests/css/motion/offset-path-string-001.html
This commit is contained in:
Noemi Erli 2018-08-22 12:49:02 +03:00
commit a3eb003ecf
68 changed files with 2220 additions and 905 deletions

View File

@ -141,9 +141,6 @@ endif
endif
DEFINES += -DLPROJ_ROOT=$(LPROJ_ROOT)
ifdef MOZ_SYSTEM_ICU
DEFINES += -DMOZ_SYSTEM_ICU
endif
ifdef CLANG_CXX
DEFINES += -DCLANG_CXX
endif

View File

@ -17,6 +17,7 @@ if test -n "$MOZ_SYSTEM_ICU"; then
PKG_CHECK_MODULES(MOZ_ICU, icu-i18n >= 59.1)
CFLAGS="$CFLAGS $MOZ_ICU_CFLAGS"
CXXFLAGS="$CXXFLAGS $MOZ_ICU_CFLAGS"
AC_DEFINE(MOZ_SYSTEM_ICU)
fi
AC_SUBST(MOZ_SYSTEM_ICU)

View File

@ -114,6 +114,7 @@ class MachCommands(MachCommandBase):
valgrind_args = [
valgrind,
'--sym-offsets=yes',
'--smc-check=all-non-file',
'--vex-iropt-register-updates=allregs-at-mem-access',
'--gen-suppressions=all',

View File

@ -26,9 +26,6 @@ if CONFIG['HOST_OS_ARCH'] != 'WINNT':
]
HostProgram('nsinstall_real')
if CONFIG['MOZ_SYSTEM_ICU']:
DEFINES['MOZ_SYSTEM_ICU'] = True
PYTHON_UNITTEST_MANIFESTS += [
'tests/python.ini',
]

View File

@ -53,6 +53,7 @@ support-files =
[browser_animation_inspector_exists.js]
[browser_animation_keyframes-graph_computed-value-path-01.js]
[browser_animation_keyframes-graph_computed-value-path-02.js]
[browser_animation_keyframes-graph_computed-value-path-03.js]
[browser_animation_keyframes-graph_computed-value-path_easing-hint.js]
skip-if = (verify && !debug)
[browser_animation_keyframes-graph_keyframe-marker.js]

View File

@ -3,11 +3,8 @@
"use strict";
// Test for following ComputedValuePath component:
// * element existence
// * path segments
// * fill color by animation type
// * stop color if the animation type is color
// Test for ComputedValuePath of animations that consist by multi types of animated
// properties.
requestLongerTimeout(2);
@ -160,180 +157,6 @@ const TEST_DATA = [
},
],
},
{
targetClass: "middle-keyframe",
properties: [
{
name: "background-color",
computedValuePathClass: "color-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 0, y: 100 },
{ x: 500, y: 100 },
{ x: 1000, y: 100 },
],
expectedStopColors: [
{ offset: 0, color: "rgb(255, 0, 0)" },
{ offset: 0.5, color: "rgb(0, 0, 255)" },
{ offset: 1, color: "rgb(0, 255, 0)" },
]
},
{
name: "background-repeat",
computedValuePathClass: "discrete-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 249.999, y: 0 },
{ x: 250, y: 100 },
{ x: 749.999, y: 100 },
{ x: 750, y: 0 },
{ x: 1000, y: 0 },
],
},
{
name: "font-size",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 250, y: 50 },
{ x: 500, y: 100 },
{ x: 750, y: 50 },
{ x: 1000, y: 0 },
],
},
{
name: "margin-left",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 250, y: 50 },
{ x: 500, y: 100 },
{ x: 750, y: 50 },
{ x: 1000, y: 0 },
],
},
{
name: "opacity",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 250, y: 50 },
{ x: 500, y: 100 },
{ x: 750, y: 50 },
{ x: 1000, y: 0 },
],
},
{
name: "text-align",
computedValuePathClass: "discrete-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 249.999, y: 0 },
{ x: 250, y: 100 },
{ x: 749.999, y: 100 },
{ x: 750, y: 0 },
{ x: 1000, y: 0 },
],
},
{
name: "transform",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 250, y: 50 },
{ x: 500, y: 100 },
{ x: 750, y: 50 },
{ x: 1000, y: 0 },
],
},
],
},
{
targetClass: "steps-keyframe",
properties: [
{
name: "background-color",
computedValuePathClass: "color-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 0, y: 100 },
{ x: 500, y: 100 },
{ x: 1000, y: 100 },
],
expectedStopColors: [
{ offset: 0, color: "rgb(255, 0, 0)" },
{ offset: 0.499, color: "rgb(255, 0, 0)" },
{ offset: 0.5, color: "rgb(128, 128, 0)" },
{ offset: 0.999, color: "rgb(128, 128, 0)" },
{ offset: 1, color: "rgb(0, 255, 0)" },
]
},
{
name: "background-repeat",
computedValuePathClass: "discrete-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 100 },
{ x: 1000, y: 100 },
],
},
{
name: "font-size",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 500, y: 0 },
{ x: 500, y: 50 },
{ x: 1000, y: 50 },
{ x: 1000, y: 100 },
],
},
{
name: "margin-left",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 50 },
{ x: 999.999, y: 50 },
{ x: 1000, y: 100 },
],
},
{
name: "opacity",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 50 },
{ x: 999.999, y: 50 },
{ x: 1000, y: 100 },
],
},
{
name: "text-align",
computedValuePathClass: "discrete-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 100 },
{ x: 1000, y: 100 },
],
},
{
name: "transform",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 500, y: 0 },
{ x: 500, y: 50 },
{ x: 1000, y: 50 },
{ x: 1000, y: 100 },
],
},
],
},
];
add_task(async function() {

View File

@ -3,9 +3,8 @@
"use strict";
// Test for following ComputedValuePath component:
// * element existence
// * path segments
// Test for ComputedValuePath of animations that consist by one animated property
// on complexed keyframes.
requestLongerTimeout(2);

View File

@ -0,0 +1,190 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test for ComputedValuePath of animations that consist by multi types of animated
// properties on complexed keyframes.
requestLongerTimeout(2);
const TEST_DATA = [
{
targetClass: "middle-keyframe",
properties: [
{
name: "background-color",
computedValuePathClass: "color-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 0, y: 100 },
{ x: 500, y: 100 },
{ x: 1000, y: 100 },
],
expectedStopColors: [
{ offset: 0, color: "rgb(255, 0, 0)" },
{ offset: 0.5, color: "rgb(0, 0, 255)" },
{ offset: 1, color: "rgb(0, 255, 0)" },
]
},
{
name: "background-repeat",
computedValuePathClass: "discrete-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 249.999, y: 0 },
{ x: 250, y: 100 },
{ x: 749.999, y: 100 },
{ x: 750, y: 0 },
{ x: 1000, y: 0 },
],
},
{
name: "font-size",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 250, y: 50 },
{ x: 500, y: 100 },
{ x: 750, y: 50 },
{ x: 1000, y: 0 },
],
},
{
name: "margin-left",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 250, y: 50 },
{ x: 500, y: 100 },
{ x: 750, y: 50 },
{ x: 1000, y: 0 },
],
},
{
name: "opacity",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 250, y: 50 },
{ x: 500, y: 100 },
{ x: 750, y: 50 },
{ x: 1000, y: 0 },
],
},
{
name: "text-align",
computedValuePathClass: "discrete-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 249.999, y: 0 },
{ x: 250, y: 100 },
{ x: 749.999, y: 100 },
{ x: 750, y: 0 },
{ x: 1000, y: 0 },
],
},
{
name: "transform",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 250, y: 50 },
{ x: 500, y: 100 },
{ x: 750, y: 50 },
{ x: 1000, y: 0 },
],
},
],
},
{
targetClass: "steps-keyframe",
properties: [
{
name: "background-color",
computedValuePathClass: "color-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 0, y: 100 },
{ x: 500, y: 100 },
{ x: 1000, y: 100 },
],
expectedStopColors: [
{ offset: 0, color: "rgb(255, 0, 0)" },
{ offset: 0.499, color: "rgb(255, 0, 0)" },
{ offset: 0.5, color: "rgb(128, 128, 0)" },
{ offset: 0.999, color: "rgb(128, 128, 0)" },
{ offset: 1, color: "rgb(0, 255, 0)" },
]
},
{
name: "background-repeat",
computedValuePathClass: "discrete-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 100 },
{ x: 1000, y: 100 },
],
},
{
name: "font-size",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 500, y: 0 },
{ x: 500, y: 50 },
{ x: 1000, y: 50 },
{ x: 1000, y: 100 },
],
},
{
name: "margin-left",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 50 },
{ x: 999.999, y: 50 },
{ x: 1000, y: 100 },
],
},
{
name: "opacity",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 50 },
{ x: 999.999, y: 50 },
{ x: 1000, y: 100 },
],
},
{
name: "text-align",
computedValuePathClass: "discrete-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 499.999, y: 0 },
{ x: 500, y: 100 },
{ x: 1000, y: 100 },
],
},
{
name: "transform",
computedValuePathClass: "distance-path",
expectedPathSegments: [
{ x: 0, y: 0 },
{ x: 500, y: 0 },
{ x: 500, y: 50 },
{ x: 1000, y: 50 },
{ x: 1000, y: 100 },
],
},
],
},
];
add_task(async function() {
await testKeyframesGraphComputedValuePath(TEST_DATA);
});

View File

@ -11,10 +11,12 @@ const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const DeviceModal = createFactory(require("./DeviceModal"));
const Toolbar = createFactory(require("./Toolbar"));
const Viewports = createFactory(require("./Viewports"));
loader.lazyGetter(this, "DeviceModal",
() => createFactory(require("./DeviceModal")));
const {
addCustomDevice,
removeCustomDevice,
@ -255,15 +257,18 @@ class App extends Component {
onRemoveDeviceAssociation,
onResizeViewport,
}),
DeviceModal({
deviceAdderViewportTemplate,
devices,
onAddCustomDevice,
onDeviceListUpdate,
onRemoveCustomDevice,
onUpdateDeviceDisplayed,
onUpdateDeviceModal,
})
devices.isModalOpen ?
DeviceModal({
deviceAdderViewportTemplate,
devices,
onAddCustomDevice,
onDeviceListUpdate,
onRemoveCustomDevice,
onUpdateDeviceDisplayed,
onUpdateDeviceModal,
})
:
null
);
}
}

View File

@ -27,25 +27,22 @@ class DeviceAdder extends PureComponent {
constructor(props) {
super(props);
this.state = {};
const {
height,
width,
} = this.props.viewportTemplate;
this.state = {
deviceAdderDisplayed: false,
height,
width,
};
this.onChangeSize = this.onChangeSize.bind(this);
this.onDeviceAdderShow = this.onDeviceAdderShow.bind(this);
this.onDeviceAdderSave = this.onDeviceAdderSave.bind(this);
}
componentWillReceiveProps(nextProps) {
const {
width,
height,
} = nextProps.viewportTemplate;
this.setState({
width,
height,
});
}
onChangeSize(_, width, height) {
this.setState({
width,
@ -68,6 +65,7 @@ class DeviceAdder extends PureComponent {
if (!this.pixelRatioInput.checkValidity()) {
return;
}
if (devices.custom.find(device => device.name == this.nameInput.value)) {
this.nameInput.setCustomValidity("Device name already in use");
return;
@ -76,6 +74,7 @@ class DeviceAdder extends PureComponent {
this.setState({
deviceAdderDisplayed: false,
});
onAddCustomDevice({
name: this.nameInput.value,
width: this.state.width,

View File

@ -32,6 +32,11 @@ class DeviceModal extends PureComponent {
super(props);
this.state = {};
for (const type of this.props.devices.types) {
for (const device of this.props.devices[type]) {
this.state[device.name] = device.displayed;
}
}
this.onAddCustomDevice = this.onAddCustomDevice.bind(this);
this.onDeviceCheckboxChange = this.onDeviceCheckboxChange.bind(this);
@ -43,26 +48,6 @@ class DeviceModal extends PureComponent {
window.addEventListener("keydown", this.onKeyDown, true);
}
componentWillReceiveProps(nextProps) {
const {
devices: oldDevices,
} = this.props;
const {
devices,
} = nextProps;
// Refresh component state only when model transitions from closed to open
if (!oldDevices.isModalOpen && devices.isModalOpen) {
for (const type of devices.types) {
for (const device of devices[type]) {
this.setState({
[device.name]: device.displayed,
});
}
}
}
}
componentWillUnmount() {
window.removeEventListener("keydown", this.onKeyDown, true);
}

View File

@ -9,9 +9,7 @@ const TEST_URL = "data:text/html;charset=utf-8,";
const Types = require("devtools/client/responsive.html/types");
addRDMTask(TEST_URL, async function({ ui }) {
const { store, document } = ui.toolWindow;
const modal = document.querySelector("#device-modal-wrapper");
const closeButton = document.querySelector("#device-close-button");
const { document, store } = ui.toolWindow;
// Wait until the viewport has been added and the device list has been loaded
await waitUntilState(store, state => state.viewports.length == 1
@ -26,20 +24,17 @@ addRDMTask(TEST_URL, async function({ ui }) {
.filter(cb => !cb.checked)[0];
const value = uncheckedCb.value;
uncheckedCb.click();
closeButton.click();
document.getElementById("device-close-button").click();
ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
"The device modal is closed on exit.");
ok(!store.getState().devices.isModalOpen, "The device modal is closed on exit.");
info("Check that the device list remains unchanged after exitting.");
const preferredDevicesAfter = _loadPreferredDevices();
is(preferredDevicesBefore.added.size, preferredDevicesAfter.added.size,
"Got expected number of added devices.");
is(preferredDevicesBefore.removed.size, preferredDevicesAfter.removed.size,
"Got expected number of removed devices.");
ok(!preferredDevicesAfter.removed.has(value),
value + " was not added to removed device list.");
});

View File

@ -23,10 +23,8 @@ const Types = require("devtools/client/responsive.html/types");
addRDMTask(TEST_URL, async function({ ui }) {
const { toolWindow } = ui;
const { store, document } = toolWindow;
const { document, store } = toolWindow;
const deviceSelector = document.getElementById("device-selector");
const modal = document.getElementById("device-modal-wrapper");
const submitButton = document.getElementById("device-submit-button");
// Wait until the viewport has been added and the device list has been loaded
await waitUntilState(store, state => state.viewports.length == 1
@ -60,10 +58,9 @@ addRDMTask(TEST_URL, async function({ ui }) {
.filter(cb => !cb.checked)[0];
const value = uncheckedCb.value;
uncheckedCb.click();
submitButton.click();
document.getElementById("device-submit-button").click();
ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
"The device modal is closed on submit.");
ok(!store.getState().devices.isModalOpen, "The device modal is closed on submit.");
info("Checking that the new device is added to the user preference list.");
let preferredDevices = _loadPreferredDevices();
@ -80,6 +77,7 @@ addRDMTask(TEST_URL, async function({ ui }) {
info("Reopen device modal and check new device is correctly checked");
await openDeviceModal(ui);
ok([...document.querySelectorAll(".device-input-checkbox")]
.filter(cb => cb.checked && cb.value === value)[0],
value + " is checked in the device modal.");
@ -90,7 +88,7 @@ addRDMTask(TEST_URL, async function({ ui }) {
.filter(cb => cb.checked && cb.value != value)[0];
const checkedVal = checkedCb.value;
checkedCb.click();
submitButton.click();
document.getElementById("device-submit-button").click();
info("Checking that the device is removed from the user preference list.");
preferredDevices = _loadPreferredDevices();
@ -117,7 +115,7 @@ addRDMTask(TEST_URL, async function({ ui }) {
addRDMTask(TEST_URL, async function({ ui }) {
const { toolWindow } = ui;
const { store, document } = toolWindow;
const { document, store } = toolWindow;
// Wait until the viewport has been added and the device list has been loaded
await waitUntilState(store, state => state.viewports.length == 1

View File

@ -228,16 +228,14 @@ async function testViewportResize(ui, selector, moveBy,
}
async function openDeviceModal(ui) {
const { document } = ui.toolWindow;
const modal = document.getElementById("device-modal-wrapper");
info("Checking initial device modal state");
ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
"The device modal is closed by default.");
const { document, store } = ui.toolWindow;
info("Opening device modal through device selector.");
const onModalOpen = waitUntilState(store, state => state.devices.isModalOpen);
await selectMenuItem(ui, "#device-selector", getStr("responsive.editDeviceList2"));
await onModalOpen;
const modal = document.getElementById("device-modal-wrapper");
ok(modal.classList.contains("opened") && !modal.classList.contains("closed"),
"The device modal is displayed.");
}
@ -414,7 +412,7 @@ async function testUserAgentFromBrowser(browser, expected) {
function addDeviceInModal(ui, device) {
const { Simulate } =
ui.toolWindow.require("devtools/client/shared/vendor/react-dom-test-utils");
const { store, document } = ui.toolWindow;
const { document, store } = ui.toolWindow;
const nameInput = document.querySelector("#device-adder-name input");
const [ widthInput, heightInput ] =

View File

@ -210,6 +210,7 @@ exports.ANIMATION_TYPE_FOR_LONGHANDS = [
"min-block-size",
"-moz-min-font-size-ratio",
"min-inline-size",
"offset-path",
"padding-block-end",
"padding-block-start",
"padding-inline-end",

View File

@ -2949,6 +2949,7 @@ exports.CSS_PROPERTIES = {
"rotate",
"scale",
"translate",
"offset-path",
"scroll-behavior",
"scroll-snap-type-x",
"scroll-snap-type-y",
@ -7415,6 +7416,20 @@ exports.CSS_PROPERTIES = {
"unset"
]
},
"offset-path": {
"isInherited": false,
"subproperties": [
"offset-path"
],
"supports": [],
"values": [
"inherit",
"initial",
"none",
"path",
"unset"
]
},
"opacity": {
"isInherited": false,
"subproperties": [
@ -9331,6 +9346,10 @@ exports.PREFERENCES = [
"font-variation-settings",
"layout.css.font-variations.enabled"
],
[
"offset-path",
"layout.css.motion-path.enabled"
],
[
"rotate",
"layout.css.individual-transform.enabled"

View File

@ -3,46 +3,52 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const paymentSrv = Cc["@mozilla.org/dom/payments/payment-request-service;1"].getService(Ci.nsIPaymentRequestService);
const paymentSrv = Cc[
"@mozilla.org/dom/payments/payment-request-service;1"
].getService(Ci.nsIPaymentRequestService);
const InvalidDetailsUIService = {
showPayment: function(requestId) {
showPayment(requestId) {
paymentSrv.changeShippingOption(requestId, "");
},
abortPayment: function(requestId) {
let abortResponse = Cc["@mozilla.org/dom/payments/payment-abort-action-response;1"].
createInstance(Ci.nsIPaymentAbortActionResponse);
abortPayment(requestId) {
const abortResponse = Cc[
"@mozilla.org/dom/payments/payment-abort-action-response;1"
].createInstance(Ci.nsIPaymentAbortActionResponse);
abortResponse.init(requestId, Ci.nsIPaymentActionResponse.ABORT_SUCCEEDED);
paymentSrv.respondPayment(abortResponse.QueryInterface(Ci.nsIPaymentActionResponse));
},
completePayment: function(requestId) {
},
updatePayment: function(requestId) {
paymentSrv.respondPayment(
abortResponse.QueryInterface(Ci.nsIPaymentActionResponse)
);
},
completePayment(requestId) {},
updatePayment(requestId) {},
QueryInterface: ChromeUtils.generateQI([Ci.nsIPaymentUIService]),
};
function emitTestFail(message) {
sendAsyncMessage("test-fail", message);
}
function checkLowerCaseCurrency() {
const paymentEnum = paymentSrv.enumerate();
if (!paymentEnum.hasMoreElements()) {
emitTestFail("PaymentRequestService should have at least one payment request.");
const msg =
"PaymentRequestService should have at least one payment request.";
sendAsyncMessage("test-fail", msg);
}
while (paymentEnum.hasMoreElements()) {
let payRequest = paymentEnum.getNext().QueryInterface(Ci.nsIPaymentRequest);
const payRequest = paymentEnum
.getNext()
.QueryInterface(Ci.nsIPaymentRequest);
if (!payRequest) {
emitTestFail("Fail to get existing payment request.");
sendAsyncMessage("test-fail", "Fail to get existing payment request.");
break;
}
if (payRequest.paymentDetails.totalItem.amount.currency != "USD") {
emitTestFail("Currency of PaymentItem total should be 'USD', but got " +
payRequest.paymentDetails.totalItem.amount.currency + ".");
const { currency } = payRequest.paymentDetails.totalItem.amount;
if (currency != "USD") {
const msg =
"Currency of PaymentItem total should be 'USD', but got ${currency}";
sendAsyncMessage("check-complete");
}
}
paymentSrv.cleanup();
@ -51,10 +57,10 @@ function checkLowerCaseCurrency() {
addMessageListener("check-lower-case-currency", checkLowerCaseCurrency);
addMessageListener("set-update-with-invalid-details-ui-service", function() {
paymentSrv.setTestingUIService(InvalidDetailsUIService.QueryInterface(Ci.nsIPaymentUIService));
addMessageListener("set-update-with-invalid-details-ui-service", () => {
paymentSrv.setTestingUIService(
InvalidDetailsUIService.QueryInterface(Ci.nsIPaymentUIService)
);
});
addMessageListener("teardown", function() {
sendAsyncMessage("teardown-complete");
});
addMessageListener("teardown", () => sendAsyncMessage("teardown-complete"));

View File

@ -1,376 +1,353 @@
<!DOCTYPE HTML>
<html>
<meta charset="utf-8">
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1367669
https://bugzilla.mozilla.org/show_bug.cgi?id=1388661
-->
<head>
<meta charset="utf-8">
<title>Test for PaymentRequest API currency amount validation</title>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript">
<title>Test for PaymentRequest API currency amount validation</title>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script>
"use strict";
SimpleTest.waitForExplicitFinish();
"use strict";
SimpleTest.waitForExplicitFinish();
const gUrl = SimpleTest.getTestFileURL(
"CurrencyAmountValidationChromeScript.js"
);
const gScript = SpecialPowers.loadChromeScript(gUrl);
var gUrl = SimpleTest.getTestFileURL('CurrencyAmountValidationChromeScript.js');
var gScript = SpecialPowers.loadChromeScript(gUrl);
function testFailHandler(message) {
ok(false, message);
}
gScript.addMessageListener("test-fail", testFailHandler);
function testFailHandler(message) {
ok(false, message);
}
gScript.addMessageListener("test-fail", testFailHandler);
const defaultMethods = [{
const defaultMethods = [
{
supportedMethods: "basic-card",
}];
const defaultDetails = {
total: {
label: "total",
amount: {
currency: "usd",
value: "1.00",
},
},
];
const defaultDetails = {
total: {
label: "total",
amount: {
currency: "usd",
value: "1.00",
},
};
},
};
const specialAmountDetails = {
total: {
label: "total",
amount: {
currency: "usd",
value: {
toString() {
throw "42";
},
const specialAmountDetails = {
total: {
label: "total",
amount: {
currency: "usd",
value: {
toString() {
throw "42";
},
},
},
};
},
};
const wellFormedCurrencyCodes = [
"BOB",
"EUR",
"usd", // currency codes are case-insensitive
"XdR",
"xTs",
];
const wellFormedCurrencyCodes = [
"BOB",
"EUR",
"usd", // currency codes are case-insensitive
"XdR",
"xTs",
];
const invalidCurrencyCodes = [
"",
"€",
"$",
"SFr.",
"DM",
"KR₩",
"702",
"ßP",
"ınr",
"invalid",
"in",
"123",
];
const invalidCurrencyCodes = [
"",
"€",
"$",
"SFr.",
"DM",
"KR₩",
"702",
"ßP",
"ınr",
"invalid",
"in",
"123",
];
const updatedInvalidCurrencyDetails = {
total: {
label: "Total",
amount: {
currency: "Invalid",
value: "1.00"
}
const updatedInvalidCurrencyDetails = {
total: {
label: "Total",
amount: {
currency: "Invalid",
value: "1.00",
},
};
},
};
const updatedInvalidAmountDetails = {
total: {
label: "Total",
amount: {
currency: "USD",
value: "-1.00",
},
const updatedInvalidAmountDetails = {
total: {
label: "Total",
amount: {
currency: "USD",
value: "-1.00",
},
}
},
};
const invalidAmounts = [
"-",
"notdigits",
"ALSONOTDIGITS",
"10.",
".99",
"-10.",
"-.99",
"10-",
"1-0",
"1.0.0",
"1/3",
"",
null,
" 1.0 ",
" 1.0 ",
"1.0 ",
"USD$1.0",
"$1.0",
{
toString() {
return " 1.0";
},
const invalidAmounts = [
"-",
"notdigits",
"ALSONOTDIGITS",
"10.",
".99",
"-10.",
"-.99",
"10-",
"1-0",
"1.0.0",
"1/3",
"",
null,
" 1.0 ",
" 1.0 ",
"1.0 ",
"USD$1.0",
"$1.0",
{
toString() {
return " 1.0";
},
undefined,
];
const invalidTotalAmounts = invalidAmounts.concat([
"-1",
"-1.0",
"-1.00",
"-1000.000",
]);
},
undefined,
];
const invalidTotalAmounts = invalidAmounts.concat([
"-1",
"-1.0",
"-1.00",
"-1000.000",
]);
function updateWithInvalidCurrency() {
return new Promise((resolve, reject) => {
resolve(updatedInvalidCurrencyDetails);
});
}
function updateWithInvalidAmount() {
return new Promise((resolve, reject) => {
resolve(updatedInvalidAmountDetails);
});
}
function updateWithInvalidAmount() {
return new Promise((resolve, reject) => {
resolve(updatedInvalidAmountDetails);
});
}
function testWithLowerCaseCurrency() {
return new Promise((resolve, reject) => {
const payRequest = new PaymentRequest(defaultMethods, defaultDetails);
ok(payRequest, "PaymentRequest should be created");
gScript.addMessageListener("check-complete", function checkCompleteHandler() {
async function testWithLowerCaseCurrency() {
const payRequest = new PaymentRequest(defaultMethods, defaultDetails);
return new Promise(resolve => {
gScript.addMessageListener(
"check-complete",
function checkCompleteHandler() {
gScript.removeMessageListener("check-complete", checkCompleteHandler);
resolve();
});
gScript.sendAsyncMessage("check-lower-case-currency");
});
}
function testWithWellFormedCurrencyCodes() {
return new Promise((resolve, reject) => {
for (const currency of wellFormedCurrencyCodes) {
let details = {
total: {
label: "Well Formed Currency",
amount: {
currency: currency,
value: "1.00",
},
},
};
try {
const payRequest = new PaymentRequest(defaultMethods, details);
} catch (e) {
ok(false, "Unexpected error while creating payment request with well formed currency("
+ currency + ") " + e.name + ".");
}
}
resolve();
});
}
function testWithInvalidCurrencyCodes() {
return new Promise((resolve, reject) => {
for (const invalidCurrency of invalidCurrencyCodes) {
let invalidDetails = {
total: {
label: "Invalid Currency",
amount: {
currency: invalidCurrency,
value: "1.00",
},
},
};
try {
const payRequest = new PaymentRequest(defaultMethods, invalidDetails);
ok(false, "Expected fail to create PaymentRequest with invalid currency(" + invalidCurrency + ").");
} catch (e) {
is(e.name, "RangeError", "Expected rejected with 'RangeError', but got " + e.name + ".");
}
}
resolve();
});
}
function testUpdateWithInvalidCurrency() {
const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true);
gScript.sendAsyncMessage("set-update-with-invalid-details-ui-service");
return new Promise((resolve, reject) => {
const payRequest = new PaymentRequest(defaultMethods, defaultDetails);
payRequest.addEventListener("shippingaddresschange", event => {
event.updateWith(updateWithInvalidCurrency());
});
payRequest.addEventListener("shippingoptionchange", event => {
event.updateWith(updateWithInvalidCurrency());
});
payRequest.show().then((result) => {
ok(false, "Should be rejected with 'RangeError', but got resolved");
resolve();
}, (result) => {
is(result.name, "RangeError", "Should be rejected with 'RangeError', but got " + result.name + ".");
resolve();
}).catch(e => {
ok(false, "Unexpected error: " + e.name);
resolve();
}).finally(handler.destruct);
});
}
function testUpdateWithInvalidAmount() {
const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(true);
gScript.sendAsyncMessage("set-update-with-invalid-details-ui-service");
return new Promise((resolve, reject) => {
const payRequest = new PaymentRequest(defaultMethods, defaultDetails);
payRequest.addEventListener("shippingaddresschange", event => {
event.updateWith(updateWithInvalidAmount());
});
payRequest.addEventListener("shippingoptionchange", event => {
event.updateWith(updateWithInvalidAmount());
});
payRequest.show().then((result) => {
ok(false, "Should be rejected with 'TypeError', but got resolved");
resolve();
}, (result) => {
is(result.name, "TypeError", "Should be rejected with 'TypeError', but got " + result.name + ".");
resolve();
}).catch(e => {
ok(false, "Unexpected error: " + e.name);
resolve();
}).finally(handler.destruct);
});
}
function testSpecialAmount() {
return new Promise((resolve, reject) => {
try {
new PaymentRequest([{supportedMethods: "basic-card"}],
specialAmountDetails);
ok(false, "Should throw '42', but got resolved.");
resolve();
} catch (e) {
is(e, "42", "Expected throw '42'. but got " + e);
resolve();
}
});
}
function testInvalidTotalAmounts() {
return new Promise((resolve, reject) => {
for (const amount of invalidTotalAmounts) {
try {
new PaymentRequest(
[
{
supportedMethods: "basic-card",
},
],
{
total: {
label: "",
amount: {
currency: "USD",
value: amount,
},
},
}
);
ok(false, "Should throw 'TypeError', but got resolved.");
resolve();
}
catch (err) {
is(err.name, "TypeError",
"Expected 'TypeError', but got '" + err.name + "'");
resolve();
};
}
});
}
function testInvalidAmounts() {
return new Promise((resolve, reject) => {
for (const amount of invalidAmounts) {
try {
new PaymentRequest(
[
{
supportedMethods: "basic-card",
},
],
{
total: {
label: "",
amount: {
currency: "USD",
value: "1.00",
},
},
displayItems: [
{
label: "",
amount: {
currency: "USD",
value: amount,
},
},
],
}
);
ok(false, "Should throw 'TypeError', but got resolved.");
resolve();
}
catch (err) {
is(err.name, "TypeError",
"Expected 'TypeError', but got '" + err.name + "'");
resolve();
};
}
});
}
function teardown() {
gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() {
gScript.removeMessageListener("teardown-complete", teardownCompleteHandler);
gScript.removeMessageListener("test-fail", testFailHandler)
gScript.destroy();
SimpleTest.finish();
});
gScript.sendAsyncMessage("teardown");
}
function runTests() {
testInvalidTotalAmounts()
.then(testSpecialAmount)
.then(testInvalidAmounts)
.then(testWithLowerCaseCurrency)
.then(testWithWellFormedCurrencyCodes)
.then(testWithInvalidCurrencyCodes)
.then(testUpdateWithInvalidAmount)
.then(testUpdateWithInvalidCurrency)
.then(teardown)
.catch( e => {
ok(false, "Unexpected error: " + e.name);
SimpleTest.finish();
});
}
window.addEventListener('load', function() {
SpecialPowers.pushPrefEnv({
'set': [
['dom.payments.request.enabled', true],
]
}, runTests);
);
gScript.sendAsyncMessage("check-lower-case-currency");
});
}
</script>
</head>
function testWithWellFormedCurrencyCodes() {
for (const currency of wellFormedCurrencyCodes) {
const details = {
total: {
label: "Well Formed Currency",
amount: {
currency: currency,
value: "1.00",
},
},
};
try {
const payRequest = new PaymentRequest(defaultMethods, details);
} catch (e) {
const msg = `Unexpected error while creating payment request with well-formed currency (${currency}) ${
e.name
}`;
ok(false, msg);
}
}
}
function testWithInvalidCurrencyCodes() {
for (const invalidCurrency of invalidCurrencyCodes) {
const invalidDetails = {
total: {
label: "Invalid Currency",
amount: {
currency: invalidCurrency,
value: "1.00",
},
},
};
try {
const payRequest = new PaymentRequest(defaultMethods, invalidDetails);
ok(
false,
`Creating a Payment Request with invalid currency (${invalidCurrency}) must throw.`
);
} catch (e) {
is(
e.name,
"RangeError",
`Expected rejected with 'RangeError', but got '${e.name}'.`
);
}
}
}
async function testUpdateWithInvalidCurrency() {
const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(
true
);
gScript.sendAsyncMessage("set-update-with-invalid-details-ui-service");
const payRequest = new PaymentRequest(defaultMethods, defaultDetails);
payRequest.addEventListener("shippingaddresschange", event => {
event.updateWith(Promise.resolve(updatedInvalidCurrencyDetails));
});
payRequest.addEventListener("shippingoptionchange", event => {
event.updateWith(updatedInvalidCurrencyDetails);
});
try {
await payRequest.show();
ok(false, "Should have rejected with 'RangeError'");
} catch (err) {
is(
err.name,
"RangeError",
`Should be rejected with 'RangeError', but got '${err.name}'.`
);
}
handler.destruct();
}
async function testUpdateWithInvalidAmount() {
const handler = SpecialPowers.getDOMWindowUtils(window).setHandlingUserInput(
true
);
gScript.sendAsyncMessage("set-update-with-invalid-details-ui-service");
const payRequest = new PaymentRequest(defaultMethods, defaultDetails);
payRequest.addEventListener("shippingaddresschange", event => {
event.updateWith(updateWithInvalidAmount());
});
payRequest.addEventListener("shippingoptionchange", event => {
event.updateWith(updateWithInvalidAmount());
});
try {
await payRequest.show();
ok(false, "Should be rejected with 'TypeError'");
} catch (err) {
is(
err.name,
"TypeError",
`Should be rejected with 'TypeError', but got ${err.name}.`
);
}
handler.destruct();
}
function testSpecialAmount() {
try {
new PaymentRequest(defaultMethods, specialAmountDetails);
ok(false, "Should throw '42', but got resolved.");
} catch (e) {
is(e, "42", "Expected throw '42'. but got " + e);
}
}
function testInvalidTotalAmounts() {
for (const invalidAmount of invalidTotalAmounts) {
try {
const invalidDetails = {
total: {
label: "",
amount: {
currency: "USD",
value: invalidAmount,
},
},
};
new PaymentRequest(defaultMethods, invalidDetails);
ok(false, "Should throw 'TypeError', but got resolved.");
} catch (err) {
is(err.name, "TypeError", `Expected 'TypeError', but got '${err.name}'`);
}
}
}
function testInvalidAmounts() {
for (const invalidAmount of invalidAmounts) {
try {
new PaymentRequest(defaultMethods, {
total: {
label: "",
amount: {
currency: "USD",
value: "1.00",
},
},
displayItems: [
{
label: "",
amount: {
currency: "USD",
value: invalidAmount,
},
},
],
});
ok(false, "Should throw 'TypeError', but got resolved.");
} catch (err) {
is(err.name, "TypeError", `Expected 'TypeError', but got '${err.name}'.`);
}
}
}
function teardown() {
return new Promise(resolve => {
gScript.addMessageListener(
"teardown-complete",
function teardownCompleteHandler() {
gScript.removeMessageListener(
"teardown-complete",
teardownCompleteHandler
);
gScript.removeMessageListener("test-fail", testFailHandler);
gScript.destroy();
SimpleTest.finish();
resolve();
}
);
gScript.sendAsyncMessage("teardown");
});
}
async function runTests() {
try {
testInvalidTotalAmounts();
testSpecialAmount();
testInvalidAmounts();
testWithWellFormedCurrencyCodes();
testWithInvalidCurrencyCodes();
await testUpdateWithInvalidAmount();
await testUpdateWithInvalidCurrency();
await testWithLowerCaseCurrency();
await teardown();
} catch (e) {
console.error(e);
ok(false, "Unexpected error: " + e.name);
SimpleTest.finish();
}
}
window.addEventListener("load", () => {
SpecialPowers.pushPrefEnv(
{
set: [["dom.payments.request.enabled", true]],
},
runTests
);
});
</script>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1367669">Mozilla Bug 1367669</a>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1388661">Mozilla Bug 1388661</a>
</body>
</html>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1388661">Mozilla Bug 1388661</a>

View File

@ -26,12 +26,41 @@ using namespace mozilla;
using namespace mozilla::dom::SVGPathSeg_Binding;
using namespace mozilla::gfx;
static bool IsMoveto(uint16_t aSegType)
static inline bool IsMoveto(uint16_t aSegType)
{
return aSegType == PATHSEG_MOVETO_ABS ||
aSegType == PATHSEG_MOVETO_REL;
}
static inline bool
IsMoveto(StylePathCommand::Tag aSegType)
{
return aSegType == StylePathCommand::Tag::MoveTo;
}
static inline bool
IsValidType(uint16_t aSegType)
{
return SVGPathSegUtils::IsValidType(aSegType);
}
static inline bool
IsValidType(StylePathCommand::Tag aSegType)
{
return aSegType != StylePathCommand::Tag::Unknown;
}
static inline bool
IsClosePath(uint16_t aSegType) {
return aSegType == PATHSEG_CLOSEPATH;
}
static inline bool
IsClosePath(StylePathCommand::Tag aSegType)
{
return aSegType == StylePathCommand::Tag::ClosePath;
}
nsresult
SVGPathData::CopyFrom(const SVGPathData& rhs)
{
@ -262,18 +291,18 @@ ApproximateZeroLengthSubpathSquareCaps(PathBuilder* aPB,
aPB->MoveTo(aPoint);
}
#define MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT \
do { \
if (!subpathHasLength && hasLineCaps && aStrokeWidth > 0 && \
subpathContainsNonMoveTo && \
SVGPathSegUtils::IsValidType(prevSegType) && \
(!IsMoveto(prevSegType) || segType == PATHSEG_CLOSEPATH)) { \
ApproximateZeroLengthSubpathSquareCaps(builder, segStart, aStrokeWidth);\
} \
#define MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT \
do { \
if (!subpathHasLength && hasLineCaps && aStrokeWidth > 0 && \
subpathContainsNonMoveTo && \
IsValidType(prevSegType) && \
(!IsMoveto(prevSegType) || IsClosePath(segType))) { \
ApproximateZeroLengthSubpathSquareCaps(aBuilder, segStart, aStrokeWidth);\
} \
} while(0)
already_AddRefed<Path>
SVGPathData::BuildPath(PathBuilder* builder,
SVGPathData::BuildPath(PathBuilder* aBuilder,
uint8_t aStrokeLineCap,
Float aStrokeWidth) const
{
@ -309,20 +338,20 @@ SVGPathData::BuildPath(PathBuilder* builder,
subpathContainsNonMoveTo = true;
MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT;
segEnd = pathStart;
builder->Close();
aBuilder->Close();
break;
case PATHSEG_MOVETO_ABS:
MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT;
pathStart = segEnd = Point(mData[i], mData[i+1]);
builder->MoveTo(segEnd);
aBuilder->MoveTo(segEnd);
subpathHasLength = false;
break;
case PATHSEG_MOVETO_REL:
MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT;
pathStart = segEnd = segStart + Point(mData[i], mData[i+1]);
builder->MoveTo(segEnd);
aBuilder->MoveTo(segEnd);
subpathHasLength = false;
break;
@ -330,7 +359,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
segEnd = Point(mData[i], mData[i+1]);
if (segEnd != segStart) {
subpathHasLength = true;
builder->LineTo(segEnd);
aBuilder->LineTo(segEnd);
}
break;
@ -338,7 +367,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
segEnd = segStart + Point(mData[i], mData[i+1]);
if (segEnd != segStart) {
subpathHasLength = true;
builder->LineTo(segEnd);
aBuilder->LineTo(segEnd);
}
break;
@ -348,7 +377,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
segEnd = Point(mData[i+4], mData[i+5]);
if (segEnd != segStart || segEnd != cp1 || segEnd != cp2) {
subpathHasLength = true;
builder->BezierTo(cp1, cp2, segEnd);
aBuilder->BezierTo(cp1, cp2, segEnd);
}
break;
@ -358,7 +387,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
segEnd = segStart + Point(mData[i+4], mData[i+5]);
if (segEnd != segStart || segEnd != cp1 || segEnd != cp2) {
subpathHasLength = true;
builder->BezierTo(cp1, cp2, segEnd);
aBuilder->BezierTo(cp1, cp2, segEnd);
}
break;
@ -370,7 +399,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
tcp2 = cp1 + (segEnd - cp1) / 3;
if (segEnd != segStart || segEnd != cp1) {
subpathHasLength = true;
builder->BezierTo(tcp1, tcp2, segEnd);
aBuilder->BezierTo(tcp1, tcp2, segEnd);
}
break;
@ -382,7 +411,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
tcp2 = cp1 + (segEnd - cp1) / 3;
if (segEnd != segStart || segEnd != cp1) {
subpathHasLength = true;
builder->BezierTo(tcp1, tcp2, segEnd);
aBuilder->BezierTo(tcp1, tcp2, segEnd);
}
break;
@ -397,12 +426,12 @@ SVGPathData::BuildPath(PathBuilder* builder,
if (segEnd != segStart) {
subpathHasLength = true;
if (radii.x == 0.0f || radii.y == 0.0f) {
builder->LineTo(segEnd);
aBuilder->LineTo(segEnd);
} else {
nsSVGArcConverter converter(segStart, segEnd, radii, mData[i+2],
mData[i+3] != 0, mData[i+4] != 0);
while (converter.GetNextSegment(&cp1, &cp2, &segEnd)) {
builder->BezierTo(cp1, cp2, segEnd);
aBuilder->BezierTo(cp1, cp2, segEnd);
}
}
}
@ -413,7 +442,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
segEnd = Point(mData[i], segStart.y);
if (segEnd != segStart) {
subpathHasLength = true;
builder->LineTo(segEnd);
aBuilder->LineTo(segEnd);
}
break;
@ -421,7 +450,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
segEnd = segStart + Point(mData[i], 0.0f);
if (segEnd != segStart) {
subpathHasLength = true;
builder->LineTo(segEnd);
aBuilder->LineTo(segEnd);
}
break;
@ -429,7 +458,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
segEnd = Point(segStart.x, mData[i]);
if (segEnd != segStart) {
subpathHasLength = true;
builder->LineTo(segEnd);
aBuilder->LineTo(segEnd);
}
break;
@ -437,7 +466,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
segEnd = segStart + Point(0.0f, mData[i]);
if (segEnd != segStart) {
subpathHasLength = true;
builder->LineTo(segEnd);
aBuilder->LineTo(segEnd);
}
break;
@ -447,7 +476,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
segEnd = Point(mData[i+2], mData[i+3]);
if (segEnd != segStart || segEnd != cp1 || segEnd != cp2) {
subpathHasLength = true;
builder->BezierTo(cp1, cp2, segEnd);
aBuilder->BezierTo(cp1, cp2, segEnd);
}
break;
@ -457,7 +486,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
segEnd = segStart + Point(mData[i+2], mData[i+3]);
if (segEnd != segStart || segEnd != cp1 || segEnd != cp2) {
subpathHasLength = true;
builder->BezierTo(cp1, cp2, segEnd);
aBuilder->BezierTo(cp1, cp2, segEnd);
}
break;
@ -469,7 +498,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
tcp2 = cp1 + (segEnd - cp1) / 3;
if (segEnd != segStart || segEnd != cp1) {
subpathHasLength = true;
builder->BezierTo(tcp1, tcp2, segEnd);
aBuilder->BezierTo(tcp1, tcp2, segEnd);
}
break;
@ -481,7 +510,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
tcp2 = cp1 + (segEnd - cp1) / 3;
if (segEnd != segStart || segEnd != cp1) {
subpathHasLength = true;
builder->BezierTo(tcp1, tcp2, segEnd);
aBuilder->BezierTo(tcp1, tcp2, segEnd);
}
break;
@ -503,7 +532,7 @@ SVGPathData::BuildPath(PathBuilder* builder,
MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT;
return builder->Finish();
return aBuilder->Finish();
}
already_AddRefed<Path>
@ -524,6 +553,209 @@ SVGPathData::BuildPathForMeasuring() const
return BuildPath(builder, NS_STYLE_STROKE_LINECAP_BUTT, 0);
}
// We could simplify this function because this is only used by CSS motion path
// and clip-path, which don't render the SVG Path. i.e. The returned path is
// used as a reference.
/* static */ already_AddRefed<Path>
SVGPathData::BuildPath(const nsTArray<StylePathCommand>& aPath,
PathBuilder* aBuilder,
uint8_t aStrokeLineCap,
Float aStrokeWidth)
{
if (aPath.IsEmpty() || !aPath[0].IsMoveTo()) {
return nullptr; // paths without an initial moveto are invalid
}
auto toGfxPoint = [](const StyleCoordPair& aPair) {
return Point(aPair._0, aPair._1);
};
auto isCubicType = [](StylePathCommand::Tag aType) {
return aType == StylePathCommand::Tag::CurveTo ||
aType == StylePathCommand::Tag::SmoothCurveTo;
};
auto isQuadraticType = [](StylePathCommand::Tag aType) {
return aType == StylePathCommand::Tag::QuadBezierCurveTo ||
aType == StylePathCommand::Tag::SmoothQuadBezierCurveTo;
};
bool hasLineCaps = aStrokeLineCap != NS_STYLE_STROKE_LINECAP_BUTT;
bool subpathHasLength = false; // visual length
bool subpathContainsNonMoveTo = false;
StylePathCommand::Tag segType = StylePathCommand::Tag::Unknown;
StylePathCommand::Tag prevSegType = StylePathCommand::Tag::Unknown;
Point pathStart(0.0, 0.0); // start point of [sub]path
Point segStart(0.0, 0.0);
Point segEnd;
Point cp1, cp2; // previous bezier's control points
Point tcp1, tcp2; // temporaries
// Regarding cp1 and cp2: If the previous segment was a cubic bezier curve,
// then cp2 is its second control point. If the previous segment was a
// quadratic curve, then cp1 is its (only) control point.
for (const StylePathCommand& cmd: aPath) {
segType = cmd.tag;
switch (segType) {
case StylePathCommand::Tag::ClosePath:
// set this early to allow drawing of square caps for "M{x},{y} Z":
subpathContainsNonMoveTo = true;
MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT;
segEnd = pathStart;
aBuilder->Close();
break;
case StylePathCommand::Tag::MoveTo: {
MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT;
const Point& p = toGfxPoint(cmd.move_to.point);
pathStart = segEnd = cmd.move_to.absolute ? p : segStart + p;
aBuilder->MoveTo(segEnd);
subpathHasLength = false;
break;
}
case StylePathCommand::Tag::LineTo: {
const Point& p = toGfxPoint(cmd.line_to.point);
segEnd = cmd.line_to.absolute ? p : segStart + p;
if (segEnd != segStart) {
subpathHasLength = true;
aBuilder->LineTo(segEnd);
}
break;
}
case StylePathCommand::Tag::CurveTo:
cp1 = toGfxPoint(cmd.curve_to.control1);
cp2 = toGfxPoint(cmd.curve_to.control2);
segEnd = toGfxPoint(cmd.curve_to.point);
if (!cmd.curve_to.absolute) {
cp1 += segStart;
cp2 += segStart;
segEnd += segStart;
}
if (segEnd != segStart || segEnd != cp1 || segEnd != cp2) {
subpathHasLength = true;
aBuilder->BezierTo(cp1, cp2, segEnd);
}
break;
case StylePathCommand::Tag::QuadBezierCurveTo:
cp1 = toGfxPoint(cmd.quad_bezier_curve_to.control1);
segEnd = toGfxPoint(cmd.quad_bezier_curve_to.point);
if (!cmd.quad_bezier_curve_to.absolute) {
cp1 += segStart;
segEnd += segStart; // set before setting tcp2!
}
// Convert quadratic curve to cubic curve:
tcp1 = segStart + (cp1 - segStart) * 2 / 3;
tcp2 = cp1 + (segEnd - cp1) / 3;
if (segEnd != segStart || segEnd != cp1) {
subpathHasLength = true;
aBuilder->BezierTo(tcp1, tcp2, segEnd);
}
break;
case StylePathCommand::Tag::EllipticalArc: {
const auto& arc = cmd.elliptical_arc;
Point radii(arc.rx, arc.ry);
segEnd = toGfxPoint(arc.point);
if (!arc.absolute) {
segEnd += segStart;
}
if (segEnd != segStart) {
subpathHasLength = true;
if (radii.x == 0.0f || radii.y == 0.0f) {
aBuilder->LineTo(segEnd);
} else {
nsSVGArcConverter converter(segStart, segEnd, radii, arc.angle,
arc.large_arc_flag, arc.sweep_flag);
while (converter.GetNextSegment(&cp1, &cp2, &segEnd)) {
aBuilder->BezierTo(cp1, cp2, segEnd);
}
}
}
break;
}
case StylePathCommand::Tag::HorizontalLineTo:
if (cmd.horizontal_line_to.absolute) {
segEnd = Point(cmd.horizontal_line_to.x, segStart.y);
} else {
segEnd = segStart + Point(cmd.horizontal_line_to.x, 0.0f);
}
if (segEnd != segStart) {
subpathHasLength = true;
aBuilder->LineTo(segEnd);
}
break;
case StylePathCommand::Tag::VerticalLineTo:
if (cmd.vertical_line_to.absolute) {
segEnd = Point(segStart.x, cmd.vertical_line_to.y);
} else {
segEnd = segStart + Point(0.0f, cmd.vertical_line_to.y);
}
if (segEnd != segStart) {
subpathHasLength = true;
aBuilder->LineTo(segEnd);
}
break;
case StylePathCommand::Tag::SmoothCurveTo:
cp1 = isCubicType(prevSegType) ? segStart * 2 - cp2 : segStart;
cp2 = toGfxPoint(cmd.smooth_curve_to.control2);
segEnd = toGfxPoint(cmd.smooth_curve_to.point);
if (!cmd.smooth_curve_to.absolute) {
cp2 += segStart;
segEnd += segStart;
}
if (segEnd != segStart || segEnd != cp1 || segEnd != cp2) {
subpathHasLength = true;
aBuilder->BezierTo(cp1, cp2, segEnd);
}
break;
case StylePathCommand::Tag::SmoothQuadBezierCurveTo: {
cp1 = isQuadraticType(prevSegType) ? segStart * 2 - cp1 : segStart;
// Convert quadratic curve to cubic curve:
tcp1 = segStart + (cp1 - segStart) * 2 / 3;
const Point& p = toGfxPoint(cmd.smooth_quad_bezier_curve_to.point);
// set before setting tcp2!
segEnd = cmd.smooth_quad_bezier_curve_to.absolute ? p : segStart + p;
tcp2 = cp1 + (segEnd - cp1) / 3;
if (segEnd != segStart || segEnd != cp1) {
subpathHasLength = true;
aBuilder->BezierTo(tcp1, tcp2, segEnd);
}
break;
}
case StylePathCommand::Tag::Unknown:
MOZ_ASSERT_UNREACHABLE("Unacceptable path segment type");
return nullptr;
}
subpathContainsNonMoveTo = !IsMoveto(segType);
prevSegType = segType;
segStart = segEnd;
}
MOZ_ASSERT(prevSegType == segType,
"prevSegType should be left at the final segType");
MAYBE_APPROXIMATE_ZERO_LENGTH_SUBPATH_SQUARE_CAPS_TO_DT;
return aBuilder->Finish();
}
static double
AngleOfVector(const Point& aVector)
{

View File

@ -169,6 +169,16 @@ public:
already_AddRefed<Path> BuildPath(PathBuilder* aBuilder,
uint8_t aCapStyle,
Float aStrokeWidth) const;
/**
* This function tries to build the path by an array of StylePathCommand,
* which is generated by cbindgen from Rust (see ServoStyleConsts.h).
* Basically, this is a variant of the above BuildPath() functions.
*/
static already_AddRefed<Path>
BuildPath(const nsTArray<StylePathCommand>& aPath,
PathBuilder* aBuilder,
uint8_t aCapStyle,
Float aStrokeWidth);
const_iterator begin() const { return mData.Elements(); }
const_iterator end() const { return mData.Elements() + mData.Length(); }

View File

@ -81,6 +81,7 @@ EXPORTS.mozilla.dom += [
'SVGMatrix.h',
'SVGMetadataElement.h',
'SVGMPathElement.h',
'SVGPathData.h',
'SVGPathElement.h',
'SVGPatternElement.h',
'SVGPolygonElement.h',

View File

@ -2562,10 +2562,11 @@ CommonPerformPromiseAllRace(JSContext *cx, PromiseForOfIterator& iterator, Handl
return false;
}
// If either the object to depend on or the object that gets
// blocked isn't a, maybe-wrapped, Promise instance, we ignore it.
// All this does is lose some small amount of debug information in
// scenarios that are highly unlikely to occur in useful code.
// If either the object to depend on (`nextPromiseObj`) or the
// object that gets blocked (`resultPromise`) isn't a,
// maybe-wrapped, Promise instance, we ignore it. All this does is
// lose some small amount of debug information in scenarios that
// are highly unlikely to occur in useful code.
if (nextPromiseObj->is<PromiseObject>() && resultPromise->is<PromiseObject>()) {
Handle<PromiseObject*> promise = nextPromiseObj.as<PromiseObject>();
if (!AddDummyPromiseReactionForDebugger(cx, promise, blockedPromise))
@ -2999,7 +3000,12 @@ Promise_static_species(JSContext* cx, unsigned argc, Value* vp)
// ES2016, 25.4.5.1, implemented in Promise.js.
enum class IncumbentGlobalObject {
Yes, No
// Do not use the incumbent global, this is a special case used by the
// debugger.
No,
// Use incumbent global, this is the normal operation.
Yes
};
static PromiseReactionRecord*
@ -3007,16 +3013,45 @@ NewReactionRecord(JSContext* cx, Handle<PromiseCapability> resultCapability,
HandleValue onFulfilled, HandleValue onRejected,
IncumbentGlobalObject incumbentGlobalObjectOption)
{
// Either of the following conditions must be met:
// * resultCapability.promise is a PromiseObject
// * resultCapability.resolve and resultCapability.resolve are callable
// except for Async Generator, there resultPromise can be nullptr.
#ifdef DEBUG
if (resultCapability.promise() && !resultCapability.promise()->is<PromiseObject>()) {
MOZ_ASSERT(resultCapability.resolve());
MOZ_ASSERT(IsCallable(resultCapability.resolve()));
MOZ_ASSERT(resultCapability.reject());
MOZ_ASSERT(IsCallable(resultCapability.reject()));
if (resultCapability.promise()) {
if (incumbentGlobalObjectOption == IncumbentGlobalObject::Yes) {
if (resultCapability.promise()->is<PromiseObject>()) {
// If `resultCapability.promise` is a Promise object,
// `resultCapability.{resolve,reject}` may be optimized out,
// but if they're not, they should be callable.
MOZ_ASSERT_IF(resultCapability.resolve(),
IsCallable(resultCapability.resolve()));
MOZ_ASSERT_IF(resultCapability.reject(),
IsCallable(resultCapability.reject()));
} else {
// If `resultCapability.promise` is a non-Promise object
// (including wrapped Promise object),
// `resultCapability.{resolve,reject}` should be callable.
MOZ_ASSERT(resultCapability.resolve());
MOZ_ASSERT(IsCallable(resultCapability.resolve()));
MOZ_ASSERT(resultCapability.reject());
MOZ_ASSERT(IsCallable(resultCapability.reject()));
}
} else {
// For debugger usage, `resultCapability.promise` should be a
// maybe-wrapped Promise object. The other fields are not used.
//
// This is the only case where we allow `resolve` and `reject` to
// be null when the `promise` field is not a PromiseObject.
JSObject* unwrappedPromise = UncheckedUnwrap(resultCapability.promise());
MOZ_ASSERT(unwrappedPromise->is<PromiseObject>());
MOZ_ASSERT(!resultCapability.resolve());
MOZ_ASSERT(!resultCapability.reject());
}
} else {
// `resultCapability.promise` is null for the following cases:
// * resulting Promise is known to be unused
// * Async Generator
// In any case, other fields are also not used.
MOZ_ASSERT(!resultCapability.resolve());
MOZ_ASSERT(!resultCapability.reject());
MOZ_ASSERT(incumbentGlobalObjectOption == IncumbentGlobalObject::Yes);
}
#endif
@ -4018,6 +4053,9 @@ AddDummyPromiseReactionForDebugger(JSContext* cx, Handle<PromiseObject*> promise
if (promise->state() != JS::PromiseState::Pending)
return true;
// `dependentPromise` should be a maybe-wrapped Promise.
MOZ_ASSERT(UncheckedUnwrap(dependentPromise)->is<PromiseObject>());
// Leave resolve and reject as null.
Rooted<PromiseCapability> capability(cx);
capability.promise().set(dependentPromise);

View File

@ -28,7 +28,7 @@ SparseBitmap::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf)
SparseBitmap::BitBlock&
SparseBitmap::createBlock(Data::AddPtr p, size_t blockId, AutoEnterOOMUnsafeRegion& oomUnsafe)
{
MOZ_ASSERT(!p && p.isValid());
MOZ_ASSERT(!p);
BitBlock* block = js_new<BitBlock>();
if (!block || !data.add(p, blockId, block))
oomUnsafe.crash("Bitmap OOM");

View File

@ -0,0 +1,13 @@
var P = newGlobal().eval(`
(class extends Promise {
static resolve(o) {
return o;
}
});
`);
var alwaysPending = new Promise(() => {});
function neverCalled() {
assertEq(true, false);
}
P.race([alwaysPending]).then(neverCalled, neverCalled);

View File

@ -0,0 +1,15 @@
var lfLogBuffer = `
function testOuterForInVar() {
return eval("for (var x in {}); (function() { return delete x; })");
}
testOuterForInVar();
`;
loadFile(lfLogBuffer);
loadFile(lfLogBuffer);
function loadFile(lfVarx) {
try {
oomTest(function() {
eval(lfVarx);
});
} catch (lfVare) {}
}

View File

@ -448,8 +448,34 @@ BEGIN_TEST(testHashLazyStorage)
set.compact();
CHECK(set.capacity() == 0);
// lookupForAdd() instantiates, even if not followed by add().
set.lookupForAdd(1);
auto p = set.lookupForAdd(1);
CHECK(set.capacity() == 0);
CHECK(set.add(p, 1));
CHECK(set.capacity() == minCap);
CHECK(set.has(1));
set.clear();
set.compact();
CHECK(set.capacity() == 0);
p = set.lookupForAdd(1);
CHECK(set.putNew(2));
CHECK(set.capacity() == minCap);
CHECK(set.relookupOrAdd(p, 1, 1));
CHECK(set.capacity() == minCap);
CHECK(set.has(1));
set.clear();
set.compact();
CHECK(set.capacity() == 0);
CHECK(set.putNew(1));
p = set.lookupForAdd(1);
set.clear();
set.compact();
CHECK(set.count() == 0);
CHECK(set.relookupOrAdd(p, 1, 1));
CHECK(set.count() == 1);
CHECK(set.capacity() == minCap);
set.clear();

View File

@ -76,6 +76,7 @@
#include "mozilla/dom/DOMRect.h"
#include "mozilla/dom/DOMStringList.h"
#include "mozilla/dom/KeyframeEffect.h"
#include "mozilla/dom/SVGPathData.h"
#include "mozilla/layers/APZCCallbackHelper.h"
#include "imgIRequest.h"
#include "nsIImageLoadingContent.h"
@ -10266,3 +10267,94 @@ nsLayoutUtils::StyleForScrollbar(nsIFrame* aScrollbarPart)
// held strongly by the element.
return style.get();
}
static float
ResolveTransformOrigin(const nsStyleCoord& aCoord,
TransformReferenceBox& aRefBox,
TransformReferenceBox::DimensionGetter aGetter)
{
float result = 0.0;
const float scale = mozilla::AppUnitsPerCSSPixel();
if (aCoord.GetUnit() == eStyleUnit_Calc) {
const nsStyleCoord::Calc *calc = aCoord.GetCalcValue();
result = NSAppUnitsToFloatPixels((aRefBox.*aGetter)(), scale) *
calc->mPercent +
NSAppUnitsToFloatPixels(calc->mLength, scale);
} else if (aCoord.GetUnit() == eStyleUnit_Percent) {
result = NSAppUnitsToFloatPixels((aRefBox.*aGetter)(), scale) *
aCoord.GetPercentValue();
} else {
MOZ_ASSERT(aCoord.GetUnit() == eStyleUnit_Coord, "unexpected unit");
result = NSAppUnitsToFloatPixels(aCoord.GetCoordValue(), scale);
}
return result;
}
/* static */ Maybe<MotionPathData>
nsLayoutUtils::ResolveMotionPath(const nsIFrame* aFrame)
{
MOZ_ASSERT(aFrame);
const nsStyleDisplay* display = aFrame->StyleDisplay();
if (!display->mMotion || !display->mMotion->HasPath()) {
return Nothing();
}
const UniquePtr<StyleMotion>& motion = display->mMotion;
// Bug 1429299 - Implement offset-distance for motion path. For now, we use
// the default value, i.e. 0%.
float distance = 0.0;
float angle = 0.0;
Point point;
if (motion->OffsetPath().GetType() == StyleShapeSourceType::Path) {
// Build the path and compute the point and angle for creating the
// equivalent translate and rotate.
// Here we only need to build a valid path for motion path, so
// using the default values of stroke-width, stoke-linecap, and fill-rule
// is fine for now because what we want is get the point and its normal
// vector along the path, instead of rendering it.
// FIXME: Bug 1484780, we should cache the path to avoid rebuilding it here
// at every restyle. (Caching the path avoids the cost of flattening it
// again each time.)
RefPtr<DrawTarget> drawTarget =
gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget();
RefPtr<PathBuilder> builder =
drawTarget->CreatePathBuilder(FillRule::FILL_WINDING);
RefPtr<gfx::Path> gfxPath =
SVGPathData::BuildPath(motion->OffsetPath().GetPath()->Path(),
builder,
NS_STYLE_STROKE_LINECAP_BUTT,
0.0);
if (!gfxPath) {
return Nothing();
}
float pathLength = gfxPath->ComputeLength();
float computedDistance = distance * pathLength;
Point tangent;
point = gfxPath->ComputePointAtLength(computedDistance, &tangent);
// Bug 1429301 - Implement offset-rotate for motion path.
// After implement offset-rotate, |angle| will be adjusted more.
// For now, the default value of offset-rotate is "auto", so we use the
// directional tangent vector.
angle = atan2(tangent.y, tangent.x);
} else {
// Bug 1480665: Implement ray() function.
NS_WARNING("Unsupported offset-path value");
}
// Compute the offset for motion path translate.
// We need to resolve transform-origin here to calculate the correct path
// translate. (i.e. Center transform-origin on the path.)
TransformReferenceBox refBox(aFrame);
Point origin(
ResolveTransformOrigin(display->mTransformOrigin[0],
refBox,
&TransformReferenceBox::Width),
ResolveTransformOrigin(display->mTransformOrigin[1],
refBox,
&TransformReferenceBox::Height)
);
// Bug 1186329: the translate parameters will be adjusted more after we
// implement offset-position and offset-anchor.
return Some(MotionPathData { point - origin, angle });
}

View File

@ -119,6 +119,11 @@ struct DisplayPortMarginsPropertyData {
uint32_t mPriority;
};
struct MotionPathData {
gfx::Point mTranslate;
float mRotate;
};
} // namespace mozilla
// For GetDisplayPort
@ -3113,6 +3118,12 @@ public:
*/
static ComputedStyle* StyleForScrollbar(nsIFrame* aScrollbarPart);
/**
* Generate the motion path transform result.
**/
static mozilla::Maybe<mozilla::MotionPathData>
ResolveMotionPath(const nsIFrame* aFrame);
private:
static uint32_t sFontSizeInflationEmPerLine;
static uint32_t sFontSizeInflationMinTwips;

View File

@ -2409,6 +2409,10 @@ nsFloatManager::FloatInfo::FloatInfo(nsIFrame* aFrame,
MOZ_ASSERT_UNREACHABLE("shape-outside doesn't have URL source type!");
return;
case StyleShapeSourceType::Path:
MOZ_ASSERT_UNREACHABLE("shape-outside doesn't have Path source type!");
return;
case StyleShapeSourceType::Image: {
float shapeImageThreshold = styleDisplay->mShapeImageThreshold;
mShapeInfo = ShapeInfo::CreateImageShape(shapeOutside.GetShapeImage(),

View File

@ -249,22 +249,30 @@ static void
IncrementScaleRestyleCountIfNeeded(nsIFrame* aFrame, LayerActivity* aActivity)
{
const nsStyleDisplay* display = aFrame->StyleDisplay();
RefPtr<nsCSSValueSharedList> transformList = display->GetCombinedTransform();
if (!transformList) {
if (!display->mSpecifiedTransform &&
!display->HasIndividualTransform() &&
!(display->mMotion && display->mMotion->HasPath())) {
// The transform was removed.
aActivity->mPreviousTransformScale = Nothing();
IncrementMutationCount(&aActivity->mRestyleCounts[LayerActivity::ACTIVITY_SCALE]);
IncrementMutationCount(
&aActivity->mRestyleCounts[LayerActivity::ACTIVITY_SCALE]);
return;
}
// Compute the new scale due to the CSS transform property.
bool dummyBool;
nsStyleTransformMatrix::TransformReferenceBox refBox(aFrame);
Matrix4x4 transform =
nsStyleTransformMatrix::ReadTransforms(transformList->mHead,
refBox,
AppUnitsPerCSSPixel(),
&dummyBool);
Matrix4x4 transform = nsStyleTransformMatrix::ReadTransforms(
display->mIndividualTransform
? display->mIndividualTransform->mHead
: nullptr,
nsLayoutUtils::ResolveMotionPath(aFrame),
display->mSpecifiedTransform
? display->mSpecifiedTransform->mHead
: nullptr,
refBox,
AppUnitsPerCSSPixel(),
&dummyBool);
Matrix transform2D;
if (!transform.Is2D(&transform2D)) {
// We don't attempt to handle 3D transforms; just assume the scale changed.

View File

@ -7849,11 +7849,14 @@ nsDisplayTransform::ComputePerspectiveMatrix(const nsIFrame* aFrame,
return true;
}
nsDisplayTransform::FrameTransformProperties::FrameTransformProperties(const nsIFrame* aFrame,
float aAppUnitsPerPixel,
const nsRect* aBoundsOverride)
nsDisplayTransform::FrameTransformProperties::FrameTransformProperties(
const nsIFrame* aFrame,
float aAppUnitsPerPixel,
const nsRect* aBoundsOverride)
: mFrame(aFrame)
, mTransformList(aFrame->StyleDisplay()->GetCombinedTransform())
, mIndividualTransformList(aFrame->StyleDisplay()->mIndividualTransform)
, mMotion(nsLayoutUtils::ResolveMotionPath(aFrame))
, mTransformList(aFrame->StyleDisplay()->mSpecifiedTransform)
, mToTransformOrigin(GetDeltaToTransformOrigin(aFrame, aAppUnitsPerPixel, aBoundsOverride))
{
}
@ -7922,10 +7925,16 @@ nsDisplayTransform::GetResultingTransformMatrixInternal(const FrameTransformProp
frame && frame->IsSVGTransformed(&svgTransform,
&parentsChildrenOnlyTransform);
/* Transformed frames always have a transform, or are preserving 3d (and might still have perspective!) */
if (aProperties.mTransformList) {
result = nsStyleTransformMatrix::ReadTransforms(aProperties.mTransformList->mHead,
refBox, aAppUnitsPerPixel,
&dummyBool);
if (aProperties.HasTransform()) {
result = nsStyleTransformMatrix::ReadTransforms(
aProperties.mIndividualTransformList
? aProperties.mIndividualTransformList->mHead
: nullptr,
aProperties.mMotion,
aProperties.mTransformList
? aProperties.mTransformList->mHead
: nullptr,
refBox, aAppUnitsPerPixel, &dummyBool);
} else if (hasSVGTransforms) {
// Correct the translation components for zoom:
float pixelsPerCSSPx = AppUnitsPerCSSPixel() /

View File

@ -68,6 +68,7 @@ enum class nsDisplayOwnLayerFlags;
namespace mozilla {
class FrameLayerBuilder;
struct MotionPathData;
namespace layers {
class Layer;
class ImageLayer;
@ -6577,6 +6578,11 @@ public:
FrameTransformProperties(const nsIFrame* aFrame,
float aAppUnitsPerPixel,
const nsRect* aBoundsOverride);
// This constructor is used on the compositor (for animations).
// Bug 1186329, Bug 1425837, If we want to support compositor animationsf
// or individual transforms and motion path, we may need to update this.
// For now, let mIndividualTransformList and mMotion as nullptr and
// Nothing().
FrameTransformProperties(RefPtr<const nsCSSValueSharedList>&&
aTransformList,
const Point3D& aToTransformOrigin)
@ -6585,7 +6591,14 @@ public:
, mToTransformOrigin(aToTransformOrigin)
{}
bool HasTransform() const
{
return mIndividualTransformList || mTransformList || mMotion.isSome();
}
const nsIFrame* mFrame;
const RefPtr<const nsCSSValueSharedList> mIndividualTransformList;
const mozilla::Maybe<mozilla::MotionPathData> mMotion;
const RefPtr<const nsCSSValueSharedList> mTransformList;
const Point3D mToTransformOrigin;
};

View File

@ -1950,6 +1950,35 @@ Gecko_NewShapeImage(mozilla::StyleShapeSource* aShape)
aShape->SetShapeImage(MakeUnique<nsStyleImage>());
}
void
Gecko_NewStyleSVGPath(mozilla::StyleShapeSource* aShape)
{
MOZ_ASSERT(aShape);
aShape->SetPath(MakeUnique<mozilla::StyleSVGPath>());
}
void
Gecko_SetStyleMotion(UniquePtr<mozilla::StyleMotion>* aMotion,
mozilla::StyleMotion* aValue)
{
MOZ_ASSERT(aMotion);
aMotion->reset(aValue);
}
mozilla::StyleMotion*
Gecko_NewStyleMotion()
{
return new StyleMotion();
}
void
Gecko_CopyStyleMotions(mozilla::UniquePtr<mozilla::StyleMotion>* aMotion,
const mozilla::StyleMotion* aOther)
{
MOZ_ASSERT(aMotion);
*aMotion = aOther ? MakeUnique<StyleMotion>(*aOther) : nullptr;
}
void
Gecko_ResetFilters(nsStyleEffects* effects, size_t new_len)
{

View File

@ -524,6 +524,12 @@ void Gecko_NewBasicShape(mozilla::StyleShapeSource* shape,
mozilla::StyleBasicShapeType type);
void Gecko_NewShapeImage(mozilla::StyleShapeSource* shape);
void Gecko_StyleShapeSource_SetURLValue(mozilla::StyleShapeSource* shape, mozilla::css::URLValue* uri);
void Gecko_NewStyleSVGPath(mozilla::StyleShapeSource* shape);
void Gecko_SetStyleMotion(mozilla::UniquePtr<mozilla::StyleMotion>* aMotion,
mozilla::StyleMotion* aValue);
mozilla::StyleMotion* Gecko_NewStyleMotion();
void Gecko_CopyStyleMotions(mozilla::UniquePtr<mozilla::StyleMotion>* motion,
const mozilla::StyleMotion* other);
void Gecko_ResetFilters(nsStyleEffects* effects, size_t new_len);
void Gecko_CopyFiltersFrom(nsStyleEffects* aSrc, nsStyleEffects* aDest);

View File

@ -473,6 +473,7 @@ structs-types = [
"mozilla::FontWeight",
"mozilla::MallocSizeOf",
"mozilla::OriginFlags",
"mozilla::StyleMotion",
"mozilla::UniquePtr",
"mozilla::StyleDisplayMode",
"ServoRawOffsetArc",

View File

@ -94,6 +94,7 @@ SERIALIZED_PREDEFINED_TYPES = [
"NonNegativeLength",
"NonNegativeLengthOrPercentage",
"ListStyleType",
"OffsetPath",
"Opacity",
"Resize",
"url::ImageUrlOrNone",

View File

@ -5048,6 +5048,11 @@ nsComputedDOMStyle::GetShapeSource(
SetValueToStyleImage(*aShapeSource.GetShapeImage(), val);
return val.forget();
}
case StyleShapeSourceType::Path: {
// Bug 1246764: we have to support this for clip-path. For now, no one
// uses this.
MOZ_ASSERT_UNREACHABLE("Unexpected SVG Path type.");
}
}
return nullptr;
}

View File

@ -198,6 +198,7 @@ enum class StyleShapeSourceType : uint8_t {
Image, // shape-outside only
Shape,
Box,
Path, // SVG path function
};
// -moz-stack-sizing

View File

@ -1039,6 +1039,9 @@ StyleShapeSource::operator==(const StyleShapeSource& aOther) const
case StyleShapeSourceType::Box:
return mReferenceBox == aOther.mReferenceBox;
case StyleShapeSourceType::Path:
return *mSVGPath == *aOther.mSVGPath;
}
MOZ_ASSERT_UNREACHABLE("Unexpected shape source type!");
@ -1090,6 +1093,15 @@ StyleShapeSource::SetBasicShape(UniquePtr<StyleBasicShape> aBasicShape,
mType = StyleShapeSourceType::Shape;
}
void
StyleShapeSource::SetPath(UniquePtr<StyleSVGPath> aPath)
{
MOZ_ASSERT(aPath);
DoDestroy();
new (&mSVGPath) UniquePtr<StyleSVGPath>(std::move(aPath));
mType = StyleShapeSourceType::Path;
}
void
StyleShapeSource::SetReferenceBox(StyleGeometryBox aReferenceBox)
{
@ -1123,6 +1135,10 @@ StyleShapeSource::DoCopy(const StyleShapeSource& aOther)
case StyleShapeSourceType::Box:
SetReferenceBox(aOther.GetReferenceBox());
break;
case StyleShapeSourceType::Path:
SetPath(MakeUnique<StyleSVGPath>(*aOther.GetPath()));
break;
}
}
@ -1137,6 +1153,9 @@ StyleShapeSource::DoDestroy()
case StyleShapeSourceType::URL:
mShapeImage.~UniquePtr<nsStyleImage>();
break;
case StyleShapeSourceType::Path:
mSVGPath.~UniquePtr<StyleSVGPath>();
break;
case StyleShapeSourceType::None:
case StyleShapeSourceType::Box:
// Not a union type, so do nothing.
@ -3617,7 +3636,10 @@ nsStyleDisplay::nsStyleDisplay(const nsStyleDisplay& aSource)
, mSpecifiedRotate(aSource.mSpecifiedRotate)
, mSpecifiedTranslate(aSource.mSpecifiedTranslate)
, mSpecifiedScale(aSource.mSpecifiedScale)
, mCombinedTransform(aSource.mCombinedTransform)
, mIndividualTransform(aSource.mIndividualTransform)
, mMotion(aSource.mMotion
? MakeUnique<StyleMotion>(*aSource.mMotion)
: nullptr)
, mTransformOrigin{ aSource.mTransformOrigin[0],
aSource.mTransformOrigin[1],
aSource.mTransformOrigin[2] }
@ -3682,9 +3704,8 @@ nsStyleDisplay::~nsStyleDisplay()
mSpecifiedTranslate);
ReleaseSharedListOnMainThread("nsStyleDisplay::mSpecifiedScale",
mSpecifiedScale);
ReleaseSharedListOnMainThread("nsStyleDisplay::mCombinedTransform",
mCombinedTransform);
ReleaseSharedListOnMainThread("nsStyleDisplay::mIndividualTransform",
mIndividualTransform);
MOZ_COUNT_DTOR(nsStyleDisplay);
}
@ -3712,7 +3733,7 @@ nsStyleDisplay::FinishStyle(
}
}
GenerateCombinedTransform();
GenerateCombinedIndividualTransform();
}
static inline nsChangeHint
@ -3735,6 +3756,29 @@ CompareTransformValues(const RefPtr<nsCSSValueSharedList>& aList,
return result;
}
static inline nsChangeHint
CompareMotionValues(const StyleMotion* aMotion,
const StyleMotion* aNewMotion)
{
nsChangeHint result = nsChangeHint(0);
// TODO: Bug 1482737: This probably doesn't need to UpdateOverflow
// (or UpdateTransformLayer) if there's already a transform.
if (!aMotion != !aNewMotion ||
(aMotion && *aMotion != *aNewMotion)) {
// Set the same hints as what we use for transform because motion path is
// a kind of transform and will be combined with other transforms.
result |= nsChangeHint_UpdateTransformLayer;
if ((aMotion && aMotion->HasPath()) &&
(aNewMotion && aNewMotion->HasPath())) {
result |= nsChangeHint_UpdatePostTransformOverflow;
} else {
result |= nsChangeHint_UpdateOverflow;
}
}
return result;
}
nsChangeHint
nsStyleDisplay::CalcDifference(const nsStyleDisplay& aNewData) const
{
@ -3866,6 +3910,7 @@ nsStyleDisplay::CalcDifference(const nsStyleDisplay& aNewData) const
aNewData.mSpecifiedTranslate);
transformHint |= CompareTransformValues(mSpecifiedScale,
aNewData.mSpecifiedScale);
transformHint |= CompareMotionValues(mMotion.get(), aNewData.mMotion.get());
const nsChangeHint kUpdateOverflowAndRepaintHint =
nsChangeHint_UpdateOverflow | nsChangeHint_RepaintFrame;
@ -3985,17 +4030,17 @@ nsStyleDisplay::CalcDifference(const nsStyleDisplay& aNewData) const
}
void
nsStyleDisplay::GenerateCombinedTransform()
nsStyleDisplay::GenerateCombinedIndividualTransform()
{
// FIXME(emilio): This should probably be called from somewhere like what we
// do for image layers, instead of FinishStyle.
//
// This does and undoes the work a ton of times in Stylo.
mCombinedTransform = nullptr;
mIndividualTransform = nullptr;
// Follow the order defined in the spec to append transform functions.
// https://drafts.csswg.org/css-transforms-2/#ctm
AutoTArray<nsCSSValueSharedList*, 4> shareLists;
AutoTArray<nsCSSValueSharedList*, 3> shareLists;
if (mSpecifiedTranslate) {
shareLists.AppendElement(mSpecifiedTranslate.get());
}
@ -4005,24 +4050,20 @@ nsStyleDisplay::GenerateCombinedTransform()
if (mSpecifiedScale) {
shareLists.AppendElement(mSpecifiedScale.get());
}
if (mSpecifiedTransform) {
shareLists.AppendElement(mSpecifiedTransform.get());
}
if (shareLists.Length() == 0) {
return;
}
if (shareLists.Length() == 1) {
mCombinedTransform = shareLists[0];
mIndividualTransform = shareLists[0];
return;
}
// In common, we may have 3 transform functions(for rotate, translate and
// scale) in mSpecifiedTransform, one rotate function in mSpecifiedRotate,
// one translate function in mSpecifiedTranslate, and one scale function in
// mSpecifiedScale. So 6 slots are enough for the most cases.
AutoTArray<nsCSSValueList*, 6> valueLists;
// In common, we may have 3 transform functions:
// 1. one rotate function in mSpecifiedRotate,
// 2. one translate function in mSpecifiedTranslate,
// 3. one scale function in mSpecifiedScale.
AutoTArray<nsCSSValueList*, 3> valueLists;
for (auto list: shareLists) {
if (list) {
valueLists.AppendElement(list->mHead->Clone());
@ -4037,8 +4078,9 @@ nsStyleDisplay::GenerateCombinedTransform()
valueLists[i]->mNext = valueLists[i + 1];
}
mCombinedTransform = new nsCSSValueSharedList(valueLists[0]);
mIndividualTransform = new nsCSSValueSharedList(valueLists[0]);
}
// --------------------
// nsStyleVisibility
//

View File

@ -1971,6 +1971,27 @@ private:
nsStyleCorners mRadius;
};
struct StyleSVGPath final
{
const nsTArray<StylePathCommand>& Path() const
{
return mPath;
}
bool operator==(const StyleSVGPath& aOther) const
{
return mPath == aOther.mPath;
}
bool operator!=(const StyleSVGPath& aOther) const
{
return !(*this == aOther);
}
private:
nsTArray<StylePathCommand> mPath;
};
struct StyleShapeSource final
{
StyleShapeSource();
@ -2035,6 +2056,13 @@ struct StyleShapeSource final
void SetReferenceBox(StyleGeometryBox aReferenceBox);
const StyleSVGPath* GetPath() const
{
MOZ_ASSERT(mType == StyleShapeSourceType::Path, "Wrong shape source type!");
return mSVGPath.get();
}
void SetPath(UniquePtr<StyleSVGPath> aPath);
private:
void* operator new(size_t) = delete;
@ -2044,13 +2072,41 @@ private:
union {
mozilla::UniquePtr<StyleBasicShape> mBasicShape;
mozilla::UniquePtr<nsStyleImage> mShapeImage;
// TODO: Bug 1429298, implement SVG Path function.
mozilla::UniquePtr<StyleSVGPath> mSVGPath;
// TODO: Bug 1480665, implement ray() function.
};
StyleShapeSourceType mType = StyleShapeSourceType::None;
StyleGeometryBox mReferenceBox = StyleGeometryBox::NoBox;
};
struct StyleMotion final
{
bool operator==(const StyleMotion& aOther) const
{
return mOffsetPath == aOther.mOffsetPath;
}
bool operator!=(const StyleMotion& aOther) const
{
return !(*this == aOther);
}
const StyleShapeSource& OffsetPath() const
{
return mOffsetPath;
}
bool HasPath() const
{
// Bug 1186329: We have to check other acceptable types after supporting
// different values of offset-path. e.g. basic-shapes, ray.
return mOffsetPath.GetType() == StyleShapeSourceType::Path;
}
private:
StyleShapeSource mOffsetPath;
};
} // namespace mozilla
struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleDisplay
@ -2125,12 +2181,10 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleDisplay
RefPtr<nsCSSValueSharedList> mSpecifiedRotate;
RefPtr<nsCSSValueSharedList> mSpecifiedTranslate;
RefPtr<nsCSSValueSharedList> mSpecifiedScale;
// Used to store the final combination of mSpecifiedTranslate,
// mSpecifiedRotate, mSpecifiedScale and mSpecifiedTransform.
// Use GetCombinedTransform() to get the final transform, instead of
// accessing mCombinedTransform directly.
RefPtr<nsCSSValueSharedList> mCombinedTransform;
// Used to store the final combination of mSpecifiedRotate,
// mSpecifiedTranslate, and mSpecifiedScale.
RefPtr<nsCSSValueSharedList> mIndividualTransform;
mozilla::UniquePtr<mozilla::StyleMotion> mMotion;
nsStyleCoord mTransformOrigin[3]; // percent, coord, calc, 3rd param is coord, calc only
nsStyleCoord mChildPerspective; // none, coord
@ -2380,7 +2434,8 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleDisplay
return mSpecifiedTransform || mSpecifiedRotate || mSpecifiedTranslate ||
mSpecifiedScale ||
mTransformStyle == NS_STYLE_TRANSFORM_STYLE_PRESERVE_3D ||
(mWillChangeBitField & NS_STYLE_WILL_CHANGE_TRANSFORM);
(mWillChangeBitField & NS_STYLE_WILL_CHANGE_TRANSFORM) ||
(mMotion && mMotion->HasPath());
}
bool HasIndividualTransform() const {
@ -2470,21 +2525,17 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleDisplay
inline bool IsFixedPosContainingBlockForTransformSupportingFrames() const;
/**
* Returns the final combined transform.
* Returns the final combined individual transform.
**/
already_AddRefed<nsCSSValueSharedList> GetCombinedTransform() const {
if (mCombinedTransform) {
return do_AddRef(mCombinedTransform);
}
// backward compatible to gecko-backed style system.
return mSpecifiedTransform ? do_AddRef(mSpecifiedTransform) : nullptr;
already_AddRefed<nsCSSValueSharedList> GetCombinedTransform() const
{
return mIndividualTransform ? do_AddRef(mIndividualTransform) : nullptr;
}
private:
// Helpers for above functions, which do some but not all of the tests
// for them (since transform must be tested separately for each).
void GenerateCombinedTransform();
void GenerateCombinedIndividualTransform();
};
struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleTable

View File

@ -926,14 +926,12 @@ SetIdentityMatrix(nsCSSValue::Array* aMatrix)
}
}
Matrix4x4
ReadTransforms(const nsCSSValueList* aList,
TransformReferenceBox& aRefBox,
float aAppUnitsPerMatrixUnit,
bool* aContains3dTransform)
static void
ReadTransformsImpl(Matrix4x4& aMatrix,
const nsCSSValueList* aList,
TransformReferenceBox& aRefBox,
bool* aContains3dTransform)
{
Matrix4x4 result;
for (const nsCSSValueList* curr = aList; curr != nullptr; curr = curr->mNext) {
const nsCSSValue &currElem = curr->mValue;
if (currElem.GetUnit() != eCSSUnit_Function) {
@ -947,9 +945,55 @@ ReadTransforms(const nsCSSValueList* aList,
"Incoming function is too short!");
/* Read in a single transform matrix. */
MatrixForTransformFunction(result, currElem.GetArrayValue(), aRefBox,
MatrixForTransformFunction(aMatrix, currElem.GetArrayValue(), aRefBox,
aContains3dTransform);
}
}
Matrix4x4
ReadTransforms(const nsCSSValueList* aList,
TransformReferenceBox& aRefBox,
float aAppUnitsPerMatrixUnit,
bool* aContains3dTransform)
{
Matrix4x4 result;
ReadTransformsImpl(result, aList, aRefBox, aContains3dTransform);
float scale = float(AppUnitsPerCSSPixel()) / aAppUnitsPerMatrixUnit;
result.PreScale(1/scale, 1/scale, 1/scale);
result.PostScale(scale, scale, scale);
return result;
}
Matrix4x4
ReadTransforms(const nsCSSValueList* aIndividualTransforms,
const Maybe<MotionPathData>& aMotion,
const nsCSSValueList* aTransform,
TransformReferenceBox& aRefBox,
float aAppUnitsPerMatrixUnit,
bool* aContains3dTransform)
{
Matrix4x4 result;
if (aIndividualTransforms) {
ReadTransformsImpl(result, aIndividualTransforms, aRefBox,
aContains3dTransform);
}
if (aMotion.isSome()) {
// Create the equivalent translate and rotate function, according to the
// order in spec. We combine the translate and then the rotate.
// https://drafts.fxtf.org/motion-1/#calculating-path-transform
result.PreTranslate(aMotion->mTranslate.x, aMotion->mTranslate.y, 0.0);
if (aMotion->mRotate != 0.0) {
result.RotateZ(aMotion->mRotate);
}
}
if (aTransform) {
ReadTransformsImpl(result, aTransform, aRefBox, aContains3dTransform);
}
float scale = float(AppUnitsPerCSSPixel()) / aAppUnitsPerMatrixUnit;
result.PreScale(1/scale, 1/scale, 1/scale);

View File

@ -24,6 +24,10 @@ class nsPresContext;
struct gfxQuaternion;
struct nsRect;
namespace mozilla {
struct MotionPathData;
}
/**
* A helper to generate gfxMatrixes from css transform functions.
*/
@ -200,6 +204,16 @@ namespace nsStyleTransformMatrix {
float aAppUnitsPerMatrixUnit,
bool* aContains3dTransform);
// Generate the gfx::Matrix for CSS Transform Module Level 2.
// https://drafts.csswg.org/css-transforms-2/#ctm
mozilla::gfx::Matrix4x4
ReadTransforms(const nsCSSValueList* aIndividualTransforms,
const mozilla::Maybe<mozilla::MotionPathData>& aMotion,
const nsCSSValueList* aTransform,
TransformReferenceBox& aRefBox,
float aAppUnitsPerMatrixUnit,
bool* aContains3dTransform);
/**
* Given two nsStyleCoord values, compute the 2d position with respect to the
* given TransformReferenceBox that these values describe, in device pixels.

View File

@ -8173,6 +8173,28 @@ if (IsCSSPropertyPrefEnabled("layout.css.scrollbar-width.enabled")) {
};
}
if (IsCSSPropertyPrefEnabled("layout.css.motion-path.enabled")) {
gCSSProperties["offset-path"] = {
domProp: "offsetPath",
inherited: false,
type: CSS_TYPE_LONGHAND,
initial_values: [ "none" ],
other_values: [
"path('M 10 10 20 20 H 90 V 90 Z')",
"path('M10 10 20,20H90V90Z')",
"path('M 10 10 C 20 20, 40 20, 50 10')",
"path('M 10 80 C 40 10, 65 10, 95 80 S 1.5e2 150, 180 80')",
"path('M 10 80 Q 95 10 180 80')",
"path('M 10 80 Q 52.5 10, 95 80 T 180 80')",
"path('M 80 80 A 45 45, 0, 0, 0, 1.25e2 1.25e2 L 125 80 Z')",
"path('M100-200h20z')",
"path('M10,10L20.6.5z')"
],
invalid_values: [ "path('')", "path()", "path(a)", "path('M 10 Z')" ,
"path('M 10-10 20')", "path('M 10 10 C 20 20 40 20')" ]
};
}
const OVERFLOW_MOZKWS = [
"-moz-scrollbars-none",
"-moz-scrollbars-horizontal",

View File

@ -1209,6 +1209,18 @@ public:
{
}
// This constructor is used only by AddPtr() within lookupForAdd().
explicit Ptr(const HashTable& aTable)
: mEntry(nullptr)
#ifdef DEBUG
, mTable(&aTable)
, mGeneration(aTable.generation())
#endif
{
}
bool isValid() const { return !!mEntry; }
public:
Ptr()
: mEntry(nullptr)
@ -1219,8 +1231,6 @@ public:
{
}
bool isValid() const { return !!mEntry; }
bool found() const
{
if (!isValid()) {
@ -1286,6 +1296,21 @@ public:
{
}
// This constructor is used when lookupForAdd() is performed on a table
// lacking entry storage; it leaves mEntry null but initializes everything
// else.
AddPtr(const HashTable& aTable, HashNumber aHashNumber)
: Ptr(aTable)
, mKeyHash(aHashNumber)
#ifdef DEBUG
, mMutationCount(aTable.mMutationCount)
#endif
{
MOZ_ASSERT(isLive());
}
bool isLive() const { return isLiveHash(mKeyHash); }
public:
AddPtr()
: mKeyHash(0)
@ -2030,7 +2055,7 @@ public:
return true; // Capacity is already sufficient.
}
RebuildStatus status = changeTableSize(bestCapacity, DontReportFailure);
RebuildStatus status = changeTableSize(bestCapacity, ReportFailure);
MOZ_ASSERT(status != NotOverloaded);
return status != RehashFailed;
}
@ -2107,16 +2132,12 @@ public:
return AddPtr();
}
HashNumber keyHash = prepareHash(aLookup);
if (!mTable) {
uint32_t newCapacity = rawCapacity();
RebuildStatus status = changeTableSize(newCapacity, ReportFailure);
MOZ_ASSERT(status != NotOverloaded);
if (status == RehashFailed) {
return AddPtr();
}
return AddPtr(*this, keyHash);
}
HashNumber keyHash = prepareHash(aLookup);
// Directly call the constructor in the return statement to avoid
// excess copying when building with Visual Studio 2017.
// See bug 1385181.
@ -2133,7 +2154,7 @@ public:
MOZ_ASSERT(!(aPtr.mKeyHash & sCollisionBit));
// Check for error from ensureHash() here.
if (!aPtr.isValid()) {
if (!aPtr.isLive()) {
return false;
}
@ -2142,14 +2163,25 @@ public:
MOZ_ASSERT(aPtr.mMutationCount == mMutationCount);
#endif
// Changing an entry from removed to live does not affect whether we
// are overloaded and can be handled separately.
if (aPtr.mEntry->isRemoved()) {
if (!aPtr.isValid()) {
MOZ_ASSERT(!mTable && mEntryCount == 0);
uint32_t newCapacity = rawCapacity();
RebuildStatus status = changeTableSize(newCapacity, ReportFailure);
MOZ_ASSERT(status != NotOverloaded);
if (status == RehashFailed) {
return false;
}
aPtr.mEntry = &findNonLiveEntry(aPtr.mKeyHash);
} else if (aPtr.mEntry->isRemoved()) {
// Changing an entry from removed to live does not affect whether we are
// overloaded and can be handled separately.
if (!this->checkSimulatedOOM()) {
return false;
}
mRemovedCount--;
aPtr.mKeyHash |= sCollisionBit;
} else {
// Preserve the validity of |aPtr.mEntry|.
RebuildStatus status = rehashIfOverloaded();
@ -2210,20 +2242,27 @@ public:
Args&&... aArgs)
{
// Check for error from ensureHash() here.
if (!aPtr.isValid()) {
if (!aPtr.isLive()) {
return false;
}
#ifdef DEBUG
aPtr.mGeneration = generation();
aPtr.mMutationCount = mMutationCount;
#endif
{
if (mTable) {
ReentrancyGuard g(*this);
// Check that aLookup has not been destroyed.
MOZ_ASSERT(prepareHash(aLookup) == aPtr.mKeyHash);
aPtr.mEntry = &lookup<ForAdd>(aLookup, aPtr.mKeyHash);
if (aPtr.found()) {
return true;
}
} else {
// Clear aPtr so it's invalid; add() will allocate storage and redo the
// lookup.
aPtr.mEntry = nullptr;
}
return aPtr.found() || add(aPtr, std::forward<Args>(aArgs)...);
return add(aPtr, std::forward<Args>(aArgs)...);
}
void remove(Ptr aPtr)

View File

@ -3073,6 +3073,13 @@ pref("layout.css.ruby.intercharacter.enabled", false);
// Is support for overscroll-behavior enabled?
pref("layout.css.overscroll-behavior.enabled", true);
// Is support for motion-path enabled?
#ifdef RELEASE_OR_BETA
pref("layout.css.motion-path.enabled", false);
#else
pref("layout.css.motion-path.enabled", true);
#endif
// pref for which side vertical scrollbars should be on
// 0 = end-side in UI direction
// 1 = end-side in document/content direction

View File

@ -7,6 +7,7 @@ autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated usi
* a. Alternatively, you can clone `https://github.com/eqrion/cbindgen` and use a tagged release
* 2. Run `rustup run nightly cbindgen toolkit/library/rust/ --lockfile Cargo.lock --crate style -o layout/style/ServoStyleConsts.h`
*/"""
include_guard = "mozilla_ServoStyleConsts_h"
include_version = true
braces = "SameLine"
line_length = 80
@ -22,5 +23,5 @@ derive_helper_methods = true
[export]
prefix = "Style"
include = ["StyleDisplay", "StyleAppearance", "StyleDisplayMode"]
item_types = ["enums"]
include = ["StyleDisplay", "StyleAppearance", "StyleDisplayMode", "StylePathCommand"]
item_types = ["enums", "structs", "typedefs"]

View File

@ -638,6 +638,7 @@ pub mod basic_shape {
use values::computed::basic_shape::{BasicShape, ClippingShape, FloatAreaShape, ShapeRadius};
use values::computed::border::{BorderCornerRadius, BorderRadius};
use values::computed::length::LengthOrPercentage;
use values::computed::motion::OffsetPath;
use values::computed::position;
use values::computed::url::ComputedUrl;
use values::generics::basic_shape::{BasicShape as GenericBasicShape, InsetRect, Polygon};
@ -669,6 +670,7 @@ pub mod basic_shape {
Some(ShapeSource::Shape(shape, reference_box))
},
StyleShapeSourceType::URL | StyleShapeSourceType::Image => None,
StyleShapeSourceType::Path => None,
}
}
}
@ -710,6 +712,29 @@ pub mod basic_shape {
}
}
impl<'a> From<&'a StyleShapeSource> for OffsetPath {
fn from(other: &'a StyleShapeSource) -> Self {
use gecko_bindings::structs::StylePathCommand;
use values::specified::motion::{SVGPathData, PathCommand};
match other.mType {
StyleShapeSourceType::Path => {
let gecko_path = unsafe { &*other.__bindgen_anon_1.mSVGPath.as_ref().mPtr };
let result: Vec<PathCommand> =
gecko_path.mPath.iter().map(|gecko: &StylePathCommand| {
// unsafe: cbindgen ensures the representation is the same.
unsafe{ ::std::mem::transmute(*gecko) }
}).collect();
OffsetPath::Path(SVGPathData::new(result.into_boxed_slice()))
},
StyleShapeSourceType::None => OffsetPath::none(),
StyleShapeSourceType::Shape |
StyleShapeSourceType::Box |
StyleShapeSourceType::URL |
StyleShapeSourceType::Image => unreachable!("Unsupported offset-path type"),
}
}
}
impl<'a> From<&'a StyleBasicShape> for BasicShape {
fn from(other: &'a StyleBasicShape) -> Self {
match other.mType {

View File

@ -3053,7 +3053,7 @@ fn static_assert() {
scroll-snap-points-x scroll-snap-points-y
scroll-snap-type-x scroll-snap-type-y scroll-snap-coordinate
perspective-origin -moz-binding will-change
overscroll-behavior-x overscroll-behavior-y
offset-path overscroll-behavior-x overscroll-behavior-y
overflow-clip-box-inline overflow-clip-box-block
perspective-origin -moz-binding will-change
shape-outside contain touch-action translate
@ -3681,6 +3681,51 @@ fn static_assert() {
${impl_simple_copy("contain", "mContain")}
${impl_simple_type_with_conversion("touch_action")}
pub fn set_offset_path(&mut self, v: longhands::offset_path::computed_value::T) {
use gecko_bindings::bindings::{Gecko_NewStyleMotion, Gecko_NewStyleSVGPath};
use gecko_bindings::bindings::Gecko_SetStyleMotion;
use gecko_bindings::structs::StyleShapeSourceType;
use values::specified::OffsetPath;
let motion = unsafe { Gecko_NewStyleMotion().as_mut().unwrap() };
match v {
OffsetPath::None => motion.mOffsetPath.mType = StyleShapeSourceType::None,
OffsetPath::Path(servo_path) => {
motion.mOffsetPath.mType = StyleShapeSourceType::Path;
let gecko_path = unsafe {
let ref mut source = motion.mOffsetPath;
Gecko_NewStyleSVGPath(source);
&mut source.__bindgen_anon_1.mSVGPath.as_mut().mPtr.as_mut().unwrap().mPath
};
unsafe { gecko_path.set_len(servo_path.commands().len() as u32) };
debug_assert_eq!(gecko_path.len(), servo_path.commands().len());
for (servo, gecko) in servo_path.commands().iter().zip(gecko_path.iter_mut()) {
// unsafe: cbindgen ensures the representation is the same.
*gecko = unsafe { transmute(*servo) };
}
},
}
unsafe { Gecko_SetStyleMotion(&mut self.gecko.mMotion, motion) };
}
pub fn clone_offset_path(&self) -> longhands::offset_path::computed_value::T {
use values::specified::OffsetPath;
match unsafe { self.gecko.mMotion.mPtr.as_ref() } {
None => OffsetPath::none(),
Some(v) => (&v.mOffsetPath).into()
}
}
pub fn copy_offset_path_from(&mut self, other: &Self) {
use gecko_bindings::bindings::Gecko_CopyStyleMotions;
unsafe { Gecko_CopyStyleMotions(&mut self.gecko.mMotion, other.gecko.mMotion.mPtr) };
}
pub fn reset_offset_path(&mut self, other: &Self) {
self.copy_offset_path_from(other);
}
</%self:impl_trait>
<%def name="simple_image_array_property(name, shorthand, field_name)">

View File

@ -356,6 +356,17 @@ ${helpers.predefined_type(
servo_restyle_damage="reflow_out_of_flow"
)}
// Motion Path Module Level 1
${helpers.predefined_type(
"offset-path",
"OffsetPath",
"computed::OffsetPath::none()",
animation_value_type="none",
gecko_pref="layout.css.motion-path.enabled",
flags="CREATES_STACKING_CONTEXT FIXPOS_CB",
spec="https://drafts.fxtf.org/motion-1/#offset-path-property"
)}
// CSSOM View Module
// https://www.w3.org/TR/cssom-view-1/
${helpers.single_keyword("scroll-behavior",

View File

@ -65,6 +65,7 @@ pub use self::length::{NonNegativeLengthOrPercentage, NonNegativeLengthOrPercent
pub use self::list::Quotes;
#[cfg(feature = "gecko")]
pub use self::list::ListStyleType;
pub use self::motion::OffsetPath;
pub use self::outline::OutlineStyle;
pub use self::percentage::{Percentage, NonNegativePercentage};
pub use self::position::{GridAutoFlow, GridTemplateAreas, Position, ZIndex};
@ -100,6 +101,7 @@ pub mod gecko;
pub mod image;
pub mod length;
pub mod list;
pub mod motion;
pub mod outline;
pub mod percentage;
pub mod position;

View File

@ -0,0 +1,10 @@
/* 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/. */
//! Computed types for CSS values that are related to motion path.
/// A computed offset-path. The computed value is as specified value.
///
/// https://drafts.fxtf.org/motion-1/#offset-path-property
pub use values::specified::motion::OffsetPath as OffsetPath;

View File

@ -58,6 +58,7 @@ pub use self::length::{NonNegativeLengthOrPercentage, NonNegativeLengthOrPercent
pub use self::list::Quotes;
#[cfg(feature = "gecko")]
pub use self::list::ListStyleType;
pub use self::motion::OffsetPath;
pub use self::outline::OutlineStyle;
pub use self::rect::LengthOrNumberRect;
pub use self::resolution::Resolution;
@ -101,6 +102,7 @@ pub mod image;
pub mod length;
pub mod list;
pub mod outline;
pub mod motion;
pub mod percentage;
pub mod position;
pub mod rect;

View File

@ -0,0 +1,558 @@
/* 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/. */
//! Specified types for CSS values that are related to motion path.
use cssparser::Parser;
use parser::{Parse, ParserContext};
use std::fmt::{self, Write};
use std::iter::Peekable;
use std::str::Chars;
use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
use style_traits::values::SequenceWriter;
use values::CSSFloat;
/// The offset-path value.
///
/// https://drafts.fxtf.org/motion-1/#offset-path-property
#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss)]
pub enum OffsetPath {
// We could merge SVGPathData into ShapeSource, so we could reuse them. However,
// we don't want to support other value for offset-path, so use SVGPathData only for now.
/// Path value for path(<string>).
#[css(function)]
Path(SVGPathData),
/// None value.
None,
// Bug 1186329: Implement ray(), <basic-shape>, <geometry-box>, and <url>.
}
impl OffsetPath {
/// Return None.
#[inline]
pub fn none() -> Self {
OffsetPath::None
}
}
impl Parse for OffsetPath {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>
) -> Result<Self, ParseError<'i>> {
// Parse none.
if input.try(|i| i.expect_ident_matching("none")).is_ok() {
return Ok(OffsetPath::none());
}
// Parse possible functions.
let location = input.current_source_location();
let function = input.expect_function()?.clone();
input.parse_nested_block(move |i| {
match_ignore_ascii_case! { &function,
// Bug 1186329: Implement the parser for ray(), <basic-shape>, <geometry-box>,
// and <url>.
"path" => SVGPathData::parse(context, i).map(OffsetPath::Path),
_ => {
Err(location.new_custom_error(
StyleParseErrorKind::UnexpectedFunction(function.clone())
))
},
}
})
}
}
/// SVG Path parser.
struct PathParser<'a> {
chars: Peekable<Chars<'a>>,
path: Vec<PathCommand>,
}
macro_rules! parse_arguments {
(
$parser:ident,
$abs:ident,
$enum:ident,
[ $para:ident => $func:ident $(, $other_para:ident => $other_func:ident)* ]
) => {
{
loop {
let $para = $func(&mut $parser.chars)?;
$(
skip_comma_wsp(&mut $parser.chars);
let $other_para = $other_func(&mut $parser.chars)?;
)*
$parser.path.push(PathCommand::$enum { $para $(, $other_para)*, $abs });
// End of string or the next character is a possible new command.
if !skip_wsp(&mut $parser.chars) ||
$parser.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) {
break;
}
skip_comma_wsp(&mut $parser.chars);
}
Ok(())
}
}
}
impl<'a> PathParser<'a> {
/// Parse a sub-path.
fn parse_subpath(&mut self) -> Result<(), ()> {
// Handle "moveto" Command first. If there is no "moveto", this is not a valid sub-path
// (i.e. not a valid moveto-drawto-command-group).
self.parse_moveto()?;
// Handle other commands.
loop {
skip_wsp(&mut self.chars);
if self.chars.peek().map_or(true, |m| *m == 'M' || *m == 'm') {
break;
}
match self.chars.next() {
Some(command) => {
let abs = command.is_uppercase();
macro_rules! parse_command {
( $($($p:pat)|+ => $parse_func:ident,)* ) => {
match command {
$(
$($p)|+ => {
skip_wsp(&mut self.chars);
self.$parse_func(abs)?;
},
)*
_ => return Err(()),
}
}
}
parse_command!(
'Z' | 'z' => parse_closepath,
'L' | 'l' => parse_lineto,
'H' | 'h' => parse_h_lineto,
'V' | 'v' => parse_v_lineto,
'C' | 'c' => parse_curveto,
'S' | 's' => parse_smooth_curveto,
'Q' | 'q' => parse_quadratic_bezier_curveto,
'T' | 't' => parse_smooth_quadratic_bezier_curveto,
'A' | 'a' => parse_elliprical_arc,
);
},
_ => break, // no more commands.
}
}
Ok(())
}
/// Parse "moveto" command.
fn parse_moveto(&mut self) -> Result<(), ()> {
let command = match self.chars.next() {
Some(c) if c == 'M' || c == 'm' => c,
_ => return Err(()),
};
skip_wsp(&mut self.chars);
let point = parse_coord(&mut self.chars)?;
let absolute = command == 'M';
self.path.push(PathCommand::MoveTo { point, absolute } );
// End of string or the next character is a possible new command.
if !skip_wsp(&mut self.chars) ||
self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) {
return Ok(());
}
skip_comma_wsp(&mut self.chars);
// If a moveto is followed by multiple pairs of coordinates, the subsequent
// pairs are treated as implicit lineto commands.
self.parse_lineto(absolute)
}
/// Parse "closepath" command.
fn parse_closepath(&mut self, _absolute: bool) -> Result<(), ()> {
self.path.push(PathCommand::ClosePath);
Ok(())
}
/// Parse "lineto" command.
fn parse_lineto(&mut self, absolute: bool) -> Result<(), ()> {
parse_arguments!(self, absolute, LineTo, [ point => parse_coord ])
}
/// Parse horizontal "lineto" command.
fn parse_h_lineto(&mut self, absolute: bool) -> Result<(), ()> {
parse_arguments!(self, absolute, HorizontalLineTo, [ x => parse_number ])
}
/// Parse vertical "lineto" command.
fn parse_v_lineto(&mut self, absolute: bool) -> Result<(), ()> {
parse_arguments!(self, absolute, VerticalLineTo, [ y => parse_number ])
}
/// Parse cubic Bézier curve command.
fn parse_curveto(&mut self, absolute: bool) -> Result<(), ()> {
parse_arguments!(self, absolute, CurveTo, [
control1 => parse_coord, control2 => parse_coord, point => parse_coord
])
}
/// Parse smooth "curveto" command.
fn parse_smooth_curveto(&mut self, absolute: bool) -> Result<(), ()> {
parse_arguments!(self, absolute, SmoothCurveTo, [
control2 => parse_coord, point => parse_coord
])
}
/// Parse quadratic Bézier curve command.
fn parse_quadratic_bezier_curveto(&mut self, absolute: bool) -> Result<(), ()> {
parse_arguments!(self, absolute, QuadBezierCurveTo, [
control1 => parse_coord, point => parse_coord
])
}
/// Parse smooth quadratic Bézier curveto command.
fn parse_smooth_quadratic_bezier_curveto(&mut self, absolute: bool) -> Result<(), ()> {
parse_arguments!(self, absolute, SmoothQuadBezierCurveTo, [ point => parse_coord ])
}
/// Parse elliptical arc curve command.
fn parse_elliprical_arc(&mut self, absolute: bool) -> Result<(), ()> {
// Parse a flag whose value is '0' or '1'; otherwise, return Err(()).
let parse_flag = |iter: &mut Peekable<Chars>| -> Result<bool, ()> {
let value = match iter.peek() {
Some(c) if *c == '0' || *c == '1' => *c == '1',
_ => return Err(()),
};
iter.next();
Ok(value)
};
parse_arguments!(self, absolute, EllipticalArc, [
rx => parse_number,
ry => parse_number,
angle => parse_number,
large_arc_flag => parse_flag,
sweep_flag => parse_flag,
point => parse_coord
])
}
}
/// The SVG path data.
///
/// https://www.w3.org/TR/SVG11/paths.html#PathData
#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue)]
pub struct SVGPathData(Box<[PathCommand]>);
impl SVGPathData {
/// Return SVGPathData by a slice of PathCommand.
#[inline]
pub fn new(cmd: Box<[PathCommand]>) -> Self {
debug_assert!(!cmd.is_empty());
SVGPathData(cmd)
}
/// Get the array of PathCommand.
#[inline]
pub fn commands(&self) -> &[PathCommand] {
debug_assert!(!self.0.is_empty());
&self.0
}
}
impl ToCss for SVGPathData {
#[inline]
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: fmt::Write
{
dest.write_char('"')?;
{
let mut writer = SequenceWriter::new(dest, " ");
for command in self.0.iter() {
writer.item(command)?;
}
}
dest.write_char('"')
}
}
impl Parse for SVGPathData {
// We cannot use cssparser::Parser to parse a SVG path string because the spec wants to make
// the SVG path string as compact as possible. (i.e. The whitespaces may be dropped.)
// e.g. "M100 200L100 200" is a valid SVG path string. If we use tokenizer, the first ident
// is "M100", instead of "M", and this is not correct. Therefore, we use a Peekable
// str::Char iterator to check each character.
fn parse<'i, 't>(
_context: &ParserContext,
input: &mut Parser<'i, 't>
) -> Result<Self, ParseError<'i>> {
let location = input.current_source_location();
let path_string = input.expect_string()?.as_ref();
if path_string.is_empty() {
// Treat an empty string as invalid, so we will not set it.
return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
// Parse the svg path string as multiple sub-paths.
let mut path_parser = PathParser {
chars: path_string.chars().peekable(),
path: Vec::new(),
};
while skip_wsp(&mut path_parser.chars) {
if path_parser.parse_subpath().is_err() {
return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
}
Ok(SVGPathData::new(path_parser.path.into_boxed_slice()))
}
}
/// The SVG path command.
/// The fields of these commands are self-explanatory, so we skip the documents.
/// Note: the index of the control points, e.g. control1, control2, are mapping to the control
/// points of the Bézier curve in the spec.
///
/// https://www.w3.org/TR/SVG11/paths.html#PathData
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo)]
#[allow(missing_docs)]
#[repr(C, u8)]
pub enum PathCommand {
/// The unknown type.
/// https://www.w3.org/TR/SVG/paths.html#__svg__SVGPathSeg__PATHSEG_UNKNOWN
Unknown,
/// The "moveto" command.
MoveTo { point: CoordPair, absolute: bool },
/// The "lineto" command.
LineTo { point: CoordPair, absolute: bool },
/// The horizontal "lineto" command.
HorizontalLineTo { x: CSSFloat, absolute: bool },
/// The vertical "lineto" command.
VerticalLineTo { y: CSSFloat, absolute: bool },
/// The cubic Bézier curve command.
CurveTo { control1: CoordPair, control2: CoordPair, point: CoordPair, absolute: bool },
/// The smooth curve command.
SmoothCurveTo { control2: CoordPair, point: CoordPair, absolute: bool },
/// The quadratic Bézier curve command.
QuadBezierCurveTo { control1: CoordPair, point: CoordPair, absolute: bool },
/// The smooth quadratic Bézier curve command.
SmoothQuadBezierCurveTo { point: CoordPair, absolute: bool },
/// The elliptical arc curve command.
EllipticalArc {
rx: CSSFloat,
ry: CSSFloat,
angle: CSSFloat,
large_arc_flag: bool,
sweep_flag: bool,
point: CoordPair,
absolute: bool
},
/// The "closepath" command.
ClosePath,
}
impl ToCss for PathCommand {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: fmt::Write
{
use self::PathCommand::*;
match *self {
Unknown => dest.write_str("X"),
ClosePath => dest.write_str("Z"),
MoveTo { point, absolute } => {
dest.write_char(if absolute { 'M' } else { 'm' })?;
dest.write_char(' ')?;
point.to_css(dest)
}
LineTo { point, absolute } => {
dest.write_char(if absolute { 'L' } else { 'l' })?;
dest.write_char(' ')?;
point.to_css(dest)
}
CurveTo { control1, control2, point, absolute } => {
dest.write_char(if absolute { 'C' } else { 'c' })?;
dest.write_char(' ')?;
control1.to_css(dest)?;
dest.write_char(' ')?;
control2.to_css(dest)?;
dest.write_char(' ')?;
point.to_css(dest)
},
QuadBezierCurveTo { control1, point, absolute } => {
dest.write_char(if absolute { 'Q' } else { 'q' })?;
dest.write_char(' ')?;
control1.to_css(dest)?;
dest.write_char(' ')?;
point.to_css(dest)
},
EllipticalArc { rx, ry, angle, large_arc_flag, sweep_flag, point, absolute } => {
dest.write_char(if absolute { 'A' } else { 'a' })?;
dest.write_char(' ')?;
rx.to_css(dest)?;
dest.write_char(' ')?;
ry.to_css(dest)?;
dest.write_char(' ')?;
angle.to_css(dest)?;
dest.write_char(' ')?;
(large_arc_flag as i32).to_css(dest)?;
dest.write_char(' ')?;
(sweep_flag as i32).to_css(dest)?;
dest.write_char(' ')?;
point.to_css(dest)
},
HorizontalLineTo { x, absolute } => {
dest.write_char(if absolute { 'H' } else { 'h' })?;
dest.write_char(' ')?;
x.to_css(dest)
},
VerticalLineTo { y, absolute } => {
dest.write_char(if absolute { 'V' } else { 'v' })?;
dest.write_char(' ')?;
y.to_css(dest)
},
SmoothCurveTo { control2, point, absolute } => {
dest.write_char(if absolute { 'S' } else { 's' })?;
dest.write_char(' ')?;
control2.to_css(dest)?;
dest.write_char(' ')?;
point.to_css(dest)
},
SmoothQuadBezierCurveTo { point, absolute } => {
dest.write_char(if absolute { 'T' } else { 't' })?;
dest.write_char(' ')?;
point.to_css(dest)
},
}
}
}
/// The path coord type.
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss)]
#[repr(C)]
pub struct CoordPair(CSSFloat, CSSFloat);
impl CoordPair {
/// Create a CoordPair.
#[inline]
pub fn new(x: CSSFloat, y: CSSFloat) -> Self {
CoordPair(x, y)
}
}
/// Parse a pair of numbers into CoordPair.
fn parse_coord(iter: &mut Peekable<Chars>) -> Result<CoordPair, ()> {
let x = parse_number(iter)?;
skip_comma_wsp(iter);
let y = parse_number(iter)?;
Ok(CoordPair::new(x, y))
}
/// This is a special version which parses the number for SVG Path. e.g. "M 0.6.5" should be parsed
/// as MoveTo with a coordinate of ("0.6", ".5"), instead of treating 0.6.5 as a non-valid floating
/// point number. In other words, the logic here is similar with that of
/// tokenizer::consume_numeric, which also consumes the number as many as possible, but here the
/// input is a Peekable and we only accept an integer of a floating point number.
///
/// The "number" syntax in https://www.w3.org/TR/SVG/paths.html#PathDataBNF
fn parse_number(iter: &mut Peekable<Chars>) -> Result<CSSFloat, ()> {
// 1. Check optional sign.
let sign = if iter.peek().map_or(false, |&sign: &char| sign == '+' || sign == '-') {
if iter.next().unwrap() == '-' { -1. } else { 1. }
} else {
1.
};
// 2. Check integer part.
let mut integral_part: f64 = 0.;
let got_dot = if !iter.peek().map_or(false, |&n: &char| n == '.') {
// If the first digit in integer part is neither a dot nor a digit, this is not a number.
if iter.peek().map_or(true, |n: &char| !n.is_ascii_digit()) {
return Err(());
}
while iter.peek().map_or(false, |n: &char| n.is_ascii_digit()) {
integral_part =
integral_part * 10. + iter.next().unwrap().to_digit(10).unwrap() as f64;
}
iter.peek().map_or(false, |&n: &char| n == '.')
} else {
true
};
// 3. Check fractional part.
let mut fractional_part: f64 = 0.;
if got_dot {
// Consume '.'.
iter.next();
// If the first digit in fractional part is not a digit, this is not a number.
if iter.peek().map_or(true, |n: &char| !n.is_ascii_digit()) {
return Err(());
}
let mut factor = 0.1;
while iter.peek().map_or(false, |n: &char| n.is_ascii_digit()) {
fractional_part += iter.next().unwrap().to_digit(10).unwrap() as f64 * factor;
factor *= 0.1;
}
}
let mut value = sign * (integral_part + fractional_part);
// 4. Check exp part. The segment name of SVG Path doesn't include 'E' or 'e', so it's ok to
// treat the numbers after 'E' or 'e' are in the exponential part.
if iter.peek().map_or(false, |&exp: &char| exp == 'E' || exp == 'e') {
// Consume 'E' or 'e'.
iter.next();
let exp_sign = if iter.peek().map_or(false, |&sign: &char| sign == '+' || sign == '-') {
if iter.next().unwrap() == '-' { -1. } else { 1. }
} else {
1.
};
let mut exp: f64 = 0.;
while iter.peek().map_or(false, |n: &char| n.is_ascii_digit()) {
exp = exp * 10. + iter.next().unwrap().to_digit(10).unwrap() as f64;
}
value *= f64::powf(10., exp * exp_sign);
}
if value.is_finite() {
Ok(value.min(::std::f32::MAX as f64).max(::std::f32::MIN as f64) as CSSFloat)
} else {
Err(())
}
}
/// Skip all svg whitespaces, and return true if |iter| hasn't finished.
#[inline]
fn skip_wsp(iter: &mut Peekable<Chars>) -> bool {
// Note: SVG 1.1 defines the whitespaces as \u{9}, \u{20}, \u{A}, \u{D}.
// However, SVG 2 has one extra whitespace: \u{C}.
// Therefore, we follow the newest spec for the definition of whitespace,
// i.e. \u{9}, \u{20}, \u{A}, \u{C}, \u{D}, by is_ascii_whitespace().
while iter.peek().map_or(false, |c: &char| c.is_ascii_whitespace()) {
iter.next();
}
iter.peek().is_some()
}
/// Skip all svg whitespaces and one comma, and return true if |iter| hasn't finished.
#[inline]
fn skip_comma_wsp(iter: &mut Peekable<Chars>) -> bool {
if !skip_wsp(iter) {
return false;
}
if *iter.peek().unwrap() != ',' {
return true;
}
iter.next();
skip_wsp(iter)
}

View File

@ -165103,9 +165103,21 @@
{}
]
],
"css/motion/offset-path-string.html": [
"css/motion/offset-path-string-001.html": [
[
"/css/motion/offset-path-string.html",
"/css/motion/offset-path-string-001.html",
[
[
"/css/motion/offset-path-string-ref.html",
"=="
]
],
{}
]
],
"css/motion/offset-path-string-002.html": [
[
"/css/motion/offset-path-string-002.html",
[
[
"/css/motion/offset-path-string-ref.html",
@ -568451,14 +568463,18 @@
"6c39e7b8f4cfafe05c07d166eb65570432912b7a",
"reftest"
],
"css/motion/offset-path-string-001.html": [
"79d957d82b8e3c603ed16598f461a805c90681dd",
"reftest"
],
"css/motion/offset-path-string-002.html": [
"0d2fcbbb661c2fe0e5b57ff780d78b2f8b6f627b",
"reftest"
],
"css/motion/offset-path-string-ref.html": [
"5c5ff5f6f2ddc4696f2d51266199fe052464d9e6",
"support"
],
"css/motion/offset-path-string.html": [
"79d957d82b8e3c603ed16598f461a805c90681dd",
"reftest"
],
"css/motion/offset-rotate-001.html": [
"55147698a7f2f02a57f0fe3adc8b33257d1e212f",
"reftest"
@ -568500,11 +568516,11 @@
"testharness"
],
"css/motion/parsing/offset-path-parsing-invalid.html": [
"c0a32486922b4b1b482817f409571e1e6c4219f7",
"7fbd06a508a322ac0969eb11c4299de50fd254e7",
"testharness"
],
"css/motion/parsing/offset-path-parsing-valid.html": [
"c1e229e1a05a4c85845384ace9b884125f579415",
"e7797686e4ac524ac9dc9f8525dbd5a24adeec29",
"testharness"
],
"css/motion/parsing/offset-position-parsing-invalid.html": [

View File

@ -0,0 +1 @@
prefs: [layout.css.motion-path.enabled:true]

View File

@ -1,7 +1,4 @@
[offset-path-interpolation-001.html]
["path('M 0 0 H 1 H 2')" and "path('M 0 0 H 3')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 0 0 H 1 H 2')" and "path('M 0 0 H 3')" at progress -1]
expected: FAIL
@ -20,9 +17,6 @@
[Animation between "path('M 0 0 H 1 H 2')" and "path('M 0 0 H 3')" at progress 2]
expected: FAIL
["path('M 1 2 L 3 4 Z')" and "none" are valid offset-path values]
expected: FAIL
[Animation between "path('M 1 2 L 3 4 Z')" and "none" at progress -1]
expected: FAIL
@ -32,18 +26,6 @@
[Animation between "path('M 1 2 L 3 4 Z')" and "none" at progress 0.125]
expected: FAIL
[Animation between "path('M 1 2 L 3 4 Z')" and "none" at progress 0.875]
expected: FAIL
[Animation between "path('M 1 2 L 3 4 Z')" and "none" at progress 1]
expected: FAIL
[Animation between "path('M 1 2 L 3 4 Z')" and "none" at progress 2]
expected: FAIL
["path('M 10 0 H 11')" and "path('M 20 0 V 2')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 10 0 H 11')" and "path('M 20 0 V 2')" at progress -1]
expected: FAIL
@ -62,9 +44,6 @@
[Animation between "path('M 10 0 H 11')" and "path('M 20 0 V 2')" at progress 2]
expected: FAIL
["path('M 1 2 L 4 6 Z')" and "path('M 1 2 H 4 V 6')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 1 2 L 4 6 Z')" and "path('M 1 2 H 4 V 6')" at progress -1]
expected: FAIL
@ -83,9 +62,6 @@
[Animation between "path('M 1 2 L 4 6 Z')" and "path('M 1 2 H 4 V 6')" at progress 2]
expected: FAIL
["path('M 0 0 Z')" and "path('M 0 0 Z')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 0 0 Z')" and "path('M 0 0 Z')" at progress -1]
expected: FAIL
@ -104,9 +80,6 @@
[Animation between "path('M 0 0 Z')" and "path('M 0 0 Z')" at progress 2]
expected: FAIL
["path('M 20 70')" and "path('M 100 30')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 20 70')" and "path('M 100 30')" at progress -1]
expected: FAIL
@ -125,9 +98,6 @@
[Animation between "path('M 20 70')" and "path('M 100 30')" at progress 2]
expected: FAIL
["path('m 20 70')" and "path('m 100 30')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 20 70')" and "path('m 100 30')" at progress -1]
expected: FAIL
@ -146,9 +116,6 @@
[Animation between "path('m 20 70')" and "path('m 100 30')" at progress 2]
expected: FAIL
["path('m 100 200 L 120 270')" and "path('m 100 200 L 200 230')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 100 200 L 120 270')" and "path('m 100 200 L 200 230')" at progress -1]
expected: FAIL
@ -167,9 +134,6 @@
[Animation between "path('m 100 200 L 120 270')" and "path('m 100 200 L 200 230')" at progress 2]
expected: FAIL
["path('m 100 200 l 20 70')" and "path('m 100 200 l 100 30')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 100 200 l 20 70')" and "path('m 100 200 l 100 30')" at progress -1]
expected: FAIL

View File

@ -1,7 +1,4 @@
[offset-path-interpolation-002.html]
["path('M 20 10 C 32 42 52 62 120 2200')" and "path('M 20 10 C 40 50 60 70 200 3000')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 20 10 C 32 42 52 62 120 2200')" and "path('M 20 10 C 40 50 60 70 200 3000')" at progress -1]
expected: FAIL
@ -20,9 +17,6 @@
[Animation between "path('M 20 10 C 32 42 52 62 120 2200')" and "path('M 20 10 C 40 50 60 70 200 3000')" at progress 2]
expected: FAIL
["path('m 20 10 c 12 32 32 52 100 2190')" and "path('m 20 10 c 20 40 40 60 180 2990')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 20 10 c 12 32 32 52 100 2190')" and "path('m 20 10 c 20 40 40 60 180 2990')" at progress -1]
expected: FAIL
@ -41,9 +35,6 @@
[Animation between "path('m 20 10 c 12 32 32 52 100 2190')" and "path('m 20 10 c 20 40 40 60 180 2990')" at progress 2]
expected: FAIL
["path('M 20 10 Q 32 42 120 2200')" and "path('M 20 10 Q 40 50 200 3000')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 20 10 Q 32 42 120 2200')" and "path('M 20 10 Q 40 50 200 3000')" at progress -1]
expected: FAIL
@ -62,9 +53,6 @@
[Animation between "path('M 20 10 Q 32 42 120 2200')" and "path('M 20 10 Q 40 50 200 3000')" at progress 2]
expected: FAIL
["path('m 20 10 q 12 32 100 2190')" and "path('m 20 10 q 20 40 180 2990')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 20 10 q 12 32 100 2190')" and "path('m 20 10 q 20 40 180 2990')" at progress -1]
expected: FAIL
@ -83,9 +71,6 @@
[Animation between "path('m 20 10 q 12 32 100 2190')" and "path('m 20 10 q 20 40 180 2990')" at progress 2]
expected: FAIL
["path('M 100 400 A 10 20 30 1 0 140 450')" and "path('M 300 200 A 50 60 70 0 1 380 290')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 100 400 A 10 20 30 1 0 140 450')" and "path('M 300 200 A 50 60 70 0 1 380 290')" at progress -1]
expected: FAIL
@ -104,9 +89,6 @@
[Animation between "path('M 100 400 A 10 20 30 1 0 140 450')" and "path('M 300 200 A 50 60 70 0 1 380 290')" at progress 2]
expected: FAIL
["path('m 100 400 a 10 20 30 1 0 40 50')" and "path('m 300 200 a 50 60 70 0 1 80 90')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 100 400 a 10 20 30 1 0 40 50')" and "path('m 300 200 a 50 60 70 0 1 80 90')" at progress -1]
expected: FAIL

View File

@ -1,7 +1,4 @@
[offset-path-interpolation-003.html]
["path('M 50 60 H 70')" and "path('M 10 140 H 270')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 50 60 H 70')" and "path('M 10 140 H 270')" at progress -1]
expected: FAIL
@ -20,9 +17,6 @@
[Animation between "path('M 50 60 H 70')" and "path('M 10 140 H 270')" at progress 2]
expected: FAIL
["path('m 50 60 h 20')" and "path('m 10 140 h 260')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 50 60 h 20')" and "path('m 10 140 h 260')" at progress -1]
expected: FAIL
@ -41,9 +35,6 @@
[Animation between "path('m 50 60 h 20')" and "path('m 10 140 h 260')" at progress 2]
expected: FAIL
["path('M 50 60 V 70')" and "path('M 10 140 V 270')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 50 60 V 70')" and "path('M 10 140 V 270')" at progress -1]
expected: FAIL
@ -62,9 +53,6 @@
[Animation between "path('M 50 60 V 70')" and "path('M 10 140 V 270')" at progress 2]
expected: FAIL
["path('m 50 60 v 10')" and "path('m 10 140 v 130')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 50 60 v 10')" and "path('m 10 140 v 130')" at progress -1]
expected: FAIL
@ -83,9 +71,6 @@
[Animation between "path('m 50 60 v 10')" and "path('m 10 140 v 130')" at progress 2]
expected: FAIL
["path('M 12 34 S 45 67 89 123')" and "path('M 20 26 S 61 51 113 99')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 12 34 S 45 67 89 123')" and "path('M 20 26 S 61 51 113 99')" at progress -1]
expected: FAIL
@ -104,9 +89,6 @@
[Animation between "path('M 12 34 S 45 67 89 123')" and "path('M 20 26 S 61 51 113 99')" at progress 2]
expected: FAIL
["path('m 12 34 s 33 33 77 89')" and "path('m 20 26 s 41 25 93 73')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 12 34 s 33 33 77 89')" and "path('m 20 26 s 41 25 93 73')" at progress -1]
expected: FAIL
@ -125,9 +107,6 @@
[Animation between "path('m 12 34 s 33 33 77 89')" and "path('m 20 26 s 41 25 93 73')" at progress 2]
expected: FAIL
["path('M 12 34 T 45 67')" and "path('M 20 26 T 61 51')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 12 34 T 45 67')" and "path('M 20 26 T 61 51')" at progress -1]
expected: FAIL
@ -146,9 +125,6 @@
[Animation between "path('M 12 34 T 45 67')" and "path('M 20 26 T 61 51')" at progress 2]
expected: FAIL
["path('m 12 34 t 33 33')" and "path('m 20 26 t 41 25')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 12 34 t 33 33')" and "path('m 20 26 t 41 25')" at progress -1]
expected: FAIL

View File

@ -1,7 +1,4 @@
[offset-path-interpolation-004.html]
["path('M 0 0 L 100 100 M 100 200 L 200 200 Z L 200 100 Z')" and "path('M 0 0 L 100 100 m 0 100 l 100 0 z l 300 100 z')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 0 0 L 100 100 M 100 200 L 200 200 Z L 200 100 Z')" and "path('M 0 0 L 100 100 m 0 100 l 100 0 z l 300 100 z')" at progress -1]
expected: FAIL
@ -20,9 +17,6 @@
[Animation between "path('M 0 0 L 100 100 M 100 200 L 200 200 Z L 200 100 Z')" and "path('M 0 0 L 100 100 m 0 100 l 100 0 z l 300 100 z')" at progress 2]
expected: FAIL
["path('M 0 0 L 100 100 M 100 200 L 200 200 Z L 200 100 Z')" and "path('M 0 0 L 100 100 m 0 100 l 100 0 z l 100 -100 z')" are valid offset-path values]
expected: FAIL
[Animation between "path('M 0 0 L 100 100 M 100 200 L 200 200 Z L 200 100 Z')" and "path('M 0 0 L 100 100 m 0 100 l 100 0 z l 100 -100 z')" at progress -1]
expected: FAIL
@ -41,9 +35,6 @@
[Animation between "path('M 0 0 L 100 100 M 100 200 L 200 200 Z L 200 100 Z')" and "path('M 0 0 L 100 100 m 0 100 l 100 0 z l 100 -100 z')" at progress 2]
expected: FAIL
["path('m 10 20 l 40 50 z l 40 60 z m 60 70 l 90 60 z t 70 130')" and "path('M 210 220 L 170 190 Z L 90 120 Z M 110 130 L 200 230 Z T 220 220')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 10 20 l 40 50 z l 40 60 z m 60 70 l 90 60 z t 70 130')" and "path('M 210 220 L 170 190 Z L 90 120 Z M 110 130 L 200 230 Z T 220 220')" at progress -1]
expected: FAIL
@ -62,9 +53,6 @@
[Animation between "path('m 10 20 l 40 50 z l 40 60 z m 60 70 l 90 60 z t 70 130')" and "path('M 210 220 L 170 190 Z L 90 120 Z M 110 130 L 200 230 Z T 220 220')" at progress 2]
expected: FAIL
["path('m 10 20 c 40 50 30 60 80 70 c 120 130 170 140 110 160')" and "path('M 130 100 C 130 150 120 160 210 170 C 290 300 340 310 320 330')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 10 20 c 40 50 30 60 80 70 c 120 130 170 140 110 160')" and "path('M 130 100 C 130 150 120 160 210 170 C 290 300 340 310 320 330')" at progress -1]
expected: FAIL
@ -83,9 +71,6 @@
[Animation between "path('m 10 20 c 40 50 30 60 80 70 c 120 130 170 140 110 160')" and "path('M 130 100 C 130 150 120 160 210 170 C 290 300 340 310 320 330')" at progress 2]
expected: FAIL
["path('m 10 20 q 30 60 40 50 q 110 80 90 80')" and "path('M 130 100 Q 120 160 130 150 Q 200 150 180 190')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 10 20 q 30 60 40 50 q 110 80 90 80')" and "path('M 130 100 Q 120 160 130 150 Q 200 150 180 190')" at progress -1]
expected: FAIL
@ -104,9 +89,6 @@
[Animation between "path('m 10 20 q 30 60 40 50 q 110 80 90 80')" and "path('M 130 100 Q 120 160 130 150 Q 200 150 180 190')" at progress 2]
expected: FAIL
["path('m 10 20 s 30 60 40 50 s 110 60 90 70')" and "path('M 130 140 S 120 160 130 150 S 200 170 140 180')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 10 20 s 30 60 40 50 s 110 60 90 70')" and "path('M 130 140 S 120 160 130 150 S 200 170 140 180')" at progress -1]
expected: FAIL
@ -125,9 +107,6 @@
[Animation between "path('m 10 20 s 30 60 40 50 s 110 60 90 70')" and "path('M 130 140 S 120 160 130 150 S 200 170 140 180')" at progress 2]
expected: FAIL
["path('m 10 20 h 30 v 60 h 10 v -10 l 110 60')" and "path('M 130 140 H 120 V 160 H 130 V 150 L 200 170')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 10 20 h 30 v 60 h 10 v -10 l 110 60')" and "path('M 130 140 H 120 V 160 H 130 V 150 L 200 170')" at progress -1]
expected: FAIL
@ -146,9 +125,6 @@
[Animation between "path('m 10 20 h 30 v 60 h 10 v -10 l 110 60')" and "path('M 130 140 H 120 V 160 H 130 V 150 L 200 170')" at progress 2]
expected: FAIL
["path('m 10 20 a 10 20 30 1 0 40 50 a 110 120 30 1 1 140 50')" and "path('M 18 12 A 50 100 70 0 1 90 110 A 150 160 70 0 1 70 80')" are valid offset-path values]
expected: FAIL
[Animation between "path('m 10 20 a 10 20 30 1 0 40 50 a 110 120 30 1 1 140 50')" and "path('M 18 12 A 50 100 70 0 1 90 110 A 150 160 70 0 1 70 80')" at progress -1]
expected: FAIL

View File

@ -32,27 +32,9 @@
[Animation between "ray(0deg closest-corner)" and "none" at progress 0.125]
expected: FAIL
[Animation between "ray(0deg closest-corner)" and "none" at progress 0.875]
expected: FAIL
[Animation between "ray(0deg closest-corner)" and "none" at progress 1]
expected: FAIL
[Animation between "ray(0deg closest-corner)" and "none" at progress 2]
expected: FAIL
["none" and "ray(20deg closest-side)" are valid offset-path values]
expected: FAIL
[Animation between "none" and "ray(20deg closest-side)" at progress -1]
expected: FAIL
[Animation between "none" and "ray(20deg closest-side)" at progress 0]
expected: FAIL
[Animation between "none" and "ray(20deg closest-side)" at progress 0.125]
expected: FAIL
[Animation between "none" and "ray(20deg closest-side)" at progress 0.875]
expected: FAIL

View File

@ -0,0 +1,3 @@
[offset-path-string-001.html]
expected: FAIL
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1429299

View File

@ -1,2 +0,0 @@
[offset-path-string.html]
expected: FAIL

View File

@ -5,12 +5,6 @@
[Serialization should round-trip after setting e.style['offset-path'\] = "fill-box ellipse(50% 60%)"]
expected: FAIL
[e.style['offset-path'\] = "none" should set the property value]
expected: FAIL
[Serialization should round-trip after setting e.style['offset-path'\] = "none"]
expected: FAIL
[e.style['offset-path'\] = "ray(0rad closest-side)" should set the property value]
expected: FAIL
@ -44,18 +38,6 @@
[e.style['offset-path'\] = "ray(calc(180deg - 45deg) farthest-side)" should set the property value]
expected: FAIL
[e.style['offset-path'\] = "path('m 0 0 h -100')" should set the property value]
expected: FAIL
[Serialization should round-trip after setting e.style['offset-path'\] = "path('m 0 0 h -100')"]
expected: FAIL
[e.style['offset-path'\] = "path('M 0 0 L 100 100 M 100 200 L 200 200 Z L 300 300 Z')" should set the property value]
expected: FAIL
[Serialization should round-trip after setting e.style['offset-path'\] = "path('M 0 0 L 100 100 M 100 200 L 200 200 Z L 300 300 Z')"]
expected: FAIL
[e.style['offset-path'\] = "url(\\"http://www.example.com/index.html#polyline1\\")" should set the property value]
expected: FAIL

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>CSS Motion Path: path(string) paths</title>
<link rel="help" href="https://drafts.fxtf.org/motion-1/#offset-path-property">
<link rel="match" href="offset-path-string-ref.html">
<meta name="assert" content="This tests that path(<string>) generates a rotation and translation.">
<style>
#target {
position: absolute;
left: 300px;
top: 0px;
width: 300px;
height: 200px;
background-color: lime;
transform-origin: 0px 0px;
offset-path: path('m 0 120 v 200');
}
</style>
</head>
<body>
<div id="target"></div>
</body>
</html>

View File

@ -14,7 +14,7 @@
<script>
// arc path segments must have at least 7 arguments.
// https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
test_invalid_value("offset-path", "path('M 20 30 A 60 70 80')");
test_invalid_value("offset-path", 'path("M 20 30 A 60 70 80")');
test_invalid_value("offset-path", "ray(0 sides)");
test_invalid_value("offset-path", "ray(0deg)");

View File

@ -21,8 +21,8 @@ test_valid_value("offset-path", "ray(270deg farthest-corner contain)");
test_valid_value("offset-path", "ray(-720deg sides)");
test_valid_value("offset-path", "ray(calc(180deg - 45deg) farthest-side)", "ray(calc(135deg) farthest-side)");
test_valid_value("offset-path", "path('m 0 0 h -100')");
test_valid_value("offset-path", "path('M 0 0 L 100 100 M 100 200 L 200 200 Z L 300 300 Z')");
test_valid_value("offset-path", 'path("m 0 0 h -100")');
test_valid_value("offset-path", 'path("M 0 0 L 100 100 M 100 200 L 200 200 Z L 300 300 Z")');
test_valid_value("offset-path", 'url("http://www.example.com/index.html#polyline1")');

View File

@ -36,10 +36,18 @@ use std::boxed::Box;
use std::env;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[cfg(target_os = "android")]
use std::os::raw::c_int;
#[cfg(target_os = "android")]
use log::Level;
#[cfg(not(target_os = "android"))]
use log::Log;
use std::panic;
extern "C" {
fn gfx_critical_note(msg: *const c_char);
#[cfg(target_os = "android")]
fn __android_log_write(prio: c_int, tag: *const c_char, text: *const c_char) -> c_int;
}
struct GeckoLogger {
@ -84,6 +92,29 @@ impl GeckoLogger {
}
}
}
#[cfg(not(target_os = "android"))]
fn log_out(&self, record: &log::Record) {
self.logger.log(record);
}
#[cfg(target_os = "android")]
fn log_out(&self, record: &log::Record) {
let msg = CString::new(format!("{}", record.args())).unwrap();
let tag = CString::new(record.module_path().unwrap()).unwrap();
let prio = match record.metadata().level() {
Level::Error => 6 /* ERROR */,
Level::Warn => 5 /* WARN */,
Level::Info => 4 /* INFO */,
Level::Debug => 3 /* DEBUG */,
Level::Trace => 2 /* VERBOSE */,
};
// Output log directly to android log, since env_logger can output log
// only to stderr or stdout.
unsafe {
__android_log_write(prio, tag.as_ptr(), msg.as_ptr());
}
}
}
impl log::Log for GeckoLogger {
@ -94,7 +125,7 @@ impl log::Log for GeckoLogger {
fn log(&self, record: &log::Record) {
// Forward log to gfxCriticalNote, if the log should be in gfx crash log.
self.maybe_log_to_gfx_critical_note(record);
self.logger.log(record);
self.log_out(record);
}
fn flush(&self) { }