geckodriver icon indicating copy to clipboard operation
geckodriver copied to clipboard

offsets are from the center of element instead of from the top-left corner

Open srkannan opened this issue 7 years ago • 16 comments

In order to help us efficiently investigate your issue, please provide the following information:

Platform and application details

  • Platform: macOS Sierra 10.12.5
  • Firefox: Firefox 54
  • Selenium: 3.4.0

Steps to reproduce

In org.openqa.selenium.interactions.Actions the x, y offsets for moveToElement has been Offset from the top-left corner. But with geckodriver I'm getting: Jun 16, 2017 5:55:05 PM org.openqa.selenium.interactions.Actions moveToElement INFO: When using the W3C Action commands, offsets are from the center of element

Has the offset changed to center of element instead of top-left corner? While chromedriver, old firefox driver and safaridriver calculates the offset from top-left corner is this a bug in geckodriver?

  • [ ] Reproducable testcase: Try moveToElement method in org.openqa.selenium.interactions.Actions It differs between old firefox drivers.

  • [ ] A trace level log: Jun 16, 2017 5:55:05 PM org.openqa.selenium.interactions.Actions moveToElement INFO: When using the W3C Action commands, offsets are from the center of element

srkannan avatar Jun 19 '17 18:06 srkannan

moveToElement is a shim provided by Selenium so it’s possible there is a bug with that. But without a reproducible test case and a trace-level geckodriver log, it is hard to which level this fails at. Can you please provide that?

andreastt avatar Jun 19 '17 19:06 andreastt

The problem is the x,y offsets I used for old firefox driver and chromedriver don't work for geckodriver. So I get 'indexoutofbounds' exception when using the x,y offsets that I used for earlier versions of the driver. I will try to come up with a reproducible generic test case.

srkannan avatar Jun 19 '17 19:06 srkannan

Sure, but please note that neither FirefoxDriver (from Selenium) or chromedriver are implementations of the WebDriver specification. It would be useful to see a trace-level log.

andreastt avatar Jun 19 '17 19:06 andreastt

Here is a simple code to demonstrate the problem. The actual application I automate needs drawing that is why I rely on the Action.moveToElement but I just picked this one to demo the problem.

System.setProperty("webdriver.gecko.driver", "/Users/Downloads/geckodriver"); System.setProperty("webdriver.chrome.driver", "/Users/Downloads/chromedriver"); WebDriver driver = new FirefoxDriver(); // WebDriver driver = new ChromeDriver(); driver.get("http://book.theautomatedtester.co.uk/"); Actions action = new Actions(driver); WebElement mainbody = driver.findElement(By.xpath("html/body/div[2]/ul/li[1]")); action.moveToElement(mainbody,10,10); action.click().build().perform();

Please run this code using geckdriver and then comment out the FirefoxDriver and use the chromedriver line by uncommenting. Notice that the moveToElement works fine for chrome(it worked in old firefox driver as well) but in geckodriver it fails as the offset is computed from the center of the element instead of from top-left corner.

srkannan avatar Jun 19 '17 20:06 srkannan

I don’t know what action.moveToElement does in the Selenium client, which is why I asked to see the geckodriver trace log so I can see what primitives are actually sent across the wire to geckodriver.

That it “worked before” is not a great bug.

andreastt avatar Jun 20 '17 13:06 andreastt

Below is the log:

1497979442395	geckodriver	INFO	Listening on 127.0.0.1:44950
1497979446968	geckodriver::marionette	INFO	Starting browser /Applications/Firefox.app/Contents/MacOS/firefox-bin with args ["-marionette"]
1497979453319	Marionette	INFO	Listening on port 59254
1497979454432	Marionette	DEBUG	loaded listener.js
2017-06-20 10:24:14.896 plugin-container[90592:1542522] *** CFMessagePort: bootstrap_register(): failed 1100 (0x44c) 'Permission denied', port = 0x9c3b, name = 'com.apple.tsm.portname'
See /usr/include/servers/bootstrap_defs.h for the error codes.
2017-06-20 10:24:14.897 plugin-container[90592:1542522] *** CFMessagePort: bootstrap_register(): failed 1100 (0x44c) 'Permission denied', port = 0x9f03, name = 'com.apple.CFPasteboardClient'
See /usr/include/servers/bootstrap_defs.h for the error codes.
Jun 20, 2017 10:24:14 AM org.openqa.selenium.remote.ProtocolHandshake createSession
INFO: Detected dialect: W3C
1497979455174	Marionette	DEBUG	Received DOM event "beforeunload" for "about:blank"
1497979455350	Marionette	DEBUG	Received DOM event "pagehide" for "about:blank"
1497979455350	Marionette	DEBUG	Received DOM event "unload" for "about:blank"
1497979455782	Marionette	DEBUG	Received DOM event "DOMContentLoaded" for "http://book.theautomatedtester.co.uk/"
1497979455785	Marionette	DEBUG	Received DOM event "pageshow" for "http://book.theautomatedtester.co.uk/"
Jun 20, 2017 10:24:15 AM org.openqa.selenium.interactions.Actions moveToElement
INFO: When using the W3C Action commands, offsets are from the center of element

srkannan avatar Jun 20 '17 17:06 srkannan

With the old protocol (Wire) the command to move the cursor was moveto and the provided offset was relative to the top-left corner of the targeted element:

https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#sessionsessionidmoveto

But with the new W3C specification and implementation in marionette, moving the cursor to an element now requires a composite action where the offset is relative to the element's center:

https://w3c.github.io/webdriver/webdriver-spec.html#dfn-dispatch-a-pointermove-action

An object that represents a web element Let element be equal to the result of trying to get a known element with argument origin.

Let x element and y element be the result of calculating the in-view center point of element.

Let x equal x element + x offset, and y equal y element + y offset.

It seems that the specification was updated to accommodate the implementation of marionette which was already relative to center even before Firefox 45.

So this behavior is by design.

The current api doesn't provide a way to directly move the cursor with an offset relative to the top-left corner. It would make much more sense to provide an offset relative to the top-left corner especially when testing canvas.

That said, I would still consider this an issue, first because it breaks a previous feature and second because this change doesn't bring any value to the api.

florentbr avatar Jun 21 '17 16:06 florentbr

The current api doesn't provide a way to directly move the cursor with an offset relative to the top-left corner. It would make much more sense to provide an offset relative to the top-left corner especially when testing canvas.

I think this is a fair point. @florentbr, do you want to raise an issue against the specification?

cc @AutomatedTester

andreastt avatar Jul 10 '17 17:07 andreastt

I can add some info based on Selenium Java test suite, there is a test against this page: https://github.com/SeleniumHQ/selenium/blob/master/common/src/web/mousePositionTracker.html

  public void testMovingMouseToRelativeElementOffset() {
    driver.get(pages.mouseTrackerPage);

    WebElement trackerDiv = driver.findElement(By.id("mousetracker"));
    new Actions(driver).moveToElement(trackerDiv, 95, 195).perform();

    WebElement reporter = driver.findElement(By.id("status"));

    wait.until(fuzzyMatchingOfCoordinates(reporter, 95, 195));
  }

The result is "78, 191" instead of expected "95, 195"

Trace log:

1503771497876	geckodriver	INFO	geckodriver 0.18.0
1503771497877	webdriver::httpapi	DEBUG	Creating routes
1503771497887	geckodriver	INFO	Listening on 127.0.0.1:36706
1503771498529	webdriver::server	DEBUG	-> POST /session {
  "desiredCapabilities": {
    "nativeEvents": false,
    "firefox_binary": {
      "extraEnv": {},
      "hCode": 2008966511,
      "class": "org.openqa.selenium.firefox.FirefoxBinary",
      "timeout": 45000
    },
    "marionette": true,
    "acceptInsecureCerts": true,
    "browserName": "firefox",
    "moz:firefoxOptions": {
      "binary": "c:\\Program Files\\Nightly\\firefox.exe",
      "prefs": {},
      "args": []
    },
    "platformName": "ANY",
    "version": "",
    "platform": "ANY"
  },
  "requiredCapabilities": {},
  "capabilities": {
    "desiredCapabilities": {
      "nativeEvents": false,
      "firefox_binary": {
        "extraEnv": {},
        "hCode": 2008966511,
        "class": "org.openqa.selenium.firefox.FirefoxBinary",
        "timeout": 45000
      },
      "marionette": true,
      "acceptInsecureCerts": true,
      "browserName": "firefox",
      "moz:firefoxOptions": {
        "binary": "c:\\Program Files\\Nightly\\firefox.exe",
        "prefs": {},
        "args": []
      },
      "platformName": "ANY",
      "version": "",
      "platform": "ANY"
    },
    "requiredCapabilities": {},
    "alwaysMatch": {
      "acceptInsecureCerts": true
    },
    "firstMatch": [
      {
        "browserName": "firefox",
        "moz:firefoxOptions": {
          "binary": "c:\\Program Files\\Nightly\\firefox.exe",
          "prefs": {},
          "args": []
        }
      }
    ]
  }
}
1503771498547	geckodriver::capabilities	DEBUG	Trying to read firefox version from ini files
1503771498548	geckodriver::capabilities	DEBUG	Found version 57.0a1
1503771498560	geckodriver::marionette	INFO	Starting browser c:\Program Files\Nightly\firefox.exe with args ["-marionette"]
1503771498918	addons.xpi	WARN	Error parsing extensions state: [Exception... "Component returned failure code: 0x80520012 (NS_ERROR_FILE_NOT_FOUND) [amIAddonManagerStartup.readStartupData]"  nsresult: "0x80520012 (NS_ERROR_FILE_NOT_FOUND)"  location: "JS frame :: resource://gre/modules/addons/XPIProvider.jsm :: loadExtensionState :: line 1523"  data: no] Stack trace: loadExtensionState()@resource://gre/modules/addons/XPIProvider.jsm:1523 < getInstallState()@resource://gre/modules/addons/XPIProvider.jsm:1558 < checkForChanges()@resource://gre/modules/addons/XPIProvider.jsm:3093 < startup()@resource://gre/modules/addons/XPIProvider.jsm:2160 < callProvider()@resource://gre/modules/AddonManager.jsm:263 < _startProvider()@resource://gre/modules/AddonManager.jsm:733 < startup()@resource://gre/modules/AddonManager.jsm:900 < startup()@resource://gre/modules/AddonManager.jsm:3084 < observe()@jar:file:///C:/Program%20Files/Nightly/omni.ja!/components/addonManager.js:65
1503771499180	Marionette	DEBUG	Received observer notification "profile-after-change"
1503771499273	Marionette	DEBUG	Received observer notification "command-line-startup"
1503771499273	Marionette	INFO	Enabled via --marionette
1503771499564	geckodriver::marionette	TRACE	  connection attempt 0/600
1503771500664	geckodriver::marionette	TRACE	  connection attempt 1/600
Unable to read VR Path Registry from C:\Users\alexei\AppData\Local\openvr\openvrpaths.vrpath
[Child 5292] WARNING: pipe error: 109: file z:/build/build/src/ipc/chromium/src/chrome/common/ipc_channel_win.cc, line 346
1503771501526	Marionette	DEBUG	Received observer notification "sessionstore-windows-restored"
1503771501765	geckodriver::marionette	TRACE	  connection attempt 2/600
1503771502122	Marionette	DEBUG	Setting recommended pref toolkit.cosmeticAnimations.enabled to false
1503771502122	Marionette	DEBUG	Setting recommended pref datareporting.policy.dataSubmissionPolicyAccepted to false
1503771502122	Marionette	DEBUG	Setting recommended pref dom.file.createInChild to true
1503771502123	Marionette	INFO	Listening on port 63806
1503771502372	geckodriver::marionette	DEBUG	Connected to Marionette on localhost:63806
1503771502375	Marionette	DEBUG	Accepted connection 0 from 127.0.0.1:63821
1503771502375	geckodriver::marionette	TRACE	<- {"applicationType":"gecko","marionetteProtocol":3}
1503771502375	geckodriver::marionette	TRACE	-> 163:[0,1,"newSession",{"acceptInsecureCerts":true,"browserName":"firefox","capabilities":{"desiredCapabilities":{"acceptInsecureCerts":true,"browserName":"firefox"}}}]
1503771502376	Marionette	TRACE	0 -> [0,1,"newSession",{"acceptInsecureCerts":true,"browserName":"firefox","capabilities":{"desiredCapabilities":{"acceptInsecureCerts":true,"browserName":"firefox"}}}]
1503771502378	Marionette	WARN	TLS certificate errors will be ignored for this session
1503771502427	Marionette	DEBUG	Register listener.js for window 4294967297
1503771502446	Marionette	TRACE	0 <- [1,1,null,{"sessionId":"c716d576-e364-4122-9a4d-1962333ff7dc","capabilities":{"browserName":"firefox","browserVersion":"57.0a1","platformName":"windows_nt","platformVersion":"6.1","pageLoadStrategy":"normal","acceptInsecureCerts":true,"timeouts":{"implicit":0,"pageLoad":300000,"script":30000},"rotatable":false,"specificationLevel":0,"moz:processID":9276,"moz:profile":"C:\\Users\\alexei\\AppData\\Local\\Temp\\rust_mozprofile.KmpLScKEztfq","moz:accessibilityChecks":false,"moz:headless":false}}]
1503771502447	geckodriver::marionette	TRACE	<- [1,1,null,{"sessionId":"c716d576-e364-4122-9a4d-1962333ff7dc","capabilities":{"browserName":"firefox","browserVersion":"57.0a1","platformName":"windows_nt","platformVersion":"6.1","pageLoadStrategy":"normal","acceptInsecureCerts":true,"timeouts":{"implicit":0,"pageLoad":300000,"script":30000},"rotatable":false,"specificationLevel":0,"moz:processID":9276,"moz:profile":"C:\\Users\\alexei\\AppData\\Local\\Temp\\rust_mozprofile.KmpLScKEztfq","moz:accessibilityChecks":false,"moz:headless":false}}]
1503771502447	webdriver::server	DEBUG	<- 200 OK {"value": {"sessionId":"c716d576-e364-4122-9a4d-1962333ff7dc","capabilities":{"acceptInsecureCerts":true,"browserName":"firefox","browserVersion":"57.0a1","moz:accessibilityChecks":false,"moz:headless":false,"moz:processID":9276,"moz:profile":"C:\\Users\\alexei\\AppData\\Local\\Temp\\rust_mozprofile.KmpLScKEztfq","pageLoadStrategy":"normal","platformName":"windows_nt","platformVersion":"6.1","rotatable":false,"specificationLevel":0,"timeouts":{"implicit":0,"pageLoad":300000,"script":30000}}}}
1503771502803	webdriver::server	DEBUG	-> POST /session/c716d576-e364-4122-9a4d-1962333ff7dc/url {"url":"http://comp1:43833/common/mousePositionTracker.html"}
1503771502803	geckodriver::marionette	TRACE	-> 73:[0,2,"get",{"url":"http://comp1:43833/common/mousePositionTracker.html"}]
1503771502805	Marionette	TRACE	0 -> [0,2,"get",{"url":"http://comp1:43833/common/mousePositionTracker.html"}]
1503771502811	Marionette	DEBUG	Received DOM event "beforeunload" for "about:blank"
1503771502883	Marionette	DEBUG	Received DOM event "pagehide" for "about:blank"
1503771502883	Marionette	DEBUG	Received DOM event "unload" for "about:blank"
1503771502969	Marionette	DEBUG	Received DOM event "DOMContentLoaded" for "http://comp1:43833/common/mousePositionTracker.html"
1503771502971	Marionette	DEBUG	Received DOM event "pageshow" for "http://comp1:43833/common/mousePositionTracker.html"
1503771502981	Marionette	TRACE	0 <- [1,2,null,{}]
1503771502997	geckodriver::marionette	TRACE	<- [1,2,null,{}]
1503771502997	webdriver::server	DEBUG	<- 200 OK {"value": {}}
1503771503004	webdriver::server	DEBUG	-> POST /session/c716d576-e364-4122-9a4d-1962333ff7dc/element {"value":"#mousetracker","using":"css selector"}
1503771503004	geckodriver::marionette	TRACE	-> 68:[0,3,"findElement",{"using":"css selector","value":"#mousetracker"}]
1503771503015	Marionette	TRACE	0 -> [0,3,"findElement",{"using":"css selector","value":"#mousetracker"}]
1503771503019	Marionette	TRACE	0 <- [1,3,null,{"value":{"element-6066-11e4-a52e-4f735466cecf":"3d0d6af0-087b-462a-bd96-03fcb57b3a88","ELEMENT":"3d0d6af0-087b-462a-bd96-03fcb57b3a88"}}]
1503771503020	geckodriver::marionette	TRACE	<- [1,3,null,{"value":{"element-6066-11e4-a52e-4f735466cecf":"3d0d6af0-087b-462a-bd96-03fcb57b3a88","ELEMENT":"3d0d6af0-087b-462a-bd96-03fcb57b3a88"}}]
1503771503020	webdriver::server	DEBUG	<- 200 OK {"value":{"element-6066-11e4-a52e-4f735466cecf":"3d0d6af0-087b-462a-bd96-03fcb57b3a88"}}
1503771503035	webdriver::server	DEBUG	-> POST /session/c716d576-e364-4122-9a4d-1962333ff7dc/actions {"actions":[{"id":"default mouse","type":"pointer","parameters":{"pointerType":"mouse"},"actions":[{"duration":100,"x":95,"y":195,"type":"pointerMove","origin":{"ELEMENT":"3d0d6af0-087b-462a-bd96-03fcb57b3a88","element-6066-11e4-a52e-4f735466cecf":"3d0d6af0-087b-462a-bd96-03fcb57b3a88"}}]}]}
1503771503036	geckodriver::marionette	TRACE	-> 266:[0,4,"performActions",{"actions":[{"actions":[{"duration":100,"origin":{"element-6066-11e4-a52e-4f735466cecf":"3d0d6af0-087b-462a-bd96-03fcb57b3a88"},"type":"pointerMove","x":95,"y":195}],"id":"default mouse","parameters":{"pointerType":"mouse"},"type":"pointer"}]}]
1503771503046	Marionette	TRACE	0 -> [0,4,"performActions",{"actions":[{"actions":[{"duration":100,"origin":{"element-6066-11e4-a52e-4f735466cecf":"3d0d6af0-087b-462a-bd96-03fcb57b3a88"},"type":"pointerMove","x":95,"y":195}],"id":"default mouse","parameters":{"pointerType":"mouse"},"type":"pointer"}]}]
1503771503171	Marionette	TRACE	0 <- [1,4,null,{}]
1503771503171	geckodriver::marionette	TRACE	<- [1,4,null,{}]
1503771503171	webdriver::server	DEBUG	<- 200 OK {"value": {}}
1503771503174	webdriver::server	DEBUG	-> POST /session/c716d576-e364-4122-9a4d-1962333ff7dc/element {"value":"#status","using":"css selector"}
1503771503174	geckodriver::marionette	TRACE	-> 62:[0,5,"findElement",{"using":"css selector","value":"#status"}]
1503771503176	Marionette	TRACE	0 -> [0,5,"findElement",{"using":"css selector","value":"#status"}]
1503771503178	Marionette	TRACE	0 <- [1,5,null,{"value":{"element-6066-11e4-a52e-4f735466cecf":"d4586469-bad5-4bb7-8b65-d3d2d5c38421","ELEMENT":"d4586469-bad5-4bb7-8b65-d3d2d5c38421"}}]
1503771503178	geckodriver::marionette	TRACE	<- [1,5,null,{"value":{"element-6066-11e4-a52e-4f735466cecf":"d4586469-bad5-4bb7-8b65-d3d2d5c38421","ELEMENT":"d4586469-bad5-4bb7-8b65-d3d2d5c38421"}}]
1503771503178	webdriver::server	DEBUG	<- 200 OK {"value":{"element-6066-11e4-a52e-4f735466cecf":"d4586469-bad5-4bb7-8b65-d3d2d5c38421"}}
1503771503182	webdriver::server	DEBUG	-> GET /session/c716d576-e364-4122-9a4d-1962333ff7dc/element/d4586469-bad5-4bb7-8b65-d3d2d5c38421/text 
1503771503183	geckodriver::marionette	TRACE	-> 68:[0,6,"getElementText",{"id":"d4586469-bad5-4bb7-8b65-d3d2d5c38421"}]
1503771503184	Marionette	TRACE	0 -> [0,6,"getElementText",{"id":"d4586469-bad5-4bb7-8b65-d3d2d5c38421"}]
1503771503193	Marionette	TRACE	0 <- [1,6,null,{"value":"78, 191"}]
1503771503193	geckodriver::marionette	TRACE	<- [1,6,null,{"value":"78, 191"}]
1503771503193	webdriver::server	DEBUG	<- 200 OK {"value":"78, 191"}
1503771503698	webdriver::server	DEBUG	-> GET /session/c716d576-e364-4122-9a4d-1962333ff7dc/element/d4586469-bad5-4bb7-8b65-d3d2d5c38421/text 
1503771503698	geckodriver::marionette	TRACE	-> 68:[0,7,"getElementText",{"id":"d4586469-bad5-4bb7-8b65-d3d2d5c38421"}]
1503771503699	Marionette	TRACE	0 -> [0,7,"getElementText",{"id":"d4586469-bad5-4bb7-8b65-d3d2d5c38421"}]
1503771503704	Marionette	TRACE	0 <- [1,7,null,{"value":"78, 191"}]
1503771503704	geckodriver::marionette	TRACE	<- [1,7,null,{"value":"78, 191"}]
1503771503704	webdriver::server	DEBUG	<- 200 OK {"value":"78, 191"}

barancev avatar Aug 26 '17 18:08 barancev

Any updates on this issue?

Debanjan-B avatar Aug 22 '18 21:08 Debanjan-B

@andreastt based on your comment from July last year (https://github.com/mozilla/geckodriver/issues/789#issuecomment-314178944) it looks like that no spec issue has been filed, or?

whimboo avatar Aug 23 '18 10:08 whimboo

I do not see any changes from FF or on geckodriver. I am using geckodriver 24 with FF 65.0.2 I see the same behaviour that it takes centre of any given element. We did an implementation to take the offset and click top-left corner.

I feel the solution to this problem has been left hanging in lot of places where I see similar questions. I recently had this problem with geckodriver 19 and FF 57. We also upgraded to FF 65 and gecko being 24. So, gecko is still continuing to have W3C specs intact but not the same with chromedriver (or not?).

We resolved this situation by,

WebElement ele = findElement(By.xpath("//div[contains(@class, 'columnClass')]"));

// Action to move to the element top-left corner
Actions userAction = new Actions(getDriver());
int getTopLeftY = ((ele.getSize().getHeight()/2) - ele.getSize().getHeight());
int getTopLeftX =  (ele.getSize().getWidth()/2) - ele.getSize().getWidth();
userAction.moveToElement(ele, getTopLeftX, getTopLeftY).click().perform();

From this if you want to move to any point at the element. you may want to

int positionX = getTopLeftX + 100;
int positionY = getTopLeftY + 100;

Which will click at the point where you are anticipating.

raguravindran avatar Mar 20 '19 06:03 raguravindran

@raguravindran I assume you didn't run chromedriver in w3c mode? It should fail the same way.

Also please read the documentation of geckodriver and use the following capability for now:

https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html#moz-usenonspeccompliantpointerorigin

whimboo avatar Mar 27 '19 12:03 whimboo

The bug is in selenium. The drivers for Chrome and Edge Dev (chromium), since version 75, both comply with the w3c. Selenium need to update their javadocs to say that it's from the centre. It would probably be too much of a faff to fix it in the selenium code because they won't know the dimensions of the element.

JoolsCaesar avatar Jun 07 '19 15:06 JoolsCaesar

There is precedence for high-level Selenium client commands to run extra steps to alter the behaviour of commands. WebElement#submit() is one such example, as Selenium offered a way to submit a form but WebDriver (by the standard) doesn’t.

So it’s technically possible to mitigate the difference in behaviour for Selenium users by the clients first getting the bounding box of the element and then changing the click point using an action primitive. That said, if recent Chrome and Edge drivers are aligned with geckodriver’s behaviour perhaps it is not a priority.

andreastt avatar Jun 15 '19 12:06 andreastt

int getTopLeftY = ((ele.getSize().getHeight()/2) - ele.getSize().getHeight()); int getTopLeftX = (ele.getSize().getWidth()/2) - ele.getSize().getWidth();

height/2 is wrong because of oveflow: scroll in parent and element height bigger than screen height (when a part of element hidden)

mixalbl4-127 avatar Jan 31 '22 07:01 mixalbl4-127

Is anyone still having issues with this particular behavior? If yes please report and provide a testcase + trace log. Otherwise I'm going to close this issue soon. Thanks.

whimboo avatar May 02 '23 06:05 whimboo

Due to lack of response I'm going to close this issue for now. I'm happy to reopen if there are still use cases out there which do not work with the center of element approach.

whimboo avatar May 08 '23 06:05 whimboo