From 5950bade3937db02b871a2ce27fc4e8adc38d9c6 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 27 Oct 2022 04:42:25 -0400 Subject: [PATCH] nb/webagg: Move mouse events to outer canvas div This fixes the resize grip on WebKit, which is a bit buggy [1, 2]. We additionally need to ignore pointer events on both canvases, even though their `z-index` puts the `div` on top. For mouse events, we no longer prevent the default handler in most browsers, because that will block the resize grip from working now that it is handling events from the same `div`. Due to the same bugs in WebKit, we _do_ still need to prevent the default handler there, or else any mouse drags (i.e., for pan and zoom), will cause the browser to try and select the canvas. The mouse position calculation is somewhat simplified as well. To ensure keyboard events make it to the canvas `div`, it now has a `tabindex`. [1] https://bugs.webkit.org/show_bug.cgi?id=144526 [2] https://bugs.webkit.org/show_bug.cgi?id=181818 --- lib/matplotlib/backends/web_backend/js/mpl.js | 105 +++++++++--------- 1 file changed, 54 insertions(+), 51 deletions(-) diff --git a/lib/matplotlib/backends/web_backend/js/mpl.js b/lib/matplotlib/backends/web_backend/js/mpl.js index 3909f786f301..e77ba1aaf9e9 100644 --- a/lib/matplotlib/backends/web_backend/js/mpl.js +++ b/lib/matplotlib/backends/web_backend/js/mpl.js @@ -112,6 +112,7 @@ mpl.figure.prototype._init_canvas = function () { var fig = this; var canvas_div = (this.canvas_div = document.createElement('div')); + canvas_div.setAttribute('tabindex', '0'); canvas_div.setAttribute( 'style', 'border: 1px solid #ddd;' + @@ -122,7 +123,8 @@ mpl.figure.prototype._init_canvas = function () { 'outline: 0;' + 'overflow: hidden;' + 'position: relative;' + - 'resize: both;' + 'resize: both;' + + 'z-index: 2;' ); function on_keyboard_event_closure(name) { @@ -145,7 +147,13 @@ mpl.figure.prototype._init_canvas = function () { var canvas = (this.canvas = document.createElement('canvas')); canvas.classList.add('mpl-canvas'); - canvas.setAttribute('style', 'box-sizing: content-box;'); + canvas.setAttribute( + 'style', + 'box-sizing: content-box;' + + 'pointer-events: none;' + + 'position: relative;' + + 'z-index: 0;' + ); this.context = canvas.getContext('2d'); @@ -165,7 +173,12 @@ mpl.figure.prototype._init_canvas = function () { )); rubberband_canvas.setAttribute( 'style', - 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;' + 'box-sizing: content-box;' + + 'left: 0;' + + 'pointer-events: none;' + + 'position: absolute;' + + 'top: 0;' + + 'z-index: 1;' ); // Apply a ponyfill if ResizeObserver is not implemented by browser. @@ -215,10 +228,10 @@ mpl.figure.prototype._init_canvas = function () { canvas.setAttribute('width', width * fig.ratio); canvas.setAttribute('height', height * fig.ratio); } - canvas.setAttribute( - 'style', - 'width: ' + width + 'px; height: ' + height + 'px;' - ); + /* This rescales the canvas back to display pixels, so that it + * appears correct on HiDPI screens. */ + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; rubberband_canvas.setAttribute('width', width); rubberband_canvas.setAttribute('height', height); @@ -234,34 +247,53 @@ mpl.figure.prototype._init_canvas = function () { this.resizeObserverInstance.observe(canvas_div); function on_mouse_event_closure(name) { - return function (event) { - return fig.mouse_event(event, name); - }; + /* User Agent sniffing is bad, but WebKit is busted: + * https://bugs.webkit.org/show_bug.cgi?id=144526 + * https://bugs.webkit.org/show_bug.cgi?id=181818 + * The worst that happens here is that they get an extra browser + * selection when dragging, if this check fails to catch them. + */ + var UA = navigator.userAgent; + var isWebKit = /AppleWebKit/.test(UA) && !/Chrome/.test(UA); + if(isWebKit) { + return function (event) { + /* This prevents the web browser from automatically changing to + * the text insertion cursor when the button is pressed. We + * want to control all of the cursor setting manually through + * the 'cursor' event from matplotlib */ + event.preventDefault() + return fig.mouse_event(event, name); + }; + } else { + return function (event) { + return fig.mouse_event(event, name); + }; + } } - rubberband_canvas.addEventListener( + canvas_div.addEventListener( 'mousedown', on_mouse_event_closure('button_press') ); - rubberband_canvas.addEventListener( + canvas_div.addEventListener( 'mouseup', on_mouse_event_closure('button_release') ); - rubberband_canvas.addEventListener( + canvas_div.addEventListener( 'dblclick', on_mouse_event_closure('dblclick') ); // Throttle sequential mouse events to 1 every 20ms. - rubberband_canvas.addEventListener( + canvas_div.addEventListener( 'mousemove', on_mouse_event_closure('motion_notify') ); - rubberband_canvas.addEventListener( + canvas_div.addEventListener( 'mouseenter', on_mouse_event_closure('figure_enter') ); - rubberband_canvas.addEventListener( + canvas_div.addEventListener( 'mouseleave', on_mouse_event_closure('figure_leave') ); @@ -289,7 +321,7 @@ mpl.figure.prototype._init_canvas = function () { }; // Disable right mouse context menu. - this.rubberband_canvas.addEventListener('contextmenu', function (_e) { + canvas_div.addEventListener('contextmenu', function (_e) { event.preventDefault(); return false; }); @@ -444,7 +476,7 @@ mpl.figure.prototype.handle_figure_label = function (fig, msg) { }; mpl.figure.prototype.handle_cursor = function (fig, msg) { - fig.rubberband_canvas.style.cursor = msg['cursor']; + fig.canvas_div.style.cursor = msg['cursor']; }; mpl.figure.prototype.handle_message = function (fig, msg) { @@ -556,30 +588,6 @@ mpl.figure.prototype._make_on_message_function = function (fig) { }; }; -// from https://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas -mpl.findpos = function (e) { - //this section is from http://www.quirksmode.org/js/events_properties.html - var targ; - if (!e) { - e = window.event; - } - if (e.target) { - targ = e.target; - } else if (e.srcElement) { - targ = e.srcElement; - } - if (targ.nodeType === 3) { - // defeat Safari bug - targ = targ.parentNode; - } - - // pageX,Y are the mouse positions relative to the document - var boundingRect = targ.getBoundingClientRect(); - var x = e.pageX - (boundingRect.left + document.body.scrollLeft); - var y = e.pageY - (boundingRect.top + document.body.scrollTop); - - return { x: x, y: y }; -}; /* * return a copy of an object with only non-object keys @@ -596,15 +604,15 @@ function simpleKeys(original) { } mpl.figure.prototype.mouse_event = function (event, name) { - var canvas_pos = mpl.findpos(event); - if (name === 'button_press') { this.canvas.focus(); this.canvas_div.focus(); } - var x = canvas_pos.x * this.ratio; - var y = canvas_pos.y * this.ratio; + // from https://stackoverflow.com/q/1114465 + var boundingRect = this.canvas.getBoundingClientRect(); + var x = (event.clientX - boundingRect.left) * this.ratio; + var y = (event.clientY - boundingRect.top) * this.ratio; this.send_message(name, { x: x, @@ -614,11 +622,6 @@ mpl.figure.prototype.mouse_event = function (event, name) { guiEvent: simpleKeys(event), }); - /* This prevents the web browser from automatically changing to - * the text insertion cursor when the button is pressed. We want - * to control all of the cursor setting manually through the - * 'cursor' event from matplotlib */ - event.preventDefault(); return false; };