ENH Add support in micropip for installing binary wheels. This PR is on top of #2589.

We use `packaging.tags.sys_tags` to get the list of supported tags
then use `packaging.utils.parse_wheel_filename` to get the set of
tags the current wheel implement then check if one of the wheel's
tags is a supported tag. This is a fully accurate check method and
could also catch things like abi3 wheels that are compatible with
multiple Python versions.

This allows micropip to be used to install binary emscripten wheels.
In order to manage the ABI issues, I also changed the wheel platform
tag to `emscripten_2_0_27_wasm32` so we can tell which version of
emscripten the wheel was built with.

The normal way that the platform tag is generated is by substituting
in the `uname` OS name and version info. Emscripten has dummy
information in `uname` instead of any actual version, so I patched
`uname` to report the Emscripten version. See upstream issue:
https://github.com/emscripten-core/emscripten/issues/16977

### Checklists

- [x] Add a [CHANGELOG](https://github.com/pyodide/pyodide/blob/main/docs/project/changelog.md) entry
- [x] Add / update tests

import math

from matplotlib.backend_bases import FigureCanvasBase, NavigationToolbar2, TimerBase

from js import document
from pyodide import (
    add_event_listener,
    clear_interval,
    clear_timeout,
    set_interval,
    set_timeout,
)

try:
    from js import devicePixelRatio as DEVICE_PIXEL_RATIO
except ImportError:
    DEVICE_PIXEL_RATIO = 1


class FigureCanvasWasm(FigureCanvasBase):
    supports_blit = False

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._idle_scheduled = False
        self._id = "matplotlib_" + hex(id(self))[2:]
        self._title = ""
        self._ratio = 1
        matplotlib_figure_styles = self._add_matplotlib_styles()
        if document.getElementById("matplotlib-figure-styles") is None:
            document.head.appendChild(matplotlib_figure_styles)

    def _add_matplotlib_styles(self):
        toolbar_buttons_css_content = """
            button.matplotlib-toolbar-button {
                font-size: 14px;
                color: #495057;
                text-transform: uppercase;
                background: #e9ecef;
                padding: 9px 18px;
                border: 1px solid #fff;
                border-radius: 4px;
                transition-duration: 0.4s;
            }

            button.matplotlib-toolbar-button#text {
                font-family: -apple-system, BlinkMacSystemFont,
                "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
                "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
                sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
                "Segoe UI Symbol";
            }

            button.matplotlib-toolbar-button:hover {
                color: #fff;
                background: #495057;
            }
        """
        toolbar_buttons_style_element = document.createElement("style")
        toolbar_buttons_style_element.id = "matplotlib-figure-styles"
        toolbar_buttons_css = document.createTextNode(toolbar_buttons_css_content)
        toolbar_buttons_style_element.appendChild(toolbar_buttons_css)
        return toolbar_buttons_style_element

    def get_element(self, name):
        """
        Looks up an HTMLElement created for this figure.
        """
        # TODO: Should we store a reference here instead of always looking it
        # up? I'm a little concerned about weird Python/JS
        # cross-memory-management issues...
        return document.getElementById(self._id + name)

    def get_dpi_ratio(self, context):
        """
        Gets the ratio of physical pixels to logical pixels for the given HTML
        Canvas context.

        This is typically 2 on a HiDPI ("Retina") display, and 1 otherwise.
        """
        backing_store = (
            getattr(context, "backingStorePixelRatio", 0)
            or getattr(context, "webkitBackingStorePixel", 0)
            or getattr(context, "mozBackingStorePixelRatio", 0)
            or getattr(context, "msBackingStorePixelRatio", 0)
            or getattr(context, "oBackingStorePixelRatio", 0)
            or getattr(context, "backendStorePixelRatio", 0)
            or 1
        )
        return DEVICE_PIXEL_RATIO / backing_store

    def create_root_element(self):
        # Designed to be overridden by subclasses
        return document.createElement("div")

    def show(self):
        # If we've already shown this canvas elsewhere, don't create a new one,
        # just reuse it and scroll to the existing one.
        existing = self.get_element("")
        if existing is not None:
            self.draw_idle()
            existing.scrollIntoView()
            return

        # Disable the right-click context menu.
        # Doesn't work in all browsers.
        def ignore(event):
            event.preventDefault()
            return False

        # Create the main canvas and determine the physical to logical pixel
        # ratio
        canvas = document.createElement("canvas")
        context = canvas.getContext("2d")
        self._ratio = self.get_dpi_ratio(context)

        width, height = self.get_width_height()
        width *= self._ratio
        height *= self._ratio
        div = self.create_root_element()
        add_event_listener(div, "contextmenu", ignore)
        div.setAttribute(
            "style",
            "margin: 0 auto; text-align: center;" + f"width: {width / self._ratio}px",
        )
        div.id = self._id

        # The top bar
        top = document.createElement("div")
        top.id = self._id + "top"
        top.setAttribute("style", "font-weight: bold; text-align: center")
        top.textContent = self._title
        div.appendChild(top)

        # A div containing two canvases stacked on top of one another:
        #   - The bottom for rendering matplotlib content
        #   - The top for rendering interactive elements, such as the zoom
        #     rubberband
        canvas_div = document.createElement("div")
        canvas_div.setAttribute("style", "position: relative")

        canvas.id = self._id + "canvas"
        canvas.setAttribute("width", width)
        canvas.setAttribute("height", height)
        canvas.setAttribute(
            "style",
            "left: 0; top: 0; z-index: 0; outline: 0;"
            + "width: {}px; height: {}px".format(
                width / self._ratio, height / self._ratio
            ),
        )
        canvas_div.appendChild(canvas)

        rubberband = document.createElement("canvas")
        rubberband.id = self._id + "rubberband"
        rubberband.setAttribute("width", width)
        rubberband.setAttribute("height", height)
        rubberband.setAttribute(
            "style",
            "position: absolute; left: 0; top: 0; z-index: 0; "
            + "outline: 0; width: {}px; height: {}px".format(
                width / self._ratio, height / self._ratio
            ),
        )
        # Canvas must have a "tabindex" attr in order to receive keyboard
        # events
        rubberband.setAttribute("tabindex", "0")
        # Event handlers are added to the canvas "on top", even though most of
        # the activity happens in the canvas below.
        add_event_listener(rubberband, "mousemove", self.onmousemove)
        add_event_listener(rubberband, "mouseup", self.onmouseup)
        add_event_listener(rubberband, "mousedown", self.onmousedown)
        add_event_listener(rubberband, "mouseenter", self.onmouseenter)
        add_event_listener(rubberband, "mouseleave", self.onmouseleave)
        add_event_listener(rubberband, "keyup", self.onkeyup)
        add_event_listener(rubberband, "keydown", self.onkeydown)
        context = rubberband.getContext("2d")
        context.strokeStyle = "#000000"
        context.setLineDash([2, 2])
        canvas_div.appendChild(rubberband)

        div.appendChild(canvas_div)

        # The bottom bar, with toolbar and message display
        bottom = document.createElement("div")
        toolbar = self.toolbar.get_element()
        bottom.appendChild(toolbar)
        message = document.createElement("div")
        message.id = self._id + "message"
        message.setAttribute("style", "min-height: 1.5em")
        bottom.appendChild(message)
        div.appendChild(bottom)

        self.draw()

    def draw(self):
        pass

    def draw_idle(self):
        if not self._idle_scheduled:
            self._idle_scheduled = True
            set_timeout(self.draw, 1)

    def set_message(self, message):
        message_display = self.get_element("message")
        if message_display is not None:
            message_display.textContent = message

    def _convert_mouse_event(self, event):
        width, height = self.get_width_height()
        x = event.offsetX
        y = height - event.offsetY
        button = event.button + 1
        # Disable the right-click context menu in some browsers
        if button == 3:
            event.preventDefault()
            event.stopPropagation()
        if button == 2:
            button = 3
        return x, y, button

    def onmousemove(self, event):
        x, y, button = self._convert_mouse_event(event)
        self.motion_notify_event(x, y, guiEvent=event)

    def onmouseup(self, event):
        x, y, button = self._convert_mouse_event(event)
        self.button_release_event(x, y, button, guiEvent=event)

    def onmousedown(self, event):
        x, y, button = self._convert_mouse_event(event)
        self.button_press_event(x, y, button, guiEvent=event)

    def onmouseenter(self, event):
        # When the mouse is over the figure, get keyboard focus
        self.get_element("rubberband").focus()
        self.enter_notify_event(guiEvent=event)

    def onmouseleave(self, event):
        # When the mouse leaves the figure, drop keyboard focus
        self.get_element("rubberband").blur()
        self.leave_notify_event(guiEvent=event)

    def onscroll(self, event):
        x, y, button = self._convert_mouse_event(event)
        self.scroll_event(x, y, event.deltaX, guiEvent=event)

    _cursor_map = {0: "pointer", 1: "default", 2: "crosshair", 3: "move"}

    def set_cursor(self, cursor):
        rubberband = self.get_element("rubberband")
        if rubberband is not None:
            rubberband.style.cursor = self._cursor_map.get(cursor, 0)

    # http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
    _SHIFT_LUT = {
        59: ":",
        61: "+",
        173: "_",
        186: ":",
        187: "+",
        188: "<",
        189: "_",
        190: ">",
        191: "?",
        192: "~",
        219: "{",
        220: "|",
        221: "}",
        222: '"',
    }

    _LUT = {
        8: "backspace",
        9: "tab",
        13: "enter",
        16: "shift",
        17: "control",
        18: "alt",
        19: "pause",
        20: "caps",
        27: "escape",
        32: " ",
        33: "pageup",
        34: "pagedown",
        35: "end",
        36: "home",
        37: "left",
        38: "up",
        39: "right",
        40: "down",
        45: "insert",
        46: "delete",
        91: "super",
        92: "super",
        93: "select",
        106: "*",
        107: "+",
        109: "-",
        110: ".",
        111: "/",
        144: "num_lock",
        145: "scroll_lock",
        186: ":",
        187: "=",
        188: ",",
        189: "-",
        190: ".",
        191: "/",
        192: "`",
        219: "[",
        220: "\\",
        221: "]",
        222: "'",
    }

    def _convert_key_event(self, event):
        code = int(event.which)
        value = chr(code)
        shift = event.shiftKey and code != 16
        ctrl = event.ctrlKey and code != 17
        alt = event.altKey and code != 18

        # letter keys
        if 65 <= code <= 90:
            if not shift:
                value = value.lower()
            else:
                shift = False
        # number keys
        elif 48 <= code <= 57:
            if shift:
                value = ")!@#$%^&*("[int(value)]
                shift = False
        # function keys
        elif 112 <= code <= 123:
            value = "f%s" % (code - 111)
        # number pad keys
        elif 96 <= code <= 105:
            value = "%s" % (code - 96)
        # keys with shift alternatives
        elif code in self._SHIFT_LUT and shift:
            value = self._SHIFT_LUT[code]
            shift = False
        elif code in self._LUT:
            value = self._LUT[code]

        key = []
        if shift:
            key.append("shift")
        if ctrl:
            key.append("ctrl")
        if alt:
            key.append("alt")
        key.append(value)
        return "+".join(key)

    def onkeydown(self, event):
        key = self._convert_key_event(event)
        self.key_press_event(key, guiEvent=event)

    def onkeyup(self, event):
        key = self._convert_key_event(event)
        self.key_release_event(key, guiEvent=event)

    def get_window_title(self):
        top = self.get_element("top")
        return top.textContent

    def set_window_title(self, title):
        top = self.get_element("top")
        self._title = title
        if top is not None:
            top.textContent = title

    # def resize_event(self):
    #     # TODO
    #     pass

    # def close_event(self):
    #     # TODO
    #     pass

    def draw_rubberband(self, x0, y0, x1, y1):
        rubberband = self.get_element("rubberband")
        width, height = self.get_width_height()
        y0 = height - y0
        y1 = height - y1
        x0 = math.floor(x0) + 0.5
        y0 = math.floor(y0) + 0.5
        x1 = math.floor(x1) + 0.5
        y1 = math.floor(y1) + 0.5
        if x1 < x0:
            x0, x1 = x1, x0
        if y1 < y0:
            y0, y1 = y1, y0
        context = rubberband.getContext("2d")
        context.clearRect(0, 0, width * self._ratio, height * self._ratio)
        context.strokeRect(
            x0 * self._ratio,
            y0 * self._ratio,
            (x1 - x0) * self._ratio,
            (y1 - y0) * self._ratio,
        )

    def remove_rubberband(self):
        rubberband = self.get_element("rubberband")
        width, height = self.get_width_height()
        context = rubberband.getContext("2d")
        context.clearRect(0, 0, width * self._ratio, height * self._ratio)

    def new_timer(self, *args, **kwargs):
        return TimerWasm(*args, **kwargs)


_FONTAWESOME_ICONS = {
    "home": "fa-home",
    "back": "fa-arrow-left",
    "forward": "fa-arrow-right",
    "zoom_to_rect": "fa-search-plus",
    "move": "fa-arrows",
    "download": "fa-download",
    None: None,
}


FILE_TYPES = {"png": "image/png", "svg": "image/svg+xml", "pdf": "application/pdf"}


class NavigationToolbar2Wasm(NavigationToolbar2):
    def _init_toolbar(self):
        pass

    def get_element(self):
        # Create the HTML content for the toolbar
        div = document.createElement("span")

        def add_spacer():
            span = document.createElement("span")
            span.style.minWidth = "16px"
            span.textContent = "\u00a0"
            div.appendChild(span)

        for _text, _tooltip_text, image_file, name_of_method in self.toolitems:
            if image_file in _FONTAWESOME_ICONS:
                if image_file is None:
                    add_spacer()
                else:
                    button = document.createElement("button")
                    button.classList.add("fa")
                    button.classList.add(_FONTAWESOME_ICONS[image_file])
                    button.classList.add("matplotlib-toolbar-button")
                    add_event_listener(button, "click", getattr(self, name_of_method))
                    div.appendChild(button)

        for format, _mimetype in sorted(list(FILE_TYPES.items())):
            button = document.createElement("button")
            button.classList.add("fa")
            button.textContent = format
            button.classList.add("matplotlib-toolbar-button")
            button.id = "text"
            add_event_listener(button, "click", self.ondownload)
            div.appendChild(button)

        return div

    def ondownload(self, event):
        format = event.target.textContent
        self.download(format, FILE_TYPES[format])

    def download(self, format, mimetype):
        pass

    def set_message(self, message):
        self.canvas.set_message(message)

    def set_cursor(self, cursor):
        self.canvas.set_cursor(cursor)

    def draw_rubberband(self, event, x0, y0, x1, y1):
        self.canvas.draw_rubberband(x0, y0, x1, y1)

    def remove_rubberband(self):
        self.canvas.remove_rubberband()


class TimerWasm(TimerBase):
    def _timer_start(self):
        self._timer_stop()
        if self._single:
            self._timer: int | None = set_timeout(self._on_timer, self.interval)
        else:
            self._timer = set_interval(self._on_timer, self.interval)

    def _timer_stop(self):
        if self._timer is None:
            return
        elif self._single:
            clear_timeout(self._timer)
            self._timer = None
        else:
            clear_interval(self._timer)
            self._timer = None

    def _timer_set_interval(self):
        # Only stop and restart it if the timer has already been started
        if self._timer is not None:
            self._timer_stop()
            self._timer_start()

import math

from matplotlib.backend_bases import FigureCanvasBase, NavigationToolbar2, TimerBase

from js import document
from pyodide import (
    add_event_listener,
    clear_interval,
    clear_timeout,
    set_interval,
    set_timeout,
)

try:
    from js import devicePixelRatio as DEVICE_PIXEL_RATIO
except ImportError:
    DEVICE_PIXEL_RATIO = 1


class FigureCanvasWasm(FigureCanvasBase):
    supports_blit = False

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._idle_scheduled = False
        self._id = "matplotlib_" + hex(id(self))[2:]
        self._title = ""
        self._ratio = 1
        matplotlib_figure_styles = self._add_matplotlib_styles()
        if document.getElementById("matplotlib-figure-styles") is None:
            document.head.appendChild(matplotlib_figure_styles)

    def _add_matplotlib_styles(self):
        toolbar_buttons_css_content = """
            button.matplotlib-toolbar-button {
                font-size: 14px;
                color: #495057;
                text-transform: uppercase;
                background: #e9ecef;
                padding: 9px 18px;
                border: 1px solid #fff;
                border-radius: 4px;
                transition-duration: 0.4s;
            }

            button.matplotlib-toolbar-button#text {
                font-family: -apple-system, BlinkMacSystemFont,
                "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
                "Fira Sans", "Droid Sans", "Helvetica Neue", Arial,
                sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
                "Segoe UI Symbol";
            }

            button.matplotlib-toolbar-button:hover {
                color: #fff;
                background: #495057;
            }
        """
        toolbar_buttons_style_element = document.createElement("style")
        toolbar_buttons_style_element.id = "matplotlib-figure-styles"
        toolbar_buttons_css = document.createTextNode(toolbar_buttons_css_content)
        toolbar_buttons_style_element.appendChild(toolbar_buttons_css)
        return toolbar_buttons_style_element

    def get_element(self, name):
        """
        Looks up an HTMLElement created for this figure.
        """
        # TODO: Should we store a reference here instead of always looking it
        # up? I'm a little concerned about weird Python/JS
        # cross-memory-management issues...
        return document.getElementById(self._id + name)

    def get_dpi_ratio(self, context):
        """
        Gets the ratio of physical pixels to logical pixels for the given HTML
        Canvas context.

        This is typically 2 on a HiDPI ("Retina") display, and 1 otherwise.
        """
        backing_store = (
            getattr(context, "backingStorePixelRatio", 0)
            or getattr(context, "webkitBackingStorePixel", 0)
            or getattr(context, "mozBackingStorePixelRatio", 0)
            or getattr(context, "msBackingStorePixelRatio", 0)
            or getattr(context, "oBackingStorePixelRatio", 0)
            or getattr(context, "backendStorePixelRatio", 0)
            or 1
        )
        return DEVICE_PIXEL_RATIO / backing_store

    def create_root_element(self):
        # Designed to be overridden by subclasses
        return document.createElement("div")

    def show(self):
        # If we've already shown this canvas elsewhere, don't create a new one,
        # just reuse it and scroll to the existing one.
        existing = self.get_element("")
        if existing is not None:
            self.draw_idle()
            existing.scrollIntoView()
            return

        # Disable the right-click context menu.
        # Doesn't work in all browsers.
        def ignore(event):
            event.preventDefault()
            return False

        # Create the main canvas and determine the physical to logical pixel
        # ratio
        canvas = document.createElement("canvas")
        context = canvas.getContext("2d")
        self._ratio = self.get_dpi_ratio(context)

        width, height = self.get_width_height()
        width *= self._ratio
        height *= self._ratio
        div = self.create_root_element()
        add_event_listener(div, "contextmenu", ignore)
        div.setAttribute(
            "style",
            "margin: 0 auto; text-align: center;" + f"width: {width / self._ratio}px",
        )
        div.id = self._id

        # The top bar
        top = document.createElement("div")
        top.id = self._id + "top"
        top.setAttribute("style", "font-weight: bold; text-align: center")
        top.textContent = self._title
        div.appendChild(top)

        # A div containing two canvases stacked on top of one another:
        #   - The bottom for rendering matplotlib content
        #   - The top for rendering interactive elements, such as the zoom
        #     rubberband
        canvas_div = document.createElement("div")
        canvas_div.setAttribute("style", "position: relative")

        canvas.id = self._id + "canvas"
        canvas.setAttribute("width", width)
        canvas.setAttribute("height", height)
        canvas.setAttribute(
            "style",
            "left: 0; top: 0; z-index: 0; outline: 0;"
            + "width: {}px; height: {}px".format(
                width / self._ratio, height / self._ratio
            ),
        )
        canvas_div.appendChild(canvas)

        rubberband = document.createElement("canvas")
        rubberband.id = self._id + "rubberband"
        rubberband.setAttribute("width", width)
        rubberband.setAttribute("height", height)
        rubberband.setAttribute(
            "style",
            "position: absolute; left: 0; top: 0; z-index: 0; "
            + "outline: 0; width: {}px; height: {}px".format(
                width / self._ratio, height / self._ratio
            ),
        )
        # Canvas must have a "tabindex" attr in order to receive keyboard
        # events
        rubberband.setAttribute("tabindex", "0")
        # Event handlers are added to the canvas "on top", even though most of
        # the activity happens in the canvas below.
        add_event_listener(rubberband, "mousemove", self.onmousemove)
        add_event_listener(rubberband, "mouseup", self.onmouseup)
        add_event_listener(rubberband, "mousedown", self.onmousedown)
        add_event_listener(rubberband, "mouseenter", self.onmouseenter)
        add_event_listener(rubberband, "mouseleave", self.onmouseleave)
        add_event_listener(rubberband, "keyup", self.onkeyup)
        add_event_listener(rubberband, "keydown", self.onkeydown)
        context = rubberband.getContext("2d")
        context.strokeStyle = "#000000"
        context.setLineDash([2, 2])
        canvas_div.appendChild(rubberband)

        div.appendChild(canvas_div)

        # The bottom bar, with toolbar and message display
        bottom = document.createElement("div")
        toolbar = self.toolbar.get_element()
        bottom.appendChild(toolbar)
        message = document.createElement("div")
        message.id = self._id + "message"
        message.setAttribute("style", "min-height: 1.5em")
        bottom.appendChild(message)
        div.appendChild(bottom)

        self.draw()

    def draw(self):
        pass

    def draw_idle(self):
        if not self._idle_scheduled:
            self._idle_scheduled = True
            set_timeout(self.draw, 1)

    def set_message(self, message):
        message_display = self.get_element("message")
        if message_display is not None:
            message_display.textContent = message

    def _convert_mouse_event(self, event):
        width, height = self.get_width_height()
        x = event.offsetX
        y = height - event.offsetY
        button = event.button + 1
        # Disable the right-click context menu in some browsers
        if button == 3:
            event.preventDefault()
            event.stopPropagation()
        if button == 2:
            button = 3
        return x, y, button

    def onmousemove(self, event):
        x, y, button = self._convert_mouse_event(event)
        self.motion_notify_event(x, y, guiEvent=event)

    def onmouseup(self, event):
        x, y, button = self._convert_mouse_event(event)
        self.button_release_event(x, y, button, guiEvent=event)

    def onmousedown(self, event):
        x, y, button = self._convert_mouse_event(event)
        self.button_press_event(x, y, button, guiEvent=event)

    def onmouseenter(self, event):
        # When the mouse is over the figure, get keyboard focus
        self.get_element("rubberband").focus()
        self.enter_notify_event(guiEvent=event)

    def onmouseleave(self, event):
        # When the mouse leaves the figure, drop keyboard focus
        self.get_element("rubberband").blur()
        self.leave_notify_event(guiEvent=event)

    def onscroll(self, event):
        x, y, button = self._convert_mouse_event(event)
        self.scroll_event(x, y, event.deltaX, guiEvent=event)

    _cursor_map = {0: "pointer", 1: "default", 2: "crosshair", 3: "move"}

    def set_cursor(self, cursor):
        rubberband = self.get_element("rubberband")
        if rubberband is not None:
            rubberband.style.cursor = self._cursor_map.get(cursor, 0)

    # http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
    _SHIFT_LUT = {
        59: ":",
        61: "+",
        173: "_",
        186: ":",
        187: "+",
        188: "<",
        189: "_",
        190: ">",
        191: "?",
        192: "~",
        219: "{",
        220: "|",
        221: "}",
        222: '"',
    }

    _LUT = {
        8: "backspace",
        9: "tab",
        13: "enter",
        16: "shift",
        17: "control",
        18: "alt",
        19: "pause",
        20: "caps",
        27: "escape",
        32: " ",
        33: "pageup",
        34: "pagedown",
        35: "end",
        36: "home",
        37: "left",
        38: "up",
        39: "right",
        40: "down",
        45: "insert",
        46: "delete",
        91: "super",
        92: "super",
        93: "select",
        106: "*",
        107: "+",
        109: "-",
        110: ".",
        111: "/",
        144: "num_lock",
        145: "scroll_lock",
        186: ":",
        187: "=",
        188: ",",
        189: "-",
        190: ".",
        191: "/",
        192: "`",
        219: "[",
        220: "\\",
        221: "]",
        222: "'",
    }

    def _convert_key_event(self, event):
        code = int(event.which)
        value = chr(code)
        shift = event.shiftKey and code != 16
        ctrl = event.ctrlKey and code != 17
        alt = event.altKey and code != 18

        # letter keys
        if 65 <= code <= 90:
            if not shift:
                value = value.lower()
            else:
                shift = False
        # number keys
        elif 48 <= code <= 57:
            if shift:
                value = ")!@#$%^&*("[int(value)]
                shift = False
        # function keys
        elif 112 <= code <= 123:
            value = "f%s" % (code - 111)
        # number pad keys
        elif 96 <= code <= 105:
            value = "%s" % (code - 96)
        # keys with shift alternatives
        elif code in self._SHIFT_LUT and shift:
            value = self._SHIFT_LUT[code]
            shift = False
        elif code in self._LUT:
            value = self._LUT[code]

        key = []
        if shift:
            key.append("shift")
        if ctrl:
            key.append("ctrl")
        if alt:
            key.append("alt")
        key.append(value)
        return "+".join(key)

    def onkeydown(self, event):
        key = self._convert_key_event(event)
        self.key_press_event(key, guiEvent=event)

    def onkeyup(self, event):
        key = self._convert_key_event(event)
        self.key_release_event(key, guiEvent=event)

    def get_window_title(self):
        top = self.get_element("top")
        return top.textContent

    def set_window_title(self, title):
        top = self.get_element("top")
        self._title = title
        if top is not None:
            top.textContent = title

    # def resize_event(self):
    #     # TODO
    #     pass

    # def close_event(self):
    #     # TODO
    #     pass

    def draw_rubberband(self, x0, y0, x1, y1):
        rubberband = self.get_element("rubberband")
        width, height = self.get_width_height()
        y0 = height - y0
        y1 = height - y1
        x0 = math.floor(x0) + 0.5
        y0 = math.floor(y0) + 0.5
        x1 = math.floor(x1) + 0.5
        y1 = math.floor(y1) + 0.5
        if x1 < x0:
            x0, x1 = x1, x0
        if y1 < y0:
            y0, y1 = y1, y0
        context = rubberband.getContext("2d")
        context.clearRect(0, 0, width * self._ratio, height * self._ratio)
        context.strokeRect(
            x0 * self._ratio,
            y0 * self._ratio,
            (x1 - x0) * self._ratio,
            (y1 - y0) * self._ratio,
        )

    def remove_rubberband(self):
        rubberband = self.get_element("rubberband")
        width, height = self.get_width_height()
        context = rubberband.getContext("2d")
        context.clearRect(0, 0, width * self._ratio, height * self._ratio)

    def new_timer(self, *args, **kwargs):
        return TimerWasm(*args, **kwargs)


_FONTAWESOME_ICONS = {
    "home": "fa-home",
    "back": "fa-arrow-left",
    "forward": "fa-arrow-right",
    "zoom_to_rect": "fa-search-plus",
    "move": "fa-arrows",
    "download": "fa-download",
    None: None,
}


FILE_TYPES = {"png": "image/png", "svg": "image/svg+xml", "pdf": "application/pdf"}


class NavigationToolbar2Wasm(NavigationToolbar2):
    def _init_toolbar(self):
        pass

    def get_element(self):
        # Create the HTML content for the toolbar
        div = document.createElement("span")

        def add_spacer():
            span = document.createElement("span")
            span.style.minWidth = "16px"
            span.textContent = "\u00a0"
            div.appendChild(span)

        for _text, _tooltip_text, image_file, name_of_method in self.toolitems:
            if image_file in _FONTAWESOME_ICONS:
                if image_file is None:
                    add_spacer()
                else:
                    button = document.createElement("button")
                    button.classList.add("fa")
                    button.classList.add(_FONTAWESOME_ICONS[image_file])
                    button.classList.add("matplotlib-toolbar-button")
                    add_event_listener(button, "click", getattr(self, name_of_method))
                    div.appendChild(button)

        for format, _mimetype in sorted(list(FILE_TYPES.items())):
            button = document.createElement("button")
            button.classList.add("fa")
            button.textContent = format
            button.classList.add("matplotlib-toolbar-button")
            button.id = "text"
            add_event_listener(button, "click", self.ondownload)
            div.appendChild(button)

        return div

    def ondownload(self, event):
        format = event.target.textContent
        self.download(format, FILE_TYPES[format])

    def download(self, format, mimetype):
        pass

    def set_message(self, message):
        self.canvas.set_message(message)

    def set_cursor(self, cursor):
        self.canvas.set_cursor(cursor)

    def draw_rubberband(self, event, x0, y0, x1, y1):
        self.canvas.draw_rubberband(x0, y0, x1, y1)

    def remove_rubberband(self):
        self.canvas.remove_rubberband()


class TimerWasm(TimerBase):
    def _timer_start(self):
        self._timer_stop()
        if self._single:
            self._timer: int | None = set_timeout(self._on_timer, self.interval)
        else:
            self._timer = set_interval(self._on_timer, self.interval)

    def _timer_stop(self):
        if self._timer is None:
            return
        elif self._single:
            clear_timeout(self._timer)
            self._timer = None
        else:
            clear_interval(self._timer)
            self._timer = None

    def _timer_set_interval(self):
        # Only stop and restart it if the timer has already been started
        if self._timer is not None:
            self._timer_stop()
            self._timer_start()
