+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_xdg_shell The xdg_shell protocol
+ * @section page_ifaces_xdg_shell Interfaces
+ * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces
+ * - @subpage page_iface_xdg_positioner - child surface positioner
+ * - @subpage page_iface_xdg_surface - desktop user interface surface base interface
+ * - @subpage page_iface_xdg_toplevel - toplevel surface
+ * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus
+ * @section page_copyright_xdg_shell Copyright
+ *
+ *
+ * Copyright Ā© 2008-2013 Kristian HĆøgsberg
+ * Copyright Ā© 2013 Rafael Antognolli
+ * Copyright Ā© 2013 Jasper St. Pierre
+ * Copyright Ā© 2010-2013 Intel Corporation
+ * Copyright Ā© 2015-2017 Samsung Electronics Co., Ltd
+ * Copyright Ā© 2015-2017 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ */
+struct wl_output;
+struct wl_seat;
+struct wl_surface;
+struct xdg_popup;
+struct xdg_positioner;
+struct xdg_surface;
+struct xdg_toplevel;
+struct xdg_wm_base;
+
+#ifndef XDG_WM_BASE_INTERFACE
+#define XDG_WM_BASE_INTERFACE
+/**
+ * @page page_iface_xdg_wm_base xdg_wm_base
+ * @section page_iface_xdg_wm_base_desc Description
+ *
+ * The xdg_wm_base interface is exposed as a global object enabling clients
+ * to turn their wl_surfaces into windows in a desktop environment. It
+ * defines the basic functionality needed for clients and the compositor to
+ * create windows that can be dragged, resized, maximized, etc, as well as
+ * creating transient windows such as popup menus.
+ * @section page_iface_xdg_wm_base_api API
+ * See @ref iface_xdg_wm_base.
+ */
+/**
+ * @defgroup iface_xdg_wm_base The xdg_wm_base interface
+ *
+ * The xdg_wm_base interface is exposed as a global object enabling clients
+ * to turn their wl_surfaces into windows in a desktop environment. It
+ * defines the basic functionality needed for clients and the compositor to
+ * create windows that can be dragged, resized, maximized, etc, as well as
+ * creating transient windows such as popup menus.
+ */
+extern const struct wl_interface xdg_wm_base_interface;
+#endif
+#ifndef XDG_POSITIONER_INTERFACE
+#define XDG_POSITIONER_INTERFACE
+/**
+ * @page page_iface_xdg_positioner xdg_positioner
+ * @section page_iface_xdg_positioner_desc Description
+ *
+ * The xdg_positioner provides a collection of rules for the placement of a
+ * child surface relative to a parent surface. Rules can be defined to ensure
+ * the child surface remains within the visible area's borders, and to
+ * specify how the child surface changes its position, such as sliding along
+ * an axis, or flipping around a rectangle. These positioner-created rules are
+ * constrained by the requirement that a child surface must intersect with or
+ * be at least partially adjacent to its parent surface.
+ *
+ * See the various requests for details about possible rules.
+ *
+ * At the time of the request, the compositor makes a copy of the rules
+ * specified by the xdg_positioner. Thus, after the request is complete the
+ * xdg_positioner object can be destroyed or reused; further changes to the
+ * object will have no effect on previous usages.
+ *
+ * For an xdg_positioner object to be considered complete, it must have a
+ * non-zero size set by set_size, and a non-zero anchor rectangle set by
+ * set_anchor_rect. Passing an incomplete xdg_positioner object when
+ * positioning a surface raises an invalid_positioner error.
+ * @section page_iface_xdg_positioner_api API
+ * See @ref iface_xdg_positioner.
+ */
+/**
+ * @defgroup iface_xdg_positioner The xdg_positioner interface
+ *
+ * The xdg_positioner provides a collection of rules for the placement of a
+ * child surface relative to a parent surface. Rules can be defined to ensure
+ * the child surface remains within the visible area's borders, and to
+ * specify how the child surface changes its position, such as sliding along
+ * an axis, or flipping around a rectangle. These positioner-created rules are
+ * constrained by the requirement that a child surface must intersect with or
+ * be at least partially adjacent to its parent surface.
+ *
+ * See the various requests for details about possible rules.
+ *
+ * At the time of the request, the compositor makes a copy of the rules
+ * specified by the xdg_positioner. Thus, after the request is complete the
+ * xdg_positioner object can be destroyed or reused; further changes to the
+ * object will have no effect on previous usages.
+ *
+ * For an xdg_positioner object to be considered complete, it must have a
+ * non-zero size set by set_size, and a non-zero anchor rectangle set by
+ * set_anchor_rect. Passing an incomplete xdg_positioner object when
+ * positioning a surface raises an invalid_positioner error.
+ */
+extern const struct wl_interface xdg_positioner_interface;
+#endif
+#ifndef XDG_SURFACE_INTERFACE
+#define XDG_SURFACE_INTERFACE
+/**
+ * @page page_iface_xdg_surface xdg_surface
+ * @section page_iface_xdg_surface_desc Description
+ *
+ * An interface that may be implemented by a wl_surface, for
+ * implementations that provide a desktop-style user interface.
+ *
+ * It provides a base set of functionality required to construct user
+ * interface elements requiring management by the compositor, such as
+ * toplevel windows, menus, etc. The types of functionality are split into
+ * xdg_surface roles.
+ *
+ * Creating an xdg_surface does not set the role for a wl_surface. In order
+ * to map an xdg_surface, the client must create a role-specific object
+ * using, e.g., get_toplevel, get_popup. The wl_surface for any given
+ * xdg_surface can have at most one role, and may not be assigned any role
+ * not based on xdg_surface.
+ *
+ * A role must be assigned before any other requests are made to the
+ * xdg_surface object.
+ *
+ * The client must call wl_surface.commit on the corresponding wl_surface
+ * for the xdg_surface state to take effect.
+ *
+ * Creating an xdg_surface from a wl_surface which has a buffer attached or
+ * committed is a client error, and any attempts by a client to attach or
+ * manipulate a buffer prior to the first xdg_surface.configure call must
+ * also be treated as errors.
+ *
+ * After creating a role-specific object and setting it up, the client must
+ * perform an initial commit without any buffer attached. The compositor
+ * will reply with an xdg_surface.configure event. The client must
+ * acknowledge it and is then allowed to attach a buffer to map the surface.
+ *
+ * Mapping an xdg_surface-based role surface is defined as making it
+ * possible for the surface to be shown by the compositor. Note that
+ * a mapped surface is not guaranteed to be visible once it is mapped.
+ *
+ * For an xdg_surface to be mapped by the compositor, the following
+ * conditions must be met:
+ * (1) the client has assigned an xdg_surface-based role to the surface
+ * (2) the client has set and committed the xdg_surface state and the
+ * role-dependent state to the surface
+ * (3) the client has committed a buffer to the surface
+ *
+ * A newly-unmapped surface is considered to have met condition (1) out
+ * of the 3 required conditions for mapping a surface if its role surface
+ * has not been destroyed, i.e. the client must perform the initial commit
+ * again before attaching a buffer.
+ * @section page_iface_xdg_surface_api API
+ * See @ref iface_xdg_surface.
+ */
+/**
+ * @defgroup iface_xdg_surface The xdg_surface interface
+ *
+ * An interface that may be implemented by a wl_surface, for
+ * implementations that provide a desktop-style user interface.
+ *
+ * It provides a base set of functionality required to construct user
+ * interface elements requiring management by the compositor, such as
+ * toplevel windows, menus, etc. The types of functionality are split into
+ * xdg_surface roles.
+ *
+ * Creating an xdg_surface does not set the role for a wl_surface. In order
+ * to map an xdg_surface, the client must create a role-specific object
+ * using, e.g., get_toplevel, get_popup. The wl_surface for any given
+ * xdg_surface can have at most one role, and may not be assigned any role
+ * not based on xdg_surface.
+ *
+ * A role must be assigned before any other requests are made to the
+ * xdg_surface object.
+ *
+ * The client must call wl_surface.commit on the corresponding wl_surface
+ * for the xdg_surface state to take effect.
+ *
+ * Creating an xdg_surface from a wl_surface which has a buffer attached or
+ * committed is a client error, and any attempts by a client to attach or
+ * manipulate a buffer prior to the first xdg_surface.configure call must
+ * also be treated as errors.
+ *
+ * After creating a role-specific object and setting it up, the client must
+ * perform an initial commit without any buffer attached. The compositor
+ * will reply with an xdg_surface.configure event. The client must
+ * acknowledge it and is then allowed to attach a buffer to map the surface.
+ *
+ * Mapping an xdg_surface-based role surface is defined as making it
+ * possible for the surface to be shown by the compositor. Note that
+ * a mapped surface is not guaranteed to be visible once it is mapped.
+ *
+ * For an xdg_surface to be mapped by the compositor, the following
+ * conditions must be met:
+ * (1) the client has assigned an xdg_surface-based role to the surface
+ * (2) the client has set and committed the xdg_surface state and the
+ * role-dependent state to the surface
+ * (3) the client has committed a buffer to the surface
+ *
+ * A newly-unmapped surface is considered to have met condition (1) out
+ * of the 3 required conditions for mapping a surface if its role surface
+ * has not been destroyed, i.e. the client must perform the initial commit
+ * again before attaching a buffer.
+ */
+extern const struct wl_interface xdg_surface_interface;
+#endif
+#ifndef XDG_TOPLEVEL_INTERFACE
+#define XDG_TOPLEVEL_INTERFACE
+/**
+ * @page page_iface_xdg_toplevel xdg_toplevel
+ * @section page_iface_xdg_toplevel_desc Description
+ *
+ * This interface defines an xdg_surface role which allows a surface to,
+ * among other things, set window-like properties such as maximize,
+ * fullscreen, and minimize, set application-specific metadata like title and
+ * id, and well as trigger user interactive operations such as interactive
+ * resize and move.
+ *
+ * Unmapping an xdg_toplevel means that the surface cannot be shown
+ * by the compositor until it is explicitly mapped again.
+ * All active operations (e.g., move, resize) are canceled and all
+ * attributes (e.g. title, state, stacking, ...) are discarded for
+ * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to
+ * the state it had right after xdg_surface.get_toplevel. The client
+ * can re-map the toplevel by perfoming a commit without any buffer
+ * attached, waiting for a configure event and handling it as usual (see
+ * xdg_surface description).
+ *
+ * Attaching a null buffer to a toplevel unmaps the surface.
+ * @section page_iface_xdg_toplevel_api API
+ * See @ref iface_xdg_toplevel.
+ */
+/**
+ * @defgroup iface_xdg_toplevel The xdg_toplevel interface
+ *
+ * This interface defines an xdg_surface role which allows a surface to,
+ * among other things, set window-like properties such as maximize,
+ * fullscreen, and minimize, set application-specific metadata like title and
+ * id, and well as trigger user interactive operations such as interactive
+ * resize and move.
+ *
+ * Unmapping an xdg_toplevel means that the surface cannot be shown
+ * by the compositor until it is explicitly mapped again.
+ * All active operations (e.g., move, resize) are canceled and all
+ * attributes (e.g. title, state, stacking, ...) are discarded for
+ * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to
+ * the state it had right after xdg_surface.get_toplevel. The client
+ * can re-map the toplevel by perfoming a commit without any buffer
+ * attached, waiting for a configure event and handling it as usual (see
+ * xdg_surface description).
+ *
+ * Attaching a null buffer to a toplevel unmaps the surface.
+ */
+extern const struct wl_interface xdg_toplevel_interface;
+#endif
+#ifndef XDG_POPUP_INTERFACE
+#define XDG_POPUP_INTERFACE
+/**
+ * @page page_iface_xdg_popup xdg_popup
+ * @section page_iface_xdg_popup_desc Description
+ *
+ * A popup surface is a short-lived, temporary surface. It can be used to
+ * implement for example menus, popovers, tooltips and other similar user
+ * interface concepts.
+ *
+ * A popup can be made to take an explicit grab. See xdg_popup.grab for
+ * details.
+ *
+ * When the popup is dismissed, a popup_done event will be sent out, and at
+ * the same time the surface will be unmapped. See the xdg_popup.popup_done
+ * event for details.
+ *
+ * Explicitly destroying the xdg_popup object will also dismiss the popup and
+ * unmap the surface. Clients that want to dismiss the popup when another
+ * surface of their own is clicked should dismiss the popup using the destroy
+ * request.
+ *
+ * A newly created xdg_popup will be stacked on top of all previously created
+ * xdg_popup surfaces associated with the same xdg_toplevel.
+ *
+ * The parent of an xdg_popup must be mapped (see the xdg_surface
+ * description) before the xdg_popup itself.
+ *
+ * The client must call wl_surface.commit on the corresponding wl_surface
+ * for the xdg_popup state to take effect.
+ * @section page_iface_xdg_popup_api API
+ * See @ref iface_xdg_popup.
+ */
+/**
+ * @defgroup iface_xdg_popup The xdg_popup interface
+ *
+ * A popup surface is a short-lived, temporary surface. It can be used to
+ * implement for example menus, popovers, tooltips and other similar user
+ * interface concepts.
+ *
+ * A popup can be made to take an explicit grab. See xdg_popup.grab for
+ * details.
+ *
+ * When the popup is dismissed, a popup_done event will be sent out, and at
+ * the same time the surface will be unmapped. See the xdg_popup.popup_done
+ * event for details.
+ *
+ * Explicitly destroying the xdg_popup object will also dismiss the popup and
+ * unmap the surface. Clients that want to dismiss the popup when another
+ * surface of their own is clicked should dismiss the popup using the destroy
+ * request.
+ *
+ * A newly created xdg_popup will be stacked on top of all previously created
+ * xdg_popup surfaces associated with the same xdg_toplevel.
+ *
+ * The parent of an xdg_popup must be mapped (see the xdg_surface
+ * description) before the xdg_popup itself.
+ *
+ * The client must call wl_surface.commit on the corresponding wl_surface
+ * for the xdg_popup state to take effect.
+ */
+extern const struct wl_interface xdg_popup_interface;
+#endif
+
+#ifndef XDG_WM_BASE_ERROR_ENUM
+#define XDG_WM_BASE_ERROR_ENUM
+enum xdg_wm_base_error {
+ /**
+ * given wl_surface has another role
+ */
+ XDG_WM_BASE_ERROR_ROLE = 0,
+ /**
+ * xdg_wm_base was destroyed before children
+ */
+ XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1,
+ /**
+ * the client tried to map or destroy a non-topmost popup
+ */
+ XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2,
+ /**
+ * the client specified an invalid popup parent surface
+ */
+ XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3,
+ /**
+ * the client provided an invalid surface state
+ */
+ XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4,
+ /**
+ * the client provided an invalid positioner
+ */
+ XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5,
+ /**
+ * the client didnāt respond to a ping event in time
+ */
+ XDG_WM_BASE_ERROR_UNRESPONSIVE = 6,
+};
+#endif /* XDG_WM_BASE_ERROR_ENUM */
+
+/**
+ * @ingroup iface_xdg_wm_base
+ * @struct xdg_wm_base_listener
+ */
+struct xdg_wm_base_listener {
+ /**
+ * check if the client is alive
+ *
+ * The ping event asks the client if it's still alive. Pass the
+ * serial specified in the event back to the compositor by sending
+ * a "pong" request back with the specified serial. See
+ * xdg_wm_base.pong.
+ *
+ * Compositors can use this to determine if the client is still
+ * alive. It's unspecified what will happen if the client doesn't
+ * respond to the ping request, or in what timeframe. Clients
+ * should try to respond in a reasonable amount of time. The
+ * āunresponsiveā error is provided for compositors that wish
+ * to disconnect unresponsive clients.
+ *
+ * A compositor is free to ping in any way it wants, but a client
+ * must always respond to any xdg_wm_base object it created.
+ * @param serial pass this to the pong request
+ */
+ void (*ping)(void *data,
+ struct xdg_wm_base *xdg_wm_base,
+ uint32_t serial);
+};
+
+/**
+ * @ingroup iface_xdg_wm_base
+ */
+static inline int
+xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base,
+ const struct xdg_wm_base_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) xdg_wm_base,
+ (void (**)(void)) listener, data);
+}
+
+#define XDG_WM_BASE_DESTROY 0
+#define XDG_WM_BASE_CREATE_POSITIONER 1
+#define XDG_WM_BASE_GET_XDG_SURFACE 2
+#define XDG_WM_BASE_PONG 3
+
+/**
+ * @ingroup iface_xdg_wm_base
+ */
+#define XDG_WM_BASE_PING_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_xdg_wm_base
+ */
+#define XDG_WM_BASE_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_wm_base
+ */
+#define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_wm_base
+ */
+#define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_wm_base
+ */
+#define XDG_WM_BASE_PONG_SINCE_VERSION 1
+
+/** @ingroup iface_xdg_wm_base */
+static inline void
+xdg_wm_base_set_user_data(struct xdg_wm_base *xdg_wm_base, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) xdg_wm_base, user_data);
+}
+
+/** @ingroup iface_xdg_wm_base */
+static inline void *
+xdg_wm_base_get_user_data(struct xdg_wm_base *xdg_wm_base)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) xdg_wm_base);
+}
+
+static inline uint32_t
+xdg_wm_base_get_version(struct xdg_wm_base *xdg_wm_base)
+{
+ return wl_proxy_get_version((struct wl_proxy *) xdg_wm_base);
+}
+
+/**
+ * @ingroup iface_xdg_wm_base
+ *
+ * Destroy this xdg_wm_base object.
+ *
+ * Destroying a bound xdg_wm_base object while there are surfaces
+ * still alive created by this xdg_wm_base object instance is illegal
+ * and will result in a defunct_surfaces error.
+ */
+static inline void
+xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base,
+ XDG_WM_BASE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_xdg_wm_base
+ *
+ * Create a positioner object. A positioner object is used to position
+ * surfaces relative to some parent surface. See the interface description
+ * and xdg_surface.get_popup for details.
+ */
+static inline struct xdg_positioner *
+xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base)
+{
+ struct wl_proxy *id;
+
+ id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base,
+ XDG_WM_BASE_CREATE_POSITIONER, &xdg_positioner_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL);
+
+ return (struct xdg_positioner *) id;
+}
+
+/**
+ * @ingroup iface_xdg_wm_base
+ *
+ * This creates an xdg_surface for the given surface. While xdg_surface
+ * itself is not a role, the corresponding surface may only be assigned
+ * a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is
+ * illegal to create an xdg_surface for a wl_surface which already has an
+ * assigned role and this will result in a role error.
+ *
+ * This creates an xdg_surface for the given surface. An xdg_surface is
+ * used as basis to define a role to a given surface, such as xdg_toplevel
+ * or xdg_popup. It also manages functionality shared between xdg_surface
+ * based surface roles.
+ *
+ * See the documentation of xdg_surface for more details about what an
+ * xdg_surface is and how it is used.
+ */
+static inline struct xdg_surface *
+xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface *surface)
+{
+ struct wl_proxy *id;
+
+ id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base,
+ XDG_WM_BASE_GET_XDG_SURFACE, &xdg_surface_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL, surface);
+
+ return (struct xdg_surface *) id;
+}
+
+/**
+ * @ingroup iface_xdg_wm_base
+ *
+ * A client must respond to a ping event with a pong request or
+ * the client may be deemed unresponsive. See xdg_wm_base.ping
+ * and xdg_wm_base.error.unresponsive.
+ */
+static inline void
+xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base,
+ XDG_WM_BASE_PONG, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, serial);
+}
+
+#ifndef XDG_POSITIONER_ERROR_ENUM
+#define XDG_POSITIONER_ERROR_ENUM
+enum xdg_positioner_error {
+ /**
+ * invalid input provided
+ */
+ XDG_POSITIONER_ERROR_INVALID_INPUT = 0,
+};
+#endif /* XDG_POSITIONER_ERROR_ENUM */
+
+#ifndef XDG_POSITIONER_ANCHOR_ENUM
+#define XDG_POSITIONER_ANCHOR_ENUM
+enum xdg_positioner_anchor {
+ XDG_POSITIONER_ANCHOR_NONE = 0,
+ XDG_POSITIONER_ANCHOR_TOP = 1,
+ XDG_POSITIONER_ANCHOR_BOTTOM = 2,
+ XDG_POSITIONER_ANCHOR_LEFT = 3,
+ XDG_POSITIONER_ANCHOR_RIGHT = 4,
+ XDG_POSITIONER_ANCHOR_TOP_LEFT = 5,
+ XDG_POSITIONER_ANCHOR_BOTTOM_LEFT = 6,
+ XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7,
+ XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8,
+};
+#endif /* XDG_POSITIONER_ANCHOR_ENUM */
+
+#ifndef XDG_POSITIONER_GRAVITY_ENUM
+#define XDG_POSITIONER_GRAVITY_ENUM
+enum xdg_positioner_gravity {
+ XDG_POSITIONER_GRAVITY_NONE = 0,
+ XDG_POSITIONER_GRAVITY_TOP = 1,
+ XDG_POSITIONER_GRAVITY_BOTTOM = 2,
+ XDG_POSITIONER_GRAVITY_LEFT = 3,
+ XDG_POSITIONER_GRAVITY_RIGHT = 4,
+ XDG_POSITIONER_GRAVITY_TOP_LEFT = 5,
+ XDG_POSITIONER_GRAVITY_BOTTOM_LEFT = 6,
+ XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7,
+ XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8,
+};
+#endif /* XDG_POSITIONER_GRAVITY_ENUM */
+
+#ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM
+#define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM
+/**
+ * @ingroup iface_xdg_positioner
+ * constraint adjustments
+ *
+ * The constraint adjustment value define ways the compositor will adjust
+ * the position of the surface, if the unadjusted position would result
+ * in the surface being partly constrained.
+ *
+ * Whether a surface is considered 'constrained' is left to the compositor
+ * to determine. For example, the surface may be partly outside the
+ * compositor's defined 'work area', thus necessitating the child surface's
+ * position be adjusted until it is entirely inside the work area.
+ *
+ * The adjustments can be combined, according to a defined precedence: 1)
+ * Flip, 2) Slide, 3) Resize.
+ */
+enum xdg_positioner_constraint_adjustment {
+ /**
+ * don't move the child surface when constrained
+ *
+ * Don't alter the surface position even if it is constrained on
+ * some axis, for example partially outside the edge of an output.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0,
+ /**
+ * move along the x axis until unconstrained
+ *
+ * Slide the surface along the x axis until it is no longer
+ * constrained.
+ *
+ * First try to slide towards the direction of the gravity on the x
+ * axis until either the edge in the opposite direction of the
+ * gravity is unconstrained or the edge in the direction of the
+ * gravity is constrained.
+ *
+ * Then try to slide towards the opposite direction of the gravity
+ * on the x axis until either the edge in the direction of the
+ * gravity is unconstrained or the edge in the opposite direction
+ * of the gravity is constrained.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1,
+ /**
+ * move along the y axis until unconstrained
+ *
+ * Slide the surface along the y axis until it is no longer
+ * constrained.
+ *
+ * First try to slide towards the direction of the gravity on the y
+ * axis until either the edge in the opposite direction of the
+ * gravity is unconstrained or the edge in the direction of the
+ * gravity is constrained.
+ *
+ * Then try to slide towards the opposite direction of the gravity
+ * on the y axis until either the edge in the direction of the
+ * gravity is unconstrained or the edge in the opposite direction
+ * of the gravity is constrained.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2,
+ /**
+ * invert the anchor and gravity on the x axis
+ *
+ * Invert the anchor and gravity on the x axis if the surface is
+ * constrained on the x axis. For example, if the left edge of the
+ * surface is constrained, the gravity is 'left' and the anchor is
+ * 'left', change the gravity to 'right' and the anchor to 'right'.
+ *
+ * If the adjusted position also ends up being constrained, the
+ * resulting position of the flip_x adjustment will be the one
+ * before the adjustment.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4,
+ /**
+ * invert the anchor and gravity on the y axis
+ *
+ * Invert the anchor and gravity on the y axis if the surface is
+ * constrained on the y axis. For example, if the bottom edge of
+ * the surface is constrained, the gravity is 'bottom' and the
+ * anchor is 'bottom', change the gravity to 'top' and the anchor
+ * to 'top'.
+ *
+ * The adjusted position is calculated given the original anchor
+ * rectangle and offset, but with the new flipped anchor and
+ * gravity values.
+ *
+ * If the adjusted position also ends up being constrained, the
+ * resulting position of the flip_y adjustment will be the one
+ * before the adjustment.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8,
+ /**
+ * horizontally resize the surface
+ *
+ * Resize the surface horizontally so that it is completely
+ * unconstrained.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16,
+ /**
+ * vertically resize the surface
+ *
+ * Resize the surface vertically so that it is completely
+ * unconstrained.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32,
+};
+#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */
+
+#define XDG_POSITIONER_DESTROY 0
+#define XDG_POSITIONER_SET_SIZE 1
+#define XDG_POSITIONER_SET_ANCHOR_RECT 2
+#define XDG_POSITIONER_SET_ANCHOR 3
+#define XDG_POSITIONER_SET_GRAVITY 4
+#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT 5
+#define XDG_POSITIONER_SET_OFFSET 6
+#define XDG_POSITIONER_SET_REACTIVE 7
+#define XDG_POSITIONER_SET_PARENT_SIZE 8
+#define XDG_POSITIONER_SET_PARENT_CONFIGURE 9
+
+
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION 3
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_PARENT_SIZE_SINCE_VERSION 3
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_PARENT_CONFIGURE_SINCE_VERSION 3
+
+/** @ingroup iface_xdg_positioner */
+static inline void
+xdg_positioner_set_user_data(struct xdg_positioner *xdg_positioner, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) xdg_positioner, user_data);
+}
+
+/** @ingroup iface_xdg_positioner */
+static inline void *
+xdg_positioner_get_user_data(struct xdg_positioner *xdg_positioner)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) xdg_positioner);
+}
+
+static inline uint32_t
+xdg_positioner_get_version(struct xdg_positioner *xdg_positioner)
+{
+ return wl_proxy_get_version((struct wl_proxy *) xdg_positioner);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Notify the compositor that the xdg_positioner will no longer be used.
+ */
+static inline void
+xdg_positioner_destroy(struct xdg_positioner *xdg_positioner)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Set the size of the surface that is to be positioned with the positioner
+ * object. The size is in surface-local coordinates and corresponds to the
+ * window geometry. See xdg_surface.set_window_geometry.
+ *
+ * If a zero or negative size is set the invalid_input error is raised.
+ */
+static inline void
+xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, int32_t height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, width, height);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Specify the anchor rectangle within the parent surface that the child
+ * surface will be placed relative to. The rectangle is relative to the
+ * window geometry as defined by xdg_surface.set_window_geometry of the
+ * parent surface.
+ *
+ * When the xdg_positioner object is used to position a child surface, the
+ * anchor rectangle may not extend outside the window geometry of the
+ * positioned child's parent surface.
+ *
+ * If a negative size is set the invalid_input error is raised.
+ */
+static inline void
+xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y, int32_t width, int32_t height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_ANCHOR_RECT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y, width, height);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Defines the anchor point for the anchor rectangle. The specified anchor
+ * is used derive an anchor point that the child surface will be
+ * positioned relative to. If a corner anchor is set (e.g. 'top_left' or
+ * 'bottom_right'), the anchor point will be at the specified corner;
+ * otherwise, the derived anchor point will be centered on the specified
+ * edge, or in the center of the anchor rectangle if no edge is specified.
+ */
+static inline void
+xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_ANCHOR, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, anchor);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Defines in what direction a surface should be positioned, relative to
+ * the anchor point of the parent surface. If a corner gravity is
+ * specified (e.g. 'bottom_right' or 'top_left'), then the child surface
+ * will be placed towards the specified gravity; otherwise, the child
+ * surface will be centered over the anchor point on any axis that had no
+ * gravity specified. If the gravity is not in the āgravityā enum, an
+ * invalid_input error is raised.
+ */
+static inline void
+xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravity)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_GRAVITY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, gravity);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Specify how the window should be positioned if the originally intended
+ * position caused the surface to be constrained, meaning at least
+ * partially outside positioning boundaries set by the compositor. The
+ * adjustment is set by constructing a bitmask describing the adjustment to
+ * be made when the surface is constrained on that axis.
+ *
+ * If no bit for one axis is set, the compositor will assume that the child
+ * surface should not change its position on that axis when constrained.
+ *
+ * If more than one bit for one axis is set, the order of how adjustments
+ * are applied is specified in the corresponding adjustment descriptions.
+ *
+ * The default adjustment is none.
+ */
+static inline void
+xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, uint32_t constraint_adjustment)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, constraint_adjustment);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Specify the surface position offset relative to the position of the
+ * anchor on the anchor rectangle and the anchor on the surface. For
+ * example if the anchor of the anchor rectangle is at (x, y), the surface
+ * has the gravity bottom|right, and the offset is (ox, oy), the calculated
+ * surface position will be (x + ox, y + oy). The offset position of the
+ * surface is the one used for constraint testing. See
+ * set_constraint_adjustment.
+ *
+ * An example use case is placing a popup menu on top of a user interface
+ * element, while aligning the user interface element of the parent surface
+ * with some user interface element placed somewhere in the popup surface.
+ */
+static inline void
+xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_OFFSET, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * When set reactive, the surface is reconstrained if the conditions used
+ * for constraining changed, e.g. the parent window moved.
+ *
+ * If the conditions changed and the popup was reconstrained, an
+ * xdg_popup.configure event is sent with updated geometry, followed by an
+ * xdg_surface.configure event.
+ */
+static inline void
+xdg_positioner_set_reactive(struct xdg_positioner *xdg_positioner)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_REACTIVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Set the parent window geometry the compositor should use when
+ * positioning the popup. The compositor may use this information to
+ * determine the future state the popup should be constrained using. If
+ * this doesn't match the dimension of the parent the popup is eventually
+ * positioned against, the behavior is undefined.
+ *
+ * The arguments are given in the surface-local coordinate space.
+ */
+static inline void
+xdg_positioner_set_parent_size(struct xdg_positioner *xdg_positioner, int32_t parent_width, int32_t parent_height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_PARENT_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, parent_width, parent_height);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Set the serial of an xdg_surface.configure event this positioner will be
+ * used in response to. The compositor may use this information together
+ * with set_parent_size to determine what future state the popup should be
+ * constrained using.
+ */
+static inline void
+xdg_positioner_set_parent_configure(struct xdg_positioner *xdg_positioner, uint32_t serial)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_PARENT_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, serial);
+}
+
+#ifndef XDG_SURFACE_ERROR_ENUM
+#define XDG_SURFACE_ERROR_ENUM
+enum xdg_surface_error {
+ /**
+ * Surface was not fully constructed
+ */
+ XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1,
+ /**
+ * Surface was already constructed
+ */
+ XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2,
+ /**
+ * Attaching a buffer to an unconfigured surface
+ */
+ XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3,
+ /**
+ * Invalid serial number when acking a configure event
+ */
+ XDG_SURFACE_ERROR_INVALID_SERIAL = 4,
+ /**
+ * Width or height was zero or negative
+ */
+ XDG_SURFACE_ERROR_INVALID_SIZE = 5,
+ /**
+ * Surface was destroyed before its role object
+ */
+ XDG_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 6,
+};
+#endif /* XDG_SURFACE_ERROR_ENUM */
+
+/**
+ * @ingroup iface_xdg_surface
+ * @struct xdg_surface_listener
+ */
+struct xdg_surface_listener {
+ /**
+ * suggest a surface change
+ *
+ * The configure event marks the end of a configure sequence. A
+ * configure sequence is a set of one or more events configuring
+ * the state of the xdg_surface, including the final
+ * xdg_surface.configure event.
+ *
+ * Where applicable, xdg_surface surface roles will during a
+ * configure sequence extend this event as a latched state sent as
+ * events before the xdg_surface.configure event. Such events
+ * should be considered to make up a set of atomically applied
+ * configuration states, where the xdg_surface.configure commits
+ * the accumulated state.
+ *
+ * Clients should arrange their surface for the new states, and
+ * then send an ack_configure request with the serial sent in this
+ * configure event at some point before committing the new surface.
+ *
+ * If the client receives multiple configure events before it can
+ * respond to one, it is free to discard all but the last event it
+ * received.
+ * @param serial serial of the configure event
+ */
+ void (*configure)(void *data,
+ struct xdg_surface *xdg_surface,
+ uint32_t serial);
+};
+
+/**
+ * @ingroup iface_xdg_surface
+ */
+static inline int
+xdg_surface_add_listener(struct xdg_surface *xdg_surface,
+ const struct xdg_surface_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) xdg_surface,
+ (void (**)(void)) listener, data);
+}
+
+#define XDG_SURFACE_DESTROY 0
+#define XDG_SURFACE_GET_TOPLEVEL 1
+#define XDG_SURFACE_GET_POPUP 2
+#define XDG_SURFACE_SET_WINDOW_GEOMETRY 3
+#define XDG_SURFACE_ACK_CONFIGURE 4
+
+/**
+ * @ingroup iface_xdg_surface
+ */
+#define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_xdg_surface
+ */
+#define XDG_SURFACE_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_surface
+ */
+#define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_surface
+ */
+#define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_surface
+ */
+#define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_surface
+ */
+#define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1
+
+/** @ingroup iface_xdg_surface */
+static inline void
+xdg_surface_set_user_data(struct xdg_surface *xdg_surface, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) xdg_surface, user_data);
+}
+
+/** @ingroup iface_xdg_surface */
+static inline void *
+xdg_surface_get_user_data(struct xdg_surface *xdg_surface)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) xdg_surface);
+}
+
+static inline uint32_t
+xdg_surface_get_version(struct xdg_surface *xdg_surface)
+{
+ return wl_proxy_get_version((struct wl_proxy *) xdg_surface);
+}
+
+/**
+ * @ingroup iface_xdg_surface
+ *
+ * Destroy the xdg_surface object. An xdg_surface must only be destroyed
+ * after its role object has been destroyed, otherwise
+ * a defunct_role_object error is raised.
+ */
+static inline void
+xdg_surface_destroy(struct xdg_surface *xdg_surface)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface,
+ XDG_SURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_xdg_surface
+ *
+ * This creates an xdg_toplevel object for the given xdg_surface and gives
+ * the associated wl_surface the xdg_toplevel role.
+ *
+ * See the documentation of xdg_toplevel for more details about what an
+ * xdg_toplevel is and how it is used.
+ */
+static inline struct xdg_toplevel *
+xdg_surface_get_toplevel(struct xdg_surface *xdg_surface)
+{
+ struct wl_proxy *id;
+
+ id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface,
+ XDG_SURFACE_GET_TOPLEVEL, &xdg_toplevel_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL);
+
+ return (struct xdg_toplevel *) id;
+}
+
+/**
+ * @ingroup iface_xdg_surface
+ *
+ * This creates an xdg_popup object for the given xdg_surface and gives
+ * the associated wl_surface the xdg_popup role.
+ *
+ * If null is passed as a parent, a parent surface must be specified using
+ * some other protocol, before committing the initial state.
+ *
+ * See the documentation of xdg_popup for more details about what an
+ * xdg_popup is and how it is used.
+ */
+static inline struct xdg_popup *
+xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *parent, struct xdg_positioner *positioner)
+{
+ struct wl_proxy *id;
+
+ id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface,
+ XDG_SURFACE_GET_POPUP, &xdg_popup_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL, parent, positioner);
+
+ return (struct xdg_popup *) id;
+}
+
+/**
+ * @ingroup iface_xdg_surface
+ *
+ * The window geometry of a surface is its "visible bounds" from the
+ * user's perspective. Client-side decorations often have invisible
+ * portions like drop-shadows which should be ignored for the
+ * purposes of aligning, placing and constraining windows.
+ *
+ * The window geometry is double buffered, and will be applied at the
+ * time wl_surface.commit of the corresponding wl_surface is called.
+ *
+ * When maintaining a position, the compositor should treat the (x, y)
+ * coordinate of the window geometry as the top left corner of the window.
+ * A client changing the (x, y) window geometry coordinate should in
+ * general not alter the position of the window.
+ *
+ * Once the window geometry of the surface is set, it is not possible to
+ * unset it, and it will remain the same until set_window_geometry is
+ * called again, even if a new subsurface or buffer is attached.
+ *
+ * If never set, the value is the full bounds of the surface,
+ * including any subsurfaces. This updates dynamically on every
+ * commit. This unset is meant for extremely simple clients.
+ *
+ * The arguments are given in the surface-local coordinate space of
+ * the wl_surface associated with this xdg_surface.
+ *
+ * The width and height must be greater than zero. Setting an invalid size
+ * will raise an invalid_size error. When applied, the effective window
+ * geometry will be the set window geometry clamped to the bounding
+ * rectangle of the combined geometry of the surface of the xdg_surface and
+ * the associated subsurfaces.
+ */
+static inline void
+xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int32_t y, int32_t width, int32_t height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface,
+ XDG_SURFACE_SET_WINDOW_GEOMETRY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, x, y, width, height);
+}
+
+/**
+ * @ingroup iface_xdg_surface
+ *
+ * When a configure event is received, if a client commits the
+ * surface in response to the configure event, then the client
+ * must make an ack_configure request sometime before the commit
+ * request, passing along the serial of the configure event.
+ *
+ * For instance, for toplevel surfaces the compositor might use this
+ * information to move a surface to the top left only when the client has
+ * drawn itself for the maximized or fullscreen state.
+ *
+ * If the client receives multiple configure events before it
+ * can respond to one, it only has to ack the last configure event.
+ * Acking a configure event that was never sent raises an invalid_serial
+ * error.
+ *
+ * A client is not required to commit immediately after sending
+ * an ack_configure request - it may even ack_configure several times
+ * before its next surface commit.
+ *
+ * A client may send multiple ack_configure requests before committing, but
+ * only the last request sent before a commit indicates which configure
+ * event the client really is responding to.
+ *
+ * Sending an ack_configure request consumes the serial number sent with
+ * the request, as well as serial numbers sent by all configure events
+ * sent on this xdg_surface prior to the configure event referenced by
+ * the committed serial.
+ *
+ * It is an error to issue multiple ack_configure requests referencing a
+ * serial from the same configure event, or to issue an ack_configure
+ * request referencing a serial from a configure event issued before the
+ * event identified by the last ack_configure request for the same
+ * xdg_surface. Doing so will raise an invalid_serial error.
+ */
+static inline void
+xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface,
+ XDG_SURFACE_ACK_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, serial);
+}
+
+#ifndef XDG_TOPLEVEL_ERROR_ENUM
+#define XDG_TOPLEVEL_ERROR_ENUM
+enum xdg_toplevel_error {
+ /**
+ * provided value is not a valid variant of the resize_edge enum
+ */
+ XDG_TOPLEVEL_ERROR_INVALID_RESIZE_EDGE = 0,
+ /**
+ * invalid parent toplevel
+ */
+ XDG_TOPLEVEL_ERROR_INVALID_PARENT = 1,
+ /**
+ * client provided an invalid min or max size
+ */
+ XDG_TOPLEVEL_ERROR_INVALID_SIZE = 2,
+};
+#endif /* XDG_TOPLEVEL_ERROR_ENUM */
+
+#ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM
+#define XDG_TOPLEVEL_RESIZE_EDGE_ENUM
+/**
+ * @ingroup iface_xdg_toplevel
+ * edge values for resizing
+ *
+ * These values are used to indicate which edge of a surface
+ * is being dragged in a resize operation.
+ */
+enum xdg_toplevel_resize_edge {
+ XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0,
+ XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1,
+ XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM = 2,
+ XDG_TOPLEVEL_RESIZE_EDGE_LEFT = 4,
+ XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT = 5,
+ XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT = 6,
+ XDG_TOPLEVEL_RESIZE_EDGE_RIGHT = 8,
+ XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9,
+ XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10,
+};
+#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */
+
+#ifndef XDG_TOPLEVEL_STATE_ENUM
+#define XDG_TOPLEVEL_STATE_ENUM
+/**
+ * @ingroup iface_xdg_toplevel
+ * types of state on the surface
+ *
+ * The different state values used on the surface. This is designed for
+ * state values like maximized, fullscreen. It is paired with the
+ * configure event to ensure that both the client and the compositor
+ * setting the state can be synchronized.
+ *
+ * States set in this way are double-buffered. They will get applied on
+ * the next commit.
+ */
+enum xdg_toplevel_state {
+ /**
+ * the surface is maximized
+ * the surface is maximized
+ *
+ * The surface is maximized. The window geometry specified in the
+ * configure event must be obeyed by the client.
+ *
+ * The client should draw without shadow or other decoration
+ * outside of the window geometry.
+ */
+ XDG_TOPLEVEL_STATE_MAXIMIZED = 1,
+ /**
+ * the surface is fullscreen
+ * the surface is fullscreen
+ *
+ * The surface is fullscreen. The window geometry specified in
+ * the configure event is a maximum; the client cannot resize
+ * beyond it. For a surface to cover the whole fullscreened area,
+ * the geometry dimensions must be obeyed by the client. For more
+ * details, see xdg_toplevel.set_fullscreen.
+ */
+ XDG_TOPLEVEL_STATE_FULLSCREEN = 2,
+ /**
+ * the surface is being resized
+ * the surface is being resized
+ *
+ * The surface is being resized. The window geometry specified in
+ * the configure event is a maximum; the client cannot resize
+ * beyond it. Clients that have aspect ratio or cell sizing
+ * configuration can use a smaller size, however.
+ */
+ XDG_TOPLEVEL_STATE_RESIZING = 3,
+ /**
+ * the surface is now activated
+ * the surface is now activated
+ *
+ * Client window decorations should be painted as if the window
+ * is active. Do not assume this means that the window actually has
+ * keyboard or pointer focus.
+ */
+ XDG_TOPLEVEL_STATE_ACTIVATED = 4,
+ /**
+ * the surfaceās left edge is tiled
+ *
+ * The window is currently in a tiled layout and the left edge is
+ * considered to be adjacent to another part of the tiling grid.
+ * @since 2
+ */
+ XDG_TOPLEVEL_STATE_TILED_LEFT = 5,
+ /**
+ * the surfaceās right edge is tiled
+ *
+ * The window is currently in a tiled layout and the right edge
+ * is considered to be adjacent to another part of the tiling grid.
+ * @since 2
+ */
+ XDG_TOPLEVEL_STATE_TILED_RIGHT = 6,
+ /**
+ * the surfaceās top edge is tiled
+ *
+ * The window is currently in a tiled layout and the top edge is
+ * considered to be adjacent to another part of the tiling grid.
+ * @since 2
+ */
+ XDG_TOPLEVEL_STATE_TILED_TOP = 7,
+ /**
+ * the surfaceās bottom edge is tiled
+ *
+ * The window is currently in a tiled layout and the bottom edge
+ * is considered to be adjacent to another part of the tiling grid.
+ * @since 2
+ */
+ XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8,
+};
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2
+#endif /* XDG_TOPLEVEL_STATE_ENUM */
+
+#ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM
+#define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM
+enum xdg_toplevel_wm_capabilities {
+ /**
+ * show_window_menu is available
+ */
+ XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU = 1,
+ /**
+ * set_maximized and unset_maximized are available
+ */
+ XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE = 2,
+ /**
+ * set_fullscreen and unset_fullscreen are available
+ */
+ XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN = 3,
+ /**
+ * set_minimized is available
+ */
+ XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE = 4,
+};
+#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM */
+
+/**
+ * @ingroup iface_xdg_toplevel
+ * @struct xdg_toplevel_listener
+ */
+struct xdg_toplevel_listener {
+ /**
+ * suggest a surface change
+ *
+ * This configure event asks the client to resize its toplevel
+ * surface or to change its state. The configured state should not
+ * be applied immediately. See xdg_surface.configure for details.
+ *
+ * The width and height arguments specify a hint to the window
+ * about how its surface should be resized in window geometry
+ * coordinates. See set_window_geometry.
+ *
+ * If the width or height arguments are zero, it means the client
+ * should decide its own window dimension. This may happen when the
+ * compositor needs to configure the state of the surface but
+ * doesn't have any information about any previous or expected
+ * dimension.
+ *
+ * The states listed in the event specify how the width/height
+ * arguments should be interpreted, and possibly how it should be
+ * drawn.
+ *
+ * Clients must send an ack_configure in response to this event.
+ * See xdg_surface.configure and xdg_surface.ack_configure for
+ * details.
+ */
+ void (*configure)(void *data,
+ struct xdg_toplevel *xdg_toplevel,
+ int32_t width,
+ int32_t height,
+ struct wl_array *states);
+ /**
+ * surface wants to be closed
+ *
+ * The close event is sent by the compositor when the user wants
+ * the surface to be closed. This should be equivalent to the user
+ * clicking the close button in client-side decorations, if your
+ * application has any.
+ *
+ * This is only a request that the user intends to close the
+ * window. The client may choose to ignore this request, or show a
+ * dialog to ask the user to save their data, etc.
+ */
+ void (*close)(void *data,
+ struct xdg_toplevel *xdg_toplevel);
+ /**
+ * recommended window geometry bounds
+ *
+ * The configure_bounds event may be sent prior to a
+ * xdg_toplevel.configure event to communicate the bounds a window
+ * geometry size is recommended to constrain to.
+ *
+ * The passed width and height are in surface coordinate space. If
+ * width and height are 0, it means bounds is unknown and
+ * equivalent to as if no configure_bounds event was ever sent for
+ * this surface.
+ *
+ * The bounds can for example correspond to the size of a monitor
+ * excluding any panels or other shell components, so that a
+ * surface isn't created in a way that it cannot fit.
+ *
+ * The bounds may change at any point, and in such a case, a new
+ * xdg_toplevel.configure_bounds will be sent, followed by
+ * xdg_toplevel.configure and xdg_surface.configure.
+ * @since 4
+ */
+ void (*configure_bounds)(void *data,
+ struct xdg_toplevel *xdg_toplevel,
+ int32_t width,
+ int32_t height);
+ /**
+ * compositor capabilities
+ *
+ * This event advertises the capabilities supported by the
+ * compositor. If a capability isn't supported, clients should hide
+ * or disable the UI elements that expose this functionality. For
+ * instance, if the compositor doesn't advertise support for
+ * minimized toplevels, a button triggering the set_minimized
+ * request should not be displayed.
+ *
+ * The compositor will ignore requests it doesn't support. For
+ * instance, a compositor which doesn't advertise support for
+ * minimized will ignore set_minimized requests.
+ *
+ * Compositors must send this event once before the first
+ * xdg_surface.configure event. When the capabilities change,
+ * compositors must send this event again and then send an
+ * xdg_surface.configure event.
+ *
+ * The configured state should not be applied immediately. See
+ * xdg_surface.configure for details.
+ *
+ * The capabilities are sent as an array of 32-bit unsigned
+ * integers in native endianness.
+ * @param capabilities array of 32-bit capabilities
+ * @since 5
+ */
+ void (*wm_capabilities)(void *data,
+ struct xdg_toplevel *xdg_toplevel,
+ struct wl_array *capabilities);
+};
+
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+static inline int
+xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel,
+ const struct xdg_toplevel_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) xdg_toplevel,
+ (void (**)(void)) listener, data);
+}
+
+#define XDG_TOPLEVEL_DESTROY 0
+#define XDG_TOPLEVEL_SET_PARENT 1
+#define XDG_TOPLEVEL_SET_TITLE 2
+#define XDG_TOPLEVEL_SET_APP_ID 3
+#define XDG_TOPLEVEL_SHOW_WINDOW_MENU 4
+#define XDG_TOPLEVEL_MOVE 5
+#define XDG_TOPLEVEL_RESIZE 6
+#define XDG_TOPLEVEL_SET_MAX_SIZE 7
+#define XDG_TOPLEVEL_SET_MIN_SIZE 8
+#define XDG_TOPLEVEL_SET_MAXIMIZED 9
+#define XDG_TOPLEVEL_UNSET_MAXIMIZED 10
+#define XDG_TOPLEVEL_SET_FULLSCREEN 11
+#define XDG_TOPLEVEL_UNSET_FULLSCREEN 12
+#define XDG_TOPLEVEL_SET_MINIMIZED 13
+
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION 4
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION 5
+
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1
+
+/** @ingroup iface_xdg_toplevel */
+static inline void
+xdg_toplevel_set_user_data(struct xdg_toplevel *xdg_toplevel, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) xdg_toplevel, user_data);
+}
+
+/** @ingroup iface_xdg_toplevel */
+static inline void *
+xdg_toplevel_get_user_data(struct xdg_toplevel *xdg_toplevel)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) xdg_toplevel);
+}
+
+static inline uint32_t
+xdg_toplevel_get_version(struct xdg_toplevel *xdg_toplevel)
+{
+ return wl_proxy_get_version((struct wl_proxy *) xdg_toplevel);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * This request destroys the role surface and unmaps the surface;
+ * see "Unmapping" behavior in interface section for details.
+ */
+static inline void
+xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Set the "parent" of this surface. This surface should be stacked
+ * above the parent surface and all other ancestor surfaces.
+ *
+ * Parent surfaces should be set on dialogs, toolboxes, or other
+ * "auxiliary" surfaces, so that the parent is raised when the dialog
+ * is raised.
+ *
+ * Setting a null parent for a child surface unsets its parent. Setting
+ * a null parent for a surface which currently has no parent is a no-op.
+ *
+ * Only mapped surfaces can have child surfaces. Setting a parent which
+ * is not mapped is equivalent to setting a null parent. If a surface
+ * becomes unmapped, its children's parent is set to the parent of
+ * the now-unmapped surface. If the now-unmapped surface has no parent,
+ * its children's parent is unset. If the now-unmapped surface becomes
+ * mapped again, its parent-child relationship is not restored.
+ *
+ * The parent toplevel must not be one of the child toplevel's
+ * descendants, and the parent must be different from the child toplevel,
+ * otherwise the invalid_parent protocol error is raised.
+ */
+static inline void
+xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel *parent)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_PARENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, parent);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Set a short title for the surface.
+ *
+ * This string may be used to identify the surface in a task bar,
+ * window list, or other user interface elements provided by the
+ * compositor.
+ *
+ * The string must be encoded in UTF-8.
+ */
+static inline void
+xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_TITLE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, title);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Set an application identifier for the surface.
+ *
+ * The app ID identifies the general class of applications to which
+ * the surface belongs. The compositor can use this to group multiple
+ * surfaces together, or to determine how to launch a new application.
+ *
+ * For D-Bus activatable applications, the app ID is used as the D-Bus
+ * service name.
+ *
+ * The compositor shell will try to group application surfaces together
+ * by their app ID. As a best practice, it is suggested to select app
+ * ID's that match the basename of the application's .desktop file.
+ * For example, "org.freedesktop.FooViewer" where the .desktop file is
+ * "org.freedesktop.FooViewer.desktop".
+ *
+ * Like other properties, a set_app_id request can be sent after the
+ * xdg_toplevel has been mapped to update the property.
+ *
+ * See the desktop-entry specification [0] for more details on
+ * application identifiers and how they relate to well-known D-Bus
+ * names and .desktop files.
+ *
+ * [0] https://standards.freedesktop.org/desktop-entry-spec/
+ */
+static inline void
+xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_APP_ID, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, app_id);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Clients implementing client-side decorations might want to show
+ * a context menu when right-clicking on the decorations, giving the
+ * user a menu that they can use to maximize or minimize the window.
+ *
+ * This request asks the compositor to pop up such a window menu at
+ * the given position, relative to the local surface coordinates of
+ * the parent surface. There are no guarantees as to what menu items
+ * the window menu contains, or even if a window menu will be drawn
+ * at all.
+ *
+ * This request must be used in response to some sort of user action
+ * like a button press, key press, or touch down event.
+ */
+static inline void
+xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, int32_t x, int32_t y)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SHOW_WINDOW_MENU, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, x, y);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Start an interactive, user-driven move of the surface.
+ *
+ * This request must be used in response to some sort of user action
+ * like a button press, key press, or touch down event. The passed
+ * serial is used to determine the type of interactive move (touch,
+ * pointer, etc).
+ *
+ * The server may ignore move requests depending on the state of
+ * the surface (e.g. fullscreen or maximized), or if the passed serial
+ * is no longer valid.
+ *
+ * If triggered, the surface will lose the focus of the device
+ * (wl_pointer, wl_touch, etc) used for the move. It is up to the
+ * compositor to visually indicate that the move is taking place, such as
+ * updating a pointer cursor, during the move. There is no guarantee
+ * that the device focus will return when the move is completed.
+ */
+static inline void
+xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_MOVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Start a user-driven, interactive resize of the surface.
+ *
+ * This request must be used in response to some sort of user action
+ * like a button press, key press, or touch down event. The passed
+ * serial is used to determine the type of interactive resize (touch,
+ * pointer, etc).
+ *
+ * The server may ignore resize requests depending on the state of
+ * the surface (e.g. fullscreen or maximized).
+ *
+ * If triggered, the client will receive configure events with the
+ * "resize" state enum value and the expected sizes. See the "resize"
+ * enum value for more details about what is required. The client
+ * must also acknowledge configure events using "ack_configure". After
+ * the resize is completed, the client will receive another "configure"
+ * event without the resize state.
+ *
+ * If triggered, the surface also will lose the focus of the device
+ * (wl_pointer, wl_touch, etc) used for the resize. It is up to the
+ * compositor to visually indicate that the resize is taking place,
+ * such as updating a pointer cursor, during the resize. There is no
+ * guarantee that the device focus will return when the resize is
+ * completed.
+ *
+ * The edges parameter specifies how the surface should be resized, and
+ * is one of the values of the resize_edge enum. Values not matching
+ * a variant of the enum will cause a protocol error. The compositor
+ * may use this information to update the surface position for example
+ * when dragging the top left corner. The compositor may also use
+ * this information to adapt its behavior, e.g. choose an appropriate
+ * cursor image.
+ */
+static inline void
+xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, uint32_t edges)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_RESIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, edges);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Set a maximum size for the window.
+ *
+ * The client can specify a maximum size so that the compositor does
+ * not try to configure the window beyond this size.
+ *
+ * The width and height arguments are in window geometry coordinates.
+ * See xdg_surface.set_window_geometry.
+ *
+ * Values set in this way are double-buffered. They will get applied
+ * on the next commit.
+ *
+ * The compositor can use this information to allow or disallow
+ * different states like maximize or fullscreen and draw accurate
+ * animations.
+ *
+ * Similarly, a tiling window manager may use this information to
+ * place and resize client windows in a more effective way.
+ *
+ * The client should not rely on the compositor to obey the maximum
+ * size. The compositor may decide to ignore the values set by the
+ * client and request a larger size.
+ *
+ * If never set, or a value of zero in the request, means that the
+ * client has no expected maximum size in the given dimension.
+ * As a result, a client wishing to reset the maximum size
+ * to an unspecified state can use zero for width and height in the
+ * request.
+ *
+ * Requesting a maximum size to be smaller than the minimum size of
+ * a surface is illegal and will result in an invalid_size error.
+ *
+ * The width and height must be greater than or equal to zero. Using
+ * strictly negative values for width or height will result in a
+ * invalid_size error.
+ */
+static inline void
+xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_MAX_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Set a minimum size for the window.
+ *
+ * The client can specify a minimum size so that the compositor does
+ * not try to configure the window below this size.
+ *
+ * The width and height arguments are in window geometry coordinates.
+ * See xdg_surface.set_window_geometry.
+ *
+ * Values set in this way are double-buffered. They will get applied
+ * on the next commit.
+ *
+ * The compositor can use this information to allow or disallow
+ * different states like maximize or fullscreen and draw accurate
+ * animations.
+ *
+ * Similarly, a tiling window manager may use this information to
+ * place and resize client windows in a more effective way.
+ *
+ * The client should not rely on the compositor to obey the minimum
+ * size. The compositor may decide to ignore the values set by the
+ * client and request a smaller size.
+ *
+ * If never set, or a value of zero in the request, means that the
+ * client has no expected minimum size in the given dimension.
+ * As a result, a client wishing to reset the minimum size
+ * to an unspecified state can use zero for width and height in the
+ * request.
+ *
+ * Requesting a minimum size to be larger than the maximum size of
+ * a surface is illegal and will result in an invalid_size error.
+ *
+ * The width and height must be greater than or equal to zero. Using
+ * strictly negative values for width and height will result in a
+ * invalid_size error.
+ */
+static inline void
+xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_MIN_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Maximize the surface.
+ *
+ * After requesting that the surface should be maximized, the compositor
+ * will respond by emitting a configure event. Whether this configure
+ * actually sets the window maximized is subject to compositor policies.
+ * The client must then update its content, drawing in the configured
+ * state. The client must also acknowledge the configure when committing
+ * the new content (see ack_configure).
+ *
+ * It is up to the compositor to decide how and where to maximize the
+ * surface, for example which output and what region of the screen should
+ * be used.
+ *
+ * If the surface was already maximized, the compositor will still emit
+ * a configure event with the "maximized" state.
+ *
+ * If the surface is in a fullscreen state, this request has no direct
+ * effect. It may alter the state the surface is returned to when
+ * unmaximized unless overridden by the compositor.
+ */
+static inline void
+xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Unmaximize the surface.
+ *
+ * After requesting that the surface should be unmaximized, the compositor
+ * will respond by emitting a configure event. Whether this actually
+ * un-maximizes the window is subject to compositor policies.
+ * If available and applicable, the compositor will include the window
+ * geometry dimensions the window had prior to being maximized in the
+ * configure event. The client must then update its content, drawing it in
+ * the configured state. The client must also acknowledge the configure
+ * when committing the new content (see ack_configure).
+ *
+ * It is up to the compositor to position the surface after it was
+ * unmaximized; usually the position the surface had before maximizing, if
+ * applicable.
+ *
+ * If the surface was already not maximized, the compositor will still
+ * emit a configure event without the "maximized" state.
+ *
+ * If the surface is in a fullscreen state, this request has no direct
+ * effect. It may alter the state the surface is returned to when
+ * unmaximized unless overridden by the compositor.
+ */
+static inline void
+xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_UNSET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Make the surface fullscreen.
+ *
+ * After requesting that the surface should be fullscreened, the
+ * compositor will respond by emitting a configure event. Whether the
+ * client is actually put into a fullscreen state is subject to compositor
+ * policies. The client must also acknowledge the configure when
+ * committing the new content (see ack_configure).
+ *
+ * The output passed by the request indicates the client's preference as
+ * to which display it should be set fullscreen on. If this value is NULL,
+ * it's up to the compositor to choose which display will be used to map
+ * this surface.
+ *
+ * If the surface doesn't cover the whole output, the compositor will
+ * position the surface in the center of the output and compensate with
+ * with border fill covering the rest of the output. The content of the
+ * border fill is undefined, but should be assumed to be in some way that
+ * attempts to blend into the surrounding area (e.g. solid black).
+ *
+ * If the fullscreened surface is not opaque, the compositor must make
+ * sure that other screen content not part of the same surface tree (made
+ * up of subsurfaces, popups or similarly coupled surfaces) are not
+ * visible below the fullscreened surface.
+ */
+static inline void
+xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output *output)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, output);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Make the surface no longer fullscreen.
+ *
+ * After requesting that the surface should be unfullscreened, the
+ * compositor will respond by emitting a configure event.
+ * Whether this actually removes the fullscreen state of the client is
+ * subject to compositor policies.
+ *
+ * Making a surface unfullscreen sets states for the surface based on the following:
+ * * the state(s) it may have had before becoming fullscreen
+ * * any state(s) decided by the compositor
+ * * any state(s) requested by the client while the surface was fullscreen
+ *
+ * The compositor may include the previous window geometry dimensions in
+ * the configure event, if applicable.
+ *
+ * The client must also acknowledge the configure when committing the new
+ * content (see ack_configure).
+ */
+static inline void
+xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_UNSET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Request that the compositor minimize your surface. There is no
+ * way to know if the surface is currently minimized, nor is there
+ * any way to unset minimization on this surface.
+ *
+ * If you are looking to throttle redrawing when minimized, please
+ * instead use the wl_surface.frame event for this, as this will
+ * also work with live previews on windows in Alt-Tab, Expose or
+ * similar compositor features.
+ */
+static inline void
+xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_MINIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0);
+}
+
+#ifndef XDG_POPUP_ERROR_ENUM
+#define XDG_POPUP_ERROR_ENUM
+enum xdg_popup_error {
+ /**
+ * tried to grab after being mapped
+ */
+ XDG_POPUP_ERROR_INVALID_GRAB = 0,
+};
+#endif /* XDG_POPUP_ERROR_ENUM */
+
+/**
+ * @ingroup iface_xdg_popup
+ * @struct xdg_popup_listener
+ */
+struct xdg_popup_listener {
+ /**
+ * configure the popup surface
+ *
+ * This event asks the popup surface to configure itself given
+ * the configuration. The configured state should not be applied
+ * immediately. See xdg_surface.configure for details.
+ *
+ * The x and y arguments represent the position the popup was
+ * placed at given the xdg_positioner rule, relative to the upper
+ * left corner of the window geometry of the parent surface.
+ *
+ * For version 2 or older, the configure event for an xdg_popup is
+ * only ever sent once for the initial configuration. Starting with
+ * version 3, it may be sent again if the popup is setup with an
+ * xdg_positioner with set_reactive requested, or in response to
+ * xdg_popup.reposition requests.
+ * @param x x position relative to parent surface window geometry
+ * @param y y position relative to parent surface window geometry
+ * @param width window geometry width
+ * @param height window geometry height
+ */
+ void (*configure)(void *data,
+ struct xdg_popup *xdg_popup,
+ int32_t x,
+ int32_t y,
+ int32_t width,
+ int32_t height);
+ /**
+ * popup interaction is done
+ *
+ * The popup_done event is sent out when a popup is dismissed by
+ * the compositor. The client should destroy the xdg_popup object
+ * at this point.
+ */
+ void (*popup_done)(void *data,
+ struct xdg_popup *xdg_popup);
+ /**
+ * signal the completion of a repositioned request
+ *
+ * The repositioned event is sent as part of a popup
+ * configuration sequence, together with xdg_popup.configure and
+ * lastly xdg_surface.configure to notify the completion of a
+ * reposition request.
+ *
+ * The repositioned event is to notify about the completion of a
+ * xdg_popup.reposition request. The token argument is the token
+ * passed in the xdg_popup.reposition request.
+ *
+ * Immediately after this event is emitted, xdg_popup.configure and
+ * xdg_surface.configure will be sent with the updated size and
+ * position, as well as a new configure serial.
+ *
+ * The client should optionally update the content of the popup,
+ * but must acknowledge the new popup configuration for the new
+ * position to take effect. See xdg_surface.ack_configure for
+ * details.
+ * @param token reposition request token
+ * @since 3
+ */
+ void (*repositioned)(void *data,
+ struct xdg_popup *xdg_popup,
+ uint32_t token);
+};
+
+/**
+ * @ingroup iface_xdg_popup
+ */
+static inline int
+xdg_popup_add_listener(struct xdg_popup *xdg_popup,
+ const struct xdg_popup_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) xdg_popup,
+ (void (**)(void)) listener, data);
+}
+
+#define XDG_POPUP_DESTROY 0
+#define XDG_POPUP_GRAB 1
+#define XDG_POPUP_REPOSITION 2
+
+/**
+ * @ingroup iface_xdg_popup
+ */
+#define XDG_POPUP_CONFIGURE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_popup
+ */
+#define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_popup
+ */
+#define XDG_POPUP_REPOSITIONED_SINCE_VERSION 3
+
+/**
+ * @ingroup iface_xdg_popup
+ */
+#define XDG_POPUP_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_popup
+ */
+#define XDG_POPUP_GRAB_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_popup
+ */
+#define XDG_POPUP_REPOSITION_SINCE_VERSION 3
+
+/** @ingroup iface_xdg_popup */
+static inline void
+xdg_popup_set_user_data(struct xdg_popup *xdg_popup, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) xdg_popup, user_data);
+}
+
+/** @ingroup iface_xdg_popup */
+static inline void *
+xdg_popup_get_user_data(struct xdg_popup *xdg_popup)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) xdg_popup);
+}
+
+static inline uint32_t
+xdg_popup_get_version(struct xdg_popup *xdg_popup)
+{
+ return wl_proxy_get_version((struct wl_proxy *) xdg_popup);
+}
+
+/**
+ * @ingroup iface_xdg_popup
+ *
+ * This destroys the popup. Explicitly destroying the xdg_popup
+ * object will also dismiss the popup, and unmap the surface.
+ *
+ * If this xdg_popup is not the "topmost" popup, a protocol error
+ * will be sent.
+ */
+static inline void
+xdg_popup_destroy(struct xdg_popup *xdg_popup)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup,
+ XDG_POPUP_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_xdg_popup
+ *
+ * This request makes the created popup take an explicit grab. An explicit
+ * grab will be dismissed when the user dismisses the popup, or when the
+ * client destroys the xdg_popup. This can be done by the user clicking
+ * outside the surface, using the keyboard, or even locking the screen
+ * through closing the lid or a timeout.
+ *
+ * If the compositor denies the grab, the popup will be immediately
+ * dismissed.
+ *
+ * This request must be used in response to some sort of user action like a
+ * button press, key press, or touch down event. The serial number of the
+ * event should be passed as 'serial'.
+ *
+ * The parent of a grabbing popup must either be an xdg_toplevel surface or
+ * another xdg_popup with an explicit grab. If the parent is another
+ * xdg_popup it means that the popups are nested, with this popup now being
+ * the topmost popup.
+ *
+ * Nested popups must be destroyed in the reverse order they were created
+ * in, e.g. the only popup you are allowed to destroy at all times is the
+ * topmost one.
+ *
+ * When compositors choose to dismiss a popup, they may dismiss every
+ * nested grabbing popup as well. When a compositor dismisses popups, it
+ * will follow the same dismissing order as required from the client.
+ *
+ * If the topmost grabbing popup is destroyed, the grab will be returned to
+ * the parent of the popup, if that parent previously had an explicit grab.
+ *
+ * If the parent is a grabbing popup which has already been dismissed, this
+ * popup will be immediately dismissed. If the parent is a popup that did
+ * not take an explicit grab, an error will be raised.
+ *
+ * During a popup grab, the client owning the grab will receive pointer
+ * and touch events for all their surfaces as normal (similar to an
+ * "owner-events" grab in X11 parlance), while the top most grabbing popup
+ * will always have keyboard focus.
+ */
+static inline void
+xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t serial)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup,
+ XDG_POPUP_GRAB, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, seat, serial);
+}
+
+/**
+ * @ingroup iface_xdg_popup
+ *
+ * Reposition an already-mapped popup. The popup will be placed given the
+ * details in the passed xdg_positioner object, and a
+ * xdg_popup.repositioned followed by xdg_popup.configure and
+ * xdg_surface.configure will be emitted in response. Any parameters set
+ * by the previous positioner will be discarded.
+ *
+ * The passed token will be sent in the corresponding
+ * xdg_popup.repositioned event. The new popup position will not take
+ * effect until the corresponding configure event is acknowledged by the
+ * client. See xdg_popup.repositioned for details. The token itself is
+ * opaque, and has no other special meaning.
+ *
+ * If multiple reposition requests are sent, the compositor may skip all
+ * but the last one.
+ *
+ * If the popup is repositioned in response to a configure event for its
+ * parent, the client should send an xdg_positioner.set_parent_configure
+ * and possibly an xdg_positioner.set_parent_size request to allow the
+ * compositor to properly constrain the popup.
+ *
+ * If the popup is repositioned together with a parent that is being
+ * resized, but not in response to a configure event, the client should
+ * send an xdg_positioner.set_parent_size request.
+ */
+static inline void
+xdg_popup_reposition(struct xdg_popup *xdg_popup, struct xdg_positioner *positioner, uint32_t token)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup,
+ XDG_POPUP_REPOSITION, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, positioner, token);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/gio/app/internal/wm/window.go b/gio/app/internal/wm/window.go
new file mode 100644
index 0000000..82e3c38
--- /dev/null
+++ b/gio/app/internal/wm/window.go
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// package wm implements platform specific windows
+// and GPU contexts.
+package wm
+
+import (
+ "errors"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/unit"
+)
+
+type Size struct {
+ Width unit.Value
+ Height unit.Value
+}
+
+type Options struct {
+ Size *Size
+ MinSize *Size
+ MaxSize *Size
+ Title *string
+ WindowMode *WindowMode
+}
+
+type WindowMode uint8
+
+const (
+ Windowed WindowMode = iota
+ Fullscreen
+)
+
+type FrameEvent struct {
+ system.FrameEvent
+
+ Sync bool
+}
+
+type Callbacks interface {
+ SetDriver(d Driver)
+ Event(e event.Event)
+}
+
+type Context interface {
+ API() gpu.API
+ Present() error
+ MakeCurrent() error
+ Release()
+ Lock()
+ Unlock()
+}
+
+// ErrDeviceLost is returned from Context.Present when
+// the underlying GPU device is gone and should be
+// recreated.
+var ErrDeviceLost = errors.New("GPU device lost")
+
+// Driver is the interface for the platform implementation
+// of a window.
+type Driver interface {
+ // SetAnimating sets the animation flag. When the window is animating,
+ // FrameEvents are delivered as fast as the display can handle them.
+ SetAnimating(anim bool)
+ // ShowTextInput updates the virtual keyboard state.
+ ShowTextInput(show bool)
+ NewContext() (Context, error)
+
+ // ReadClipboard requests the clipboard content.
+ ReadClipboard()
+ // WriteClipboard requests a clipboard write.
+ WriteClipboard(s string)
+
+ // Option processes option changes.
+ Option(opts *Options)
+
+ // SetCursor updates the current cursor to name.
+ SetCursor(name pointer.CursorName)
+
+ // Close the window.
+ Close()
+}
+
+type windowRendezvous struct {
+ in chan windowAndOptions
+ out chan windowAndOptions
+ errs chan error
+}
+
+type windowAndOptions struct {
+ window Callbacks
+ opts *Options
+}
+
+func newWindowRendezvous() *windowRendezvous {
+ wr := &windowRendezvous{
+ in: make(chan windowAndOptions),
+ out: make(chan windowAndOptions),
+ errs: make(chan error),
+ }
+ go func() {
+ var main windowAndOptions
+ var out chan windowAndOptions
+ for {
+ select {
+ case w := <-wr.in:
+ var err error
+ if main.window != nil {
+ err = errors.New("multiple windows are not supported")
+ }
+ wr.errs <- err
+ main = w
+ out = wr.out
+ case out <- main:
+ }
+ }
+ }()
+ return wr
+}
diff --git a/gio/app/internal/xkb/xkb_unix.go b/gio/app/internal/xkb/xkb_unix.go
new file mode 100644
index 0000000..be72a58
--- /dev/null
+++ b/gio/app/internal/xkb/xkb_unix.go
@@ -0,0 +1,322 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build (linux && !android) || freebsd || openbsd
+// +build linux,!android freebsd openbsd
+
+// Package xkb implements a Go interface for the X Keyboard Extension library.
+package xkb
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "syscall"
+ "unicode"
+ "unicode/utf8"
+ "unsafe"
+
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+)
+
+/*
+#cgo linux pkg-config: xkbcommon
+#cgo freebsd openbsd CFLAGS: -I/usr/local/include
+#cgo freebsd openbsd LDFLAGS: -L/usr/local/lib -lxkbcommon
+
+#include
+#include
+#include
+*/
+import "C"
+
+type Context struct {
+ Ctx *C.struct_xkb_context
+ keyMap *C.struct_xkb_keymap
+ state *C.struct_xkb_state
+ compTable *C.struct_xkb_compose_table
+ compState *C.struct_xkb_compose_state
+ utf8Buf []byte
+}
+
+var (
+ _XKB_MOD_NAME_CTRL = []byte("Control\x00")
+ _XKB_MOD_NAME_SHIFT = []byte("Shift\x00")
+ _XKB_MOD_NAME_ALT = []byte("Mod1\x00")
+ _XKB_MOD_NAME_LOGO = []byte("Mod4\x00")
+)
+
+func (x *Context) Destroy() {
+ if x.compState != nil {
+ C.xkb_compose_state_unref(x.compState)
+ x.compState = nil
+ }
+ if x.compTable != nil {
+ C.xkb_compose_table_unref(x.compTable)
+ x.compTable = nil
+ }
+ x.DestroyKeymapState()
+ if x.Ctx != nil {
+ C.xkb_context_unref(x.Ctx)
+ x.Ctx = nil
+ }
+}
+
+func New() (*Context, error) {
+ ctx := &Context{
+ Ctx: C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS),
+ }
+ if ctx.Ctx == nil {
+ return nil, errors.New("newXKB: xkb_context_new failed")
+ }
+ locale := os.Getenv("LC_ALL")
+ if locale == "" {
+ locale = os.Getenv("LC_CTYPE")
+ }
+ if locale == "" {
+ locale = os.Getenv("LANG")
+ }
+ if locale == "" {
+ locale = "C"
+ }
+ cloc := C.CString(locale)
+ defer C.free(unsafe.Pointer(cloc))
+ ctx.compTable = C.xkb_compose_table_new_from_locale(ctx.Ctx, cloc,
+ C.XKB_COMPOSE_COMPILE_NO_FLAGS)
+ if ctx.compTable == nil {
+ ctx.Destroy()
+ return nil, errors.New("newXKB: xkb_compose_table_new_from_locale failed")
+ }
+ ctx.compState = C.xkb_compose_state_new(ctx.compTable,
+ C.XKB_COMPOSE_STATE_NO_FLAGS)
+ if ctx.compState == nil {
+ ctx.Destroy()
+ return nil, errors.New("newXKB: xkb_compose_state_new failed")
+ }
+ return ctx, nil
+}
+
+func (x *Context) DestroyKeymapState() {
+ if x.state != nil {
+ C.xkb_state_unref(x.state)
+ x.state = nil
+ }
+ if x.keyMap != nil {
+ C.xkb_keymap_unref(x.keyMap)
+ x.keyMap = nil
+ }
+}
+
+// SetKeymap sets the keymap and state. The context takes ownership of the
+// keymap and state and frees them in Destroy.
+func (x *Context) SetKeymap(xkbKeyMap, xkbState unsafe.Pointer) {
+ x.DestroyKeymapState()
+ x.keyMap = (*C.struct_xkb_keymap)(xkbKeyMap)
+ x.state = (*C.struct_xkb_state)(xkbState)
+}
+
+func (x *Context) LoadKeymap(format int, fd int, size int) error {
+ x.DestroyKeymapState()
+ mapData, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ,
+ syscall.MAP_SHARED)
+ if err != nil {
+ return fmt.Errorf("newXKB: mmap of keymap failed: %v", err)
+ }
+ defer syscall.Munmap(mapData)
+ keyMap := C.xkb_keymap_new_from_buffer(x.Ctx,
+ (*C.char)(unsafe.Pointer(&mapData[0])), C.size_t(size-1),
+ C.XKB_KEYMAP_FORMAT_TEXT_V1, C.XKB_KEYMAP_COMPILE_NO_FLAGS)
+ if keyMap == nil {
+ return errors.New("newXKB: xkb_keymap_new_from_buffer failed")
+ }
+ state := C.xkb_state_new(keyMap)
+ if state == nil {
+ C.xkb_keymap_unref(keyMap)
+ return errors.New("newXKB: xkb_state_new failed")
+ }
+ x.keyMap = keyMap
+ x.state = state
+ return nil
+}
+
+func (x *Context) Modifiers() key.Modifiers {
+ var mods key.Modifiers
+ if C.xkb_state_mod_name_is_active(x.state,
+ (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_CTRL[0])),
+ C.XKB_STATE_MODS_EFFECTIVE) == 1 {
+ mods |= key.ModCtrl
+ }
+ if C.xkb_state_mod_name_is_active(x.state,
+ (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_SHIFT[0])),
+ C.XKB_STATE_MODS_EFFECTIVE) == 1 {
+ mods |= key.ModShift
+ }
+ if C.xkb_state_mod_name_is_active(x.state,
+ (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_ALT[0])),
+ C.XKB_STATE_MODS_EFFECTIVE) == 1 {
+ mods |= key.ModAlt
+ }
+ if C.xkb_state_mod_name_is_active(x.state,
+ (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_LOGO[0])),
+ C.XKB_STATE_MODS_EFFECTIVE) == 1 {
+ mods |= key.ModSuper
+ }
+ return mods
+}
+
+func (x *Context) DispatchKey(keyCode uint32,
+ state key.State) (events []event.Event) {
+ if x.state == nil {
+ return
+ }
+ kc := C.xkb_keycode_t(keyCode)
+ if len(x.utf8Buf) == 0 {
+ x.utf8Buf = make([]byte, 1)
+ }
+ sym := C.xkb_state_key_get_one_sym(x.state, kc)
+ if name, ok := convertKeysym(sym); ok {
+ cmd := key.Event{
+ Name: name,
+ Modifiers: x.Modifiers(),
+ State: state,
+ }
+ // Ensure that a physical backtab key is translated to
+ // Shift-Tab.
+ if sym == C.XKB_KEY_ISO_Left_Tab {
+ cmd.Modifiers |= key.ModShift
+ }
+ events = append(events, cmd)
+ }
+ C.xkb_compose_state_feed(x.compState, sym)
+ var str []byte
+ switch C.xkb_compose_state_get_status(x.compState) {
+ case C.XKB_COMPOSE_CANCELLED, C.XKB_COMPOSE_COMPOSING:
+ return
+ case C.XKB_COMPOSE_COMPOSED:
+ size := C.xkb_compose_state_get_utf8(x.compState,
+ (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf)))
+ if int(size) >= len(x.utf8Buf) {
+ x.utf8Buf = make([]byte, size+1)
+ size = C.xkb_compose_state_get_utf8(x.compState,
+ (*C.char)(unsafe.Pointer(&x.utf8Buf[0])),
+ C.size_t(len(x.utf8Buf)))
+ }
+ C.xkb_compose_state_reset(x.compState)
+ str = x.utf8Buf[:size]
+ case C.XKB_COMPOSE_NOTHING:
+ mod := x.Modifiers()
+ if mod&(key.ModCtrl|key.ModAlt|key.ModSuper) == 0 {
+ str = x.charsForKeycode(kc)
+ }
+ }
+ // Report only printable runes.
+ var n int
+ for n < len(str) {
+ r, s := utf8.DecodeRune(str)
+ if unicode.IsPrint(r) {
+ n += s
+ } else {
+ copy(str[n:], str[n+s:])
+ str = str[:len(str)-s]
+ }
+ }
+ if state == key.Press && len(str) > 0 {
+ events = append(events, key.EditEvent{Text: string(str)})
+ }
+ return
+}
+
+func (x *Context) charsForKeycode(keyCode C.xkb_keycode_t) []byte {
+ size := C.xkb_state_key_get_utf8(x.state, keyCode,
+ (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf)))
+ if int(size) >= len(x.utf8Buf) {
+ x.utf8Buf = make([]byte, size+1)
+ size = C.xkb_state_key_get_utf8(x.state, keyCode,
+ (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf)))
+ }
+ return x.utf8Buf[:size]
+}
+
+func (x *Context) IsRepeatKey(keyCode uint32) bool {
+ kc := C.xkb_keycode_t(keyCode)
+ return C.xkb_keymap_key_repeats(x.keyMap, kc) == 1
+}
+
+func (x *Context) UpdateMask(depressed, latched, locked, depressedGroup, latchedGroup, lockedGroup uint32) {
+ if x.state == nil {
+ return
+ }
+ C.xkb_state_update_mask(x.state, C.xkb_mod_mask_t(depressed),
+ C.xkb_mod_mask_t(latched), C.xkb_mod_mask_t(locked),
+ C.xkb_layout_index_t(depressedGroup),
+ C.xkb_layout_index_t(latchedGroup), C.xkb_layout_index_t(lockedGroup))
+}
+
+func convertKeysym(s C.xkb_keysym_t) (string, bool) {
+ if 'a' <= s && s <= 'z' {
+ return string(rune(s - 'a' + 'A')), true
+ }
+ if ' ' < s && s <= '~' {
+ return string(rune(s)), true
+ }
+ var n string
+ switch s {
+ case C.XKB_KEY_Escape:
+ n = key.NameEscape
+ case C.XKB_KEY_Left:
+ n = key.NameLeftArrow
+ case C.XKB_KEY_Right:
+ n = key.NameRightArrow
+ case C.XKB_KEY_Return:
+ n = key.NameReturn
+ case C.XKB_KEY_KP_Enter:
+ n = key.NameEnter
+ case C.XKB_KEY_Up:
+ n = key.NameUpArrow
+ case C.XKB_KEY_Down:
+ n = key.NameDownArrow
+ case C.XKB_KEY_Home:
+ n = key.NameHome
+ case C.XKB_KEY_End:
+ n = key.NameEnd
+ case C.XKB_KEY_BackSpace:
+ n = key.NameDeleteBackward
+ case C.XKB_KEY_Delete:
+ n = key.NameDeleteForward
+ case C.XKB_KEY_Page_Up:
+ n = key.NamePageUp
+ case C.XKB_KEY_Page_Down:
+ n = key.NamePageDown
+ case C.XKB_KEY_F1:
+ n = "F1"
+ case C.XKB_KEY_F2:
+ n = "F2"
+ case C.XKB_KEY_F3:
+ n = "F3"
+ case C.XKB_KEY_F4:
+ n = "F4"
+ case C.XKB_KEY_F5:
+ n = "F5"
+ case C.XKB_KEY_F6:
+ n = "F6"
+ case C.XKB_KEY_F7:
+ n = "F7"
+ case C.XKB_KEY_F8:
+ n = "F8"
+ case C.XKB_KEY_F9:
+ n = "F9"
+ case C.XKB_KEY_F10:
+ n = "F10"
+ case C.XKB_KEY_F11:
+ n = "F11"
+ case C.XKB_KEY_F12:
+ n = "F12"
+ case C.XKB_KEY_Tab, C.XKB_KEY_KP_Tab, C.XKB_KEY_ISO_Left_Tab:
+ n = key.NameTab
+ case 0x20, C.XKB_KEY_KP_Space:
+ n = key.NameSpace
+ default:
+ return "", false
+ }
+ return n, true
+}
diff --git a/gio/app/loop.go b/gio/app/loop.go
new file mode 100644
index 0000000..6b2a57a
--- /dev/null
+++ b/gio/app/loop.go
@@ -0,0 +1,162 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package app
+
+import (
+ "image"
+ "image/color"
+ "runtime"
+
+ "realy.lol/gio/app/internal/wm"
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/op"
+)
+
+type renderLoop struct {
+ summary string
+ drawing bool
+ err error
+
+ frames chan frame
+ results chan frameResult
+ refresh chan struct{}
+ refreshErr chan error
+ ack chan struct{}
+ stop chan struct{}
+ stopped chan struct{}
+}
+
+type frame struct {
+ viewport image.Point
+ ops *op.Ops
+}
+
+type frameResult struct {
+ profile string
+ err error
+}
+
+func newLoop(ctx wm.Context) (*renderLoop, error) {
+ l := &renderLoop{
+ frames: make(chan frame),
+ results: make(chan frameResult),
+ refresh: make(chan struct{}),
+ refreshErr: make(chan error),
+ // Ack is buffered so GPU commands can be issued after
+ // ack'ing the frame.
+ ack: make(chan struct{}, 1),
+ stop: make(chan struct{}),
+ stopped: make(chan struct{}),
+ }
+ if err := l.renderLoop(ctx); err != nil {
+ return nil, err
+ }
+ return l, nil
+}
+
+func (l *renderLoop) renderLoop(ctx wm.Context) error {
+ // GL Operations must happen on a single OS thread, so
+ // pass initialization result through a channel.
+ initErr := make(chan error)
+ go func() {
+ defer close(l.stopped)
+ runtime.LockOSThread()
+ // Don't UnlockOSThread to avoid reuse by the Go runtime.
+
+ if err := ctx.MakeCurrent(); err != nil {
+ initErr <- err
+ return
+ }
+ g, err := gpu.New(ctx.API())
+ if err != nil {
+ initErr <- err
+ return
+ }
+ defer g.Release()
+ initErr <- nil
+ loop:
+ for {
+ select {
+ case <-l.refresh:
+ l.refreshErr <- ctx.MakeCurrent()
+ case frame := <-l.frames:
+ ctx.Lock()
+ if runtime.GOOS == "js" {
+ // Use transparent black when Gio is embedded, to allow mixing of Gio and
+ // foreign content below.
+ g.Clear(color.NRGBA{A: 0x00, R: 0x00, G: 0x00, B: 0x00})
+ } else {
+ g.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
+ }
+ g.Collect(frame.viewport, frame.ops)
+ // Signal that we're done with the frame ops.
+ l.ack <- struct{}{}
+ var res frameResult
+ res.err = g.Frame()
+ if res.err == nil {
+ res.err = ctx.Present()
+ }
+ res.profile = g.Profile()
+ ctx.Unlock()
+ l.results <- res
+ case <-l.stop:
+ break loop
+ }
+ }
+ }()
+ return <-initErr
+}
+
+func (l *renderLoop) Release() {
+ // Flush error.
+ l.Flush()
+ close(l.stop)
+ <-l.stopped
+ l.stop = nil
+}
+
+func (l *renderLoop) Flush() error {
+ if l.drawing {
+ st := <-l.results
+ l.setErr(st.err)
+ if st.profile != "" {
+ l.summary = st.profile
+ }
+ l.drawing = false
+ }
+ return l.err
+}
+
+func (l *renderLoop) Summary() string {
+ return l.summary
+}
+
+func (l *renderLoop) Refresh() {
+ if l.err != nil {
+ return
+ }
+ // Make sure any pending frame is complete.
+ l.Flush()
+ l.refresh <- struct{}{}
+ l.setErr(<-l.refreshErr)
+}
+
+// Draw initiates a draw of a frame. It returns a channel
+// than signals when the frame is no longer being accessed.
+func (l *renderLoop) Draw(viewport image.Point,
+ frameOps *op.Ops) <-chan struct{} {
+ if l.err != nil {
+ l.ack <- struct{}{}
+ return l.ack
+ }
+ l.Flush()
+ l.frames <- frame{viewport, frameOps}
+ l.drawing = true
+ return l.ack
+}
+
+func (l *renderLoop) setErr(err error) {
+ if l.err == nil {
+ l.err = err
+ }
+}
diff --git a/gio/app/permission/bluetooth/main.go b/gio/app/permission/bluetooth/main.go
new file mode 100644
index 0000000..392bbbe
--- /dev/null
+++ b/gio/app/permission/bluetooth/main.go
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package bluetooth implements permissions to access Bluetooth and Bluetooth
+Low Energy hardware, including the ability to discover and pair devices.
+
+Android
+
+The following entries will be added to AndroidManifest.xml:
+
+
+
+
+
+
+
+Note that ACCESS_FINE_LOCATION is required on Android before the Bluetooth
+device may be used.
+See https://developer.android.com/guide/topics/connectivity/bluetooth.
+
+ACCESS_FINE_LOCATION is a "dangerous" permission. See documentation for
+package realy.lol/gio/app/permission for more information.
+*/
+package bluetooth
diff --git a/gio/app/permission/camera/main.go b/gio/app/permission/camera/main.go
new file mode 100644
index 0000000..1e89a31
--- /dev/null
+++ b/gio/app/permission/camera/main.go
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package camera implements permissions to access camera hardware.
+
+Android
+
+The following entries will be added to AndroidManifest.xml:
+
+
+
+
+CAMERA is a "dangerous" permission. See documentation for package
+realy.lol/gio/app/permission for more information.
+*/
+package camera
diff --git a/gio/app/permission/doc.go b/gio/app/permission/doc.go
new file mode 100644
index 0000000..878a5cb
--- /dev/null
+++ b/gio/app/permission/doc.go
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package permission includes sub-packages that should be imported
+by a Gio program or by one of its dependencies to indicate that specific
+operating-system permissions are required. For example, if a Gio
+program requires access to a device's Bluetooth interface, it
+should import "realy.lol/gio/app/permission/bluetooth" as follows:
+
+ package main
+
+ import (
+ "realy.lol/gio/app"
+ _ "realy.lol/gio/app/permission/bluetooth"
+ )
+
+ func main() {
+ ...
+ }
+
+Since there are no exported identifiers in the app/permission/bluetooth
+package, the import uses the anonymous identifier (_) as the imported
+package name.
+
+As a special case, the gogio tool detects when a program directly or
+indirectly depends on the "net" package from the Go standard library as an
+indication that the program requires network access permissions. If a program
+requires network permissions but does not directly or indirectly import
+"net", it will be necessary to add the following code somewhere in the
+program's source code:
+
+ import (
+ ...
+ _ "net"
+ )
+
+Android -- Dangerous Permissions
+
+Certain permissions on Android are marked with a protection level of
+"dangerous". This means that, in addition to including the relevant
+Gio permission packages, your app will need to prompt the user
+specifically to request access. To access the Android Activity
+required for prompting, use app.ViewEvent (only available on Android).
+app.ViewEvent exposes the underlying Android View, on which the
+getContext method returns the Activity.
+
+For more information on dangerous permissions, see:
+https://developer.android.com/guide/topics/permissions/overview#dangerous_permissions
+*/
+package permission
diff --git a/gio/app/permission/networkstate/main.go b/gio/app/permission/networkstate/main.go
new file mode 100644
index 0000000..c594219
--- /dev/null
+++ b/gio/app/permission/networkstate/main.go
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package networkstate implements permissions to access network connectivity information.
+
+Android
+
+The following entries will be added to AndroidManifest.xml:
+
+
+
+*/
+package networkstate
diff --git a/gio/app/permission/storage/main.go b/gio/app/permission/storage/main.go
new file mode 100644
index 0000000..623a624
--- /dev/null
+++ b/gio/app/permission/storage/main.go
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package storage implements read and write storage permissions
+on mobile devices.
+
+Android
+
+The following entries will be added to AndroidManifest.xml:
+
+
+
+
+READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE are "dangerous" permissions.
+See documentation for package realy.lol/gio/app/permission for more information.
+*/
+package storage
diff --git a/gio/app/sigpipe_darwin.go b/gio/app/sigpipe_darwin.go
new file mode 100644
index 0000000..aca19b7
--- /dev/null
+++ b/gio/app/sigpipe_darwin.go
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build !go1.14
+
+// Work around golang.org/issue/33384, fixed in CL 191785,
+// to be released in Go 1.14.
+
+package app
+
+import (
+ "os"
+ "os/signal"
+ "syscall"
+)
+
+func init() {
+ signal.Notify(make(chan os.Signal), syscall.SIGPIPE)
+}
diff --git a/gio/app/window.go b/gio/app/window.go
new file mode 100644
index 0000000..815e1e6
--- /dev/null
+++ b/gio/app/window.go
@@ -0,0 +1,531 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package app
+
+import (
+ "errors"
+ "fmt"
+ "image"
+ "time"
+
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/profile"
+ "realy.lol/gio/io/router"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+
+ _ "realy.lol/gio/app/internal/log"
+ "realy.lol/gio/app/internal/wm"
+)
+
+// WindowOption configures a wm.
+type Option func(opts *wm.Options)
+
+// Window represents an operating system wm.
+type Window struct {
+ driver wm.Driver
+ ctx wm.Context
+ loop *renderLoop
+
+ // driverFuncs is a channel of functions to run when
+ // the Window has a valid driver.
+ driverFuncs chan func()
+
+ out chan event.Event
+ in chan event.Event
+ ack chan struct{}
+ invalidates chan struct{}
+ frames chan *op.Ops
+ frameAck chan struct{}
+ // dead is closed when the window is destroyed.
+ dead chan struct{}
+
+ stage system.Stage
+ animating bool
+ hasNextFrame bool
+ nextFrame time.Time
+ delayedDraw *time.Timer
+
+ queue queue
+ cursor pointer.CursorName
+
+ callbacks callbacks
+}
+
+type callbacks struct {
+ w *Window
+}
+
+// queue is an event.Queue implementation that distributes system events
+// to the input handlers declared in the most recent frame.
+type queue struct {
+ q router.Router
+}
+
+// driverEvent is sent when a new native driver
+// is available for the wm.
+type driverEvent struct {
+ driver wm.Driver
+}
+
+// Pre-allocate the ack event to avoid garbage.
+var ackEvent event.Event
+
+// NewWindow creates a new window for a set of window
+// options. The options are hints; the platform is free to
+// ignore or adjust them.
+//
+// If the current program is running on iOS and Android,
+// NewWindow returns the window previously created by the
+// platform.
+//
+// Calling NewWindow more than once is not supported on
+// iOS, Android, WebAssembly.
+func NewWindow(options ...Option) *Window {
+ opts := new(wm.Options)
+ // Default options.
+ Size(unit.Px(800), unit.Px(600))(opts)
+ Title("Gio")(opts)
+
+ for _, o := range options {
+ o(opts)
+ }
+
+ w := &Window{
+ in: make(chan event.Event),
+ out: make(chan event.Event),
+ ack: make(chan struct{}),
+ invalidates: make(chan struct{}, 1),
+ frames: make(chan *op.Ops),
+ frameAck: make(chan struct{}),
+ driverFuncs: make(chan func()),
+ dead: make(chan struct{}),
+ }
+ w.callbacks.w = w
+ go w.run(opts)
+ return w
+}
+
+// Events returns the channel where events are delivered.
+func (w *Window) Events() <-chan event.Event {
+ return w.out
+}
+
+// update updates the wm. Paint operations updates the
+// window contents, input operations declare input handlers,
+// and so on. The supplied operations list completely replaces
+// the window state from previous calls.
+func (w *Window) update(frame *op.Ops) {
+ w.frames <- frame
+ <-w.frameAck
+}
+
+func (w *Window) validateAndProcess(frameStart time.Time, size image.Point,
+ sync bool, frame *op.Ops) error {
+ for {
+ if w.loop != nil {
+ if err := w.loop.Flush(); err != nil {
+ w.destroyGPU()
+ if err == wm.ErrDeviceLost {
+ continue
+ }
+ return err
+ }
+ }
+ if w.loop == nil {
+ var err error
+ w.ctx, err = w.driver.NewContext()
+ if err != nil {
+ return err
+ }
+ w.loop, err = newLoop(w.ctx)
+ if err != nil {
+ w.ctx.Release()
+ return err
+ }
+ }
+ w.processFrame(frameStart, size, frame)
+ if sync {
+ if err := w.loop.Flush(); err != nil {
+ w.destroyGPU()
+ if err == wm.ErrDeviceLost {
+ continue
+ }
+ return err
+ }
+ }
+ return nil
+ }
+}
+
+func (w *Window) processFrame(frameStart time.Time, size image.Point,
+ frame *op.Ops) {
+ sync := w.loop.Draw(size, frame)
+ w.queue.q.Frame(frame)
+ switch w.queue.q.TextInputState() {
+ case router.TextInputOpen:
+ w.driver.ShowTextInput(true)
+ case router.TextInputClose:
+ w.driver.ShowTextInput(false)
+ }
+ if txt, ok := w.queue.q.WriteClipboard(); ok {
+ go w.WriteClipboard(txt)
+ }
+ if w.queue.q.ReadClipboard() {
+ go w.ReadClipboard()
+ }
+ if w.queue.q.Profiling() {
+ frameDur := time.Since(frameStart)
+ frameDur = frameDur.Truncate(100 * time.Microsecond)
+ q := 100 * time.Microsecond
+ timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(q),
+ w.loop.Summary())
+ w.queue.q.Queue(profile.Event{Timings: timings})
+ }
+ if t, ok := w.queue.q.WakeupTime(); ok {
+ w.setNextFrame(t)
+ }
+ // Opportunistically check whether Invalidate has been called, to avoid
+ // stopping and starting animation mode.
+ select {
+ case <-w.invalidates:
+ w.setNextFrame(time.Time{})
+ default:
+ }
+ w.updateAnimation()
+ // Wait for the GPU goroutine to finish processing frame.
+ <-sync
+}
+
+// Invalidate the window such that a FrameEvent will be generated immediately.
+// If the window is inactive, the event is sent when the window becomes active.
+//
+// Note that Invalidate is intended for externally triggered updates, such as a
+// response from a network request. InvalidateOp is more efficient for animation
+// and similar internal updates.
+//
+// Invalidate is safe for concurrent use.
+func (w *Window) Invalidate() {
+ select {
+ case w.invalidates <- struct{}{}:
+ default:
+ }
+}
+
+// Option applies the options to the window.
+func (w *Window) Option(opts ...Option) {
+ go w.driverDo(func() {
+ o := new(wm.Options)
+ for _, opt := range opts {
+ opt(o)
+ }
+ w.driver.Option(o)
+ })
+}
+
+// ReadClipboard initiates a read of the clipboard in the form
+// of a clipboard.Event. Multiple reads may be coalesced
+// to a single event.
+func (w *Window) ReadClipboard() {
+ go w.driverDo(func() {
+ w.driver.ReadClipboard()
+ })
+}
+
+// WriteClipboard writes a string to the clipboard.
+func (w *Window) WriteClipboard(s string) {
+ go w.driverDo(func() {
+ w.driver.WriteClipboard(s)
+ })
+}
+
+// SetCursorName changes the current window cursor to name.
+func (w *Window) SetCursorName(name pointer.CursorName) {
+ go w.driverDo(func() {
+ w.driver.SetCursor(name)
+ })
+}
+
+// Close the wm. The window's event loop should exit when it receives
+// system.DestroyEvent.
+//
+// Currently, only macOS, Windows and X11 drivers implement this functionality,
+// all others are stubbed.
+func (w *Window) Close() {
+ go w.driverDo(func() {
+ w.driver.Close()
+ })
+}
+
+// driverDo waits for the window to have a valid driver attached and calls f.
+// It does nothing if the if the window was destroyed while waiting.
+func (w *Window) driverDo(f func()) {
+ select {
+ case w.driverFuncs <- f:
+ case <-w.dead:
+ }
+}
+
+func (w *Window) updateAnimation() {
+ animate := false
+ if w.delayedDraw != nil {
+ w.delayedDraw.Stop()
+ w.delayedDraw = nil
+ }
+ if w.stage >= system.StageRunning && w.hasNextFrame {
+ if dt := time.Until(w.nextFrame); dt <= 0 {
+ animate = true
+ } else {
+ w.delayedDraw = time.NewTimer(dt)
+ }
+ }
+ if animate != w.animating {
+ w.animating = animate
+ w.driver.SetAnimating(animate)
+ }
+}
+
+func (w *Window) setNextFrame(at time.Time) {
+ if !w.hasNextFrame || at.Before(w.nextFrame) {
+ w.hasNextFrame = true
+ w.nextFrame = at
+ }
+}
+
+func (c *callbacks) SetDriver(d wm.Driver) {
+ c.Event(driverEvent{d})
+}
+
+func (c *callbacks) Event(e event.Event) {
+ select {
+ case c.w.in <- e:
+ <-c.w.ack
+ case <-c.w.dead:
+ }
+}
+
+func (w *Window) waitAck() {
+ // Send a dummy event; when it gets through we
+ // know the application has processed the previous event.
+ w.out <- ackEvent
+}
+
+// Prematurely destroy the window and wait for the native window
+// destroy event.
+func (w *Window) destroy(err error) {
+ w.destroyGPU()
+ // Ack the current event.
+ w.ack <- struct{}{}
+ w.out <- system.DestroyEvent{Err: err}
+ close(w.dead)
+ for e := range w.in {
+ w.ack <- struct{}{}
+ if _, ok := e.(system.DestroyEvent); ok {
+ return
+ }
+ }
+}
+
+func (w *Window) destroyGPU() {
+ if w.loop != nil {
+ w.loop.Release()
+ w.loop = nil
+ }
+ if w.ctx != nil {
+ w.ctx.Release()
+ w.ctx = nil
+ }
+}
+
+// waitFrame waits for the client to either call FrameEvent.Frame
+// or to continue event handling. It returns whether the client
+// called Frame or not.
+func (w *Window) waitFrame() (*op.Ops, bool) {
+ select {
+ case frame := <-w.frames:
+ // The client called FrameEvent.Frame.
+ return frame, true
+ case w.out <- ackEvent:
+ // The client ignored FrameEvent and continued processing
+ // events.
+ return nil, false
+ }
+}
+
+func (w *Window) run(opts *wm.Options) {
+ defer close(w.in)
+ defer close(w.out)
+ if err := wm.NewWindow(&w.callbacks, opts); err != nil {
+ w.out <- system.DestroyEvent{Err: err}
+ return
+ }
+ for {
+ var driverFuncs chan func()
+ if w.driver != nil {
+ driverFuncs = w.driverFuncs
+ }
+ var timer <-chan time.Time
+ if w.delayedDraw != nil {
+ timer = w.delayedDraw.C
+ }
+ select {
+ case <-timer:
+ w.setNextFrame(time.Time{})
+ w.updateAnimation()
+ case <-w.invalidates:
+ w.setNextFrame(time.Time{})
+ w.updateAnimation()
+ case f := <-driverFuncs:
+ f()
+ case e := <-w.in:
+ switch e2 := e.(type) {
+ case system.StageEvent:
+ if w.loop != nil {
+ if e2.Stage < system.StageRunning {
+ w.destroyGPU()
+ } else {
+ w.loop.Refresh()
+ }
+ }
+ w.stage = e2.Stage
+ w.updateAnimation()
+ w.out <- e
+ w.waitAck()
+ case wm.FrameEvent:
+ if e2.Size == (image.Point{}) {
+ panic(errors.New("internal error: zero-sized Draw"))
+ }
+ if w.stage < system.StageRunning {
+ // No drawing if not visible.
+ break
+ }
+ frameStart := time.Now()
+ w.hasNextFrame = false
+ e2.Frame = w.update
+ e2.Queue = &w.queue
+ w.out <- e2.FrameEvent
+ if w.loop != nil {
+ if e2.Sync {
+ w.loop.Refresh()
+ }
+ }
+ frame, gotFrame := w.waitFrame()
+ err := w.validateAndProcess(frameStart, e2.Size, e2.Sync, frame)
+ if gotFrame {
+ // We're done with frame, let the client continue.
+ w.frameAck <- struct{}{}
+ }
+ if err != nil {
+ w.destroyGPU()
+ w.destroy(err)
+ return
+ }
+ w.updateCursor()
+ case *system.CommandEvent:
+ w.out <- e
+ w.waitAck()
+ case driverEvent:
+ w.driver = e2.driver
+ case system.DestroyEvent:
+ w.destroyGPU()
+ w.out <- e2
+ w.ack <- struct{}{}
+ return
+ case event.Event:
+ if w.queue.q.Queue(e2) {
+ w.setNextFrame(time.Time{})
+ w.updateAnimation()
+ }
+ w.updateCursor()
+ w.out <- e
+ }
+ w.ack <- struct{}{}
+ }
+ }
+}
+
+func (w *Window) updateCursor() {
+ if c := w.queue.q.Cursor(); c != w.cursor {
+ w.cursor = c
+ w.SetCursorName(c)
+ }
+}
+
+func (q *queue) Events(k event.Tag) []event.Event {
+ return q.q.Events(k)
+}
+
+const (
+ // Windowed is the normal window mode with OS specific window decorations.
+ Windowed = wm.Windowed
+ // Fullscreen is the full screen window mode.
+ Fullscreen = wm.Fullscreen
+)
+
+// WindowMode sets the window mode.
+//
+// Supported platforms are macOS, X11 and Windows.
+func WindowMode(mode wm.WindowMode) Option {
+ return func(opts *wm.Options) {
+ opts.WindowMode = &mode
+ }
+}
+
+// Title sets the title of the wm.
+func Title(t string) Option {
+ return func(opts *wm.Options) {
+ opts.Title = &t
+ }
+}
+
+// Size sets the size of the wm.
+func Size(w, h unit.Value) Option {
+ if w.V <= 0 {
+ panic("width must be larger than or equal to 0")
+ }
+ if h.V <= 0 {
+ panic("height must be larger than or equal to 0")
+ }
+ return func(opts *wm.Options) {
+ opts.Size = &wm.Size{
+ Width: w,
+ Height: h,
+ }
+ }
+}
+
+// MaxSize sets the maximum size of the wm.
+func MaxSize(w, h unit.Value) Option {
+ if w.V <= 0 {
+ panic("width must be larger than or equal to 0")
+ }
+ if h.V <= 0 {
+ panic("height must be larger than or equal to 0")
+ }
+ return func(opts *wm.Options) {
+ opts.MaxSize = &wm.Size{
+ Width: w,
+ Height: h,
+ }
+ }
+}
+
+// MinSize sets the minimum size of the wm.
+func MinSize(w, h unit.Value) Option {
+ if w.V <= 0 {
+ panic("width must be larger than or equal to 0")
+ }
+ if h.V <= 0 {
+ panic("height must be larger than or equal to 0")
+ }
+ return func(opts *wm.Options) {
+ opts.MinSize = &wm.Size{
+ Width: w,
+ Height: h,
+ }
+ }
+}
+
+func (driverEvent) ImplementsEvent() {}
diff --git a/gio/cmd/go.local.sum b/gio/cmd/go.local.sum
new file mode 100644
index 0000000..c197ac2
--- /dev/null
+++ b/gio/cmd/go.local.sum
@@ -0,0 +1,53 @@
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o=
+github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
+github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg=
+github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
+github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194=
+github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
+github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
+github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
+github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
+github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
+github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210304124612-50617c2ba197 h1:7+SpRyhoo46QjKkYInQXpcfxx3TYFEYkn131lwGE9/0=
+golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/gio/cmd/go.sum b/gio/cmd/go.sum
new file mode 100644
index 0000000..c197ac2
--- /dev/null
+++ b/gio/cmd/go.sum
@@ -0,0 +1,53 @@
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o=
+github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
+github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg=
+github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
+github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194=
+github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
+github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
+github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
+github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
+github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
+github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210304124612-50617c2ba197 h1:7+SpRyhoo46QjKkYInQXpcfxx3TYFEYkn131lwGE9/0=
+golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/gio/cmd/gogio/android_test.go b/gio/cmd/gogio/android_test.go
new file mode 100644
index 0000000..e73386f
--- /dev/null
+++ b/gio/cmd/gogio/android_test.go
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main_test
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "image"
+ "image/png"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+)
+
+type AndroidTestDriver struct {
+ driverBase
+
+ sdkDir string
+ adbPath string
+}
+
+var rxAdbDevice = regexp.MustCompile(`(.*)\s+device$`)
+
+func (d *AndroidTestDriver) Start(path string) {
+ d.sdkDir = os.Getenv("ANDROID_SDK_ROOT")
+ if d.sdkDir == "" {
+ d.Skipf("Android SDK is required; set $ANDROID_SDK_ROOT")
+ }
+ d.adbPath = filepath.Join(d.sdkDir, "platform-tools", "adb")
+ if _, err := os.Stat(d.adbPath); os.IsNotExist(err) {
+ d.Skipf("adb not found")
+ }
+
+ devOut := bytes.TrimSpace(d.adb("devices"))
+ devices := rxAdbDevice.FindAllSubmatch(devOut, -1)
+ switch len(devices) {
+ case 0:
+ d.Skipf("no Android devices attached via adb; skipping")
+ case 1:
+ default:
+ d.Skipf("multiple Android devices attached via adb; skipping")
+ }
+
+ // If the device is attached but asleep, it's probably just charging.
+ // Don't use it; the screen needs to be on and unlocked for the test to
+ // work.
+ if !bytes.Contains(
+ d.adb("shell", "dumpsys", "power"),
+ []byte(" mWakefulness=Awake"),
+ ) {
+ d.Skipf("Android device isn't awake; skipping")
+ }
+
+ // First, build the app.
+ apk := filepath.Join(d.tempDir("gio-endtoend-android"), "e2e.apk")
+ d.gogio("-target=android", "-appid="+appid, "-o="+apk, path)
+
+ // Make sure the app isn't installed already, and try to uninstall it
+ // when we finish. Previous failed test runs might have left the app.
+ d.tryUninstall()
+ d.adb("install", apk)
+ d.Cleanup(d.tryUninstall)
+
+ // Force our e2e app to be fullscreen, so that the android system bar at
+ // the top doesn't mess with our screenshots.
+ // TODO(mvdan): is there a way to do this via gio, so that we don't need
+ // to set up a global Android setting via the shell?
+ d.adb("shell", "settings", "put", "global", "policy_control", "immersive.full="+appid)
+
+ // Make sure the app isn't already running.
+ d.adb("shell", "pm", "clear", appid)
+
+ // Start listening for log messages.
+ {
+ ctx, cancel := context.WithCancel(context.Background())
+ cmd := exec.CommandContext(ctx, d.adbPath,
+ "logcat",
+ "-s", // suppress other logs
+ "-T1", // don't show previous log messages
+ appid+":*", // show all logs from our gio app ID
+ )
+ output, err := cmd.StdoutPipe()
+ if err != nil {
+ d.Fatal(err)
+ }
+ cmd.Stderr = cmd.Stdout
+ d.output = output
+ if err := cmd.Start(); err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(cancel)
+ }
+
+ // Start the app.
+ d.adb("shell", "monkey", "-p", appid, "1")
+
+ // Wait for the gio app to render.
+ d.waitForFrame()
+}
+
+func (d *AndroidTestDriver) Screenshot() image.Image {
+ out := d.adb("shell", "screencap", "-p")
+ img, err := png.Decode(bytes.NewReader(out))
+ if err != nil {
+ d.Fatal(err)
+ }
+ return img
+}
+
+func (d *AndroidTestDriver) tryUninstall() {
+ cmd := exec.Command(d.adbPath, "shell", "pm", "uninstall", appid)
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ if bytes.Contains(out, []byte("Unknown package")) {
+ // The package is not installed. Don't log anything.
+ return
+ }
+ d.Logf("could not uninstall: %v\n%s", err, out)
+ }
+}
+
+func (d *AndroidTestDriver) adb(args ...interface{}) []byte {
+ strs := []string{}
+ for _, arg := range args {
+ strs = append(strs, fmt.Sprint(arg))
+ }
+ cmd := exec.Command(d.adbPath, strs...)
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ d.Errorf("%s", out)
+ d.Fatal(err)
+ }
+ return out
+}
+
+func (d *AndroidTestDriver) Click(x, y int) {
+ d.adb("shell", "input", "tap", x, y)
+
+ // Wait for the gio app to render after this click.
+ d.waitForFrame()
+}
diff --git a/gio/cmd/gogio/androidbuild.go b/gio/cmd/gogio/androidbuild.go
new file mode 100644
index 0000000..4a055b9
--- /dev/null
+++ b/gio/cmd/gogio/androidbuild.go
@@ -0,0 +1,1032 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "archive/zip"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "text/template"
+
+ "golang.org/x/sync/errgroup"
+ "golang.org/x/tools/go/packages"
+)
+
+type androidTools struct {
+ buildtools string
+ androidjar string
+}
+
+// zip.Writer with a sticky error.
+type zipWriter struct {
+ err error
+ w *zip.Writer
+}
+
+// Writer that saves any errors.
+type errWriter struct {
+ w io.Writer
+ err *error
+}
+
+var exeSuffix string
+
+type manifestData struct {
+ AppID string
+ Version int
+ MinSDK int
+ TargetSDK int
+ Permissions []string
+ Features []string
+ IconSnip string
+ AppName string
+}
+
+const (
+ themes = `
+
+
+`
+ themesV21 = `
+
+
+`
+)
+
+func init() {
+ if runtime.GOOS == "windows" {
+ exeSuffix = ".exe"
+ }
+}
+
+func buildAndroid(tmpDir string, bi *buildInfo) error {
+ sdk := os.Getenv("ANDROID_SDK_ROOT")
+ if sdk == "" {
+ return errors.New("please set ANDROID_SDK_ROOT to the Android SDK path")
+ }
+ if _, err := os.Stat(sdk); err != nil {
+ return err
+ }
+ platform, err := latestPlatform(sdk)
+ if err != nil {
+ return err
+ }
+ buildtools, err := latestTools(sdk)
+ if err != nil {
+ return err
+ }
+
+ tools := &androidTools{
+ buildtools: buildtools,
+ androidjar: filepath.Join(platform, "android.jar"),
+ }
+ perms := []string{"default"}
+ const permPref = "realy.lol/gio/app/permission/"
+ cfg := &packages.Config{
+ Mode: packages.NeedName +
+ packages.NeedFiles +
+ packages.NeedImports +
+ packages.NeedDeps,
+ Env: append(
+ os.Environ(),
+ "GOOS=android",
+ "CGO_ENABLED=1",
+ ),
+ }
+ pkgs, err := packages.Load(cfg, bi.pkgPath)
+ if err != nil {
+ return err
+ }
+ var extraJars []string
+ visitedPkgs := make(map[string]bool)
+ var visitPkg func(*packages.Package) error
+ visitPkg = func(p *packages.Package) error {
+ if len(p.GoFiles) == 0 {
+ return nil
+ }
+ dir := filepath.Dir(p.GoFiles[0])
+ jars, err := filepath.Glob(filepath.Join(dir, "*.jar"))
+ if err != nil {
+ return err
+ }
+ extraJars = append(extraJars, jars...)
+ switch {
+ case p.PkgPath == "net":
+ perms = append(perms, "network")
+ case strings.HasPrefix(p.PkgPath, permPref):
+ perms = append(perms, p.PkgPath[len(permPref):])
+ }
+
+ for _, imp := range p.Imports {
+ if !visitedPkgs[imp.ID] {
+ visitPkg(imp)
+ visitedPkgs[imp.ID] = true
+ }
+ }
+ return nil
+ }
+ if err := visitPkg(pkgs[0]); err != nil {
+ return err
+ }
+
+ if err := compileAndroid(tmpDir, tools, bi); err != nil {
+ return err
+ }
+ switch *buildMode {
+ case "archive":
+ return archiveAndroid(tmpDir, bi, perms)
+ case "exe":
+ file := *destPath
+ if file == "" {
+ file = fmt.Sprintf("%s.apk", bi.name)
+ }
+
+ isBundle := false
+ switch filepath.Ext(file) {
+ case ".apk":
+ case ".aab":
+ isBundle = true
+ default:
+ return fmt.Errorf("the specified output %q does not end in '.apk' or '.aab'",
+ file)
+ }
+
+ if err := exeAndroid(tmpDir, tools, bi, extraJars, perms,
+ isBundle); err != nil {
+ return err
+ }
+ if isBundle {
+ return signAAB(tmpDir, file, tools, bi)
+ }
+ return signAPK(tmpDir, file, tools, bi)
+ default:
+ panic("unreachable")
+ }
+}
+
+func compileAndroid(tmpDir string, tools *androidTools,
+ bi *buildInfo) (err error) {
+ androidHome := os.Getenv("ANDROID_SDK_ROOT")
+ if androidHome == "" {
+ return errors.New("ANDROID_SDK_ROOT is not set. Please point it to the root of the Android SDK")
+ }
+ javac, err := findJavaC()
+ if err != nil {
+ return fmt.Errorf("could not find javac: %v", err)
+ }
+ ndkRoot, err := findNDK(androidHome)
+ if err != nil {
+ return err
+ }
+ minSDK := 16
+ if bi.minsdk > minSDK {
+ minSDK = bi.minsdk
+ }
+ tcRoot := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt",
+ archNDK())
+ var builds errgroup.Group
+ for _, a := range bi.archs {
+ arch := allArchs[a]
+ clang, err := latestCompiler(tcRoot, a, minSDK)
+ if err != nil {
+ return fmt.Errorf("%s. Please make sure you have NDK >= r19c installed. Use the command `sdkmanager ndk-bundle` to install it.",
+ err)
+ }
+ if runtime.GOOS == "windows" {
+ // Because of https://github.com/android-ndk/ndk/issues/920,
+ // we need NDK r19c, not just r19b. Check for the presence of
+ // clang++.cmd which is only available in r19c.
+ clangpp := clang + "++.cmd"
+ if _, err := os.Stat(clangpp); err != nil {
+ return fmt.Errorf("NDK version r19b detected, but >= r19c is required. Use the command `sdkmanager ndk-bundle` to install it")
+ }
+ }
+ archDir := filepath.Join(tmpDir, "jni", arch.jniArch)
+ if err := os.MkdirAll(archDir, 0755); err != nil {
+ return fmt.Errorf("failed to create %q: %v", archDir, err)
+ }
+ libFile := filepath.Join(archDir, "libgio.so")
+ cmd := exec.Command(
+ "go",
+ "build",
+ "-ldflags=-w -s "+bi.ldflags,
+ "-buildmode=c-shared",
+ "-tags", bi.tags,
+ "-o", libFile,
+ bi.pkgPath,
+ )
+ cmd.Env = append(
+ os.Environ(),
+ "GOOS=android",
+ "GOARCH="+a,
+ "GOARM=7", // Avoid softfloat.
+ "CGO_ENABLED=1",
+ "CC="+clang,
+ )
+ builds.Go(func() error {
+ _, err := runCmd(cmd)
+ return err
+ })
+ }
+ appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}",
+ "realy.lol/gio/app/internal/wm"))
+ if err != nil {
+ return err
+ }
+ javaFiles, err := filepath.Glob(filepath.Join(appDir, "*.java"))
+ if err != nil {
+ return err
+ }
+ if len(javaFiles) > 0 {
+ classes := filepath.Join(tmpDir, "classes")
+ if err := os.MkdirAll(classes, 0755); err != nil {
+ return err
+ }
+ javac := exec.Command(
+ javac,
+ "-target", "1.8",
+ "-source", "1.8",
+ "-sourcepath", appDir,
+ "-bootclasspath", tools.androidjar,
+ "-d", classes,
+ )
+ javac.Args = append(javac.Args, javaFiles...)
+ builds.Go(func() error {
+ _, err := runCmd(javac)
+ return err
+ })
+ }
+ return builds.Wait()
+}
+
+func archiveAndroid(tmpDir string, bi *buildInfo, perms []string) (err error) {
+ aarFile := *destPath
+ if aarFile == "" {
+ aarFile = fmt.Sprintf("%s.aar", bi.name)
+ }
+ if filepath.Ext(aarFile) != ".aar" {
+ return fmt.Errorf("the specified output %q does not end in '.aar'",
+ aarFile)
+ }
+ aar, err := os.Create(aarFile)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := aar.Close(); err == nil {
+ err = cerr
+ }
+ }()
+ aarw := newZipWriter(aar)
+ defer aarw.Close()
+ aarw.Create("R.txt")
+ themesXML := aarw.Create("res/values/themes.xml")
+ themesXML.Write([]byte(themes))
+ themesXML21 := aarw.Create("res/values-v21/themes.xml")
+ themesXML21.Write([]byte(themesV21))
+ permissions, features := getPermissions(perms)
+ // Disable input emulation on ChromeOS.
+ manifest := aarw.Create("AndroidManifest.xml")
+ manifestSrc := manifestData{
+ AppID: bi.appID,
+ MinSDK: bi.minsdk,
+ Permissions: permissions,
+ Features: features,
+ }
+ tmpl, err := template.New("manifest").Parse(
+ `
+
+{{range .Permissions}}
+{{end}}{{range .Features}}
+{{end}}
+`)
+ if err != nil {
+ panic(err)
+ }
+ err = tmpl.Execute(manifest, manifestSrc)
+ proguard := aarw.Create("proguard.txt")
+ proguard.Write([]byte(`-keep class org.gioui.** { *; }`))
+
+ for _, a := range bi.archs {
+ arch := allArchs[a]
+ libFile := filepath.Join("jni", arch.jniArch, "libgio.so")
+ aarw.Add(filepath.ToSlash(libFile), filepath.Join(tmpDir, libFile))
+ }
+ classes := filepath.Join(tmpDir, "classes")
+ if _, err := os.Stat(classes); err == nil {
+ jarFile := filepath.Join(tmpDir, "classes.jar")
+ if err := writeJar(jarFile, classes); err != nil {
+ return err
+ }
+ aarw.Add("classes.jar", jarFile)
+ }
+ return aarw.Close()
+}
+
+func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo,
+ extraJars, perms []string, isBundle bool) (err error) {
+ classes := filepath.Join(tmpDir, "classes")
+ var classFiles []string
+ err = filepath.Walk(classes,
+ func(path string, f os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if filepath.Ext(path) == ".class" {
+ classFiles = append(classFiles, path)
+ }
+ return nil
+ })
+ classFiles = append(classFiles, extraJars...)
+ dexDir := filepath.Join(tmpDir, "apk")
+ if err := os.MkdirAll(dexDir, 0755); err != nil {
+ return err
+ }
+ if len(classFiles) > 0 {
+ d8 := exec.Command(
+ filepath.Join(tools.buildtools, "d8"),
+ "--classpath", tools.androidjar,
+ "--output", dexDir,
+ )
+ d8.Args = append(d8.Args, classFiles...)
+ if _, err := runCmd(d8); err != nil {
+ return err
+ }
+ }
+
+ // Compile resources.
+ resDir := filepath.Join(tmpDir, "res")
+ valDir := filepath.Join(resDir, "values")
+ v21Dir := filepath.Join(resDir, "values-v21")
+ for _, dir := range []string{valDir, v21Dir} {
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return err
+ }
+ }
+ iconSnip := ""
+ if _, err := os.Stat(bi.iconPath); err == nil {
+ err := buildIcons(resDir, bi.iconPath, []iconVariant{
+ {path: filepath.Join("mipmap-hdpi", "ic_launcher.png"), size: 72},
+ {path: filepath.Join("mipmap-xhdpi", "ic_launcher.png"), size: 96},
+ {path: filepath.Join("mipmap-xxhdpi", "ic_launcher.png"),
+ size: 144},
+ {path: filepath.Join("mipmap-xxxhdpi", "ic_launcher.png"),
+ size: 192},
+ })
+ if err != nil {
+ return err
+ }
+ iconSnip = `android:icon="@mipmap/ic_launcher"`
+ }
+ err = ioutil.WriteFile(filepath.Join(valDir, "themes.xml"), []byte(themes),
+ 0660)
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(filepath.Join(v21Dir, "themes.xml"),
+ []byte(themesV21), 0660)
+ if err != nil {
+ return err
+ }
+ resZip := filepath.Join(tmpDir, "resources.zip")
+ aapt2 := filepath.Join(tools.buildtools, "aapt2")
+ _, err = runCmd(exec.Command(
+ aapt2,
+ "compile",
+ "-o", resZip,
+ "--dir", resDir))
+ if err != nil {
+ return err
+ }
+
+ // Link APK.
+ // Currently, new apps must have a target SDK version of at least 30.
+ // https://developer.android.com/distribute/best-practices/develop/target-sdk
+ targetSDK := 30
+ if bi.minsdk > targetSDK {
+ targetSDK = bi.minsdk
+ }
+ minSDK := 16
+ if bi.minsdk > minSDK {
+ minSDK = bi.minsdk
+ }
+ permissions, features := getPermissions(perms)
+ appName := strings.Title(bi.name)
+ manifestSrc := manifestData{
+ AppID: bi.appID,
+ Version: bi.version,
+ MinSDK: minSDK,
+ TargetSDK: targetSDK,
+ Permissions: permissions,
+ Features: features,
+ IconSnip: iconSnip,
+ AppName: appName,
+ }
+ tmpl, err := template.New("test").Parse(
+ `
+
+
+{{range .Permissions}}
+{{end}}{{range .Features}}
+{{end}}
+
+
+
+
+
+
+
+`)
+ var manifestBuffer bytes.Buffer
+ if err := tmpl.Execute(&manifestBuffer, manifestSrc); err != nil {
+ return err
+ }
+ manifest := filepath.Join(tmpDir, "AndroidManifest.xml")
+ if err := ioutil.WriteFile(manifest, manifestBuffer.Bytes(),
+ 0660); err != nil {
+ return err
+ }
+
+ linkAPK := filepath.Join(tmpDir, "link.apk")
+
+ args := []string{
+ "link",
+ "--manifest", manifest,
+ "-I", tools.androidjar,
+ "-o", linkAPK,
+ }
+ if isBundle {
+ args = append(args, "--proto-format")
+ }
+ args = append(args, resZip)
+
+ if _, err := runCmd(exec.Command(aapt2, args...)); err != nil {
+ return err
+ }
+
+ // The Go standard library archive/zip doesn't support appending to zip
+ // files. Copy files from `link.apk` (generated by aapt2) along with classes.dex and
+ // the Go libraries to a new `app.zip` file.
+
+ // Load link.apk as zip.
+ linkAPKZip, err := zip.OpenReader(linkAPK)
+ if err != nil {
+ return err
+ }
+ defer linkAPKZip.Close()
+
+ // Create new "APK".
+ unsignedAPK := filepath.Join(tmpDir, "app.zip")
+ unsignedAPKFile, err := os.Create(unsignedAPK)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := unsignedAPKFile.Close(); err == nil {
+ err = cerr
+ }
+ }()
+ unsignedAPKZip := zip.NewWriter(unsignedAPKFile)
+ defer unsignedAPKZip.Close()
+
+ // Copy files from linkAPK to unsignedAPK.
+ for _, f := range linkAPKZip.File {
+ header := zip.FileHeader{
+ Name: f.FileHeader.Name,
+ Method: f.FileHeader.Method,
+ }
+
+ if isBundle {
+ // AAB have pre-defined folders.
+ switch header.Name {
+ case "AndroidManifest.xml":
+ header.Name = "manifest/AndroidManifest.xml"
+ }
+ }
+
+ w, err := unsignedAPKZip.CreateHeader(&header)
+ if err != nil {
+ return err
+ }
+ r, err := f.Open()
+ if err != nil {
+ return err
+ }
+ if _, err := io.Copy(w, r); err != nil {
+ return err
+ }
+ }
+
+ // Append new files (that doesn't exists inside the link.apk).
+ appendToZip := func(path string, file string) error {
+ f, err := os.Open(file)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ w, err := unsignedAPKZip.CreateHeader(&zip.FileHeader{
+ Name: filepath.ToSlash(path),
+ Method: zip.Deflate,
+ })
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(w, f)
+ return err
+ }
+
+ // Append Go binaries (libgio.so).
+ for _, a := range bi.archs {
+ arch := allArchs[a]
+ libFile := filepath.Join(arch.jniArch, "libgio.so")
+ if err := appendToZip(filepath.Join("lib", libFile),
+ filepath.Join(tmpDir, "jni", libFile)); err != nil {
+ return err
+ }
+ }
+
+ // Append classes.dex.
+ classesFolder := "classes.dex"
+ if isBundle {
+ classesFolder = "dex/classes.dex"
+ }
+ if err := appendToZip(classesFolder,
+ filepath.Join(dexDir, "classes.dex")); err != nil {
+ return err
+ }
+
+ return unsignedAPKZip.Close()
+}
+
+func signAPK(tmpDir string, apkFile string, tools *androidTools,
+ bi *buildInfo) error {
+ if err := zipalign(tools, filepath.Join(tmpDir, "app.zip"),
+ apkFile); err != nil {
+ return err
+ }
+
+ if bi.key == "" {
+ if err := defaultAndroidKeystore(tmpDir, bi); err != nil {
+ return err
+ }
+ }
+
+ _, err := runCmd(exec.Command(
+ filepath.Join(tools.buildtools, "apksigner"),
+ "sign",
+ "--ks-pass", "pass:"+bi.password,
+ "--ks", bi.key,
+ apkFile,
+ ))
+
+ return err
+}
+
+func signAAB(tmpDir string, aabFile string, tools *androidTools,
+ bi *buildInfo) error {
+ allBundleTools, err := filepath.Glob(filepath.Join(tools.buildtools,
+ "bundletool*.jar"))
+ if err != nil {
+ return err
+ }
+
+ bundletool := ""
+ for _, v := range allBundleTools {
+ bundletool = v
+ break
+ }
+
+ if bundletool == "" {
+ return fmt.Errorf("bundletool was not found at %s. Download it from https://github.com/google/bundletool/releases and move to the respective folder",
+ tools.buildtools)
+ }
+
+ _, err = runCmd(exec.Command(
+ "java",
+ "-jar", bundletool,
+ "build-bundle",
+ "--modules="+filepath.Join(tmpDir, "app.zip"),
+ "--output="+filepath.Join(tmpDir, "app.aab"),
+ ))
+ if err != nil {
+ return err
+ }
+
+ if err := zipalign(tools, filepath.Join(tmpDir, "app.aab"),
+ aabFile); err != nil {
+ return err
+ }
+
+ if bi.key == "" {
+ if err := defaultAndroidKeystore(tmpDir, bi); err != nil {
+ return err
+ }
+ }
+
+ keytoolList, err := runCmd(exec.Command(
+ "keytool",
+ "-keystore", bi.key,
+ "-list",
+ "-keypass", bi.password,
+ "-v",
+ ))
+ if err != nil {
+ return err
+ }
+
+ var alias string
+ for _, t := range strings.Split(keytoolList, "\n") {
+ if i, _ := fmt.Sscanf(t, "Alias name: %s", &alias); i > 0 {
+ break
+ }
+ }
+
+ _, err = runCmd(exec.Command(
+ filepath.Join("jarsigner"),
+ "-sigalg", "SHA256withRSA",
+ "-digestalg", "SHA-256",
+ "-keystore", bi.key,
+ "-storepass", bi.password,
+ aabFile,
+ strings.TrimSpace(alias),
+ ))
+
+ return err
+}
+
+func zipalign(tools *androidTools, input, output string) error {
+ _, err := runCmd(exec.Command(
+ filepath.Join(tools.buildtools, "zipalign"),
+ "-f",
+ "4", // 32-bit alignment.
+ input,
+ output,
+ ))
+ return err
+}
+
+func defaultAndroidKeystore(tmpDir string, bi *buildInfo) error {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return err
+ }
+
+ // Use debug.keystore, if exists.
+ bi.key = filepath.Join(home, ".android", "debug.keystore")
+ bi.password = "android"
+ if _, err := os.Stat(bi.key); err == nil {
+ return nil
+ }
+
+ // Generate new key.
+ bi.key = filepath.Join(tmpDir, "sign.keystore")
+ keytool, err := findKeytool()
+ if err != nil {
+ return err
+ }
+ _, err = runCmd(exec.Command(
+ keytool,
+ "-genkey",
+ "-keystore", bi.key,
+ "-storepass", bi.password,
+ "-alias", "android",
+ "-keyalg", "RSA", "-keysize", "2048",
+ "-validity", "10000",
+ "-noprompt",
+ "-dname", "CN=android",
+ ))
+ return err
+}
+
+func findNDK(androidHome string) (string, error) {
+ ndks, err := filepath.Glob(filepath.Join(androidHome, "ndk", "*"))
+ if err != nil {
+ return "", err
+ }
+ if bestNDK, found := latestVersionPath(ndks); found {
+ return bestNDK, nil
+ }
+ // The old NDK path was $ANDROID_SDK_ROOT/ndk-bundle.
+ ndkBundle := filepath.Join(androidHome, "ndk-bundle")
+ if _, err := os.Stat(ndkBundle); err == nil {
+ return ndkBundle, nil
+ }
+ // Certain non-standard NDK isntallations set the $ANDROID_NDK_ROOT
+ // environment variable
+ if ndkBundle, ok := os.LookupEnv("ANDROID_NDK_ROOT"); ok {
+ if _, err := os.Stat(ndkBundle); err == nil {
+ return ndkBundle, nil
+ }
+ }
+
+ return "", fmt.Errorf("no NDK found in $ANDROID_SDK_ROOT (%s). Set $ANDROID_NDK_ROOT or use `sdkmanager ndk-bundle` to install the NDK",
+ androidHome)
+}
+
+func findKeytool() (string, error) {
+ keytool, err := exec.LookPath("keytool")
+ if err == nil {
+ return keytool, err
+ }
+ javaHome := os.Getenv("JAVA_HOME")
+ if javaHome == "" {
+ return "", err
+ }
+ keytool = filepath.Join(javaHome, "jre", "bin", "keytool"+exeSuffix)
+ if _, serr := os.Stat(keytool); serr == nil {
+ return keytool, nil
+ }
+ return "", err
+}
+
+func findJavaC() (string, error) {
+ javac, err := exec.LookPath("javac")
+ if err == nil {
+ return javac, err
+ }
+ javaHome := os.Getenv("JAVA_HOME")
+ if javaHome == "" {
+ return "", err
+ }
+ javac = filepath.Join(javaHome, "bin", "javac"+exeSuffix)
+ if _, serr := os.Stat(javac); serr == nil {
+ return javac, nil
+ }
+ return "", err
+}
+
+func writeJar(jarFile, dir string) (err error) {
+ jar, err := os.Create(jarFile)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := jar.Close(); err == nil {
+ err = cerr
+ }
+ }()
+ jarw := newZipWriter(jar)
+ const manifestHeader = `Manifest-Version: 1.0
+Created-By: 1.0 (Go)
+
+`
+ jarw.Create("META-INF/MANIFEST.MF").Write([]byte(manifestHeader))
+ err = filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if f.IsDir() {
+ return nil
+ }
+ if filepath.Ext(path) == ".class" {
+ rel := filepath.ToSlash(path[len(dir)+1:])
+ jarw.Add(rel, path)
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ return jarw.Close()
+}
+
+func archNDK() string {
+ var arch string
+ switch runtime.GOARCH {
+ case "386":
+ arch = "x86"
+ case "amd64":
+ arch = "x86_64"
+ default:
+ panic("unsupported GOARCH: " + runtime.GOARCH)
+ }
+ return runtime.GOOS + "-" + arch
+}
+
+func getPermissions(ps []string) ([]string, []string) {
+ var permissions, features []string
+ seenPermissions := make(map[string]bool)
+ seenFeatures := make(map[string]bool)
+ for _, perm := range ps {
+ for _, x := range AndroidPermissions[perm] {
+ if !seenPermissions[x] {
+ permissions = append(permissions, x)
+ seenPermissions[x] = true
+ }
+ }
+ for _, x := range AndroidFeatures[perm] {
+ if !seenFeatures[x] {
+ features = append(features, x)
+ seenFeatures[x] = true
+ }
+ }
+ }
+ return permissions, features
+}
+
+func latestPlatform(sdk string) (string, error) {
+ allPlats, err := filepath.Glob(filepath.Join(sdk, "platforms", "android-*"))
+ if err != nil {
+ return "", err
+ }
+ var bestVer int
+ var bestPlat string
+ for _, platform := range allPlats {
+ _, name := filepath.Split(platform)
+ // The glob above guarantees the "android-" prefix.
+ verStr := name[len("android-"):]
+ ver, err := strconv.Atoi(verStr)
+ if err != nil {
+ continue
+ }
+ if ver < bestVer {
+ continue
+ }
+ bestVer = ver
+ bestPlat = platform
+ }
+ if bestPlat == "" {
+ return "", fmt.Errorf("no platforms found in %q", sdk)
+ }
+ return bestPlat, nil
+}
+
+func latestCompiler(tcRoot, a string, minsdk int) (string, error) {
+ arch := allArchs[a]
+ allComps, err := filepath.Glob(filepath.Join(tcRoot, "bin",
+ arch.clangArch+"*-clang"))
+ if err != nil {
+ return "", err
+ }
+ var bestVer int
+ var firstVer int
+ var bestCompiler string
+ var firstCompiler string
+ for _, compiler := range allComps {
+ var ver int
+ pattern := filepath.Join(tcRoot, "bin", arch.clangArch) + "%d-clang"
+ if n, err := fmt.Sscanf(compiler, pattern, &ver); n < 1 || err != nil {
+ continue
+ }
+ if firstCompiler == "" || ver < firstVer {
+ firstVer = ver
+ firstCompiler = compiler
+ }
+ if ver < bestVer {
+ continue
+ }
+ if ver > minsdk {
+ continue
+ }
+ bestVer = ver
+ bestCompiler = compiler
+ }
+ if bestCompiler == "" {
+ bestCompiler = firstCompiler
+ }
+ if bestCompiler == "" {
+ return "", fmt.Errorf("no NDK compiler found for architecture %s in %s",
+ a, tcRoot)
+ }
+ return bestCompiler, nil
+}
+
+func latestTools(sdk string) (string, error) {
+ allTools, err := filepath.Glob(filepath.Join(sdk, "build-tools", "*"))
+ if err != nil {
+ return "", err
+ }
+ tools, found := latestVersionPath(allTools)
+ if !found {
+ return "", fmt.Errorf("no build-tools found in %q", sdk)
+ }
+ return tools, nil
+}
+
+// latestVersionFile finds the path with the highest version
+// among paths on the form
+//
+// /some/path/major.minor.patch
+func latestVersionPath(paths []string) (string, bool) {
+ var bestVer [3]int
+ var bestDir string
+loop:
+ for _, path := range paths {
+ name := filepath.Base(path)
+ s := strings.SplitN(name, ".", 3)
+ if len(s) != len(bestVer) {
+ continue
+ }
+ var version [3]int
+ for i, v := range s {
+ v, err := strconv.Atoi(v)
+ if err != nil {
+ continue loop
+ }
+ if v < bestVer[i] {
+ continue loop
+ }
+ if v > bestVer[i] {
+ break
+ }
+ version[i] = v
+ }
+ bestVer = version
+ bestDir = path
+ }
+ return bestDir, bestDir != ""
+}
+
+func newZipWriter(w io.Writer) *zipWriter {
+ return &zipWriter{
+ w: zip.NewWriter(w),
+ }
+}
+
+func (z *zipWriter) Close() error {
+ err := z.w.Close()
+ if z.err == nil {
+ z.err = err
+ }
+ return z.err
+}
+
+func (z *zipWriter) Create(name string) io.Writer {
+ if z.err != nil {
+ return ioutil.Discard
+ }
+ w, err := z.w.Create(name)
+ if err != nil {
+ z.err = err
+ return ioutil.Discard
+ }
+ return &errWriter{w: w, err: &z.err}
+}
+
+func (z *zipWriter) Store(name, file string) {
+ z.add(name, file, false)
+}
+
+func (z *zipWriter) Add(name, file string) {
+ z.add(name, file, true)
+}
+
+func (z *zipWriter) add(name, file string, compressed bool) {
+ if z.err != nil {
+ return
+ }
+ f, err := os.Open(file)
+ if err != nil {
+ z.err = err
+ return
+ }
+ defer f.Close()
+ fh := &zip.FileHeader{
+ Name: name,
+ }
+ if compressed {
+ fh.Method = zip.Deflate
+ }
+ w, err := z.w.CreateHeader(fh)
+ if err != nil {
+ z.err = err
+ return
+ }
+ if _, err := io.Copy(w, f); err != nil {
+ z.err = err
+ return
+ }
+}
+
+func (w *errWriter) Write(p []byte) (n int, err error) {
+ if err := *w.err; err != nil {
+ return 0, err
+ }
+ n, err = w.w.Write(p)
+ *w.err = err
+ return
+}
diff --git a/gio/cmd/gogio/build_info.go b/gio/cmd/gogio/build_info.go
new file mode 100644
index 0000000..ecda1f3
--- /dev/null
+++ b/gio/cmd/gogio/build_info.go
@@ -0,0 +1,160 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+type buildInfo struct {
+ appID string
+ archs []string
+ ldflags string
+ minsdk int
+ name string
+ pkgDir string
+ pkgPath string
+ iconPath string
+ tags string
+ target string
+ version int
+ key string
+ password string
+}
+
+func newBuildInfo(pkgPath string) (*buildInfo, error) {
+ pkgMetadata, err := getPkgMetadata(pkgPath)
+ if err != nil {
+ return nil, err
+ }
+ appID := getAppID(pkgMetadata)
+ appIcon := filepath.Join(pkgMetadata.Dir, "appicon.png")
+ if *iconPath != "" {
+ appIcon = *iconPath
+ }
+ bi := &buildInfo{
+ appID: appID,
+ archs: getArchs(),
+ ldflags: getLdFlags(appID),
+ minsdk: *minsdk,
+ name: getPkgName(pkgMetadata),
+ pkgDir: pkgMetadata.Dir,
+ pkgPath: pkgPath,
+ iconPath: appIcon,
+ tags: *extraTags,
+ target: *target,
+ version: *version,
+ key: *signKey,
+ password: *signPass,
+ }
+ return bi, nil
+}
+
+func getArchs() []string {
+ if *archNames != "" {
+ return strings.Split(*archNames, ",")
+ }
+ switch *target {
+ case "js":
+ return []string{"wasm"}
+ case "ios", "tvos":
+ // Only 64-bit support.
+ return []string{"arm64", "amd64"}
+ case "android":
+ return []string{"arm", "arm64", "386", "amd64"}
+ case "windows":
+ goarch := os.Getenv("GOARCH")
+ if goarch == "" {
+ goarch = runtime.GOARCH
+ }
+ return []string{goarch}
+ default:
+ // TODO: Add flag tests.
+ panic("The target value has already been validated, this will never execute.")
+ }
+}
+
+func getLdFlags(appID string) string {
+ var ldflags []string
+ if extra := *extraLdflags; extra != "" {
+ ldflags = append(ldflags, strings.Split(extra, " ")...)
+ }
+ // Pass appID along, to be used for logging on platforms like Android.
+ ldflags = append(ldflags,
+ fmt.Sprintf("-X realy.lol/gio/app/internal/log.appID=%s", appID))
+ // Pass along all remaining arguments to the app.
+ if appArgs := flag.Args()[1:]; len(appArgs) > 0 {
+ ldflags = append(ldflags,
+ fmt.Sprintf("-X realy.lol/gio/app.extraArgs=%s",
+ strings.Join(appArgs, "|")))
+ }
+ if m := *linkMode; m != "" {
+ ldflags = append(ldflags, "-linkmode="+m)
+ }
+ return strings.Join(ldflags, " ")
+}
+
+type packageMetadata struct {
+ PkgPath string
+ Dir string
+}
+
+func getPkgMetadata(pkgPath string) (*packageMetadata, error) {
+ pkgImportPath, err := runCmd(exec.Command("go", "list", "-f",
+ "{{.ImportPath}}", pkgPath))
+ if err != nil {
+ return nil, err
+ }
+ pkgDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", pkgPath))
+ if err != nil {
+ return nil, err
+ }
+ return &packageMetadata{
+ PkgPath: pkgImportPath,
+ Dir: pkgDir,
+ }, nil
+}
+
+func getAppID(pkgMetadata *packageMetadata) string {
+ if *appID != "" {
+ return *appID
+ }
+ elems := strings.Split(pkgMetadata.PkgPath, "/")
+ domain := strings.Split(elems[0], ".")
+ name := ""
+ if len(elems) > 1 {
+ name = "." + elems[len(elems)-1]
+ }
+ if len(elems) < 2 && len(domain) < 2 {
+ name = "." + domain[0]
+ domain[0] = "localhost"
+ } else {
+ for i := 0; i < len(domain)/2; i++ {
+ opp := len(domain) - 1 - i
+ domain[i], domain[opp] = domain[opp], domain[i]
+ }
+ }
+
+ pkgDomain := strings.Join(domain, ".")
+ appid := []rune(pkgDomain + name)
+
+ // a Java-language-style package name may contain upper- and lower-case
+ // letters and underscores with individual parts separated by '.'.
+ // https://developer.android.com/guide/topics/manifest/manifest-element
+ for i, c := range appid {
+ if !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' ||
+ c == '_' || c == '.') {
+ appid[i] = '_'
+ }
+ }
+ return string(appid)
+}
+
+func getPkgName(pkgMetadata *packageMetadata) string {
+ return path.Base(pkgMetadata.PkgPath)
+}
diff --git a/gio/cmd/gogio/build_info_test.go b/gio/cmd/gogio/build_info_test.go
new file mode 100644
index 0000000..397e2a3
--- /dev/null
+++ b/gio/cmd/gogio/build_info_test.go
@@ -0,0 +1,32 @@
+package main
+
+import "testing"
+
+type expval struct {
+ in, out string
+}
+
+func TestAppID(t *testing.T) {
+ t.Parallel()
+
+ tests := []expval{
+ {"example", "localhost.example"},
+ {"example.com", "com.example"},
+ {"www.example.com", "com.example.www"},
+ {"examplecom/app", "examplecom.app"},
+ {"example.com/app", "com.example.app"},
+ {"www.example.com/app", "com.example.www.app"},
+ {"www.en.example.com/app", "com.example.en.www.app"},
+ {"example.com/dir/app", "com.example.app"},
+ {"example.com/dir.ext/app", "com.example.app"},
+ {"example.com/dir/app.ext", "com.example.app.ext"},
+ {"example-com.net/dir/app", "net.example_com.app"},
+ }
+
+ for i, test := range tests {
+ got := getAppID(&packageMetadata{PkgPath: test.in})
+ if exp := test.out; got != exp {
+ t.Errorf("(%d): expected '%s', got '%s'", i, exp, got)
+ }
+ }
+}
diff --git a/gio/cmd/gogio/doc.go b/gio/cmd/gogio/doc.go
new file mode 100644
index 0000000..6b788fd
--- /dev/null
+++ b/gio/cmd/gogio/doc.go
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+The gogio tool builds and packages Gio programs for Android, iOS/tvOS
+and WebAssembly.
+
+Run gogio with no arguments for instructions, or see the examples at
+https://realy.lol/gio.
+*/
+package main
diff --git a/gio/cmd/gogio/e2e_test.go b/gio/cmd/gogio/e2e_test.go
new file mode 100644
index 0000000..893f580
--- /dev/null
+++ b/gio/cmd/gogio/e2e_test.go
@@ -0,0 +1,334 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main_test
+
+import (
+ "bufio"
+ "errors"
+ "flag"
+ "fmt"
+ "image"
+ "image/color"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "strings"
+ "testing"
+ "time"
+)
+
+var raceEnabled = false
+
+var headless = flag.Bool("headless", true,
+ "run end-to-end tests in headless mode")
+
+const appid = "localhost.gogio.endtoend"
+
+// TestDriver is implemented by each of the platforms we can run end-to-end
+// tests on. None of its methods return any errors, as the errors are directly
+// reported to testing.T via methods like Fatal.
+type TestDriver interface {
+ initBase(t *testing.T, width, height int)
+
+ // Start opens the Gio app found at path. The driver should attempt to
+ // run the app with the base driver's width and height, and the
+ // platform's background should be white.
+ //
+ // When the function returns, the gio app must be ready to use on the
+ // platform, with its initial frame fully drawn.
+ Start(path string)
+
+ // Screenshot takes a screenshot of the Gio app on the platform.
+ Screenshot() image.Image
+
+ // Click performs a pointer click at the specified coordinates,
+ // including both press and release. It returns when the next frame is
+ // fully drawn.
+ Click(x, y int)
+}
+
+type driverBase struct {
+ *testing.T
+
+ width, height int
+
+ output io.Reader
+ frameNotifs chan bool
+}
+
+func (d *driverBase) initBase(t *testing.T, width, height int) {
+ d.T = t
+ d.width, d.height = width, height
+}
+
+func TestEndToEnd(t *testing.T) {
+ if testing.Short() {
+ t.Skipf("end-to-end tests tend to be slow")
+ }
+
+ t.Parallel()
+
+ const (
+ testdataWithGoImportPkgPath = "realy.lol/gio/cmd/gogio/testdata"
+ testdataWithRelativePkgPath = "testdata/testdata.go"
+ )
+ // Keep this list local, to not reuse TestDriver objects.
+ subtests := []struct {
+ name string
+ driver TestDriver
+ pkgPath string
+ }{
+ {"X11 using go import path", &X11TestDriver{},
+ testdataWithGoImportPkgPath},
+ {"X11", &X11TestDriver{}, testdataWithRelativePkgPath},
+ {"Wayland", &WaylandTestDriver{}, testdataWithRelativePkgPath},
+ {"JS", &JSTestDriver{}, testdataWithRelativePkgPath},
+ {"Android", &AndroidTestDriver{}, testdataWithRelativePkgPath},
+ {"Windows", &WineTestDriver{}, testdataWithRelativePkgPath},
+ }
+
+ for _, subtest := range subtests {
+ t.Run(subtest.name, func(t *testing.T) {
+ subtest := subtest // copy the changing loop variable
+ t.Parallel()
+ runEndToEndTest(t, subtest.driver, subtest.pkgPath)
+ })
+ }
+}
+
+func runEndToEndTest(t *testing.T, driver TestDriver, pkgPath string) {
+ size := image.Point{X: 800, Y: 600}
+ driver.initBase(t, size.X, size.Y)
+
+ t.Log("starting driver and gio app")
+ driver.Start(pkgPath)
+
+ beef := color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff}
+ white := color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
+ black := color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}
+ gray := color.NRGBA{R: 0xbb, G: 0xbb, B: 0xbb, A: 0xff}
+ red := color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
+
+ // These are the four colors at the beginning.
+ t.Log("taking initial screenshot")
+ withRetries(t, 4*time.Second, func() error {
+ img := driver.Screenshot()
+ size = img.Bounds().Size() // override the default size
+ return checkImageCorners(img, beef, white, black, gray)
+ })
+
+ // TODO(mvdan): implement this properly in the Wayland driver; swaymsg
+ // almost works to automate clicks, but the button presses end up in the
+ // wrong coordinates.
+ if _, ok := driver.(*WaylandTestDriver); ok {
+ return
+ }
+
+ // Click the first and last sections to turn them red.
+ t.Log("clicking twice and taking another screenshot")
+ driver.Click(1*(size.X/4), 1*(size.Y/4))
+ driver.Click(3*(size.X/4), 3*(size.Y/4))
+ withRetries(t, 4*time.Second, func() error {
+ img := driver.Screenshot()
+ return checkImageCorners(img, red, white, black, red)
+ })
+}
+
+// withRetries keeps retrying fn until it succeeds, or until the timeout is hit.
+// It uses a rudimentary kind of backoff, which starts with 100ms delays. As
+// such, timeout should generally be in the order of seconds.
+func withRetries(t *testing.T, timeout time.Duration, fn func() error) {
+ t.Helper()
+
+ timeoutTimer := time.NewTimer(timeout)
+ defer timeoutTimer.Stop()
+ backoff := 100 * time.Millisecond
+
+ tries := 0
+ var lastErr error
+ for {
+ if lastErr = fn(); lastErr == nil {
+ return
+ }
+ tries++
+ t.Logf("retrying after %s", backoff)
+
+ // Use a timer instead of a sleep, so that the timeout can stop
+ // the backoff early. Don't reuse this timer, since we're not in
+ // a hot loop, and we don't want tricky code.
+ backoffTimer := time.NewTimer(backoff)
+ defer backoffTimer.Stop()
+
+ select {
+ case <-timeoutTimer.C:
+ t.Errorf("last error: %v", lastErr)
+ t.Fatalf("hit timeout of %s after %d tries", timeout, tries)
+ case <-backoffTimer.C:
+ }
+
+ // Keep doubling it until a maximum. With the start at 100ms,
+ // we'll do: 100ms, 200ms, 400ms, 800ms, 1.6s, and 2s forever.
+ backoff *= 2
+ if max := 2 * time.Second; backoff > max {
+ backoff = max
+ }
+ }
+}
+
+type colorMismatch struct {
+ x, y int
+ wantRGB, gotRGB [3]uint32
+}
+
+func (m colorMismatch) String() string {
+ return fmt.Sprintf("%3d,%-3d got 0x%04x%04x%04x, want 0x%04x%04x%04x",
+ m.x, m.y,
+ m.gotRGB[0], m.gotRGB[1], m.gotRGB[2],
+ m.wantRGB[0], m.wantRGB[1], m.wantRGB[2],
+ )
+}
+
+func checkImageCorners(img image.Image,
+ topLeft, topRight, botLeft, botRight color.Color) error {
+ // The colors are split in four rectangular sections. Check the corners
+ // of each of the sections. We check the corners left to right, top to
+ // bottom, like when reading left-to-right text.
+
+ size := img.Bounds().Size()
+ var mismatches []colorMismatch
+
+ checkColor := func(x, y int, want color.Color) {
+ r, g, b, _ := want.RGBA()
+ got := img.At(x, y)
+ r_, g_, b_, _ := got.RGBA()
+ if r_ != r || g_ != g || b_ != b {
+ mismatches = append(mismatches, colorMismatch{
+ x: x,
+ y: y,
+ wantRGB: [3]uint32{r, g, b},
+ gotRGB: [3]uint32{r_, g_, b_},
+ })
+ }
+ }
+
+ {
+ minX, minY := 5, 5
+ maxX, maxY := (size.X/2)-5, (size.Y/2)-5
+ checkColor(minX, minY, topLeft)
+ checkColor(maxX, minY, topLeft)
+ checkColor(minX, maxY, topLeft)
+ checkColor(maxX, maxY, topLeft)
+ }
+ {
+ minX, minY := (size.X/2)+5, 5
+ maxX, maxY := size.X-5, (size.Y/2)-5
+ checkColor(minX, minY, topRight)
+ checkColor(maxX, minY, topRight)
+ checkColor(minX, maxY, topRight)
+ checkColor(maxX, maxY, topRight)
+ }
+ {
+ minX, minY := 5, (size.Y/2)+5
+ maxX, maxY := (size.X/2)-5, size.Y-5
+ checkColor(minX, minY, botLeft)
+ checkColor(maxX, minY, botLeft)
+ checkColor(minX, maxY, botLeft)
+ checkColor(maxX, maxY, botLeft)
+ }
+ {
+ minX, minY := (size.X/2)+5, (size.Y/2)+5
+ maxX, maxY := size.X-5, size.Y-5
+ checkColor(minX, minY, botRight)
+ checkColor(maxX, minY, botRight)
+ checkColor(minX, maxY, botRight)
+ checkColor(maxX, maxY, botRight)
+ }
+ if n := len(mismatches); n > 0 {
+ b := new(strings.Builder)
+ fmt.Fprintf(b, "encountered %d color mismatches:\n", n)
+ for _, m := range mismatches {
+ fmt.Fprintf(b, "%s\n", m)
+ }
+ return errors.New(b.String())
+ }
+ return nil
+}
+
+func (d *driverBase) waitForFrame() {
+ d.Helper()
+
+ if d.frameNotifs == nil {
+ // Start the goroutine that reads output lines and notifies of
+ // new frames via frameNotifs. The test doesn't wait for this
+ // goroutine to finish; it will naturally end when the output
+ // reader reaches an error like EOF.
+ d.frameNotifs = make(chan bool, 1)
+ if d.output == nil {
+ d.Fatal("need an output reader to be notified of frames")
+ }
+ go func() {
+ scanner := bufio.NewScanner(d.output)
+ for scanner.Scan() {
+ line := scanner.Text()
+ d.Log(line)
+ if strings.Contains(line, "gio frame ready") {
+ d.frameNotifs <- true
+ }
+ }
+ // Since we're only interested in the output while the
+ // app runs, and we don't know when it finishes here,
+ // ignore "already closed" pipe errors.
+ if err := scanner.Err(); err != nil && !errors.Is(err,
+ os.ErrClosed) {
+ d.Errorf("reading app output: %v", err)
+ }
+ }()
+ }
+
+ // Unfortunately, there isn't a way to select on a test failing, since
+ // testing.T doesn't have anything like a context or a "done" channel.
+ //
+ // We can't let selects block forever, since the default -test.timeout
+ // is ten minutes - far too long for tests that take seconds.
+ //
+ // For now, a static short timeout is better than nothing. 5s is plenty
+ // for our simple test app to render on any device.
+ select {
+ case <-d.frameNotifs:
+ case <-time.After(5 * time.Second):
+ d.Fatalf("timed out waiting for a frame to be ready")
+ }
+}
+
+func (d *driverBase) needPrograms(names ...string) {
+ d.Helper()
+ for _, name := range names {
+ if _, err := exec.LookPath(name); err != nil {
+ d.Skipf("%s needed to run", name)
+ }
+ }
+}
+
+func (d *driverBase) tempDir(name string) string {
+ d.Helper()
+ dir, err := ioutil.TempDir("", name)
+ if err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(func() { os.RemoveAll(dir) })
+ return dir
+}
+
+func (d *driverBase) gogio(args ...string) {
+ d.Helper()
+ prog, err := os.Executable()
+ if err != nil {
+ d.Fatal(err)
+ }
+ cmd := exec.Command(prog, args...)
+ cmd.Env = append(os.Environ(), "RUN_GOGIO=1")
+ if out, err := cmd.CombinedOutput(); err != nil {
+ d.Fatalf("gogio error: %s:\n%s", err, out)
+ }
+}
diff --git a/gio/cmd/gogio/help.go b/gio/cmd/gogio/help.go
new file mode 100644
index 0000000..c83d7a3
--- /dev/null
+++ b/gio/cmd/gogio/help.go
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+const mainUsage = `The gogio command builds and packages Gio (realy.lol/gio) programs.
+
+Usage:
+
+ gogio -target [flags] [run arguments]
+
+The gogio tool builds and packages Gio programs for platforms where additional
+metadata or support files are required.
+
+The package argument specifies an import path or a single Go source file to
+package. Any run arguments are appended to os.Args at runtime.
+
+Compiled Java class files from jar files in the package directory are
+included in Android builds.
+
+The mandatory -target flag selects the target platform: ios or android for the
+mobile platforms, tvos for Apple's tvOS, js for WebAssembly/WebGL.
+
+The -arch flag specifies a comma separated list of GOARCHs to include. The
+default is all supported architectures.
+
+The -o flag specifies an output file or directory, depending on the target.
+
+The -buildmode flag selects the build mode. Two build modes are available, exe
+and archive. Buildmode exe outputs an .ipa file for iOS or tvOS, an .apk file
+for Android or a directory with the WebAssembly module and support files for
+a browser.
+
+The -ldflags and -tags flags pass extra linker flags and tags to the go tool.
+
+As a special case for iOS or tvOS, specifying a path that ends with ".app"
+will output an app directory suitable for a simulator.
+
+The other buildmode is archive, which will output an .aar library for Android
+or a .framework for iOS and tvOS.
+
+The -icon flag specifies a path to a PNG image to use as app icon on iOS and Android.
+If left unspecified, the appicon.png file from the main package is used
+(if it exists).
+
+The -appid flag specifies the package name for Android or the bundle id for
+iOS and tvOS. A bundle id must be provisioned through Xcode before the gogio
+tool can use it.
+
+The -version flag specifies the integer version code for Android and the last
+component of the 1.0.X version for iOS and tvOS.
+
+For Android builds the -minsdk flag specify the minimum SDK level. For example,
+use -minsdk 22 to target Android 5.1 (Lollipop) and later.
+
+For Windows builds the -minsdk flag specify the minimum OS version. For example,
+use -mindk 10 to target Windows 10 only, -minsdk 6 for Windows Vista and later.
+
+The -work flag prints the path to the working directory and suppress
+its deletion.
+
+The -x flag will print all the external commands executed by the gogio tool.
+
+The -signkey flag specifies the path of the keystore, used for signing Android apk/aab files.
+
+The -signpass flag specifies the password of the keystore, ignored if -signkey is not provided.
+`
diff --git a/gio/cmd/gogio/iosbuild.go b/gio/cmd/gogio/iosbuild.go
new file mode 100644
index 0000000..9674431
--- /dev/null
+++ b/gio/cmd/gogio/iosbuild.go
@@ -0,0 +1,591 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "archive/zip"
+ "crypto/sha1"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "golang.org/x/sync/errgroup"
+)
+
+const minIOSVersion = "9.0"
+
+func buildIOS(tmpDir, target string, bi *buildInfo) error {
+ appName := bi.name
+ switch *buildMode {
+ case "archive":
+ framework := *destPath
+ if framework == "" {
+ framework = fmt.Sprintf("%s.framework", strings.Title(appName))
+ }
+ return archiveIOS(tmpDir, target, framework, bi)
+ case "exe":
+ out := *destPath
+ if out == "" {
+ out = appName + ".ipa"
+ }
+ forDevice := strings.HasSuffix(out, ".ipa")
+ // Filter out unsupported architectures.
+ for i := len(bi.archs) - 1; i >= 0; i-- {
+ switch bi.archs[i] {
+ case "arm", "arm64":
+ if forDevice {
+ continue
+ }
+ case "386", "amd64":
+ if !forDevice {
+ continue
+ }
+ }
+
+ bi.archs = append(bi.archs[:i], bi.archs[i+1:]...)
+ }
+ tmpFramework := filepath.Join(tmpDir, "Gio.framework")
+ if err := archiveIOS(tmpDir, target, tmpFramework, bi); err != nil {
+ return err
+ }
+ if !forDevice && !strings.HasSuffix(out, ".app") {
+ return fmt.Errorf("the specified output directory %q does not end in .app or .ipa",
+ out)
+ }
+ if !forDevice {
+ return exeIOS(tmpDir, target, out, bi)
+ }
+ payload := filepath.Join(tmpDir, "Payload")
+ appDir := filepath.Join(payload, appName+".app")
+ if err := os.MkdirAll(appDir, 0755); err != nil {
+ return err
+ }
+ if err := exeIOS(tmpDir, target, appDir, bi); err != nil {
+ return err
+ }
+ if err := signIOS(bi, tmpDir, appDir); err != nil {
+ return err
+ }
+ return zipDir(out, tmpDir, "Payload")
+ default:
+ panic("unreachable")
+ }
+}
+
+func signIOS(bi *buildInfo, tmpDir, app string) error {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return err
+ }
+ provPattern := filepath.Join(home, "Library", "MobileDevice",
+ "Provisioning Profiles", "*.mobileprovision")
+ provisions, err := filepath.Glob(provPattern)
+ if err != nil {
+ return err
+ }
+ provInfo := filepath.Join(tmpDir, "provision.plist")
+ var avail []string
+ for _, prov := range provisions {
+ // Decode the provision file to a plist.
+ _, err := runCmd(exec.Command("security", "cms", "-D", "-i", prov, "-o",
+ provInfo))
+ if err != nil {
+ return err
+ }
+ expUnix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c",
+ "Print:ExpirationDate", provInfo))
+ if err != nil {
+ return err
+ }
+ exp, err := time.Parse(time.UnixDate, expUnix)
+ if err != nil {
+ return fmt.Errorf("sign: failed to parse expiration date from %q: %v",
+ prov, err)
+ }
+ if exp.Before(time.Now()) {
+ continue
+ }
+ appIDPrefix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c",
+ "Print:ApplicationIdentifierPrefix:0", provInfo))
+ if err != nil {
+ return err
+ }
+ provAppID, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c",
+ "Print:Entitlements:application-identifier", provInfo))
+ if err != nil {
+ return err
+ }
+ expAppID := fmt.Sprintf("%s.%s", appIDPrefix, bi.appID)
+ avail = append(avail, provAppID)
+ if expAppID != provAppID {
+ continue
+ }
+ // Copy provisioning file.
+ embedded := filepath.Join(app, "embedded.mobileprovision")
+ if err := copyFile(embedded, prov); err != nil {
+ return err
+ }
+ certDER, err := runCmdRaw(exec.Command("/usr/libexec/PlistBuddy", "-c",
+ "Print:DeveloperCertificates:0", provInfo))
+ if err != nil {
+ return err
+ }
+ // Omit trailing newline.
+ certDER = certDER[:len(certDER)-1]
+ entitlements, err := runCmd(exec.Command("/usr/libexec/PlistBuddy",
+ "-x", "-c", "Print:Entitlements", provInfo))
+ if err != nil {
+ return err
+ }
+ entFile := filepath.Join(tmpDir, "entitlements.plist")
+ if err := ioutil.WriteFile(entFile, []byte(entitlements),
+ 0660); err != nil {
+ return err
+ }
+ identity := sha1.Sum(certDER)
+ idHex := hex.EncodeToString(identity[:])
+ _, err = runCmd(exec.Command("codesign", "-s", idHex, "-v",
+ "--entitlements", entFile, app))
+ return err
+ }
+ return fmt.Errorf("sign: no valid provisioning profile found for bundle id %q among %v",
+ bi.appID, avail)
+}
+
+func exeIOS(tmpDir, target, app string, bi *buildInfo) error {
+ if bi.appID == "" {
+ return errors.New("app id is empty; use -appid to set it")
+ }
+ if err := os.RemoveAll(app); err != nil {
+ return err
+ }
+ if err := os.Mkdir(app, 0755); err != nil {
+ return err
+ }
+ mainm := filepath.Join(tmpDir, "main.m")
+ const mainmSrc = `@import UIKit;
+@import Gio;
+
+@interface GioAppDelegate : UIResponder
+@property (strong, nonatomic) UIWindow *window;
+@end
+
+@implementation GioAppDelegate
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
+ GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil];
+ self.window.rootViewController = controller;
+ [self.window makeKeyAndVisible];
+ return YES;
+}
+@end
+
+int main(int argc, char * argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([GioAppDelegate class]));
+ }
+}`
+ if err := ioutil.WriteFile(mainm, []byte(mainmSrc), 0660); err != nil {
+ return err
+ }
+ appName := strings.Title(bi.name)
+ exe := filepath.Join(app, appName)
+ lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create")
+ var builds errgroup.Group
+ for _, a := range bi.archs {
+ clang, cflags, err := iosCompilerFor(target, a)
+ if err != nil {
+ return err
+ }
+ exeSlice := filepath.Join(tmpDir, "app-"+a)
+ lipo.Args = append(lipo.Args, exeSlice)
+ compile := exec.Command(clang, cflags...)
+ compile.Args = append(compile.Args,
+ "-Werror",
+ "-fmodules",
+ "-fobjc-arc",
+ "-x", "objective-c",
+ "-F", tmpDir,
+ "-o", exeSlice,
+ mainm,
+ )
+ builds.Go(func() error {
+ _, err := runCmd(compile)
+ return err
+ })
+ }
+ if err := builds.Wait(); err != nil {
+ return err
+ }
+ if _, err := runCmd(lipo); err != nil {
+ return err
+ }
+ infoPlist := buildInfoPlist(bi)
+ plistFile := filepath.Join(app, "Info.plist")
+ if err := ioutil.WriteFile(plistFile, []byte(infoPlist), 0660); err != nil {
+ return err
+ }
+ if _, err := os.Stat(bi.iconPath); err == nil {
+ assetPlist, err := iosIcons(bi, tmpDir, app, bi.iconPath)
+ if err != nil {
+ return err
+ }
+ // Merge assets plist with Info.plist
+ cmd := exec.Command(
+ "/usr/libexec/PlistBuddy",
+ "-c", "Merge "+assetPlist,
+ plistFile,
+ )
+ if _, err := runCmd(cmd); err != nil {
+ return err
+ }
+ }
+ if _, err := runCmd(exec.Command("plutil", "-convert", "binary1",
+ plistFile)); err != nil {
+ return err
+ }
+ return nil
+}
+
+// iosIcons builds an asset catalog and compile it with the Xcode command actool.
+// iosIcons returns the asset plist file to be merged into Info.plist.
+func iosIcons(bi *buildInfo, tmpDir, appDir, icon string) (string, error) {
+ assets := filepath.Join(tmpDir, "Assets.xcassets")
+ if err := os.Mkdir(assets, 0700); err != nil {
+ return "", err
+ }
+ appIcon := filepath.Join(assets, "AppIcon.appiconset")
+ err := buildIcons(appIcon, icon, []iconVariant{
+ {path: "ios_2x.png", size: 120},
+ {path: "ios_3x.png", size: 180},
+ // The App Store icon is not allowed to contain
+ // transparent pixels.
+ {path: "ios_store.png", size: 1024, fill: true},
+ })
+ if err != nil {
+ return "", err
+ }
+ contentJson := `{
+ "images" : [
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "ios_2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "ios_3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "ios_store.png",
+ "scale" : "1x"
+ }
+ ]
+}`
+ contentFile := filepath.Join(appIcon, "Contents.json")
+ if err := ioutil.WriteFile(contentFile, []byte(contentJson),
+ 0600); err != nil {
+ return "", err
+ }
+ assetPlist := filepath.Join(tmpDir, "assets.plist")
+ compile := exec.Command(
+ "actool",
+ "--compile", appDir,
+ "--platform", iosPlatformFor(bi.target),
+ "--minimum-deployment-target", minIOSVersion,
+ "--app-icon", "AppIcon",
+ "--output-partial-info-plist", assetPlist,
+ assets)
+ _, err = runCmd(compile)
+ return assetPlist, err
+}
+
+func buildInfoPlist(bi *buildInfo) string {
+ appName := strings.Title(bi.name)
+ platform := iosPlatformFor(bi.target)
+ var supportPlatform string
+ switch bi.target {
+ case "ios":
+ supportPlatform = "iPhoneOS"
+ case "tvos":
+ supportPlatform = "AppleTVOS"
+ }
+ return fmt.Sprintf(`
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ %s
+ CFBundleIdentifier
+ %s
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ %s
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0.%d
+ CFBundleVersion
+ %d
+ UILaunchStoryboardName
+ LaunchScreen
+ UIRequiredDeviceCapabilities
+ arm64
+ DTPlatformName
+ %s
+ DTPlatformVersion
+ 12.4
+ MinimumOSVersion
+ %s
+ UIDeviceFamily
+
+ 1
+
+ CFBundleSupportedPlatforms
+
+ %s
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ DTCompiler
+ com.apple.compilers.llvm.clang.1_0
+ DTPlatformBuild
+ 16G73
+ DTSDKBuild
+ 16G73
+ DTSDKName
+ %s12.4
+ DTXcode
+ 1030
+ DTXcodeBuild
+ 10G8
+
+`, appName, bi.appID, appName, bi.version, bi.version, platform,
+ minIOSVersion, supportPlatform, platform)
+}
+
+func iosPlatformFor(target string) string {
+ switch target {
+ case "ios":
+ return "iphoneos"
+ case "tvos":
+ return "appletvos"
+ default:
+ panic("invalid platform " + target)
+ }
+}
+
+func archiveIOS(tmpDir, target, frameworkRoot string, bi *buildInfo) error {
+ framework := filepath.Base(frameworkRoot)
+ const suf = ".framework"
+ if !strings.HasSuffix(framework, suf) {
+ return fmt.Errorf("the specified output %q does not end in '.framework'",
+ frameworkRoot)
+ }
+ framework = framework[:len(framework)-len(suf)]
+ if err := os.RemoveAll(frameworkRoot); err != nil {
+ return err
+ }
+ frameworkDir := filepath.Join(frameworkRoot, "Versions", "A")
+ for _, dir := range []string{"Headers", "Modules"} {
+ p := filepath.Join(frameworkDir, dir)
+ if err := os.MkdirAll(p, 0755); err != nil {
+ return err
+ }
+ }
+ symlinks := [][2]string{
+ {"Versions/Current/Headers", "Headers"},
+ {"Versions/Current/Modules", "Modules"},
+ {"Versions/Current/" + framework, framework},
+ {"A", filepath.Join("Versions", "Current")},
+ }
+ for _, l := range symlinks {
+ if err := os.Symlink(l[0], filepath.Join(frameworkRoot,
+ l[1])); err != nil && !os.IsExist(err) {
+ return err
+ }
+ }
+ exe := filepath.Join(frameworkDir, framework)
+ lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create")
+ var builds errgroup.Group
+ tags := bi.tags
+ goos := "ios"
+ supportsIOS, err := supportsGOOS("ios")
+ if err != nil {
+ return err
+ }
+ if !supportsIOS {
+ // Go 1.15 and earlier target iOS with GOOS=darwin, tags=ios.
+ goos = "darwin"
+ tags = "ios " + tags
+ }
+ for _, a := range bi.archs {
+ clang, cflags, err := iosCompilerFor(target, a)
+ if err != nil {
+ return err
+ }
+ lib := filepath.Join(tmpDir, "gio-"+a)
+ cmd := exec.Command(
+ "go",
+ "build",
+ "-ldflags=-s -w "+bi.ldflags,
+ "-buildmode=c-archive",
+ "-o", lib,
+ "-tags", tags,
+ bi.pkgPath,
+ )
+ lipo.Args = append(lipo.Args, lib)
+ cflagsLine := strings.Join(cflags, " ")
+ cmd.Env = append(
+ os.Environ(),
+ "GOOS="+goos,
+ "GOARCH="+a,
+ "CGO_ENABLED=1",
+ "CC="+clang,
+ "CGO_CFLAGS="+cflagsLine,
+ "CGO_LDFLAGS="+cflagsLine,
+ )
+ builds.Go(func() error {
+ _, err := runCmd(cmd)
+ return err
+ })
+ }
+ if err := builds.Wait(); err != nil {
+ return err
+ }
+ if _, err := runCmd(lipo); err != nil {
+ return err
+ }
+ appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}",
+ "realy.lol/gio/app/internal/wm"))
+ if err != nil {
+ return err
+ }
+ headerDst := filepath.Join(frameworkDir, "Headers", framework+".h")
+ headerSrc := filepath.Join(appDir, "framework_ios.h")
+ if err := copyFile(headerDst, headerSrc); err != nil {
+ return err
+ }
+ module := fmt.Sprintf(`framework module "%s" {
+ header "%[1]s.h"
+
+ export *
+}`, framework)
+ moduleFile := filepath.Join(frameworkDir, "Modules", "module.modulemap")
+ return ioutil.WriteFile(moduleFile, []byte(module), 0644)
+}
+
+func supportsGOOS(wantGoos string) (bool, error) {
+ geese, err := runCmd(exec.Command("go", "tool", "dist", "list"))
+ if err != nil {
+ return false, err
+ }
+ for _, pair := range strings.Split(geese, "\n") {
+ s := strings.SplitN(pair, "/", 2)
+ if len(s) != 2 {
+ return false, fmt.Errorf("go tool dist list: invalid GOOS/GOARCH pair: %s",
+ pair)
+ }
+ goos := s[0]
+ if goos == wantGoos {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func iosCompilerFor(target, arch string) (string, []string, error) {
+ var platformSDK string
+ var platformOS string
+ switch target {
+ case "ios":
+ platformOS = "ios"
+ platformSDK = "iphone"
+ case "tvos":
+ platformOS = "tvos"
+ platformSDK = "appletv"
+ }
+ switch arch {
+ case "arm", "arm64":
+ platformSDK += "os"
+ case "386", "amd64":
+ platformOS += "-simulator"
+ platformSDK += "simulator"
+ default:
+ return "", nil, fmt.Errorf("unsupported -arch: %s", arch)
+ }
+ sdkPath, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK,
+ "--show-sdk-path"))
+ if err != nil {
+ return "", nil, err
+ }
+ clang, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--find",
+ "clang"))
+ if err != nil {
+ return "", nil, err
+ }
+ cflags := []string{
+ "-fembed-bitcode",
+ "-arch", allArchs[arch].iosArch,
+ "-isysroot", sdkPath,
+ "-m" + platformOS + "-version-min=" + minIOSVersion,
+ }
+ return clang, cflags, nil
+}
+
+func zipDir(dst, base, dir string) (err error) {
+ f, err := os.Create(dst)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := f.Close(); err == nil {
+ err = cerr
+ }
+ }()
+ zipf := zip.NewWriter(f)
+ err = filepath.Walk(filepath.Join(base, dir),
+ func(path string, f os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if f.IsDir() {
+ return nil
+ }
+ rel := filepath.ToSlash(path[len(base)+1:])
+ entry, err := zipf.Create(rel)
+ if err != nil {
+ return err
+ }
+ src, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer src.Close()
+ _, err = io.Copy(entry, src)
+ return err
+ })
+ if err != nil {
+ return err
+ }
+ return zipf.Close()
+}
diff --git a/gio/cmd/gogio/js_test.go b/gio/cmd/gogio/js_test.go
new file mode 100644
index 0000000..2918737
--- /dev/null
+++ b/gio/cmd/gogio/js_test.go
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main_test
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "image"
+ "image/png"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os/exec"
+
+ "github.com/chromedp/cdproto/runtime"
+ "github.com/chromedp/chromedp"
+
+)
+
+type JSTestDriver struct {
+ driverBase
+
+ // ctx is the chromedp context.
+ ctx context.Context
+}
+
+func (d *JSTestDriver) Start(path string) {
+ if raceEnabled {
+ d.Skipf("js/wasm doesn't support -race; skipping")
+ }
+
+ // First, build the app.
+ dir := d.tempDir("gio-endtoend-js")
+ d.gogio("-target=js", "-o="+dir, path)
+
+ // Second, start Chrome.
+ opts := append(chromedp.DefaultExecAllocatorOptions[:],
+ chromedp.Flag("headless", *headless),
+ )
+
+ actx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
+ d.Cleanup(cancel)
+
+ ctx, cancel := chromedp.NewContext(actx,
+ // Send all logf/errf calls to t.Logf
+ chromedp.WithLogf(d.Logf),
+ )
+ d.Cleanup(cancel)
+ d.ctx = ctx
+
+ if err := chromedp.Run(ctx); err != nil {
+ if errors.Is(err, exec.ErrNotFound) {
+ d.Skipf("test requires Chrome to be installed: %v", err)
+ return
+ }
+ d.Fatal(err)
+ }
+ pr, pw := io.Pipe()
+ d.Cleanup(func() { pw.Close() })
+ d.output = pr
+ chromedp.ListenTarget(ctx, func(ev interface{}) {
+ switch ev := ev.(type) {
+ case *runtime.EventConsoleAPICalled:
+ switch ev.Type {
+ case "log", "info", "warning", "error":
+ var b bytes.Buffer
+ b.WriteString("console.")
+ b.WriteString(string(ev.Type))
+ b.WriteString("(")
+ for i, arg := range ev.Args {
+ if i > 0 {
+ b.WriteString(", ")
+ }
+ b.Write(arg.Value)
+ }
+ b.WriteString(")\n")
+ pw.Write(b.Bytes())
+ }
+ }
+ })
+
+ // Third, serve the app folder, set the browser tab dimensions, and
+ // navigate to the folder.
+ ts := httptest.NewServer(http.FileServer(http.Dir(dir)))
+ d.Cleanup(ts.Close)
+
+ if err := chromedp.Run(ctx,
+ chromedp.EmulateViewport(int64(d.width), int64(d.height)),
+ chromedp.Navigate(ts.URL),
+ ); err != nil {
+ d.Fatal(err)
+ }
+
+ // Wait for the gio app to render.
+ d.waitForFrame()
+}
+
+func (d *JSTestDriver) Screenshot() image.Image {
+ var buf []byte
+ if err := chromedp.Run(d.ctx,
+ chromedp.CaptureScreenshot(&buf),
+ ); err != nil {
+ d.Fatal(err)
+ }
+ img, err := png.Decode(bytes.NewReader(buf))
+ if err != nil {
+ d.Fatal(err)
+ }
+ return img
+}
+
+func (d *JSTestDriver) Click(x, y int) {
+ if err := chromedp.Run(d.ctx,
+ chromedp.MouseClickXY(float64(x), float64(y)),
+ ); err != nil {
+ d.Fatal(err)
+ }
+
+ // Wait for the gio app to render after this click.
+ d.waitForFrame()
+}
diff --git a/gio/cmd/gogio/jsbuild.go b/gio/cmd/gogio/jsbuild.go
new file mode 100644
index 0000000..58bccc1
--- /dev/null
+++ b/gio/cmd/gogio/jsbuild.go
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "golang.org/x/tools/go/packages"
+)
+
+func buildJS(bi *buildInfo) error {
+ out := *destPath
+ if out == "" {
+ out = bi.name
+ }
+ if err := os.MkdirAll(out, 0700); err != nil {
+ return err
+ }
+ cmd := exec.Command(
+ "go",
+ "build",
+ "-ldflags="+bi.ldflags,
+ "-tags="+bi.tags,
+ "-o", filepath.Join(out, "main.wasm"),
+ bi.pkgPath,
+ )
+ cmd.Env = append(
+ os.Environ(),
+ "GOOS=js",
+ "GOARCH=wasm",
+ )
+ _, err := runCmd(cmd)
+ if err != nil {
+ return err
+ }
+ if err := ioutil.WriteFile(filepath.Join(out, "index.html"), []byte(jsIndex), 0600); err != nil {
+ return err
+ }
+ goroot, err := runCmd(exec.Command("go", "env", "GOROOT"))
+ if err != nil {
+ return err
+ }
+ wasmJS := filepath.Join(goroot, "misc", "wasm", "wasm_exec.js")
+ if _, err := os.Stat(wasmJS); err != nil {
+ return fmt.Errorf("failed to find $GOROOT/misc/wasm/wasm_exec.js driver: %v", err)
+ }
+ pkgs, err := packages.Load(&packages.Config{
+ Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps,
+ Env: append(os.Environ(), "GOOS=js", "GOARCH=wasm"),
+ }, bi.pkgPath)
+ if err != nil {
+ return err
+ }
+ extraJS, err := findPackagesJS(pkgs[0], make(map[string]bool))
+ if err != nil {
+ return err
+ }
+
+ return mergeJSFiles(filepath.Join(out, "wasm.js"), append([]string{wasmJS}, extraJS...)...)
+}
+
+func findPackagesJS(p *packages.Package, visited map[string]bool) (extraJS []string, err error) {
+ if len(p.GoFiles) == 0 {
+ return nil, nil
+ }
+ js, err := filepath.Glob(filepath.Join(filepath.Dir(p.GoFiles[0]), "*_js.js"))
+ if err != nil {
+ return nil, err
+ }
+ extraJS = append(extraJS, js...)
+ for _, imp := range p.Imports {
+ if !visited[imp.ID] {
+ extra, err := findPackagesJS(imp, visited)
+ if err != nil {
+ return nil, err
+ }
+ extraJS = append(extraJS, extra...)
+ visited[imp.ID] = true
+ }
+ }
+ return extraJS, nil
+}
+
+// mergeJSFiles will merge all files into a single `wasm.js`. It will prepend the jsSetGo
+// and append the jsStartGo.
+func mergeJSFiles(dst string, files ...string) (err error) {
+ w, err := os.Create(dst)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := w.Close(); err != nil {
+ err = cerr
+ }
+ }()
+ _, err = io.Copy(w, strings.NewReader(jsSetGo))
+ if err != nil {
+ return err
+ }
+ for i := range files {
+ r, err := os.Open(files[i])
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(w, r)
+ r.Close()
+ if err != nil {
+ return err
+ }
+ }
+ _, err = io.Copy(w, strings.NewReader(jsStartGo))
+ return err
+}
+
+const (
+ jsIndex = `
+
+
+
+
+
+
+
+
+
+
+`
+ // jsSetGo sets the `window.go` variable.
+ jsSetGo = `(() => {
+ window.go = {argv: [], env: {}, importObject: {go: {}}};
+ const argv = new URLSearchParams(location.search).get("argv");
+ if (argv) {
+ window.go["argv"] = argv.split(" ");
+ }
+})();`
+ // jsStartGo initializes the main.wasm.
+ jsStartGo = `(() => {
+ defaultGo = new Go();
+ Object.assign(defaultGo["argv"], defaultGo["argv"].concat(go["argv"]));
+ Object.assign(defaultGo["env"], go["env"]);
+ for (let key in go["importObject"]) {
+ if (typeof defaultGo["importObject"][key] === "undefined") {
+ defaultGo["importObject"][key] = {};
+ }
+ Object.assign(defaultGo["importObject"][key], go["importObject"][key]);
+ }
+ window.go = defaultGo;
+ if (!WebAssembly.instantiateStreaming) { // polyfill
+ WebAssembly.instantiateStreaming = async (resp, importObject) => {
+ const source = await (await resp).arrayBuffer();
+ return await WebAssembly.instantiate(source, importObject);
+ };
+ }
+ WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
+ go.run(result.instance);
+ });
+})();`
+)
diff --git a/gio/cmd/gogio/main.go b/gio/cmd/gogio/main.go
new file mode 100644
index 0000000..da35401
--- /dev/null
+++ b/gio/cmd/gogio/main.go
@@ -0,0 +1,225 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "image"
+ "image/color"
+ "image/png"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "golang.org/x/image/draw"
+ "golang.org/x/sync/errgroup"
+)
+
+var (
+ target = flag.String("target", "", "specify target (ios, tvos, android, js).\n")
+ archNames = flag.String("arch", "", "specify architecture(s) to include (arm, arm64, amd64).")
+ minsdk = flag.Int("minsdk", 0, "specify the minimum supported operating system level")
+ buildMode = flag.String("buildmode", "exe", "specify buildmode (archive, exe)")
+ destPath = flag.String("o", "", "output file or directory.\nFor -target ios or tvos, use the .app suffix to target simulators.")
+ appID = flag.String("appid", "", "app identifier (for -buildmode=exe)")
+ version = flag.Int("version", 1, "app version (for -buildmode=exe)")
+ printCommands = flag.Bool("x", false, "print the commands")
+ keepWorkdir = flag.Bool("work", false, "print the name of the temporary work directory and do not delete it when exiting.")
+ linkMode = flag.String("linkmode", "", "set the -linkmode flag of the go tool")
+ extraLdflags = flag.String("ldflags", "", "extra flags to the Go linker")
+ extraTags = flag.String("tags", "", "extra tags to the Go tool")
+ iconPath = flag.String("icon", "", "specify an icon for iOS and Android")
+ signKey = flag.String("signkey", "", "specify the path of the keystore to be used to sign Android apk files.")
+ signPass = flag.String("signpass", "", "specify the password to decrypt the signkey.")
+ noStrip = flag.Bool("nostrip", false, "leave debugging symbols in produced .so files")
+)
+
+func main() {
+ flag.Usage = func() {
+ fmt.Fprint(os.Stderr, mainUsage)
+ }
+ flag.Parse()
+ if err := flagValidate(); err != nil {
+ fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
+ os.Exit(1)
+ }
+ buildInfo, err := newBuildInfo(flag.Arg(0))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
+ os.Exit(1)
+ }
+ if err := build(buildInfo); err != nil {
+ fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
+ os.Exit(1)
+ }
+ os.Exit(0)
+}
+
+func flagValidate() error {
+ pkgPathArg := flag.Arg(0)
+ if pkgPathArg == "" {
+ return errors.New("specify a package")
+ }
+ if *target == "" {
+ return errors.New("please specify -target")
+ }
+ switch *target {
+ case "ios", "tvos", "android", "js", "windows":
+ default:
+ return fmt.Errorf("invalid -target %s", *target)
+ }
+ switch *buildMode {
+ case "archive", "exe":
+ default:
+ return fmt.Errorf("invalid -buildmode %s", *buildMode)
+ }
+ return nil
+}
+
+func build(bi *buildInfo) error {
+ tmpDir, err := ioutil.TempDir("", "gogio-")
+ if err != nil {
+ return err
+ }
+ if *keepWorkdir {
+ fmt.Fprintf(os.Stderr, "WORKDIR=%s\n", tmpDir)
+ } else {
+ defer os.RemoveAll(tmpDir)
+ }
+ switch *target {
+ case "js":
+ return buildJS(bi)
+ case "ios", "tvos":
+ return buildIOS(tmpDir, *target, bi)
+ case "android":
+ return buildAndroid(tmpDir, bi)
+ case "windows":
+ return buildWindows(tmpDir, bi)
+ default:
+ panic("unreachable")
+ }
+}
+
+func runCmdRaw(cmd *exec.Cmd) ([]byte, error) {
+ if *printCommands {
+ fmt.Printf("%s\n", strings.Join(cmd.Args, " "))
+ }
+ out, err := cmd.Output()
+ if err == nil {
+ return out, nil
+ }
+ if err, ok := err.(*exec.ExitError); ok {
+ return nil, fmt.Errorf("%s failed: %s%s", strings.Join(cmd.Args, " "), out, err.Stderr)
+ }
+ return nil, err
+}
+
+func runCmd(cmd *exec.Cmd) (string, error) {
+ out, err := runCmdRaw(cmd)
+ return string(bytes.TrimSpace(out)), err
+}
+
+func copyFile(dst, src string) (err error) {
+ r, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+ w, err := os.Create(dst)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := w.Close(); err == nil {
+ err = cerr
+ }
+ }()
+ _, err = io.Copy(w, r)
+ return err
+}
+
+type arch struct {
+ iosArch string
+ jniArch string
+ clangArch string
+}
+
+var allArchs = map[string]arch{
+ "arm": {
+ iosArch: "armv7",
+ jniArch: "armeabi-v7a",
+ clangArch: "armv7a-linux-androideabi",
+ },
+ "arm64": {
+ iosArch: "arm64",
+ jniArch: "arm64-v8a",
+ clangArch: "aarch64-linux-android",
+ },
+ "386": {
+ iosArch: "i386",
+ jniArch: "x86",
+ clangArch: "i686-linux-android",
+ },
+ "amd64": {
+ iosArch: "x86_64",
+ jniArch: "x86_64",
+ clangArch: "x86_64-linux-android",
+ },
+}
+
+type iconVariant struct {
+ path string
+ size int
+ fill bool
+}
+
+func buildIcons(baseDir, icon string, variants []iconVariant) error {
+ f, err := os.Open(icon)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ img, _, err := image.Decode(f)
+ if err != nil {
+ return err
+ }
+ var resizes errgroup.Group
+ for _, v := range variants {
+ v := v
+ resizes.Go(func() (err error) {
+ path := filepath.Join(baseDir, v.path)
+ if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
+ return err
+ }
+ f, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := f.Close(); err == nil {
+ err = cerr
+ }
+ }()
+ return png.Encode(f, resizeIcon(v, img))
+ })
+ }
+ return resizes.Wait()
+}
+
+func resizeIcon(v iconVariant, img image.Image) *image.NRGBA {
+ scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: v.size, Y: v.size}})
+ op := draw.Src
+ if v.fill {
+ op = draw.Over
+ draw.Draw(scaled, scaled.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)
+ }
+ draw.CatmullRom.Scale(scaled, scaled.Bounds(), img, img.Bounds(), op, nil)
+
+ return scaled
+}
diff --git a/gio/cmd/gogio/main_test.go b/gio/cmd/gogio/main_test.go
new file mode 100644
index 0000000..98dcb27
--- /dev/null
+++ b/gio/cmd/gogio/main_test.go
@@ -0,0 +1,17 @@
+package main
+
+import (
+ "os"
+ "testing"
+)
+
+func TestMain(m *testing.M) {
+ if os.Getenv("RUN_GOGIO") != "" {
+ // Allow the end-to-end tests to call the gogio tool without
+ // having to build it from scratch, nor having to refactor the
+ // main function to avoid using global variables.
+ main()
+ os.Exit(0) // main already exits, but just in case.
+ }
+ os.Exit(m.Run())
+}
diff --git a/gio/cmd/gogio/permission.go b/gio/cmd/gogio/permission.go
new file mode 100644
index 0000000..b22fcef
--- /dev/null
+++ b/gio/cmd/gogio/permission.go
@@ -0,0 +1,33 @@
+package main
+
+var AndroidPermissions = map[string][]string{
+ "network": {
+ "android.permission.INTERNET",
+ },
+ "networkstate": {
+ "android.permission.ACCESS_NETWORK_STATE",
+ },
+ "bluetooth": {
+ "android.permission.BLUETOOTH",
+ "android.permission.BLUETOOTH_ADMIN",
+ "android.permission.ACCESS_FINE_LOCATION",
+ },
+ "camera": {
+ "android.permission.CAMERA",
+ },
+ "storage": {
+ "android.permission.READ_EXTERNAL_STORAGE",
+ "android.permission.WRITE_EXTERNAL_STORAGE",
+ },
+}
+
+var AndroidFeatures = map[string][]string{
+ "default": {`glEsVersion="0x00020000"`, `name="android.hardware.type.pc"`},
+ "bluetooth": {
+ `name="android.hardware.bluetooth"`,
+ `name="android.hardware.bluetooth_le"`,
+ },
+ "camera": {
+ `name="android.hardware.camera"`,
+ },
+}
diff --git a/gio/cmd/gogio/race_test.go b/gio/cmd/gogio/race_test.go
new file mode 100644
index 0000000..0749936
--- /dev/null
+++ b/gio/cmd/gogio/race_test.go
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build race
+
+package main_test
+
+func init() { raceEnabled = true }
diff --git a/gio/cmd/gogio/testdata/testdata.go b/gio/cmd/gogio/testdata/testdata.go
new file mode 100644
index 0000000..b5c2493
--- /dev/null
+++ b/gio/cmd/gogio/testdata/testdata.go
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// A simple app used for gogio's end-to-end tests.
+package main
+
+import (
+ "fmt"
+ "image"
+ "image/color"
+ "log"
+
+ "realy.lol/gio/app"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+)
+
+func main() {
+ go func() {
+ w := app.NewWindow()
+ if err := loop(w); err != nil {
+ log.Fatal(err)
+ }
+ }()
+ app.Main()
+}
+
+type notifyFrame int
+
+const (
+ notifyNone notifyFrame = iota
+ notifyInvalidate
+ notifyPrint
+)
+
+// notify keeps track of whether we want to print to stdout to notify the user
+// when a frame is ready. Initially we want to notify about the first frame.
+var notify = notifyInvalidate
+
+type (
+ C = layout.Context
+ D = layout.Dimensions
+)
+
+func loop(w *app.Window) error {
+ topLeft := quarterWidget{
+ color: color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff},
+ }
+ topRight := quarterWidget{
+ color: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff},
+ }
+ botLeft := quarterWidget{
+ color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff},
+ }
+ botRight := quarterWidget{
+ color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x80},
+ }
+
+ var ops op.Ops
+ for {
+ e := <-w.Events()
+ switch e := e.(type) {
+ case system.DestroyEvent:
+ return e.Err
+ case system.FrameEvent:
+ gtx := layout.NewContext(&ops, e)
+ // Clear background to white, even on embedded platforms such as webassembly.
+ paint.Fill(gtx.Ops, color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
+ layout.Flex{Axis: layout.Vertical}.Layout(gtx,
+ layout.Flexed(1, func(gtx C) D {
+ return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
+ // r1c1
+ layout.Flexed(1,
+ func(gtx C) D { return topLeft.Layout(gtx) }),
+ // r1c2
+ layout.Flexed(1,
+ func(gtx C) D { return topRight.Layout(gtx) }),
+ )
+ }),
+ layout.Flexed(1, func(gtx C) D {
+ return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
+ // r2c1
+ layout.Flexed(1,
+ func(gtx C) D { return botLeft.Layout(gtx) }),
+ // r2c2
+ layout.Flexed(1,
+ func(gtx C) D { return botRight.Layout(gtx) }),
+ )
+ }),
+ )
+
+ e.Frame(gtx.Ops)
+
+ switch notify {
+ case notifyInvalidate:
+ notify = notifyPrint
+ w.Invalidate()
+ case notifyPrint:
+ notify = notifyNone
+ fmt.Println("gio frame ready")
+ }
+ }
+ }
+}
+
+// quarterWidget paints a quarter of the screen with one color. When clicked, it
+// turns red, going back to its normal color when clicked again.
+type quarterWidget struct {
+ color color.NRGBA
+
+ clicked bool
+}
+
+var red = color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
+
+func (w *quarterWidget) Layout(gtx layout.Context) layout.Dimensions {
+ var color color.NRGBA
+ if w.clicked {
+ color = red
+ } else {
+ color = w.color
+ }
+
+ r := image.Rectangle{Max: gtx.Constraints.Max}
+ paint.FillShape(gtx.Ops, color, clip.Rect(r).Op())
+
+ pointer.Rect(image.Rectangle{
+ Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y),
+ }).Add(gtx.Ops)
+ pointer.InputOp{
+ Tag: w,
+ Types: pointer.Press,
+ }.Add(gtx.Ops)
+
+ for _, e := range gtx.Events(w) {
+ if e, ok := e.(pointer.Event); ok && e.Type == pointer.Press {
+ w.clicked = !w.clicked
+ // notify when we're done updating the frame.
+ notify = notifyInvalidate
+ }
+ }
+ return layout.Dimensions{Size: gtx.Constraints.Max}
+}
diff --git a/gio/cmd/gogio/wayland_test.go b/gio/cmd/gogio/wayland_test.go
new file mode 100644
index 0000000..df10410
--- /dev/null
+++ b/gio/cmd/gogio/wayland_test.go
@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main_test
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "fmt"
+ "image"
+ "image/png"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "sync"
+ "text/template"
+ "time"
+)
+
+type WaylandTestDriver struct {
+ driverBase
+
+ runtimeDir string
+ socket string
+ display string
+}
+
+// No bars or anything fancy. Just a white background with our dimensions.
+var tmplSwayConfig = template.Must(template.New("").Parse(`
+output * bg #FFFFFF solid_color
+output * mode {{.Width}}x{{.Height}}
+default_border none
+`))
+
+var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`)
+
+func (d *WaylandTestDriver) Start(path string) {
+ // We want os.Environ, so that it can e.g. find $DISPLAY to run within
+ // X11. wlroots env vars are documented at:
+ // https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md
+ env := os.Environ()
+ if *headless {
+ env = append(env, "WLR_BACKENDS=headless")
+ }
+
+ d.needPrograms(
+ "sway", // to run a wayland compositor
+ "grim", // to take screenshots
+ "swaymsg", // to send input
+ )
+
+ // First, build the app.
+ dir := d.tempDir("gio-endtoend-wayland")
+ bin := filepath.Join(dir, "red")
+ flags := []string{"build", "-tags", "nox11", "-o=" + bin}
+ if raceEnabled {
+ flags = append(flags, "-race")
+ }
+ flags = append(flags, path)
+ cmd := exec.Command("go", flags...)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ d.Fatalf("could not build app: %s:\n%s", err, out)
+ }
+
+ conf := filepath.Join(dir, "config")
+ f, err := os.Create(conf)
+ if err != nil {
+ d.Fatal(err)
+ }
+ defer f.Close()
+ if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{
+ d.width, d.height,
+ }); err != nil {
+ d.Fatal(err)
+ }
+
+ d.socket = filepath.Join(dir, "socket")
+ env = append(env, "SWAYSOCK="+d.socket)
+ d.runtimeDir = dir
+ env = append(env, "XDG_RUNTIME_DIR="+d.runtimeDir)
+
+ var wg sync.WaitGroup
+ d.Cleanup(wg.Wait)
+
+ // First, start sway.
+ {
+ ctx, cancel := context.WithCancel(context.Background())
+ cmd := exec.CommandContext(ctx, "sway", "--config", conf, "--verbose")
+ cmd.Env = env
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ d.Fatal(err)
+ }
+ if err := cmd.Start(); err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(cancel)
+ d.Cleanup(func() {
+ // Give it a chance to exit gracefully, cleaning up
+ // after itself. After 10ms, the deferred cancel above
+ // will signal an os.Kill.
+ cmd.Process.Signal(os.Interrupt)
+ time.Sleep(10 * time.Millisecond)
+ })
+
+ // Wait for sway to be ready. We probably don't need a deadline
+ // here.
+ br := bufio.NewReader(stderr)
+ for {
+ line, err := br.ReadString('\n')
+ if err != nil {
+ d.Fatal(err)
+ }
+ if m := rxSwayReady.FindStringSubmatch(line); m != nil {
+ d.display = m[1]
+ break
+ }
+ }
+
+ wg.Add(1)
+ go func() {
+ if err := cmd.Wait(); err != nil && ctx.Err() == nil && !strings.Contains(err.Error(), "interrupt") {
+ // Don't print all stderr, since we use --verbose.
+ // TODO(mvdan): if it's useful, probably filter
+ // errors and show them.
+ d.Error(err)
+ }
+ wg.Done()
+ }()
+ }
+
+ // Then, start our program on the sway compositor above.
+ {
+ ctx, cancel := context.WithCancel(context.Background())
+ cmd := exec.CommandContext(ctx, bin)
+ cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
+ output, err := cmd.StdoutPipe()
+ if err != nil {
+ d.Fatal(err)
+ }
+ cmd.Stderr = cmd.Stdout
+ d.output = output
+ if err := cmd.Start(); err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(cancel)
+ wg.Add(1)
+ go func() {
+ if err := cmd.Wait(); err != nil && ctx.Err() == nil {
+ d.Error(err)
+ }
+ wg.Done()
+ }()
+ }
+
+ // Wait for the gio app to render.
+ d.waitForFrame()
+}
+
+func (d *WaylandTestDriver) Screenshot() image.Image {
+ cmd := exec.Command("grim", "/dev/stdout")
+ cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ d.Errorf("%s", out)
+ d.Fatal(err)
+ }
+ img, err := png.Decode(bytes.NewReader(out))
+ if err != nil {
+ d.Fatal(err)
+ }
+ return img
+}
+
+func (d *WaylandTestDriver) swaymsg(args ...interface{}) {
+ strs := []string{"--socket", d.socket}
+ for _, arg := range args {
+ strs = append(strs, fmt.Sprint(arg))
+ }
+ cmd := exec.Command("swaymsg", strs...)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ d.Errorf("%s", out)
+ d.Fatal(err)
+ }
+}
+
+func (d *WaylandTestDriver) Click(x, y int) {
+ d.swaymsg("seat", "-", "cursor", "set", x, y)
+ d.swaymsg("seat", "-", "cursor", "press", "button1")
+ d.swaymsg("seat", "-", "cursor", "release", "button1")
+
+ // Wait for the gio app to render after this click.
+ d.waitForFrame()
+}
diff --git a/gio/cmd/gogio/windows_test.go b/gio/cmd/gogio/windows_test.go
new file mode 100644
index 0000000..996b511
--- /dev/null
+++ b/gio/cmd/gogio/windows_test.go
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main_test
+
+import (
+ "context"
+ "image"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "sync"
+ "time"
+
+ "golang.org/x/image/draw"
+)
+
+// Wine is tightly coupled with X11 at the moment, and we can reuse the same
+// methods to automate screenshots and clicks. The main difference is how we
+// build and run the app.
+
+// The only quirk is that it seems impossible for the Wine window to take the
+// entirety of the X server's dimensions, even if we try to resize it to take
+// the entire display. It seems to want to leave some vertical space empty,
+// presumably for window decorations or the "start" bar on Windows. To work
+// around that, make the X server 50x50px bigger, and crop the screenshots back
+// to the original size.
+
+type WineTestDriver struct {
+ X11TestDriver
+}
+
+func (d *WineTestDriver) Start(path string) {
+ d.needPrograms("wine")
+
+ // First, build the app.
+ bin := filepath.Join(d.tempDir("gio-endtoend-windows"), "red.exe")
+ flags := []string{"build", "-o=" + bin}
+ if raceEnabled {
+ if runtime.GOOS != "windows" {
+ // cross-compilation disables CGo, which breaks -race.
+ d.Skipf("can't cross-compile -race for Windows; skipping")
+ }
+ flags = append(flags, "-race")
+ }
+ flags = append(flags, path)
+ cmd := exec.Command("go", flags...)
+ cmd.Env = os.Environ()
+ cmd.Env = append(cmd.Env, "GOOS=windows")
+ if out, err := cmd.CombinedOutput(); err != nil {
+ d.Fatalf("could not build app: %s:\n%s", err, out)
+ }
+
+ var wg sync.WaitGroup
+ d.Cleanup(wg.Wait)
+
+ // Add 50x50px to the display dimensions, as discussed earlier.
+ d.startServer(&wg, d.width+50, d.height+50)
+
+ // Then, start our program via Wine on the X server above.
+ {
+ cacheDir, err := os.UserCacheDir()
+ if err != nil {
+ d.Fatal(err)
+ }
+ // Use a wine directory separate from the default ~/.wine, so
+ // that the user's winecfg doesn't affect our test. This will
+ // default to ~/.cache/gio-e2e-wine. We use the user's cache,
+ // to reuse a previously set up wineprefix.
+ wineprefix := filepath.Join(cacheDir, "gio-e2e-wine")
+
+ // First, ensure that wineprefix is up to date with wineboot.
+ // Wait for this separately from the first frame, as setting up
+ // a new prefix might take 5s on its own.
+ env := []string{
+ "DISPLAY=" + d.display,
+ "WINEDEBUG=fixme-all", // hide "fixme" noise
+ "WINEPREFIX=" + wineprefix,
+
+ // Disable wine-gecko (Explorer) and wine-mono (.NET).
+ // Otherwise, if not installed, wineboot will get stuck
+ // with a prompt to install them on the virtual X
+ // display. Moreover, Gio doesn't need either, and wine
+ // is faster without them.
+ "WINEDLLOVERRIDES=mscoree,mshtml=",
+ }
+ {
+ start := time.Now()
+ cmd := exec.Command("wine", "wineboot", "-i")
+ cmd.Env = env
+ // Use a combined output pipe instead of CombinedOutput,
+ // so that we only wait for the child process to exit,
+ // and we don't need to wait for all of wine's
+ // grandchildren to exit and stop writing. This is
+ // relevant as wine leaves "wineserver" lingering for
+ // three seconds by default, to be reused later.
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ d.Fatal(err)
+ }
+ cmd.Stderr = cmd.Stdout
+ if err := cmd.Run(); err != nil {
+ io.Copy(os.Stderr, stdout)
+ d.Fatal(err)
+ }
+ d.Logf("set up WINEPREFIX in %s", time.Since(start))
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ cmd := exec.CommandContext(ctx, "wine", bin)
+ cmd.Env = env
+ output, err := cmd.StdoutPipe()
+ if err != nil {
+ d.Fatal(err)
+ }
+ cmd.Stderr = cmd.Stdout
+ d.output = output
+ if err := cmd.Start(); err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(cancel)
+ wg.Add(1)
+ go func() {
+ if err := cmd.Wait(); err != nil && ctx.Err() == nil {
+ d.Error(err)
+ }
+ wg.Done()
+ }()
+ }
+ // Wait for the gio app to render.
+ d.waitForFrame()
+
+ // xdotool seems to fail at actually moving the window if we use it
+ // immediately after Gio is ready. Why?
+ // We can't tell if the windowmove operation worked until we take a
+ // screenshot, because the getwindowgeometry op reports the 0x0
+ // coordinates even if the window wasn't moved properly.
+ // A sleep of ~20ms seems to be enough on an idle laptop. Use 20x that.
+ // TODO(mvdan): revisit this, when you have a spare three hours.
+ time.Sleep(400 * time.Millisecond)
+ id := d.xdotool("search", "--sync", "--onlyvisible", "--name", "Gio")
+ d.xdotool("windowmove", "--sync", id, 0, 0)
+}
+
+func (d *WineTestDriver) Screenshot() image.Image {
+ img := d.X11TestDriver.Screenshot()
+ // Crop the screenshot back to the original dimensions.
+ cropped := image.NewRGBA(image.Rect(0, 0, d.width, d.height))
+ draw.Draw(cropped, cropped.Bounds(), img, image.Point{}, draw.Src)
+ return cropped
+}
diff --git a/gio/cmd/gogio/windowsbuild.go b/gio/cmd/gogio/windowsbuild.go
new file mode 100644
index 0000000..1af8668
--- /dev/null
+++ b/gio/cmd/gogio/windowsbuild.go
@@ -0,0 +1,412 @@
+package main
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "image/png"
+ "io"
+ "math"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "reflect"
+ "strconv"
+ "strings"
+ "text/template"
+
+ "github.com/akavel/rsrc/binutil"
+ "github.com/akavel/rsrc/coff"
+ "golang.org/x/text/encoding/unicode"
+)
+
+func buildWindows(tmpDir string, bi *buildInfo) error {
+ builder := &windowsBuilder{TempDir: tmpDir}
+ builder.DestDir = *destPath
+ if builder.DestDir == "" {
+ builder.DestDir = bi.pkgPath
+ }
+
+ name := bi.name
+ if *destPath != "" {
+ if filepath.Ext(*destPath) != ".exe" {
+ return fmt.Errorf("invalid output name %q, it must end with `.exe`", *destPath)
+ }
+ name = filepath.Base(*destPath)
+ }
+ name = strings.TrimSuffix(name, ".exe")
+ sdk := bi.minsdk
+ if sdk > 10 {
+ return fmt.Errorf("invalid minsdk (%d) it's higher than Windows 10", sdk)
+ }
+ version := strconv.Itoa(bi.version)
+ if bi.version > math.MaxUint16 {
+ return fmt.Errorf("version (%d) is larger than the maximum (%d)", bi.version, math.MaxUint16)
+ }
+
+ for _, arch := range bi.archs {
+ builder.Coff = coff.NewRSRC()
+ builder.Coff.Arch(arch)
+
+ if err := builder.embedIcon(bi.iconPath); err != nil {
+ return err
+ }
+
+ if err := builder.embedManifest(windowsManifest{
+ Version: "1.0.0." + version,
+ WindowsVersion: sdk,
+ Name: name,
+ }); err != nil {
+ return fmt.Errorf("can't create manifest: %v", err)
+ }
+
+ if err := builder.embedInfo(windowsResources{
+ Version: [2]uint32{uint32(1) << 16, uint32(bi.version)},
+ VersionHuman: "1.0.0." + version,
+ Name: name,
+ Language: 0x0400, // Process Default Language: https://docs.microsoft.com/en-us/previous-versions/ms957130(v=msdn.10)
+ }); err != nil {
+ return fmt.Errorf("can't create info: %v", err)
+ }
+
+ if err := builder.buildResource(bi, name, arch); err != nil {
+ return fmt.Errorf("can't build the resources: %v", err)
+ }
+
+ if err := builder.buildProgram(bi, name, arch); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+type (
+ windowsResources struct {
+ Version [2]uint32
+ VersionHuman string
+ Language uint16
+ Name string
+ }
+ windowsManifest struct {
+ Version string
+ WindowsVersion int
+ Name string
+ }
+ windowsBuilder struct {
+ TempDir string
+ DestDir string
+ Coff *coff.Coff
+ }
+)
+
+const (
+ // https://docs.microsoft.com/en-us/windows/win32/menurc/resource-types
+ windowsResourceIcon = 3
+ windowsResourceIconGroup = windowsResourceIcon + 11
+ windowsResourceManifest = 24
+ windowsResourceVersion = 16
+)
+
+type bufferCoff struct {
+ bytes.Buffer
+}
+
+func (b *bufferCoff) Size() int64 {
+ return int64(b.Len())
+}
+
+func (b *windowsBuilder) embedIcon(path string) (err error) {
+ iconFile, err := os.Open(path)
+ if err != nil {
+ return fmt.Errorf("can't read the icon located at %s: %v", path, err)
+ }
+ defer iconFile.Close()
+
+ iconImage, err := png.Decode(iconFile)
+ if err != nil {
+ return fmt.Errorf("can't decode the PNG file (%s): %v", path, err)
+ }
+
+ sizes := []int{16, 32, 48, 64, 128, 256}
+ var iconHeader bufferCoff
+
+ // GRPICONDIR structure.
+ if err := binary.Write(&iconHeader, binary.LittleEndian, [3]uint16{0, 1, uint16(len(sizes))}); err != nil {
+ return err
+ }
+
+ for _, size := range sizes {
+ var iconBuffer bufferCoff
+
+ if err := png.Encode(&iconBuffer, resizeIcon(iconVariant{size: size, fill: false}, iconImage)); err != nil {
+ return fmt.Errorf("can't encode image: %v", err)
+ }
+
+ b.Coff.AddResource(windowsResourceIcon, uint16(size), &iconBuffer)
+
+ if err := binary.Write(&iconHeader, binary.LittleEndian, struct {
+ Size [2]uint8
+ Color [2]uint8
+ Planes uint16
+ BitCount uint16
+ Length uint32
+ Id uint16
+ }{
+ Size: [2]uint8{uint8(size % 256), uint8(size % 256)}, // "0" means 256px.
+ Planes: 1,
+ BitCount: 32,
+ Length: uint32(iconBuffer.Len()),
+ Id: uint16(size),
+ }); err != nil {
+ return err
+ }
+ }
+
+ b.Coff.AddResource(windowsResourceIconGroup, 1, &iconHeader)
+
+ return nil
+}
+
+func (b *windowsBuilder) buildResource(buildInfo *buildInfo, name string, arch string) error {
+ out, err := os.Create(filepath.Join(buildInfo.pkgPath, name+"_windows_"+arch+".syso"))
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+ b.Coff.Freeze()
+
+ // See https://github.com/akavel/rsrc/internal/write.go#L13.
+ w := binutil.Writer{W: out}
+ binutil.Walk(b.Coff, func(v reflect.Value, path string) error {
+ if binutil.Plain(v.Kind()) {
+ w.WriteLE(v.Interface())
+ return nil
+ }
+ vv, ok := v.Interface().(binutil.SizedReader)
+ if ok {
+ w.WriteFromSized(vv)
+ return binutil.WALK_SKIP
+ }
+ return nil
+ })
+
+ if w.Err != nil {
+ return fmt.Errorf("error writing output file: %s", w.Err)
+ }
+
+ return nil
+}
+
+func (b *windowsBuilder) buildProgram(buildInfo *buildInfo, name string, arch string) error {
+ dest := b.DestDir
+ if len(buildInfo.archs) > 1 {
+ dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".exe")
+ }
+
+ cmd := exec.Command(
+ "go",
+ "build",
+ "-ldflags=-H=windowsgui "+buildInfo.ldflags,
+ "-tags="+buildInfo.tags,
+ "-o", dest,
+ buildInfo.pkgPath,
+ )
+ cmd.Env = append(
+ os.Environ(),
+ "GOOS=windows",
+ "GOARCH="+arch,
+ )
+ _, err := runCmd(cmd)
+ return err
+}
+
+func (b *windowsBuilder) embedManifest(v windowsManifest) error {
+ t, err := template.New("manifest").Parse(`
+
+
+ {{.Name}}
+
+
+ {{if (le .WindowsVersion 10)}}
+{{end}}
+ {{if (le .WindowsVersion 9)}}
+{{end}}
+ {{if (le .WindowsVersion 8)}}
+{{end}}
+ {{if (le .WindowsVersion 7)}}
+{{end}}
+ {{if (le .WindowsVersion 6)}}
+{{end}}
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+`)
+ if err != nil {
+ return err
+ }
+
+ var manifest bufferCoff
+ if err := t.Execute(&manifest, v); err != nil {
+ return err
+ }
+
+ b.Coff.AddResource(windowsResourceManifest, 1, &manifest)
+
+ return nil
+}
+
+func (b *windowsBuilder) embedInfo(v windowsResources) error {
+ page := uint16(1)
+
+ // https://docs.microsoft.com/pt-br/windows/win32/menurc/vs-versioninfo
+ t := newValue(valueBinary, "VS_VERSION_INFO", []io.WriterTo{
+ // https://docs.microsoft.com/pt-br/windows/win32/api/VerRsrc/ns-verrsrc-vs_fixedfileinfo
+ windowsInfoValueFixed{
+ Signature: 0xFEEF04BD,
+ StructVersion: 0x00010000,
+ FileVersion: v.Version,
+ ProductVersion: v.Version,
+ FileFlagMask: 0x3F,
+ FileFlags: 0,
+ FileOS: 0x40004,
+ FileType: 0x1,
+ FileSubType: 0,
+ },
+ // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringfileinfo
+ newValue(valueText, "StringFileInfo", []io.WriterTo{
+ // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringtable
+ newValue(valueText, fmt.Sprintf("%04X%04X", v.Language, page), []io.WriterTo{
+ // https://docs.microsoft.com/pt-br/windows/win32/menurc/string-str
+ newValue(valueText, "ProductVersion", v.VersionHuman),
+ newValue(valueText, "FileVersion", v.VersionHuman),
+ newValue(valueText, "FileDescription", v.Name),
+ newValue(valueText, "ProductName", v.Name),
+ // TODO include more data: gogio must have some way to provide such information (like Company Name, Copyright...)
+ }),
+ }),
+ // https://docs.microsoft.com/pt-br/windows/win32/menurc/varfileinfo
+ newValue(valueBinary, "VarFileInfo", []io.WriterTo{
+ // https://docs.microsoft.com/pt-br/windows/win32/menurc/var-str
+ newValue(valueBinary, "Translation", uint32(page)<<16|uint32(v.Language)),
+ }),
+ })
+
+ // For some reason the ValueLength of the VS_VERSIONINFO must be the byte-length of `windowsInfoValueFixed`:
+ t.ValueLength = 52
+
+ var verrsrc bufferCoff
+ if _, err := t.WriteTo(&verrsrc); err != nil {
+ return err
+ }
+
+ b.Coff.AddResource(windowsResourceVersion, 1, &verrsrc)
+
+ return nil
+}
+
+type windowsInfoValueFixed struct {
+ Signature uint32
+ StructVersion uint32
+ FileVersion [2]uint32
+ ProductVersion [2]uint32
+ FileFlagMask uint32
+ FileFlags uint32
+ FileOS uint32
+ FileType uint32
+ FileSubType uint32
+ FileDate [2]uint32
+}
+
+func (v windowsInfoValueFixed) WriteTo(w io.Writer) (_ int64, err error) {
+ return 0, binary.Write(w, binary.LittleEndian, v)
+}
+
+type windowsInfoValue struct {
+ Length uint16
+ ValueLength uint16
+ Type uint16
+ Key []byte
+ Value []byte
+}
+
+func (v windowsInfoValue) WriteTo(w io.Writer) (_ int64, err error) {
+ // binary.Write doesn't support []byte inside struct.
+ if err = binary.Write(w, binary.LittleEndian, [3]uint16{v.Length, v.ValueLength, v.Type}); err != nil {
+ return 0, err
+ }
+ if _, err = w.Write(v.Key); err != nil {
+ return 0, err
+ }
+ if _, err = w.Write(v.Value); err != nil {
+ return 0, err
+ }
+ return 0, nil
+}
+
+const (
+ valueBinary uint16 = 0
+ valueText uint16 = 1
+)
+
+func newValue(valueType uint16, key string, input interface{}) windowsInfoValue {
+ v := windowsInfoValue{
+ Type: valueType,
+ Length: 6,
+ }
+
+ padding := func(in []byte) []byte {
+ if l := uint16(len(in)) + v.Length; l%4 != 0 {
+ return append(in, make([]byte, 4-l%4)...)
+ }
+ return in
+ }
+
+ v.Key = padding(utf16Encode(key))
+ v.Length += uint16(len(v.Key))
+
+ switch in := input.(type) {
+ case string:
+ v.Value = padding(utf16Encode(in))
+ v.ValueLength = uint16(len(v.Value) / 2)
+ case []io.WriterTo:
+ var buff bytes.Buffer
+ for k := range in {
+ if _, err := in[k].WriteTo(&buff); err != nil {
+ panic(err)
+ }
+ }
+ v.Value = buff.Bytes()
+ default:
+ var buff bytes.Buffer
+ if err := binary.Write(&buff, binary.LittleEndian, in); err != nil {
+ panic(err)
+ }
+ v.ValueLength = uint16(buff.Len())
+ v.Value = buff.Bytes()
+ }
+
+ v.Length += uint16(len(v.Value))
+
+ return v
+}
+
+// utf16Encode encodes the string to UTF16 with null-termination.
+func utf16Encode(s string) []byte {
+ b, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes([]byte(s))
+ if err != nil {
+ panic(err)
+ }
+ return append(b, 0x00, 0x00) // null-termination.
+}
diff --git a/gio/cmd/gogio/x11_test.go b/gio/cmd/gogio/x11_test.go
new file mode 100644
index 0000000..9bb3174
--- /dev/null
+++ b/gio/cmd/gogio/x11_test.go
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main_test
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "image"
+ "image/png"
+ "io"
+ "math/rand"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sync"
+ "time"
+)
+
+type X11TestDriver struct {
+ driverBase
+
+ display string
+}
+
+func (d *X11TestDriver) Start(path string) {
+ // First, build the app.
+ bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red")
+ flags := []string{"build", "-tags", "nowayland", "-o=" + bin}
+ if raceEnabled {
+ flags = append(flags, "-race")
+ }
+ flags = append(flags, path)
+ cmd := exec.Command("go", flags...)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ d.Fatalf("could not build app: %s:\n%s", err, out)
+ }
+
+ var wg sync.WaitGroup
+ d.Cleanup(wg.Wait)
+
+ d.startServer(&wg, d.width, d.height)
+
+ // Then, start our program on the X server above.
+ {
+ ctx, cancel := context.WithCancel(context.Background())
+ cmd := exec.CommandContext(ctx, bin)
+ cmd.Env = []string{"DISPLAY=" + d.display}
+ output, err := cmd.StdoutPipe()
+ if err != nil {
+ d.Fatal(err)
+ }
+ cmd.Stderr = cmd.Stdout
+ d.output = output
+ if err := cmd.Start(); err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(cancel)
+ wg.Add(1)
+ go func() {
+ if err := cmd.Wait(); err != nil && ctx.Err() == nil {
+ d.Error(err)
+ }
+ wg.Done()
+ }()
+ }
+
+ // Wait for the gio app to render.
+ d.waitForFrame()
+}
+
+func (d *X11TestDriver) startServer(wg *sync.WaitGroup, width, height int) {
+ // Pick a random display number between 1 and 100,000. Most machines
+ // will only be using :0, so there's only a 0.001% chance of two
+ // concurrent test runs to run into a conflict.
+ rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
+ d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1)
+
+ var xprog string
+ xflags := []string{
+ "-wr", // we want a white background; the default is black
+ }
+ if *headless {
+ xprog = "Xvfb" // virtual X server
+ xflags = append(xflags, "-screen", "0", fmt.Sprintf("%dx%dx24", width, height))
+ } else {
+ xprog = "Xephyr" // nested X server as a window
+ xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height))
+ }
+ xflags = append(xflags, d.display)
+
+ d.needPrograms(
+ xprog, // to run the X server
+ "scrot", // to take screenshots
+ "xdotool", // to send input
+ )
+ ctx, cancel := context.WithCancel(context.Background())
+ cmd := exec.CommandContext(ctx, xprog, xflags...)
+ combined := &bytes.Buffer{}
+ cmd.Stdout = combined
+ cmd.Stderr = combined
+ if err := cmd.Start(); err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(cancel)
+ d.Cleanup(func() {
+ // Give it a chance to exit gracefully, cleaning up
+ // after itself. After 10ms, the deferred cancel above
+ // will signal an os.Kill.
+ cmd.Process.Signal(os.Interrupt)
+ time.Sleep(10 * time.Millisecond)
+ })
+
+ // Wait for the X server to be ready. The socket path isn't
+ // terribly portable, but that's okay for now.
+ withRetries(d.T, time.Second, func() error {
+ socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:])
+ _, err := os.Stat(socket)
+ return err
+ })
+
+ wg.Add(1)
+ go func() {
+ if err := cmd.Wait(); err != nil && ctx.Err() == nil {
+ // Print all output and error.
+ io.Copy(os.Stdout, combined)
+ d.Error(err)
+ }
+ wg.Done()
+ }()
+}
+
+func (d *X11TestDriver) Screenshot() image.Image {
+ cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout")
+ cmd.Env = []string{"DISPLAY=" + d.display}
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ d.Errorf("%s", out)
+ d.Fatal(err)
+ }
+ img, err := png.Decode(bytes.NewReader(out))
+ if err != nil {
+ d.Fatal(err)
+ }
+ return img
+}
+
+func (d *X11TestDriver) xdotool(args ...interface{}) string {
+ d.Helper()
+ strs := make([]string, len(args))
+ for i, arg := range args {
+ strs[i] = fmt.Sprint(arg)
+ }
+ cmd := exec.Command("xdotool", strs...)
+ cmd.Env = []string{"DISPLAY=" + d.display}
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ d.Errorf("%s", out)
+ d.Fatal(err)
+ }
+ return string(bytes.TrimSpace(out))
+}
+
+func (d *X11TestDriver) Click(x, y int) {
+ d.xdotool("mousemove", "--sync", x, y)
+ d.xdotool("click", "1")
+
+ // Wait for the gio app to render after this click.
+ d.waitForFrame()
+}
diff --git a/gio/f32/affine.go b/gio/f32/affine.go
new file mode 100644
index 0000000..667f7e9
--- /dev/null
+++ b/gio/f32/affine.go
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package f32
+
+import (
+ "fmt"
+ "math"
+)
+
+// Affine2D represents an affine 2D transformation. The zero value if Affine2D
+// represents the identity transform.
+type Affine2D struct {
+ // in order to make the zero value of Affine2D represent the identity
+ // transform we store it with the identity matrix subtracted, that is
+ // if the actual transformation matrix is:
+ // [sx, hx, ox]
+ // [hy, sy, oy]
+ // [ 0, 0, 1]
+ // we store a = sx-1 and e = sy-1
+ a, b, c float32
+ d, e, f float32
+}
+
+// NewAffine2D creates a new Affine2D transform from the matrix elements
+// in row major order. The rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1].
+func NewAffine2D(sx, hx, ox, hy, sy, oy float32) Affine2D {
+ return Affine2D{
+ a: sx - 1, b: hx, c: ox,
+ d: hy, e: sy - 1, f: oy,
+ }
+}
+
+// Offset the transformation.
+func (a Affine2D) Offset(offset Point) Affine2D {
+ return Affine2D{
+ a.a, a.b, a.c + offset.X,
+ a.d, a.e, a.f + offset.Y,
+ }
+}
+
+// Scale the transformation around the given origin.
+func (a Affine2D) Scale(origin, factor Point) Affine2D {
+ if origin == (Point{}) {
+ return a.scale(factor)
+ }
+ a = a.Offset(origin.Mul(-1))
+ a = a.scale(factor)
+ return a.Offset(origin)
+}
+
+// Rotate the transformation by the given angle (in radians) counter clockwise around the given origin.
+func (a Affine2D) Rotate(origin Point, radians float32) Affine2D {
+ if origin == (Point{}) {
+ return a.rotate(radians)
+ }
+ a = a.Offset(origin.Mul(-1))
+ a = a.rotate(radians)
+ return a.Offset(origin)
+}
+
+// Shear the transformation by the given angle (in radians) around the given origin.
+func (a Affine2D) Shear(origin Point, radiansX, radiansY float32) Affine2D {
+ if origin == (Point{}) {
+ return a.shear(radiansX, radiansY)
+ }
+ a = a.Offset(origin.Mul(-1))
+ a = a.shear(radiansX, radiansY)
+ return a.Offset(origin)
+}
+
+// Mul returns A*B.
+func (A Affine2D) Mul(B Affine2D) (r Affine2D) {
+ r.a = (A.a+1)*(B.a+1) + A.b*B.d - 1
+ r.b = (A.a+1)*B.b + A.b*(B.e+1)
+ r.c = (A.a+1)*B.c + A.b*B.f + A.c
+ r.d = A.d*(B.a+1) + (A.e+1)*B.d
+ r.e = A.d*B.b + (A.e+1)*(B.e+1) - 1
+ r.f = A.d*B.c + (A.e+1)*B.f + A.f
+ return r
+}
+
+// Invert the transformation. Note that if the matrix is close to singular
+// numerical errors may become large or infinity.
+func (a Affine2D) Invert() Affine2D {
+ if a.a == 0 && a.b == 0 && a.d == 0 && a.e == 0 {
+ return Affine2D{a: 0, b: 0, c: -a.c, d: 0, e: 0, f: -a.f}
+ }
+ a.a += 1
+ a.e += 1
+ det := a.a*a.e - a.b*a.d
+ a.a, a.e = a.e/det, a.a/det
+ a.b, a.d = -a.b/det, -a.d/det
+ temp := a.c
+ a.c = -a.a*a.c - a.b*a.f
+ a.f = -a.d*temp - a.e*a.f
+ a.a -= 1
+ a.e -= 1
+ return a
+}
+
+// Transform p by returning a*p.
+func (a Affine2D) Transform(p Point) Point {
+ return Point{
+ X: p.X*(a.a+1) + p.Y*a.b + a.c,
+ Y: p.X*a.d + p.Y*(a.e+1) + a.f,
+ }
+}
+
+// Elems returns the matrix elements of the transform in row-major order. The
+// rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1].
+func (a Affine2D) Elems() (sx, hx, ox, hy, sy, oy float32) {
+ return a.a + 1, a.b, a.c, a.d, a.e + 1, a.f
+}
+
+func (a Affine2D) scale(factor Point) Affine2D {
+ return Affine2D{
+ (a.a+1)*factor.X - 1, a.b * factor.X, a.c * factor.X,
+ a.d * factor.Y, (a.e+1)*factor.Y - 1, a.f * factor.Y,
+ }
+}
+
+func (a Affine2D) rotate(radians float32) Affine2D {
+ sin, cos := math.Sincos(float64(radians))
+ s, c := float32(sin), float32(cos)
+ return Affine2D{
+ (a.a+1)*c - a.d*s - 1, a.b*c - (a.e+1)*s, a.c*c - a.f*s,
+ (a.a+1)*s + a.d*c, a.b*s + (a.e+1)*c - 1, a.c*s + a.f*c,
+ }
+}
+
+func (a Affine2D) shear(radiansX, radiansY float32) Affine2D {
+ tx := float32(math.Tan(float64(radiansX)))
+ ty := float32(math.Tan(float64(radiansY)))
+ return Affine2D{
+ (a.a + 1) + a.d*tx - 1, a.b + (a.e+1)*tx, a.c + a.f*tx,
+ (a.a+1)*ty + a.d, a.b*ty + (a.e + 1) - 1, a.f*ty + a.f,
+ }
+}
+
+func (a Affine2D) String() string {
+ sx, hx, ox, hy, sy, oy := a.Elems()
+ return fmt.Sprintf("[[%f %f %f] [%f %f %f]]", sx, hx, ox, hy, sy, oy)
+}
diff --git a/gio/f32/affine_test.go b/gio/f32/affine_test.go
new file mode 100644
index 0000000..4077b8d
--- /dev/null
+++ b/gio/f32/affine_test.go
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package f32
+
+import (
+ "math"
+ "testing"
+)
+
+func eq(p1, p2 Point) bool {
+ tol := 1e-5
+ dx, dy := p2.X-p1.X, p2.Y-p1.Y
+ return math.Abs(math.Sqrt(float64(dx*dx+dy*dy))) < tol
+}
+
+func eqaff(x, y Affine2D) bool {
+ tol := 1e-5
+ return math.Abs(float64(x.a-y.a)) < tol &&
+ math.Abs(float64(x.b-y.b)) < tol &&
+ math.Abs(float64(x.c-y.c)) < tol &&
+ math.Abs(float64(x.d-y.d)) < tol &&
+ math.Abs(float64(x.e-y.e)) < tol &&
+ math.Abs(float64(x.f-y.f)) < tol
+}
+
+func TestTransformOffset(t *testing.T) {
+ p := Point{X: 1, Y: 2}
+ o := Point{X: 2, Y: -3}
+
+ r := Affine2D{}.Offset(o).Transform(p)
+ if !eq(r, Pt(3, -1)) {
+ t.Errorf("offset transformation mismatch: have %v, want {3 -1}", r)
+ }
+ i := Affine2D{}.Offset(o).Invert().Transform(r)
+ if !eq(i, p) {
+ t.Errorf("offset transformation inverse mismatch: have %v, want %v", i, p)
+ }
+}
+
+func TestTransformScale(t *testing.T) {
+ p := Point{X: 1, Y: 2}
+ s := Point{X: -1, Y: 2}
+
+ r := Affine2D{}.Scale(Point{}, s).Transform(p)
+ if !eq(r, Pt(-1, 4)) {
+ t.Errorf("scale transformation mismatch: have %v, want {-1 4}", r)
+ }
+ i := Affine2D{}.Scale(Point{}, s).Invert().Transform(r)
+ if !eq(i, p) {
+ t.Errorf("scale transformation inverse mismatch: have %v, want %v", i, p)
+ }
+}
+
+func TestTransformRotate(t *testing.T) {
+ p := Point{X: 1, Y: 0}
+ a := float32(math.Pi / 2)
+
+ r := Affine2D{}.Rotate(Point{}, a).Transform(p)
+ if !eq(r, Pt(0, 1)) {
+ t.Errorf("rotate transformation mismatch: have %v, want {0 1}", r)
+ }
+ i := Affine2D{}.Rotate(Point{}, a).Invert().Transform(r)
+ if !eq(i, p) {
+ t.Errorf("rotate transformation inverse mismatch: have %v, want %v", i, p)
+ }
+}
+
+func TestTransformShear(t *testing.T) {
+ p := Point{X: 1, Y: 1}
+
+ r := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Transform(p)
+ if !eq(r, Pt(2, 1)) {
+ t.Errorf("shear transformation mismatch: have %v, want {2 1}", r)
+ }
+ i := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Invert().Transform(r)
+ if !eq(i, p) {
+ t.Errorf("shear transformation inverse mismatch: have %v, want %v", i, p)
+ }
+}
+
+func TestTransformMultiply(t *testing.T) {
+ p := Point{X: 1, Y: 2}
+ o := Point{X: 2, Y: -3}
+ s := Point{X: -1, Y: 2}
+ a := float32(-math.Pi / 2)
+
+ r := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Transform(p)
+ if !eq(r, Pt(1, 3)) {
+ t.Errorf("complex transformation mismatch: have %v, want {1 3}", r)
+ }
+ i := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Invert().Transform(r)
+ if !eq(i, p) {
+ t.Errorf("complex transformation inverse mismatch: have %v, want %v", i, p)
+ }
+}
+
+func TestPrimes(t *testing.T) {
+ xa := NewAffine2D(9, 11, 13, 17, 19, 23)
+ xb := NewAffine2D(29, 31, 37, 43, 47, 53)
+
+ pa := Point{X: 2, Y: 3}
+ pb := Point{X: 5, Y: 7}
+
+ for _, test := range []struct {
+ x Affine2D
+ p Point
+ exp Point
+ }{
+ {x: xa, p: pa, exp: Pt(64, 114)},
+ {x: xa, p: pb, exp: Pt(135, 241)},
+ {x: xb, p: pa, exp: Pt(188, 280)},
+ {x: xb, p: pb, exp: Pt(399, 597)},
+ } {
+ got := test.x.Transform(test.p)
+ if !eq(got, test.exp) {
+ t.Errorf("%v.Transform(%v): have %v, want %v", test.x, test.p, got, test.exp)
+ }
+ }
+
+ for _, test := range []struct {
+ x Affine2D
+ exp Affine2D
+ }{
+ {x: xa, exp: NewAffine2D(-1.1875, 0.6875, -0.375, 1.0625, -0.5625, -0.875)},
+ {x: xb, exp: NewAffine2D(1.5666667, -1.0333333, -3.2000008, -1.4333333, 1-0.03333336, 1.7999992)},
+ } {
+ got := test.x.Invert()
+ if !eqaff(got, test.exp) {
+ t.Errorf("%v.Invert(): have %v, want %v", test.x, got, test.exp)
+ }
+ }
+
+ got := xa.Mul(xb)
+ exp := NewAffine2D(734, 796, 929, 1310, 1420, 1659)
+ if !eqaff(got, exp) {
+ t.Errorf("%v.Mul(%v): have %v, want %v", xa, xb, got, exp)
+ }
+}
+
+func TestTransformScaleAround(t *testing.T) {
+ p := Pt(-1, -1)
+ target := Pt(-6, -13)
+ pt := Affine2D{}.Scale(Pt(4, 5), Pt(2, 3)).Transform(p)
+ if !eq(pt, target) {
+ t.Log(pt, "!=", target)
+ t.Error("Scale not as expected")
+ }
+}
+
+func TestTransformRotateAround(t *testing.T) {
+ p := Pt(-1, -1)
+ pt := Affine2D{}.Rotate(Pt(1, 1), -math.Pi/2).Transform(p)
+ target := Pt(-1, 3)
+ if !eq(pt, target) {
+ t.Log(pt, "!=", target)
+ t.Error("Rotate not as expected")
+ }
+}
+
+func TestMulOrder(t *testing.T) {
+ A := Affine2D{}.Offset(Pt(100, 100))
+ B := Affine2D{}.Scale(Point{}, Pt(2, 2))
+ _ = A
+ _ = B
+
+ T1 := Affine2D{}.Offset(Pt(100, 100)).Scale(Point{}, Pt(2, 2))
+ T2 := B.Mul(A)
+
+ if T1 != T2 {
+ t.Log(T1)
+ t.Log(T2)
+ t.Error("multiplication / transform order not as expected")
+ }
+}
+
+func BenchmarkTransformOffset(b *testing.B) {
+ p := Point{X: 1, Y: 2}
+ o := Point{X: 0.5, Y: 0.5}
+ aff := Affine2D{}.Offset(o)
+
+ for i := 0; i < b.N; i++ {
+ p = aff.Transform(p)
+ }
+ _ = p
+}
+
+func BenchmarkTransformScale(b *testing.B) {
+ p := Point{X: 1, Y: 2}
+ s := Point{X: 0.5, Y: 0.5}
+ aff := Affine2D{}.Scale(Point{}, s)
+ for i := 0; i < b.N; i++ {
+ p = aff.Transform(p)
+ }
+ _ = p
+}
+
+func BenchmarkTransformRotate(b *testing.B) {
+ p := Point{X: 1, Y: 2}
+ a := float32(math.Pi / 2)
+ aff := Affine2D{}.Rotate(Point{}, a)
+ for i := 0; i < b.N; i++ {
+ p = aff.Transform(p)
+ }
+ _ = p
+}
+
+func BenchmarkTransformTranslateMultiply(b *testing.B) {
+ a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3)
+ t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5})
+
+ for i := 0; i < b.N; i++ {
+ a = a.Mul(t)
+ }
+}
+
+func BenchmarkTransformScaleMultiply(b *testing.B) {
+ a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3)
+ t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Scale(Point{}, Point{X: 0.4, Y: -0.5})
+
+ for i := 0; i < b.N; i++ {
+ a = a.Mul(t)
+ }
+}
+
+func BenchmarkTransformMultiply(b *testing.B) {
+ a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3)
+ t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Rotate(Point{}, math.Pi/7)
+
+ for i := 0; i < b.N; i++ {
+ a = a.Mul(t)
+ }
+}
diff --git a/gio/f32/f32.go b/gio/f32/f32.go
new file mode 100644
index 0000000..69745ba
--- /dev/null
+++ b/gio/f32/f32.go
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package f32 is a float32 implementation of package image's
+Point and Rectangle.
+
+The coordinate space has the origin in the top left
+corner with the axes extending right and down.
+*/
+package f32
+
+import "strconv"
+
+// A Point is a two dimensional point.
+type Point struct {
+ X, Y float32
+}
+
+// String return a string representation of p.
+func (p Point) String() string {
+ return "(" + strconv.FormatFloat(float64(p.X), 'f', -1, 32) +
+ "," + strconv.FormatFloat(float64(p.Y), 'f', -1, 32) + ")"
+}
+
+// A Rectangle contains the points (X, Y) where Min.X <= X < Max.X,
+// Min.Y <= Y < Max.Y.
+type Rectangle struct {
+ Min, Max Point
+}
+
+// String return a string representation of r.
+func (r Rectangle) String() string {
+ return r.Min.String() + "-" + r.Max.String()
+}
+
+// Rect is a shorthand for Rectangle{Point{x0, y0}, Point{x1, y1}}.
+// The returned Rectangle has x0 and y0 swapped if necessary so that
+// it's correctly formed.
+func Rect(x0, y0, x1, y1 float32) Rectangle {
+ if x0 > x1 {
+ x0, x1 = x1, x0
+ }
+ if y0 > y1 {
+ y0, y1 = y1, y0
+ }
+ return Rectangle{Point{x0, y0}, Point{x1, y1}}
+}
+
+// Pt is shorthand for Point{X: x, Y: y}.
+func Pt(x, y float32) Point {
+ return Point{X: x, Y: y}
+}
+
+// Add return the point p+p2.
+func (p Point) Add(p2 Point) Point {
+ return Point{X: p.X + p2.X, Y: p.Y + p2.Y}
+}
+
+// Sub returns the vector p-p2.
+func (p Point) Sub(p2 Point) Point {
+ return Point{X: p.X - p2.X, Y: p.Y - p2.Y}
+}
+
+// Mul returns p scaled by s.
+func (p Point) Mul(s float32) Point {
+ return Point{X: p.X * s, Y: p.Y * s}
+}
+
+// In reports whether p is in r.
+func (p Point) In(r Rectangle) bool {
+ return r.Min.X <= p.X && p.X < r.Max.X &&
+ r.Min.Y <= p.Y && p.Y < r.Max.Y
+}
+
+// Size returns r's width and height.
+func (r Rectangle) Size() Point {
+ return Point{X: r.Dx(), Y: r.Dy()}
+}
+
+// Dx returns r's width.
+func (r Rectangle) Dx() float32 {
+ return r.Max.X - r.Min.X
+}
+
+// Dy returns r's Height.
+func (r Rectangle) Dy() float32 {
+ return r.Max.Y - r.Min.Y
+}
+
+// Intersect returns the intersection of r and s.
+func (r Rectangle) Intersect(s Rectangle) Rectangle {
+ if r.Min.X < s.Min.X {
+ r.Min.X = s.Min.X
+ }
+ if r.Min.Y < s.Min.Y {
+ r.Min.Y = s.Min.Y
+ }
+ if r.Max.X > s.Max.X {
+ r.Max.X = s.Max.X
+ }
+ if r.Max.Y > s.Max.Y {
+ r.Max.Y = s.Max.Y
+ }
+ return r
+}
+
+// Union returns the union of r and s.
+func (r Rectangle) Union(s Rectangle) Rectangle {
+ if r.Min.X > s.Min.X {
+ r.Min.X = s.Min.X
+ }
+ if r.Min.Y > s.Min.Y {
+ r.Min.Y = s.Min.Y
+ }
+ if r.Max.X < s.Max.X {
+ r.Max.X = s.Max.X
+ }
+ if r.Max.Y < s.Max.Y {
+ r.Max.Y = s.Max.Y
+ }
+ return r
+}
+
+// Canon returns the canonical version of r, where Min is to
+// the upper left of Max.
+func (r Rectangle) Canon() Rectangle {
+ if r.Max.X < r.Min.X {
+ r.Min.X, r.Max.X = r.Max.X, r.Min.X
+ }
+ if r.Max.Y < r.Min.Y {
+ r.Min.Y, r.Max.Y = r.Max.Y, r.Min.Y
+ }
+ return r
+}
+
+// Empty reports whether r represents the empty area.
+func (r Rectangle) Empty() bool {
+ return r.Min.X >= r.Max.X || r.Min.Y >= r.Max.Y
+}
+
+// Add offsets r with the vector p.
+func (r Rectangle) Add(p Point) Rectangle {
+ return Rectangle{
+ Point{r.Min.X + p.X, r.Min.Y + p.Y},
+ Point{r.Max.X + p.X, r.Max.Y + p.Y},
+ }
+}
+
+// Sub offsets r with the vector -p.
+func (r Rectangle) Sub(p Point) Rectangle {
+ return Rectangle{
+ Point{r.Min.X - p.X, r.Min.Y - p.Y},
+ Point{r.Max.X - p.X, r.Max.Y - p.Y},
+ }
+}
diff --git a/gio/font/gofont/gofont.go b/gio/font/gofont/gofont.go
new file mode 100644
index 0000000..9dedcd5
--- /dev/null
+++ b/gio/font/gofont/gofont.go
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package gofont exports the Go fonts as a text.Collection.
+//
+// See https://blog.golang.org/go-fonts for a description of the
+// fonts, and the golang.org/x/image/font/gofont packages for the
+// font data.
+package gofont
+
+import (
+ "fmt"
+ "sync"
+
+ "golang.org/x/image/font/gofont/gobold"
+ "golang.org/x/image/font/gofont/gobolditalic"
+ "golang.org/x/image/font/gofont/goitalic"
+ "golang.org/x/image/font/gofont/gomedium"
+ "golang.org/x/image/font/gofont/gomediumitalic"
+ "golang.org/x/image/font/gofont/gomono"
+ "golang.org/x/image/font/gofont/gomonobold"
+ "golang.org/x/image/font/gofont/gomonobolditalic"
+ "golang.org/x/image/font/gofont/gomonoitalic"
+ "golang.org/x/image/font/gofont/goregular"
+ "golang.org/x/image/font/gofont/gosmallcaps"
+ "golang.org/x/image/font/gofont/gosmallcapsitalic"
+
+ "realy.lol/gio/font/opentype"
+ "realy.lol/gio/text"
+)
+
+var (
+ once sync.Once
+ collection []text.FontFace
+)
+
+func Collection() []text.FontFace {
+ once.Do(func() {
+ register(text.Font{}, goregular.TTF)
+ register(text.Font{Style: text.Italic}, goitalic.TTF)
+ register(text.Font{Weight: text.Bold}, gobold.TTF)
+ register(text.Font{Style: text.Italic, Weight: text.Bold},
+ gobolditalic.TTF)
+ register(text.Font{Weight: text.Medium}, gomedium.TTF)
+ register(text.Font{Weight: text.Medium, Style: text.Italic},
+ gomediumitalic.TTF)
+ register(text.Font{Variant: "Mono"}, gomono.TTF)
+ register(text.Font{Variant: "Mono", Weight: text.Bold}, gomonobold.TTF)
+ register(text.Font{Variant: "Mono", Weight: text.Bold,
+ Style: text.Italic}, gomonobolditalic.TTF)
+ register(text.Font{Variant: "Mono", Style: text.Italic},
+ gomonoitalic.TTF)
+ register(text.Font{Variant: "Smallcaps"}, gosmallcaps.TTF)
+ register(text.Font{Variant: "Smallcaps", Style: text.Italic},
+ gosmallcapsitalic.TTF)
+ // Ensure that any outside appends will not reuse the backing store.
+ n := len(collection)
+ collection = collection[:n:n]
+ })
+ return collection
+}
+
+func register(fnt text.Font, ttf []byte) {
+ face, err := opentype.Parse(ttf)
+ if err != nil {
+ panic(fmt.Errorf("failed to parse font: %v", err))
+ }
+ fnt.Typeface = "Go"
+ collection = append(collection, text.FontFace{Font: fnt, Face: face})
+}
diff --git a/gio/font/opentype/opentype.go b/gio/font/opentype/opentype.go
new file mode 100644
index 0000000..dd74e73
--- /dev/null
+++ b/gio/font/opentype/opentype.go
@@ -0,0 +1,421 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package opentype implements text layout and shaping for OpenType
+// files.
+package opentype
+
+import (
+ "bytes"
+ "io"
+ "unicode"
+ "unicode/utf8"
+
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/sfnt"
+ "golang.org/x/image/math/fixed"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/text"
+)
+
+// Font implements text.Face. Its methods are safe to use
+// concurrently.
+type Font struct {
+ font *sfnt.Font
+}
+
+// Collection is a collection of one or more fonts. When used as a text.Face,
+// each rune will be assigned a glyph from the first font in the collection
+// that supports it.
+type Collection struct {
+ fonts []*opentype
+}
+
+type opentype struct {
+ Font *sfnt.Font
+ Hinting font.Hinting
+}
+
+// a glyph represents a rune and its advance according to a Font.
+// TODO: remove this type and work on io.Readers directly.
+type glyph struct {
+ Rune rune
+ Advance fixed.Int26_6
+}
+
+// NewFont parses an SFNT font, such as TTF or OTF data, from a []byte
+// data source.
+func Parse(src []byte) (*Font, error) {
+ fnt, err := sfnt.Parse(src)
+ if err != nil {
+ return nil, err
+ }
+ return &Font{font: fnt}, nil
+}
+
+// ParseCollection parses an SFNT font collection, such as TTC or OTC data,
+// from a []byte data source.
+//
+// If passed data for a single font, a TTF or OTF instead of a TTC or OTC,
+// it will return a collection containing 1 font.
+func ParseCollection(src []byte) (*Collection, error) {
+ c, err := sfnt.ParseCollection(src)
+ if err != nil {
+ return nil, err
+ }
+ return newCollectionFrom(c)
+}
+
+// ParseCollectionReaderAt parses an SFNT collection, such as TTC or OTC data,
+// from an io.ReaderAt data source.
+//
+// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it
+// will return a collection containing 1 font.
+func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) {
+ c, err := sfnt.ParseCollectionReaderAt(src)
+ if err != nil {
+ return nil, err
+ }
+ return newCollectionFrom(c)
+}
+
+func newCollectionFrom(coll *sfnt.Collection) (*Collection, error) {
+ fonts := make([]*opentype, coll.NumFonts())
+ for i := range fonts {
+ fnt, err := coll.Font(i)
+ if err != nil {
+ return nil, err
+ }
+ fonts[i] = &opentype{
+ Font: fnt,
+ Hinting: font.HintingFull,
+ }
+ }
+ return &Collection{fonts: fonts}, nil
+}
+
+// NumFonts returns the number of fonts in the collection.
+func (c *Collection) NumFonts() int {
+ return len(c.fonts)
+}
+
+// Font returns the i'th font in the collection.
+func (c *Collection) Font(i int) (*Font, error) {
+ if i < 0 || len(c.fonts) <= i {
+ return nil, sfnt.ErrNotFound
+ }
+ return &Font{font: c.fonts[i].Font}, nil
+}
+
+func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int,
+ txt io.Reader) ([]text.Line, error) {
+ glyphs, err := readGlyphs(txt)
+ if err != nil {
+ return nil, err
+ }
+ fonts := []*opentype{{Font: f.font, Hinting: font.HintingFull}}
+ var buf sfnt.Buffer
+ return layoutText(&buf, ppem, maxWidth, fonts, glyphs)
+}
+
+func (f *Font) Shape(ppem fixed.Int26_6, str text.Layout) op.CallOp {
+ var buf sfnt.Buffer
+ return textPath(&buf, ppem,
+ []*opentype{{Font: f.font, Hinting: font.HintingFull}}, str)
+}
+
+func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics {
+ o := &opentype{Font: f.font, Hinting: font.HintingFull}
+ var buf sfnt.Buffer
+ return o.Metrics(&buf, ppem)
+}
+
+func (c *Collection) Layout(ppem fixed.Int26_6, maxWidth int,
+ txt io.Reader) ([]text.Line, error) {
+ glyphs, err := readGlyphs(txt)
+ if err != nil {
+ return nil, err
+ }
+ var buf sfnt.Buffer
+ return layoutText(&buf, ppem, maxWidth, c.fonts, glyphs)
+}
+
+func (c *Collection) Shape(ppem fixed.Int26_6, str text.Layout) op.CallOp {
+ var buf sfnt.Buffer
+ return textPath(&buf, ppem, c.fonts, str)
+}
+
+func fontForGlyph(buf *sfnt.Buffer, fonts []*opentype, r rune) *opentype {
+ if len(fonts) < 1 {
+ return nil
+ }
+ for _, f := range fonts {
+ if f.HasGlyph(buf, r) {
+ return f
+ }
+ }
+ return fonts[0] // Use replacement character from the first font if necessary
+}
+
+func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int,
+ fonts []*opentype, glyphs []glyph) ([]text.Line, error) {
+ var lines []text.Line
+ var nextLine text.Line
+ updateBounds := func(f *opentype) {
+ m := f.Metrics(sbuf, ppem)
+ if m.Ascent > nextLine.Ascent {
+ nextLine.Ascent = m.Ascent
+ }
+ // m.Height is equal to m.Ascent + m.Descent + linegap.
+ // Compute the descent including the linegap.
+ descent := m.Height - m.Ascent
+ if descent > nextLine.Descent {
+ nextLine.Descent = descent
+ }
+ b := f.Bounds(sbuf, ppem)
+ nextLine.Bounds = nextLine.Bounds.Union(b)
+ }
+ maxDotX := fixed.I(maxWidth)
+ type state struct {
+ r rune
+ f *opentype
+ adv fixed.Int26_6
+ x fixed.Int26_6
+ idx int
+ len int
+ valid bool
+ }
+ var prev, word state
+ endLine := func() {
+ if prev.f == nil && len(fonts) > 0 {
+ prev.f = fonts[0]
+ }
+ updateBounds(prev.f)
+ nextLine.Layout = toLayout(glyphs[:prev.idx:prev.idx])
+ nextLine.Width = prev.x + prev.adv
+ nextLine.Bounds.Max.X += prev.x
+ lines = append(lines, nextLine)
+ glyphs = glyphs[prev.idx:]
+ nextLine = text.Line{}
+ prev = state{}
+ word = state{}
+ }
+ for prev.idx < len(glyphs) {
+ g := &glyphs[prev.idx]
+ next := state{
+ r: g.Rune,
+ f: fontForGlyph(sbuf, fonts, g.Rune),
+ idx: prev.idx + 1,
+ len: prev.len + utf8.RuneLen(g.Rune),
+ x: prev.x + prev.adv,
+ }
+ if next.f != nil {
+ if next.f != prev.f {
+ updateBounds(next.f)
+ }
+ next.adv, next.valid = next.f.GlyphAdvance(sbuf, ppem, g.Rune)
+ }
+ if g.Rune == '\n' {
+ // The newline is zero width; use the previous
+ // character for line measurements.
+ prev.idx = next.idx
+ prev.len = next.len
+ endLine()
+ continue
+ }
+ var k fixed.Int26_6
+ if prev.valid && next.f != nil {
+ k = next.f.Kern(sbuf, ppem, prev.r, next.r)
+ }
+ // Break the line if we're out of space.
+ if prev.idx > 0 && next.x+next.adv+k > maxDotX {
+ // If the line contains no word breaks, break off the last rune.
+ if word.idx == 0 {
+ word = prev
+ }
+ next.x -= word.x + word.adv
+ next.idx -= word.idx
+ next.len -= word.len
+ prev = word
+ endLine()
+ } else if k != 0 {
+ glyphs[prev.idx-1].Advance += k
+ next.x += k
+ }
+ g.Advance = next.adv
+ if unicode.IsSpace(g.Rune) {
+ word = next
+ }
+ prev = next
+ }
+ endLine()
+ return lines, nil
+}
+
+// toLayout converts a slice of glyphs to a text.Layout.
+func toLayout(glyphs []glyph) text.Layout {
+ var buf bytes.Buffer
+ advs := make([]fixed.Int26_6, len(glyphs))
+ for i, g := range glyphs {
+ buf.WriteRune(g.Rune)
+ advs[i] = glyphs[i].Advance
+ }
+ return text.Layout{Text: buf.String(), Advances: advs}
+}
+
+func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype,
+ str text.Layout) op.CallOp {
+ var lastPos f32.Point
+ var builder clip.Path
+ ops := new(op.Ops)
+ m := op.Record(ops)
+ var x fixed.Int26_6
+ builder.Begin(ops)
+ rune := 0
+ for _, r := range str.Text {
+ if !unicode.IsSpace(r) {
+ f := fontForGlyph(buf, fonts, r)
+ if f == nil {
+ continue
+ }
+ segs, ok := f.LoadGlyph(buf, ppem, r)
+ if !ok {
+ continue
+ }
+ // Move to glyph position.
+ pos := f32.Point{
+ X: float32(x) / 64,
+ }
+ builder.Move(pos.Sub(lastPos))
+ lastPos = pos
+ var lastArg f32.Point
+ // Convert sfnt.Segments to relative segments.
+ for _, fseg := range segs {
+ nargs := 1
+ switch fseg.Op {
+ case sfnt.SegmentOpQuadTo:
+ nargs = 2
+ case sfnt.SegmentOpCubeTo:
+ nargs = 3
+ }
+ var args [3]f32.Point
+ for i := 0; i < nargs; i++ {
+ a := f32.Point{
+ X: float32(fseg.Args[i].X) / 64,
+ Y: float32(fseg.Args[i].Y) / 64,
+ }
+ args[i] = a.Sub(lastArg)
+ if i == nargs-1 {
+ lastArg = a
+ }
+ }
+ switch fseg.Op {
+ case sfnt.SegmentOpMoveTo:
+ builder.Move(args[0])
+ case sfnt.SegmentOpLineTo:
+ builder.Line(args[0])
+ case sfnt.SegmentOpQuadTo:
+ builder.Quad(args[0], args[1])
+ case sfnt.SegmentOpCubeTo:
+ builder.Cube(args[0], args[1], args[2])
+ default:
+ panic("unsupported segment op")
+ }
+ }
+ lastPos = lastPos.Add(lastArg)
+ }
+ x += str.Advances[rune]
+ rune++
+ }
+ clip.Outline{
+ Path: builder.End(),
+ }.Op().Add(ops)
+ return m.Stop()
+}
+
+func readGlyphs(r io.Reader) ([]glyph, error) {
+ var glyphs []glyph
+ buf := make([]byte, 0, 1024)
+ for {
+ n, err := r.Read(buf[len(buf):cap(buf)])
+ buf = buf[:len(buf)+n]
+ lim := len(buf)
+ // Read full runes if possible.
+ if err != io.EOF {
+ lim -= utf8.UTFMax - 1
+ }
+ i := 0
+ for i < lim {
+ c, s := utf8.DecodeRune(buf[i:])
+ i += s
+ glyphs = append(glyphs, glyph{Rune: c})
+ }
+ n = copy(buf, buf[i:])
+ buf = buf[:n]
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return nil, err
+ }
+ }
+ return glyphs, nil
+}
+
+func (f *opentype) HasGlyph(buf *sfnt.Buffer, r rune) bool {
+ g, err := f.Font.GlyphIndex(buf, r)
+ return g != 0 && err == nil
+}
+
+func (f *opentype) GlyphAdvance(buf *sfnt.Buffer, ppem fixed.Int26_6,
+ r rune) (advance fixed.Int26_6, ok bool) {
+ g, err := f.Font.GlyphIndex(buf, r)
+ if err != nil {
+ return 0, false
+ }
+ adv, err := f.Font.GlyphAdvance(buf, g, ppem, f.Hinting)
+ return adv, err == nil
+}
+
+func (f *opentype) Kern(buf *sfnt.Buffer, ppem fixed.Int26_6,
+ r0, r1 rune) fixed.Int26_6 {
+ g0, err := f.Font.GlyphIndex(buf, r0)
+ if err != nil {
+ return 0
+ }
+ g1, err := f.Font.GlyphIndex(buf, r1)
+ if err != nil {
+ return 0
+ }
+ adv, err := f.Font.Kern(buf, g0, g1, ppem, f.Hinting)
+ if err != nil {
+ return 0
+ }
+ return adv
+}
+
+func (f *opentype) Metrics(buf *sfnt.Buffer, ppem fixed.Int26_6) font.Metrics {
+ m, _ := f.Font.Metrics(buf, ppem, f.Hinting)
+ return m
+}
+
+func (f *opentype) Bounds(buf *sfnt.Buffer,
+ ppem fixed.Int26_6) fixed.Rectangle26_6 {
+ r, _ := f.Font.Bounds(buf, ppem, f.Hinting)
+ return r
+}
+
+func (f *opentype) LoadGlyph(buf *sfnt.Buffer, ppem fixed.Int26_6,
+ r rune) ([]sfnt.Segment, bool) {
+ g, err := f.Font.GlyphIndex(buf, r)
+ if err != nil {
+ return nil, false
+ }
+ segs, err := f.Font.LoadGlyph(buf, g, ppem, nil)
+ if err != nil {
+ return nil, false
+ }
+ return segs, true
+}
diff --git a/gio/font/opentype/opentype_test.go b/gio/font/opentype/opentype_test.go
new file mode 100644
index 0000000..d72708e
--- /dev/null
+++ b/gio/font/opentype/opentype_test.go
@@ -0,0 +1,222 @@
+package opentype
+
+import (
+ "bytes"
+ "compress/gzip"
+ "encoding/binary"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+ "testing"
+
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/gofont/goregular"
+ "golang.org/x/image/font/sfnt"
+ "golang.org/x/image/math/fixed"
+
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/op"
+ "realy.lol/gio/text"
+)
+
+func TestCollectionAsFace(t *testing.T) {
+ // Load two fonts with disjoint glyphs. Font 1 supports only '1', and font 2 supports only '2'.
+ // The fonts have different glyphs for the replacement character (".notdef").
+ font1, ttf1, err := decompressFontFile("testdata/only1.ttf.gz")
+ if err != nil {
+ t.Fatalf("failed to load test font 1: %v", err)
+ }
+ font2, ttf2, err := decompressFontFile("testdata/only2.ttf.gz")
+ if err != nil {
+ t.Fatalf("failed to load test font 2: %v", err)
+ }
+
+ otc := mergeFonts(ttf1, ttf2)
+ coll, err := ParseCollection(otc)
+ if err != nil {
+ t.Fatalf("failed to load merged test font: %v", err)
+ }
+
+ shapeValid1, err := shapeRune(font1, '1')
+ if err != nil {
+ t.Fatalf("failed shaping valid glyph with font 1: %v", err)
+ }
+ shapeInvalid1, err := shapeRune(font1, '3')
+ if err != nil {
+ t.Fatalf("failed shaping invalid glyph with font 1: %v", err)
+ }
+ shapeValid2, err := shapeRune(font2, '2')
+ if err != nil {
+ t.Fatalf("failed shaping valid glyph with font 2: %v", err)
+ }
+ shapeInvalid2, err := shapeRune(font2,
+ '3') // Same invalid glyph as before to test replacement glyph difference
+ if err != nil {
+ t.Fatalf("failed shaping invalid glyph with font 2: %v", err)
+ }
+ shapeCollValid1, err := shapeRune(coll, '1')
+ if err != nil {
+ t.Fatalf("failed shaping valid glyph for font 1 with font collection: %v",
+ err)
+ }
+ shapeCollValid2, err := shapeRune(coll, '2')
+ if err != nil {
+ t.Fatalf("failed shaping valid glyph for font 2 with font collection: %v",
+ err)
+ }
+ shapeCollInvalid, err := shapeRune(coll,
+ '4') // Different invalid glyph to confirm use of the replacement glyph
+ if err != nil {
+ t.Fatalf("failed shaping invalid glyph with font collection: %v", err)
+ }
+
+ // All shapes from the original fonts should be distinct because the glyphs are distinct, including the replacement
+ // glyphs.
+ distinctShapes := []op.CallOp{shapeValid1, shapeInvalid1, shapeValid2,
+ shapeInvalid2}
+ for i := 0; i < len(distinctShapes); i++ {
+ for j := i + 1; j < len(distinctShapes); j++ {
+ if areShapesEqual(distinctShapes[i], distinctShapes[j]) {
+ t.Errorf("font shapes %d and %d are not distinct", i, j)
+ }
+ }
+ }
+
+ // Font collections should render glyphs from the first supported font. Replacement glyphs should come from the
+ // first font in all cases.
+ if !areShapesEqual(shapeCollValid1, shapeValid1) {
+ t.Error("font collection did not render the valid glyph using font 1")
+ }
+ if !areShapesEqual(shapeCollValid2, shapeValid2) {
+ t.Error("font collection did not render the valid glyph using font 2")
+ }
+ if !areShapesEqual(shapeCollInvalid, shapeInvalid1) {
+ t.Error("font collection did not render the invalid glyph using the replacement from font 1")
+ }
+}
+
+func TestEmptyString(t *testing.T) {
+ face, err := Parse(goregular.TTF)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ppem := fixed.I(200)
+
+ lines, err := face.Layout(ppem, 2000, strings.NewReader(""))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(lines) == 0 {
+ t.Fatalf("Layout returned no lines for empty string; expected 1")
+ }
+ l := lines[0]
+ exp, err := face.font.Bounds(new(sfnt.Buffer), ppem, font.HintingFull)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got := l.Bounds; got != exp {
+ t.Errorf("got bounds %+v for empty string; expected %+v", got, exp)
+ }
+}
+
+func decompressFontFile(name string) (*Font, []byte, error) {
+ f, err := os.Open(name)
+ if err != nil {
+ return nil, nil, fmt.Errorf("could not open file for reading: %s: %v",
+ name, err)
+ }
+ defer f.Close()
+ gz, err := gzip.NewReader(f)
+ if err != nil {
+ return nil, nil, fmt.Errorf("font file contains invalid gzip data: %v",
+ err)
+ }
+ src, err := ioutil.ReadAll(gz)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to decompress font file: %v", err)
+ }
+ fnt, err := Parse(src)
+ if err != nil {
+ return nil, nil, fmt.Errorf("file did not contain a valid font: %v",
+ err)
+ }
+ return fnt, src, nil
+}
+
+// mergeFonts produces a trivial OpenType Collection (OTC) file for two source fonts.
+// It makes many assumptions and is not meant for general use.
+// For file format details, see https://docs.microsoft.com/en-us/typography/opentype/spec/otff
+// For a robust tool to generate these files, see https://pypi.org/project/afdko/
+func mergeFonts(ttf1, ttf2 []byte) []byte {
+ // Locations to place the two embedded fonts. All of the offsets to the fonts' internal tables will need to be
+ // shifted from the start of the file by the appropriate amount, and then everything will work as expected.
+ offset1 := uint32(20) // Length of OpenType collection headers
+ offset2 := offset1 + uint32(len(ttf1))
+
+ var buf bytes.Buffer
+ _, _ = buf.Write([]byte("ttcf\x00\x01\x00\x00\x00\x00\x00\x02"))
+ _ = binary.Write(&buf, binary.BigEndian, offset1)
+ _ = binary.Write(&buf, binary.BigEndian, offset2)
+
+ // Inline function to copy a font into the collection verbatim, except for adding an offset to all of the font's
+ // table positions.
+ copyOffsetTTF := func(ttf []byte, offset uint32) {
+ _, _ = buf.Write(ttf[:12])
+ numTables := binary.BigEndian.Uint16(ttf[4:6])
+ for i := uint16(0); i < numTables; i++ {
+ p := 12 + 16*i
+ _, _ = buf.Write(ttf[p : p+8])
+ tblLoc := binary.BigEndian.Uint32(ttf[p+8:p+12]) + offset
+ _ = binary.Write(&buf, binary.BigEndian, tblLoc)
+ _, _ = buf.Write(ttf[p+12 : p+16])
+ }
+ _, _ = buf.Write(ttf[12+16*numTables:])
+ }
+ copyOffsetTTF(ttf1, offset1)
+ copyOffsetTTF(ttf2, offset2)
+
+ return buf.Bytes()
+}
+
+// shapeRune uses a given Face to shape exactly one rune at a fixed size, then returns the resulting shape data.
+func shapeRune(f text.Face, r rune) (op.CallOp, error) {
+ ppem := fixed.I(200)
+ lines, err := f.Layout(ppem, 2000, strings.NewReader(string(r)))
+ if err != nil {
+ return op.CallOp{}, err
+ }
+ if len(lines) != 1 {
+ return op.CallOp{}, fmt.Errorf("unexpected rendering for \"U+%08X\": got %d lines (expected: 1)",
+ r, len(lines))
+ }
+ return f.Shape(ppem, lines[0].Layout), nil
+}
+
+// areShapesEqual returns true iff both given text shapes are produced with identical operations.
+func areShapesEqual(shape1, shape2 op.CallOp) bool {
+ var ops1, ops2 op.Ops
+ shape1.Add(&ops1)
+ shape2.Add(&ops2)
+ var r1, r2 ops.Reader
+ r1.Reset(&ops1)
+ r2.Reset(&ops2)
+ for {
+ encOp1, ok1 := r1.Decode()
+ encOp2, ok2 := r2.Decode()
+ if ok1 != ok2 {
+ return false
+ }
+ if !ok1 {
+ break
+ }
+ if len(encOp1.Refs) > 0 || len(encOp2.Refs) > 0 {
+ panic("unexpected ops with refs in font shaping test")
+ }
+ if !bytes.Equal(encOp1.Data, encOp2.Data) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/gio/font/opentype/testdata/only1.ttf.gz b/gio/font/opentype/testdata/only1.ttf.gz
new file mode 100644
index 0000000..544159d
Binary files /dev/null and b/gio/font/opentype/testdata/only1.ttf.gz differ
diff --git a/gio/font/opentype/testdata/only2.ttf.gz b/gio/font/opentype/testdata/only2.ttf.gz
new file mode 100644
index 0000000..87a3e68
Binary files /dev/null and b/gio/font/opentype/testdata/only2.ttf.gz differ
diff --git a/gio/gesture/gesture.go b/gio/gesture/gesture.go
new file mode 100644
index 0000000..bc0324a
--- /dev/null
+++ b/gio/gesture/gesture.go
@@ -0,0 +1,437 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package gesture implements common pointer gestures.
+
+Gestures accept low level pointer Events from an event
+Queue and detect higher level actions such as clicks
+and scrolling.
+*/
+package gesture
+
+import (
+ "image"
+ "math"
+ "runtime"
+ "time"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+
+ "realy.lol/gio/internal/fling"
+)
+
+// The duration is somewhat arbitrary.
+const doubleClickDuration = 300 * time.Millisecond
+
+// Click detects click gestures in the form
+// of ClickEvents.
+type Click struct {
+ // clickedAt is the timestamp at which
+ // the last click occurred.
+ clickedAt time.Duration
+ // clicks is incremented if successive clicks
+ // are performed within a fixed duration.
+ clicks int
+ // pressed tracks whether the pointer is pressed.
+ pressed bool
+ // entered tracks whether the pointer is inside the gesture.
+ entered bool
+ // pid is the pointer.ID.
+ pid pointer.ID
+ Button pointer.Buttons
+}
+
+type ClickState uint8
+
+// ClickEvent represent a click action, either a
+// TypePress for the beginning of a click or a
+// TypeClick for a completed click.
+type ClickEvent struct {
+ Type ClickType
+ Position f32.Point
+ Source pointer.Source
+ Modifiers key.Modifiers
+ // NumClicks records successive clicks occurring
+ // within a short duration of each other.
+ NumClicks int
+ Button pointer.Buttons
+}
+
+type ClickType uint8
+
+// Drag detects drag gestures in the form of pointer.Drag events.
+type Drag struct {
+ dragging bool
+ pid pointer.ID
+ start f32.Point
+ grab bool
+}
+
+// Scroll detects scroll gestures and reduces them to
+// scroll distances. Scroll recognizes mouse wheel
+// movements as well as drag and fling touch gestures.
+type Scroll struct {
+ dragging bool
+ axis Axis
+ estimator fling.Extrapolation
+ flinger fling.Animation
+ pid pointer.ID
+ grab bool
+ last int
+ // Leftover scroll.
+ scroll float32
+}
+
+type ScrollState uint8
+
+type Axis uint8
+
+const (
+ Horizontal Axis = iota
+ Vertical
+ Both
+)
+
+const (
+ // TypePress is reported for the first pointer
+ // press.
+ TypePress ClickType = iota
+ // TypeClick is reported when a click action
+ // is complete.
+ TypeClick
+ // TypeCancel is reported when the gesture is
+ // cancelled.
+ TypeCancel
+)
+
+const (
+ // StateIdle is the default scroll state.
+ StateIdle ScrollState = iota
+ // StateDrag is reported during drag gestures.
+ StateDragging
+ // StateFlinging is reported when a fling is
+ // in progress.
+ StateFlinging
+)
+
+var touchSlop = unit.Dp(3)
+
+// Add the handler to the operation list to receive click events.
+func (c *Click) Add(ops *op.Ops) {
+ op := pointer.InputOp{
+ Tag: c,
+ Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
+ }
+ op.Add(ops)
+}
+
+// Hovered returns whether a pointer is inside the area.
+func (c *Click) Hovered() bool {
+ return c.entered
+}
+
+// Pressed returns whether a pointer is pressing.
+func (c *Click) Pressed() bool {
+ return c.pressed
+}
+
+// Events returns the next click event, if any.
+func (c *Click) Events(q event.Queue) []ClickEvent {
+ var events []ClickEvent
+ for _, evt := range q.Events(c) {
+ // I.S(evt)
+ e, ok := evt.(pointer.Event)
+ if !ok {
+ continue
+ }
+ switch e.Type {
+ case pointer.Release:
+ if !c.pressed || c.pid != e.PointerID {
+ break
+ }
+ c.pressed = false
+ if c.entered {
+ if e.Time-c.clickedAt < doubleClickDuration ||
+ (c.clicks == 2 && e.Time-c.clickedAt < doubleClickDuration*2) {
+ c.clicks++
+ } else {
+ c.clicks = 1
+ }
+ c.clickedAt = e.Time
+ events = append(events, ClickEvent{
+ Type: TypeClick, Position: e.Position, Source: e.Source,
+ Modifiers: e.Modifiers,
+ Button: e.Buttons, NumClicks: c.clicks,
+ })
+ } else {
+ events = append(events, ClickEvent{Type: TypeCancel})
+ }
+ case pointer.Cancel:
+ wasPressed := c.pressed
+ c.pressed = false
+ c.entered = false
+ if wasPressed {
+ events = append(events, ClickEvent{Type: TypeCancel})
+ }
+ case pointer.Press:
+ if c.pressed {
+ break
+ }
+ // if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary {
+ // break
+ // }
+ if !c.entered {
+ c.pid = e.PointerID
+ }
+ if c.pid != e.PointerID {
+ break
+ }
+ c.pressed = true
+ events = append(events, ClickEvent{
+ Type: TypePress, Position: e.Position, Source: e.Source,
+ Modifiers: e.Modifiers, Button: e.Buttons,
+ })
+ case pointer.Leave:
+ if !c.pressed {
+ c.pid = e.PointerID
+ }
+ if c.pid == e.PointerID {
+ c.entered = false
+ }
+ case pointer.Enter:
+ if !c.pressed {
+ c.pid = e.PointerID
+ }
+ if c.pid == e.PointerID {
+ c.entered = true
+ }
+ }
+ }
+ return events
+}
+
+func (ClickEvent) ImplementsEvent() {}
+
+// Add the handler to the operation list to receive scroll events.
+func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) {
+ oph := pointer.InputOp{
+ Tag: s,
+ Grab: s.grab,
+ Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
+ ScrollBounds: bounds,
+ }
+ oph.Add(ops)
+ if s.flinger.Active() {
+ op.InvalidateOp{}.Add(ops)
+ }
+}
+
+// Stop any remaining fling movement.
+func (s *Scroll) Stop() {
+ s.flinger = fling.Animation{}
+}
+
+// Scroll detects the scrolling distance from the available events and
+// ongoing fling gestures.
+func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time,
+ axis Axis) int {
+ if s.axis != axis {
+ s.axis = axis
+ return 0
+ }
+ total := 0
+ for _, evt := range q.Events(s) {
+ e, ok := evt.(pointer.Event)
+ if !ok {
+ continue
+ }
+ switch e.Type {
+ case pointer.Press:
+ if s.dragging {
+ break
+ }
+ // Only scroll on touch drags, or on Android where mice
+ // drags also scroll by convention.
+ if e.Source != pointer.Touch && runtime.GOOS != "android" {
+ break
+ }
+ s.Stop()
+ s.estimator = fling.Extrapolation{}
+ v := s.val(e.Position)
+ s.last = int(math.Round(float64(v)))
+ s.estimator.Sample(e.Time, v)
+ s.dragging = true
+ s.pid = e.PointerID
+ case pointer.Release:
+ if s.pid != e.PointerID {
+ break
+ }
+ fling := s.estimator.Estimate()
+ if slop, d := float32(cfg.Px(touchSlop)), fling.Distance; d < -slop || d > slop {
+ s.flinger.Start(cfg, t, fling.Velocity)
+ }
+ fallthrough
+ case pointer.Cancel:
+ s.dragging = false
+ s.grab = false
+ case pointer.Scroll:
+ switch s.axis {
+ case Horizontal:
+ s.scroll += e.Scroll.X
+ case Vertical:
+ s.scroll += e.Scroll.Y
+ }
+ iscroll := int(s.scroll)
+ s.scroll -= float32(iscroll)
+ total += iscroll
+ case pointer.Drag:
+ if !s.dragging || s.pid != e.PointerID {
+ continue
+ }
+ val := s.val(e.Position)
+ s.estimator.Sample(e.Time, val)
+ v := int(math.Round(float64(val)))
+ dist := s.last - v
+ if e.Priority < pointer.Grabbed {
+ slop := cfg.Px(touchSlop)
+ if dist := dist; dist >= slop || -slop >= dist {
+ s.grab = true
+ }
+ } else {
+ s.last = v
+ total += dist
+ }
+ }
+ }
+ total += s.flinger.Tick(t)
+ return total
+}
+
+func (s *Scroll) val(p f32.Point) float32 {
+ if s.axis == Horizontal {
+ return p.X
+ } else {
+ return p.Y
+ }
+}
+
+// State reports the scroll state.
+func (s *Scroll) State() ScrollState {
+ switch {
+ case s.flinger.Active():
+ return StateFlinging
+ case s.dragging:
+ return StateDragging
+ default:
+ return StateIdle
+ }
+}
+
+// Add the handler to the operation list to receive drag events.
+func (d *Drag) Add(ops *op.Ops) {
+ op := pointer.InputOp{
+ Tag: d,
+ Grab: d.grab,
+ Types: pointer.Press | pointer.Drag | pointer.Release,
+ }
+ op.Add(ops)
+}
+
+// Events returns the next drag events, if any.
+func (d *Drag) Events(cfg unit.Metric, q event.Queue,
+ axis Axis) []pointer.Event {
+ var events []pointer.Event
+ for _, e := range q.Events(d) {
+ e, ok := e.(pointer.Event)
+ if !ok {
+ continue
+ }
+
+ switch e.Type {
+ case pointer.Press:
+ if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) {
+ continue
+ }
+ if d.dragging {
+ continue
+ }
+ d.dragging = true
+ d.pid = e.PointerID
+ d.start = e.Position
+ case pointer.Drag:
+ if !d.dragging || e.PointerID != d.pid {
+ continue
+ }
+ switch axis {
+ case Horizontal:
+ e.Position.Y = d.start.Y
+ case Vertical:
+ e.Position.X = d.start.X
+ case Both:
+ // Do nothing
+ }
+ if e.Priority < pointer.Grabbed {
+ diff := e.Position.Sub(d.start)
+ slop := cfg.Px(touchSlop)
+ if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
+ d.grab = true
+ }
+ }
+ case pointer.Release, pointer.Cancel:
+ if !d.dragging || e.PointerID != d.pid {
+ continue
+ }
+ d.dragging = false
+ d.grab = false
+ }
+
+ events = append(events, e)
+ }
+
+ return events
+}
+
+// Dragging reports whether it's currently in use.
+func (d *Drag) Dragging() bool { return d.dragging }
+
+func (a Axis) String() string {
+ switch a {
+ case Horizontal:
+ return "Horizontal"
+ case Vertical:
+ return "Vertical"
+ default:
+ panic("invalid Axis")
+ }
+}
+
+func (ct ClickType) String() string {
+ switch ct {
+ case TypePress:
+ return "TypePress"
+ case TypeClick:
+ return "TypeClick"
+ case TypeCancel:
+ return "TypeCancel"
+ default:
+ panic("invalid ClickType")
+ }
+}
+
+func (s ScrollState) String() string {
+ switch s {
+ case StateIdle:
+ return "StateIdle"
+ case StateDragging:
+ return "StateDragging"
+ case StateFlinging:
+ return "StateFlinging"
+ default:
+ panic("unreachable")
+ }
+}
diff --git a/gio/gesture/gesture_test.go b/gio/gesture/gesture_test.go
new file mode 100644
index 0000000..d2f69ea
--- /dev/null
+++ b/gio/gesture/gesture_test.go
@@ -0,0 +1,88 @@
+package gesture
+
+import (
+ "testing"
+ "time"
+
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/router"
+ "realy.lol/gio/op"
+)
+
+func TestMouseClicks(t *testing.T) {
+ for _, tc := range []struct {
+ label string
+ events []event.Event
+ clicks []int // number of combined clicks per click (single, double...)
+ }{
+ {
+ label: "single click",
+ events: mouseClickEvents(200 * time.Millisecond),
+ clicks: []int{1},
+ },
+ {
+ label: "double click",
+ events: mouseClickEvents(
+ 100*time.Millisecond,
+ 100*time.Millisecond+doubleClickDuration-1),
+ clicks: []int{1, 2},
+ },
+ {
+ label: "two single clicks",
+ events: mouseClickEvents(
+ 100*time.Millisecond,
+ 100*time.Millisecond+doubleClickDuration+1),
+ clicks: []int{1, 1},
+ },
+ } {
+ t.Run(tc.label, func(t *testing.T) {
+ var click Click
+ var ops op.Ops
+ click.Add(&ops)
+
+ var r router.Router
+ r.Frame(&ops)
+ r.Queue(tc.events...)
+
+ events := click.Events(&r)
+ clicks := filterMouseClicks(events)
+ if got, want := len(clicks), len(tc.clicks); got != want {
+ t.Fatalf("got %d mouse clicks, expected %d", got, want)
+ }
+
+ for i, click := range clicks {
+ if got, want := click.NumClicks, tc.clicks[i]; got != want {
+ t.Errorf("got %d combined mouse clicks, expected %d", got,
+ want)
+ }
+ }
+ })
+ }
+}
+
+func mouseClickEvents(times ...time.Duration) []event.Event {
+ press := pointer.Event{
+ Type: pointer.Press,
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ }
+ events := make([]event.Event, 0, 2*len(times))
+ for _, t := range times {
+ release := press
+ release.Type = pointer.Release
+ release.Time = t
+ events = append(events, press, release)
+ }
+ return events
+}
+
+func filterMouseClicks(events []ClickEvent) []ClickEvent {
+ var clicks []ClickEvent
+ for _, ev := range events {
+ if ev.Type == TypeClick {
+ clicks = append(clicks, ev)
+ }
+ }
+ return clicks
+}
diff --git a/gio/gesture/log.go b/gio/gesture/log.go
new file mode 100644
index 0000000..9e79319
--- /dev/null
+++ b/gio/gesture/log.go
@@ -0,0 +1,9 @@
+package gesture
+
+// import (
+// "github.com/p9c/log"
+//
+// "github.com/p9c/gel/version"
+// )
+//
+// var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase))
diff --git a/gio/giold/.builds/apple.yml b/gio/giold/.builds/apple.yml
new file mode 100644
index 0000000..fde6bcb
--- /dev/null
+++ b/gio/giold/.builds/apple.yml
@@ -0,0 +1,72 @@
+# SPDX-License-Identifier: Unlicense OR MIT
+image: debian/testing
+packages:
+ - clang
+ - cmake
+ - curl
+ - autoconf
+ - libxml2-dev
+ - libssl-dev
+ - libz-dev
+ - llvm-dev # for cctools
+ - uuid-dev ## for cctools
+ - libplist-utils # for gogio
+ - golang
+sources:
+ - https://git.sr.ht/~eliasnaur/applesdks
+ - https://git.sr.ht/~eliasnaur/gio
+ - https://git.sr.ht/~eliasnaur/giouiorg
+ - https://github.com/tpoechtrager/cctools-port.git
+ - https://github.com/tpoechtrager/apple-libtapi.git
+ - https://github.com/mackyle/xar.git
+environment:
+ APPLE_TOOLCHAIN_ROOT: /home/build/appletools
+ PATH: /home/build/go/bin:/usr/bin
+tasks:
+ - prepare_toolchain: |
+ mkdir -p $APPLE_TOOLCHAIN_ROOT
+ cd $APPLE_TOOLCHAIN_ROOT
+ tar xJf /home/build/applesdks/applesdks.tar.xz
+ mkdir bin tools
+ cd bin
+ ln -s ../toolchain/bin/x86_64-apple-darwin19-ld ld
+ ln -s ../toolchain/bin/x86_64-apple-darwin19-ar ar
+ ln -s /home/build/cctools-port/cctools/misc/lipo lipo
+ ln -s ../tools/appletoolchain xcrun
+ ln -s /usr/bin/plistutil plutil
+ cd ../tools
+ ln -s appletoolchain clang-ios
+ ln -s appletoolchain clang-macos
+ - install_appletoolchain: |
+ cd giouiorg
+ go build -o $APPLE_TOOLCHAIN_ROOT/tools ./cmd/appletoolchain
+ - build_xar: |
+ cd xar/xar
+ ac_cv_lib_crypto_OpenSSL_add_all_ciphers=yes CC=clang ./autogen.sh --prefix=/usr
+ make
+ sudo make install
+ - build_libtapi: |
+ cd apple-libtapi
+ INSTALLPREFIX=$APPLE_TOOLCHAIN_ROOT/libtapi ./build.sh
+ ./install.sh
+ - build_cctools: |
+ cd cctools-port/cctools
+ ./configure --prefix $APPLE_TOOLCHAIN_ROOT/toolchain --with-libtapi=$APPLE_TOOLCHAIN_ROOT/libtapi --target=x86_64-apple-darwin19
+ make install
+ - test_macos: |
+ cd gio
+ export PATH=/home/build/appletools/bin:$PATH
+ CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-macos GOOS=darwin CGO_ENABLED=1 go build ./...
+ - test_ios: |
+ cd gio
+ CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-ios GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -tags ios ./...
+ - install_gogio: |
+ cd gio/cmd
+ go install ./gogio
+ - test_ios_gogio: |
+ mkdir tmp
+ cd tmp
+ go mod init example.com
+ go get -d github.com/p9c/p9/pkg/gel/gio/example/kitchen
+ export PATH=/home/build/appletools/bin:$PATH
+ gogio -target ios -o app.app github.com/p9c/p9/pkg/gel/gio/example/kitchen
diff --git a/gio/giold/.builds/freebsd.yml b/gio/giold/.builds/freebsd.yml
new file mode 100644
index 0000000..1816b70
--- /dev/null
+++ b/gio/giold/.builds/freebsd.yml
@@ -0,0 +1,25 @@
+# SPDX-License-Identifier: Unlicense OR MIT
+image: freebsd/11.x
+packages:
+ - libX11
+ - libxkbcommon
+ - libXcursor
+ - libXfixes
+ - wayland
+ - mesa-libs
+ - xorg-vfbserver
+sources:
+ - https://git.sr.ht/~eliasnaur/gio
+environment:
+ PATH: /home/build/sdk/go/bin:/bin:/usr/local/bin:/usr/bin
+tasks:
+ - install_go1_14: |
+ mkdir -p /home/build/sdk
+ curl https://dl.google.com/go/go1.14.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf -
+ - test_gio: |
+ export EGL_PLATFORM=surfaceless # for headless tests
+ cd gio
+ go test ./...
+ - test_cmd: |
+ cd gio/cmd
+ go test ./...
diff --git a/gio/giold/.builds/linux.yml b/gio/giold/.builds/linux.yml
new file mode 100644
index 0000000..55cb5a5
--- /dev/null
+++ b/gio/giold/.builds/linux.yml
@@ -0,0 +1,101 @@
+# SPDX-License-Identifier: Unlicense OR MIT
+image: debian/testing
+packages:
+ - curl
+ - pkg-config
+ - libwayland-dev
+ - libx11-dev
+ - libx11-xcb-dev
+ - libxkbcommon-dev
+ - libxkbcommon-x11-dev
+ - libgles2-mesa-dev
+ - libegl1-mesa-dev
+ - libffi-dev
+ - libxcursor-dev
+ - libxrandr-dev
+ - libxinerama-dev
+ - libxi-dev
+ - libxxf86vm-dev
+ - wine
+ - xvfb
+ - xdotool
+ - scrot
+ - sway
+ - grim
+ - wine
+ - unzip
+sources:
+ - https://git.sr.ht/~eliasnaur/gio
+environment:
+ GOFLAGS: -mod=readonly
+ PATH: /home/build/sdk/go/bin:/usr/bin:/home/build/go/bin:/home/build/android/tools/bin
+ ANDROID_SDK_ROOT: /home/build/android
+ android_sdk_tools_zip: sdk-tools-linux-3859397.zip
+ android_ndk_zip: android-ndk-r20-linux-x86_64.zip
+ github_mirror: git@github.com:gioui/gio
+secrets:
+ - 75d8a1eb-5fc5-4074-8a36-db6015d6ed5a
+tasks:
+ - install_go1_14: |
+ mkdir -p /home/build/sdk
+ curl -s https://dl.google.com/go/go1.14.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
+ - test_gio: |
+ cd gio
+ export EGL_PLATFORM=surfaceless # for headless tests
+ go test -race ./...
+ GOOS=windows go test -exec=wine ./...
+ GOOS=js GOARCH=wasm go build -o /dev/null ./...
+ - install_chrome: |
+ curl -s https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
+ sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
+ sudo apt-get -qq update
+ sudo apt-get -qq install -y google-chrome-stable
+ - test_cmd: |
+ cd gio/cmd
+ go test ./...
+ go test -race ./...
+ cd gogio # since we need -modfile to point at the parent directory
+ GOFLAGS=-modfile=../go.local.mod go test
+ - install_jdk8: |
+ curl -so jdk.deb "https://cdn.azul.com/zulu/bin/zulu8.42.0.21-ca-jdk8.0.232-linux_amd64.deb"
+ sudo apt-get -qq install -y -f ./jdk.deb
+ - install_android: |
+ mkdir android
+ cd android
+ curl -so sdk-tools.zip https://dl.google.com/android/repository/$android_sdk_tools_zip
+ unzip -q sdk-tools.zip
+ rm sdk-tools.zip
+ curl -so ndk.zip https://dl.google.com/android/repository/$android_ndk_zip
+ unzip -q ndk.zip
+ rm ndk.zip
+ mv android-ndk-* ndk-bundle
+ yes|sdkmanager --licenses
+ sdkmanager "platforms;android-29" "build-tools;29.0.2"
+ - test_android: |
+ cd gio
+ CC=$ANDROID_SDK_ROOT/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build ./...
+ - install_gogio: |
+ cd gio/cmd
+ go install ./gogio
+ - test_android_gogio: |
+ mkdir tmp
+ cd tmp
+ go mod init example.com
+ go get -d github.com/p9c/p9/pkg/gel/gio/example/kitchen
+ gogio -target android github.com/p9c/p9/pkg/gel/gio/example/kitchen
+ - check_gofmt: |
+ cd gio
+ test -z "$(gofmt -s -l .)"
+ - check_sign_off: |
+ set +x -e
+ cd gio
+ for hash in $(git log -n 20 --format="%H"); do
+ message=$(git log -1 --format=%B $hash)
+ if [[ ! "$message" =~ "Signed-off-by: " ]]; then
+ echo "Missing 'Signed-off-by' in commit $hash"
+ exit 1
+ fi
+ done
+ - mirror: |
+ # mirror to github
+ ssh-keyscan github.com > "$HOME"/.ssh/known_hosts && cd gio && git push --mirror "$github_mirror" || echo "failed mirroring"
diff --git a/gio/giold/.builds/openbsd.yml b/gio/giold/.builds/openbsd.yml
new file mode 100644
index 0000000..757e80f
--- /dev/null
+++ b/gio/giold/.builds/openbsd.yml
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: Unlicense OR MIT
+image: openbsd/latest
+packages:
+ - libxkbcommon
+ - go
+sources:
+ - https://git.sr.ht/~eliasnaur/gio
+environment:
+ PATH: /home/build/sdk/go/bin:/bin:/usr/local/bin:/usr/bin
+tasks:
+ - install_go1_14: |
+ mkdir -p /home/build/sdk
+ curl https://dl.google.com/go/go1.14.src.tar.gz | tar -C /home/build/sdk -xzf -
+ cd /home/build/sdk/go/src
+ ./make.bash
+ - test_gio: |
+ cd gio
+ go test ./...
+ - test_cmd: |
+ cd gio/cmd
+ go test ./...
diff --git a/gio/giold/LICENSE b/gio/giold/LICENSE
new file mode 100644
index 0000000..81f4733
--- /dev/null
+++ b/gio/giold/LICENSE
@@ -0,0 +1,63 @@
+This project is provided under the terms of the UNLICENSE or
+the MIT license denoted by the following SPDX identifier:
+
+SPDX-License-Identifier: Unlicense OR MIT
+
+You may use the project under the terms of either license.
+
+Both licenses are reproduced below.
+
+----
+The MIT License (MIT)
+
+Copyright (c) 2019 The Gio authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+---
+
+
+
+---
+The UNLICENSE
+
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to
+---
diff --git a/gio/giold/README.md b/gio/giold/README.md
new file mode 100644
index 0000000..634cb42
--- /dev/null
+++ b/gio/giold/README.md
@@ -0,0 +1,26 @@
+# Gio - https://github.com/p9c/p9/pkg/gel/gio
+
+Immediate mode GUI programs in Go for Android, iOS, macOS, Linux,
+FreeBSD, OpenBSD, Windows, and WebAssembly (experimental).
+
+# Installation, examples, documentation
+
+Go to [github.com/p9c/p9/pkg/gel/gio](https://github.com/p9c/p9/pkg/gel/gio).
+
+[![builds.sr.ht status](https://builds.sr.ht/~eliasnaur/gio.svg)](https://builds.sr.ht/~eliasnaur/gio)
+
+## Issues
+
+File bugs and TODOs through the [issue tracker](https://todo.sr.ht/~eliasnaur/gio) or send an email
+to [~eliasnaur/gio@todo.sr.ht](mailto:~eliasnaur/gio@todo.sr.ht). For general discussion, use the
+mailing list: [~eliasnaur/gio@lists.sr.ht](mailto:~eliasnaur/gio@lists.sr.ht).
+
+## Contributing
+
+Post discussion to the [mailing list](https://lists.sr.ht/~eliasnaur/gio) and patches to
+[gio-patches](https://lists.sr.ht/~eliasnaur/gio-patches). No Sourcehut
+account is required and you can post without being subscribed.
+
+See the [contribution guide](https://github.com/p9c/p9/pkg/gel/gio/doc/contribute) for more details.
+
+An [official GitHub mirror](https://github.com/gioui/gio) is available.
diff --git a/gio/giold/app/app.go b/gio/giold/app/app.go
new file mode 100644
index 0000000..e9fbdf7
--- /dev/null
+++ b/gio/giold/app/app.go
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package app
+
+import (
+ "os"
+ "strings"
+
+ "realy.lol/gio/app/internal/wm"
+)
+
+// extraArgs contains extra arguments to append to
+// os.Args. The arguments are separated with |.
+// Useful for running programs on mobiles where the
+// command line is not available.
+// Set with the go linker flag -X.
+var extraArgs string
+
+func init() {
+ if extraArgs != "" {
+ args := strings.Split(extraArgs, "|")
+ os.Args = append(os.Args, args...)
+ }
+}
+
+// DataDir returns a path to use for application-specific
+// configuration data.
+// On desktop systems, DataDir use os.UserConfigDir.
+// On iOS NSDocumentDirectory is queried.
+// For Android Context.getFilesDir is used.
+//
+// BUG: DataDir blocks on Android until init functions
+// have completed.
+func DataDir() (string, error) {
+ return dataDir()
+}
+
+// Main must be called last from the program main function.
+// On most platforms Main blocks forever, for Android and
+// iOS it returns immediately to give control of the main
+// thread back to the system.
+//
+// Calling Main is necessary because some operating systems
+// require control of the main thread of the program for
+// running windows.
+func Main() {
+ wm.Main()
+}
diff --git a/gio/giold/app/app_android.go b/gio/giold/app/app_android.go
new file mode 100644
index 0000000..060544f
--- /dev/null
+++ b/gio/giold/app/app_android.go
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package app
+
+import (
+ "realy.lol/gio/app/internal/wm"
+)
+
+type ViewEvent = wm.ViewEvent
+
+// JavaVM returns the global JNI JavaVM.
+func JavaVM() uintptr {
+ return wm.JavaVM()
+}
+
+// AppContext returns the global Application context as a JNI
+// jobject.
+func AppContext() uintptr {
+ return wm.AppContext()
+}
diff --git a/gio/giold/app/datadir.go b/gio/giold/app/datadir.go
new file mode 100644
index 0000000..31e5453
--- /dev/null
+++ b/gio/giold/app/datadir.go
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build !android
+
+package app
+
+import "os"
+
+func dataDir() (string, error) {
+ return os.UserConfigDir()
+}
diff --git a/gio/giold/app/datadir_android.go b/gio/giold/app/datadir_android.go
new file mode 100644
index 0000000..cbbc6c4
--- /dev/null
+++ b/gio/giold/app/datadir_android.go
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build android
+// +build android
+
+package app
+
+import "C"
+
+import (
+ "os"
+ "path/filepath"
+ "sync"
+
+ "realy.lol/gio/app/internal/wm"
+)
+
+var (
+ dataDirOnce sync.Once
+ dataPath string
+)
+
+func dataDir() (string, error) {
+ dataDirOnce.Do(func() {
+ dataPath = wm.GetDataDir()
+ // Set XDG_CACHE_HOME to make os.UserCacheDir work.
+ if _, exists := os.LookupEnv("XDG_CACHE_HOME"); !exists {
+ cachePath := filepath.Join(dataPath, "cache")
+ os.Setenv("XDG_CACHE_HOME", cachePath)
+ }
+ // Set XDG_CONFIG_HOME to make os.UserConfigDir work.
+ if _, exists := os.LookupEnv("XDG_CONFIG_HOME"); !exists {
+ cfgPath := filepath.Join(dataPath, "config")
+ os.Setenv("XDG_CONFIG_HOME", cfgPath)
+ }
+ // Set HOME to make os.UserHomeDir work.
+ if _, exists := os.LookupEnv("HOME"); !exists {
+ os.Setenv("HOME", dataPath)
+ }
+ })
+ return dataPath, nil
+}
diff --git a/gio/giold/app/doc.go b/gio/giold/app/doc.go
new file mode 100644
index 0000000..fb0826a
--- /dev/null
+++ b/gio/giold/app/doc.go
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package app provides a platform-independent interface to operating system
+functionality for running graphical user interfaces.
+
+See https://realy.lol/gio for instructions to set up and run Gio programs.
+
+Windows
+
+Create a new Window by calling NewWindow. On mobile platforms or when Gio
+is embedded in another project, NewWindow merely connects with a previously
+created window.
+
+A Window is run by receiving events from its Events channel. The most
+important event is FrameEvent that prompts an update of the window
+contents and state.
+
+For example:
+
+ import "realy.lol/gio/unit"
+
+ w := app.NewWindow()
+ for e := range w.Events() {
+ if e, ok := e.(system.FrameEvent); ok {
+ ops.Reset()
+ // Add operations to ops.
+ ...
+ // Completely replace the window contents and state.
+ e.Frame(ops)
+ }
+ }
+
+A program must keep receiving events from the event channel until
+DestroyEvent is received.
+
+Main
+
+The Main function must be called from a program's main function, to hand over
+control of the main thread to operating systems that need it.
+
+Because Main is also blocking on some platforms, the event loop of a Window must run in a goroutine.
+
+For example, to display a blank but otherwise functional window:
+
+ package main
+
+ import "realy.lol/gio/app"
+
+ func main() {
+ go func() {
+ w := app.NewWindow()
+ for range w.Events() {
+ }
+ }()
+ app.Main()
+ }
+
+
+Event queue
+
+A FrameEvent's Queue method returns an event.Queue implementation that distributes
+incoming events to the event handlers declared in the last frame.
+See the realy.lol/gio/io/event package for more information about event handlers.
+
+Permissions
+
+The packages under realy.lol/gio/app/permission should be imported
+by a Gio program or by one of its dependencies to indicate that specific
+operating-system permissions are required. Please see documentation for
+package realy.lol/gio/app/permission for more information.
+*/
+package app
diff --git a/gio/giold/app/internal/log/log.go b/gio/giold/app/internal/log/log.go
new file mode 100644
index 0000000..731ae49
--- /dev/null
+++ b/gio/giold/app/internal/log/log.go
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package points standard output, standard error and the standard
+// library package log to the platform logger.
+package log
+
+var appID = "gio"
diff --git a/gio/giold/app/internal/log/log_android.go b/gio/giold/app/internal/log/log_android.go
new file mode 100644
index 0000000..1245598
--- /dev/null
+++ b/gio/giold/app/internal/log/log_android.go
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package log
+
+/*
+#cgo LDFLAGS: -llog
+
+#include
+#include
+*/
+import "C"
+
+import (
+ "bufio"
+ "log"
+ "os"
+ "runtime"
+ "syscall"
+ "unsafe"
+)
+
+// 1024 is the truncation limit from android/log.h, plus a \n.
+const logLineLimit = 1024
+
+var logTag = C.CString(appID)
+
+func init() {
+ // Android's logcat already includes timestamps.
+ log.SetFlags(log.Flags() &^ log.LstdFlags)
+ log.SetOutput(new(androidLogWriter))
+
+ // Redirect stdout and stderr to the Android logger.
+ logFd(os.Stdout.Fd())
+ logFd(os.Stderr.Fd())
+}
+
+type androidLogWriter struct {
+ // buf has room for the maximum log line, plus a terminating '\0'.
+ buf [logLineLimit + 1]byte
+}
+
+func (w *androidLogWriter) Write(data []byte) (int, error) {
+ n := 0
+ for len(data) > 0 {
+ msg := data
+ // Truncate the buffer, leaving space for the '\0'.
+ if max := len(w.buf) - 1; len(msg) > max {
+ msg = msg[:max]
+ }
+ buf := w.buf[:len(msg)+1]
+ copy(buf, msg)
+ // Terminating '\0'.
+ buf[len(msg)] = 0
+ C.__android_log_write(C.ANDROID_LOG_INFO, logTag, (*C.char)(unsafe.Pointer(&buf[0])))
+ n += len(msg)
+ data = data[len(msg):]
+ }
+ return n, nil
+}
+
+func logFd(fd uintptr) {
+ r, w, err := os.Pipe()
+ if err != nil {
+ panic(err)
+ }
+ if err := syscall.Dup3(int(w.Fd()), int(fd), syscall.O_CLOEXEC); err != nil {
+ panic(err)
+ }
+ go func() {
+ lineBuf := bufio.NewReaderSize(r, logLineLimit)
+ // The buffer to pass to C, including the terminating '\0'.
+ buf := make([]byte, lineBuf.Size()+1)
+ cbuf := (*C.char)(unsafe.Pointer(&buf[0]))
+ for {
+ line, _, err := lineBuf.ReadLine()
+ if err != nil {
+ break
+ }
+ copy(buf, line)
+ buf[len(line)] = 0
+ C.__android_log_write(C.ANDROID_LOG_INFO, logTag, cbuf)
+ }
+ // The garbage collector doesn't know that w's fd was dup'ed.
+ // Avoid finalizing w, and thereby avoid its finalizer closing its fd.
+ runtime.KeepAlive(w)
+ }()
+}
diff --git a/gio/giold/app/internal/log/log_ios.go b/gio/giold/app/internal/log/log_ios.go
new file mode 100644
index 0000000..6a041db
--- /dev/null
+++ b/gio/giold/app/internal/log/log_ios.go
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build darwin && ios
+// +build darwin,ios
+
+package log
+
+/*
+#cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c
+
+__attribute__ ((visibility ("hidden"))) void nslog(char *str);
+*/
+import "C"
+
+import (
+ "bufio"
+ "io"
+ "log"
+ "unsafe"
+
+ _ "realy.lol/gio/internal/cocoainit"
+)
+
+func init() {
+ // macOS Console already includes timestamps.
+ log.SetFlags(log.Flags() &^ log.LstdFlags)
+ log.SetOutput(newNSLogWriter())
+}
+
+func newNSLogWriter() io.Writer {
+ r, w := io.Pipe()
+ go func() {
+ // 1024 is an arbitrary truncation limit, taken from Android's
+ // log buffer size.
+ lineBuf := bufio.NewReaderSize(r, 1024)
+ // The buffer to pass to C, including the terminating '\0'.
+ buf := make([]byte, lineBuf.Size()+1)
+ cbuf := (*C.char)(unsafe.Pointer(&buf[0]))
+ for {
+ line, _, err := lineBuf.ReadLine()
+ if err != nil {
+ break
+ }
+ copy(buf, line)
+ buf[len(line)] = 0
+ C.nslog(cbuf)
+ }
+ }()
+ return w
+}
diff --git a/gio/giold/app/internal/log/log_ios.m b/gio/giold/app/internal/log/log_ios.m
new file mode 100644
index 0000000..201bc36
--- /dev/null
+++ b/gio/giold/app/internal/log/log_ios.m
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build darwin,ios
+
+@import Foundation;
+
+#include "_cgo_export.h"
+
+void nslog(char *str) {
+ NSLog(@"%@", @(str));
+}
diff --git a/gio/giold/app/internal/log/log_windows.go b/gio/giold/app/internal/log/log_windows.go
new file mode 100644
index 0000000..13c5fe4
--- /dev/null
+++ b/gio/giold/app/internal/log/log_windows.go
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package log
+
+import (
+ "log"
+ "syscall"
+ "unsafe"
+)
+
+type logger struct{}
+
+var (
+ kernel32 = syscall.NewLazyDLL("kernel32")
+ outputDebugStringW = kernel32.NewProc("OutputDebugStringW")
+ debugView *logger
+)
+
+func init() {
+ // Windows DebugView already includes timestamps.
+ if syscall.Stderr == 0 {
+ log.SetFlags(log.Flags() &^ log.LstdFlags)
+ log.SetOutput(debugView)
+ }
+}
+
+func (l *logger) Write(buf []byte) (int, error) {
+ p, err := syscall.UTF16PtrFromString(string(buf))
+ if err != nil {
+ return 0, err
+ }
+ outputDebugStringW.Call(uintptr(unsafe.Pointer(p)))
+ return len(buf), nil
+}
diff --git a/gio/giold/app/internal/windows/windows.go b/gio/giold/app/internal/windows/windows.go
new file mode 100644
index 0000000..8af575e
--- /dev/null
+++ b/gio/giold/app/internal/windows/windows.go
@@ -0,0 +1,673 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build windows
+
+package windows
+
+import (
+ "fmt"
+ "runtime"
+ "time"
+ "unsafe"
+
+ syscall "golang.org/x/sys/windows"
+)
+
+type Rect struct {
+ Left, Top, Right, Bottom int32
+}
+
+type WndClassEx struct {
+ CbSize uint32
+ Style uint32
+ LpfnWndProc uintptr
+ CnClsExtra int32
+ CbWndExtra int32
+ HInstance syscall.Handle
+ HIcon syscall.Handle
+ HCursor syscall.Handle
+ HbrBackground syscall.Handle
+ LpszMenuName *uint16
+ LpszClassName *uint16
+ HIconSm syscall.Handle
+}
+
+type Msg struct {
+ Hwnd syscall.Handle
+ Message uint32
+ WParam uintptr
+ LParam uintptr
+ Time uint32
+ Pt Point
+ LPrivate uint32
+}
+
+type Point struct {
+ X, Y int32
+}
+
+type MinMaxInfo struct {
+ PtReserved Point
+ PtMaxSize Point
+ PtMaxPosition Point
+ PtMinTrackSize Point
+ PtMaxTrackSize Point
+}
+
+type WindowPlacement struct {
+ length uint32
+ flags uint32
+ showCmd uint32
+ ptMinPosition Point
+ ptMaxPosition Point
+ rcNormalPosition Rect
+ rcDevice Rect
+}
+
+type MonitorInfo struct {
+ cbSize uint32
+ Monitor Rect
+ WorkArea Rect
+ Flags uint32
+}
+
+const (
+ TRUE = 1
+
+ CS_HREDRAW = 0x0002
+ CS_VREDRAW = 0x0001
+ CS_OWNDC = 0x0020
+
+ CW_USEDEFAULT = -2147483648
+
+ GWL_STYLE = ^(uint32(16) - 1) // -16
+ HWND_TOPMOST = ^(uint32(1) - 1) // -1
+
+ HTCLIENT = 1
+
+ IDC_ARROW = 32512
+ IDC_IBEAM = 32513
+ IDC_HAND = 32649
+ IDC_CROSS = 32515
+ IDC_SIZENS = 32645
+ IDC_SIZEWE = 32644
+ IDC_SIZEALL = 32646
+
+ INFINITE = 0xFFFFFFFF
+
+ LOGPIXELSX = 88
+
+ MDT_EFFECTIVE_DPI = 0
+
+ MONITOR_DEFAULTTOPRIMARY = 1
+
+ SIZE_MAXIMIZED = 2
+ SIZE_MINIMIZED = 1
+ SIZE_RESTORED = 0
+
+ SW_SHOWDEFAULT = 10
+
+ SWP_FRAMECHANGED = 0x0020
+ SWP_NOMOVE = 0x0002
+ SWP_NOOWNERZORDER = 0x0200
+ SWP_NOSIZE = 0x0001
+ SWP_NOZORDER = 0x0004
+
+ USER_TIMER_MINIMUM = 0x0000000A
+
+ VK_CONTROL = 0x11
+ VK_LWIN = 0x5B
+ VK_MENU = 0x12
+ VK_RWIN = 0x5C
+ VK_SHIFT = 0x10
+
+ VK_BACK = 0x08
+ VK_DELETE = 0x2e
+ VK_DOWN = 0x28
+ VK_END = 0x23
+ VK_ESCAPE = 0x1b
+ VK_HOME = 0x24
+ VK_LEFT = 0x25
+ VK_NEXT = 0x22
+ VK_PRIOR = 0x21
+ VK_RIGHT = 0x27
+ VK_RETURN = 0x0d
+ VK_SPACE = 0x20
+ VK_TAB = 0x09
+ VK_UP = 0x26
+
+ VK_F1 = 0x70
+ VK_F2 = 0x71
+ VK_F3 = 0x72
+ VK_F4 = 0x73
+ VK_F5 = 0x74
+ VK_F6 = 0x75
+ VK_F7 = 0x76
+ VK_F8 = 0x77
+ VK_F9 = 0x78
+ VK_F10 = 0x79
+ VK_F11 = 0x7A
+ VK_F12 = 0x7B
+
+ VK_OEM_1 = 0xba
+ VK_OEM_PLUS = 0xbb
+ VK_OEM_COMMA = 0xbc
+ VK_OEM_MINUS = 0xbd
+ VK_OEM_PERIOD = 0xbe
+ VK_OEM_2 = 0xbf
+ VK_OEM_3 = 0xc0
+ VK_OEM_4 = 0xdb
+ VK_OEM_5 = 0xdc
+ VK_OEM_6 = 0xdd
+ VK_OEM_7 = 0xde
+ VK_OEM_102 = 0xe2
+
+ UNICODE_NOCHAR = 65535
+
+ WM_CANCELMODE = 0x001F
+ WM_CHAR = 0x0102
+ WM_CREATE = 0x0001
+ WM_DPICHANGED = 0x02E0
+ WM_DESTROY = 0x0002
+ WM_ERASEBKGND = 0x0014
+ WM_KEYDOWN = 0x0100
+ WM_KEYUP = 0x0101
+ WM_LBUTTONDOWN = 0x0201
+ WM_LBUTTONUP = 0x0202
+ WM_MBUTTONDOWN = 0x0207
+ WM_MBUTTONUP = 0x0208
+ WM_MOUSEMOVE = 0x0200
+ WM_MOUSEWHEEL = 0x020A
+ WM_MOUSEHWHEEL = 0x020E
+ WM_PAINT = 0x000F
+ WM_CLOSE = 0x0010
+ WM_QUIT = 0x0012
+ WM_SETCURSOR = 0x0020
+ WM_SETFOCUS = 0x0007
+ WM_KILLFOCUS = 0x0008
+ WM_SHOWWINDOW = 0x0018
+ WM_SIZE = 0x0005
+ WM_SYSKEYDOWN = 0x0104
+ WM_SYSKEYUP = 0x0105
+ WM_RBUTTONDOWN = 0x0204
+ WM_RBUTTONUP = 0x0205
+ WM_TIMER = 0x0113
+ WM_UNICHAR = 0x0109
+ WM_USER = 0x0400
+ WM_GETMINMAXINFO = 0x0024
+
+ WS_CLIPCHILDREN = 0x00010000
+ WS_CLIPSIBLINGS = 0x04000000
+ WS_VISIBLE = 0x10000000
+ WS_OVERLAPPED = 0x00000000
+ WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME |
+ WS_MINIMIZEBOX | WS_MAXIMIZEBOX
+ WS_CAPTION = 0x00C00000
+ WS_SYSMENU = 0x00080000
+ WS_THICKFRAME = 0x00040000
+ WS_MINIMIZEBOX = 0x00020000
+ WS_MAXIMIZEBOX = 0x00010000
+
+ WS_EX_APPWINDOW = 0x00040000
+ WS_EX_WINDOWEDGE = 0x00000100
+
+ QS_ALLINPUT = 0x04FF
+
+ MWMO_WAITALL = 0x0001
+ MWMO_INPUTAVAILABLE = 0x0004
+
+ WAIT_OBJECT_0 = 0
+
+ PM_REMOVE = 0x0001
+ PM_NOREMOVE = 0x0000
+
+ GHND = 0x0042
+
+ CF_UNICODETEXT = 13
+ IMAGE_BITMAP = 0
+ IMAGE_ICON = 1
+ IMAGE_CURSOR = 2
+
+ LR_CREATEDIBSECTION = 0x00002000
+ LR_DEFAULTCOLOR = 0x00000000
+ LR_DEFAULTSIZE = 0x00000040
+ LR_LOADFROMFILE = 0x00000010
+ LR_LOADMAP3DCOLORS = 0x00001000
+ LR_LOADTRANSPARENT = 0x00000020
+ LR_MONOCHROME = 0x00000001
+ LR_SHARED = 0x00008000
+ LR_VGACOLOR = 0x00000080
+)
+
+var (
+ kernel32 = syscall.NewLazySystemDLL("kernel32.dll")
+ _GetModuleHandleW = kernel32.NewProc("GetModuleHandleW")
+ _GlobalAlloc = kernel32.NewProc("GlobalAlloc")
+ _GlobalFree = kernel32.NewProc("GlobalFree")
+ _GlobalLock = kernel32.NewProc("GlobalLock")
+ _GlobalUnlock = kernel32.NewProc("GlobalUnlock")
+
+ user32 = syscall.NewLazySystemDLL("user32.dll")
+ _AdjustWindowRectEx = user32.NewProc("AdjustWindowRectEx")
+ _CallMsgFilter = user32.NewProc("CallMsgFilterW")
+ _CloseClipboard = user32.NewProc("CloseClipboard")
+ _CreateWindowEx = user32.NewProc("CreateWindowExW")
+ _DefWindowProc = user32.NewProc("DefWindowProcW")
+ _DestroyWindow = user32.NewProc("DestroyWindow")
+ _DispatchMessage = user32.NewProc("DispatchMessageW")
+ _EmptyClipboard = user32.NewProc("EmptyClipboard")
+ _GetClientRect = user32.NewProc("GetClientRect")
+ _GetClipboardData = user32.NewProc("GetClipboardData")
+ _GetDC = user32.NewProc("GetDC")
+ _GetDpiForWindow = user32.NewProc("GetDpiForWindow")
+ _GetKeyState = user32.NewProc("GetKeyState")
+ _GetMessage = user32.NewProc("GetMessageW")
+ _GetMessageTime = user32.NewProc("GetMessageTime")
+ _GetMonitorInfo = user32.NewProc("GetMonitorInfoW")
+ _GetWindowLong = user32.NewProc("GetWindowLongPtrW")
+ _GetWindowPlacement = user32.NewProc("GetWindowPlacement")
+ _KillTimer = user32.NewProc("KillTimer")
+ _LoadCursor = user32.NewProc("LoadCursorW")
+ _LoadImage = user32.NewProc("LoadImageW")
+ _MonitorFromPoint = user32.NewProc("MonitorFromPoint")
+ _MonitorFromWindow = user32.NewProc("MonitorFromWindow")
+ _MoveWindow = user32.NewProc("MoveWindow")
+ _MsgWaitForMultipleObjectsEx = user32.NewProc("MsgWaitForMultipleObjectsEx")
+ _OpenClipboard = user32.NewProc("OpenClipboard")
+ _PeekMessage = user32.NewProc("PeekMessageW")
+ _PostMessage = user32.NewProc("PostMessageW")
+ _PostQuitMessage = user32.NewProc("PostQuitMessage")
+ _ReleaseCapture = user32.NewProc("ReleaseCapture")
+ _RegisterClassExW = user32.NewProc("RegisterClassExW")
+ _ReleaseDC = user32.NewProc("ReleaseDC")
+ _ScreenToClient = user32.NewProc("ScreenToClient")
+ _ShowWindow = user32.NewProc("ShowWindow")
+ _SetCapture = user32.NewProc("SetCapture")
+ _SetCursor = user32.NewProc("SetCursor")
+ _SetClipboardData = user32.NewProc("SetClipboardData")
+ _SetForegroundWindow = user32.NewProc("SetForegroundWindow")
+ _SetFocus = user32.NewProc("SetFocus")
+ _SetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
+ _SetTimer = user32.NewProc("SetTimer")
+ _SetWindowLong = user32.NewProc("SetWindowLongPtrW")
+ _SetWindowPlacement = user32.NewProc("SetWindowPlacement")
+ _SetWindowPos = user32.NewProc("SetWindowPos")
+ _SetWindowText = user32.NewProc("SetWindowTextW")
+ _TranslateMessage = user32.NewProc("TranslateMessage")
+ _UnregisterClass = user32.NewProc("UnregisterClassW")
+ _UpdateWindow = user32.NewProc("UpdateWindow")
+
+ shcore = syscall.NewLazySystemDLL("shcore")
+ _GetDpiForMonitor = shcore.NewProc("GetDpiForMonitor")
+
+ gdi32 = syscall.NewLazySystemDLL("gdi32")
+ _GetDeviceCaps = gdi32.NewProc("GetDeviceCaps")
+)
+
+func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) {
+ _AdjustWindowRectEx.Call(uintptr(unsafe.Pointer(r)), uintptr(dwStyle), uintptr(bMenu), uintptr(dwExStyle))
+ issue34474KeepAlive(r)
+}
+
+func CallMsgFilter(m *Msg, nCode uintptr) bool {
+ r, _, _ := _CallMsgFilter.Call(uintptr(unsafe.Pointer(m)), nCode)
+ issue34474KeepAlive(m)
+ return r != 0
+}
+
+func CloseClipboard() error {
+ r, _, err := _CloseClipboard.Call()
+ if r == 0 {
+ return fmt.Errorf("CloseClipboard: %v", err)
+ }
+ return nil
+}
+
+func CreateWindowEx(dwExStyle uint32, lpClassName uint16, lpWindowName string, dwStyle uint32, x, y, w, h int32, hWndParent, hMenu, hInstance syscall.Handle, lpParam uintptr) (syscall.Handle, error) {
+ wname := syscall.StringToUTF16Ptr(lpWindowName)
+ hwnd, _, err := _CreateWindowEx.Call(
+ uintptr(dwExStyle),
+ uintptr(lpClassName),
+ uintptr(unsafe.Pointer(wname)),
+ uintptr(dwStyle),
+ uintptr(x), uintptr(y),
+ uintptr(w), uintptr(h),
+ uintptr(hWndParent),
+ uintptr(hMenu),
+ uintptr(hInstance),
+ uintptr(lpParam))
+ issue34474KeepAlive(wname)
+ if hwnd == 0 {
+ return 0, fmt.Errorf("CreateWindowEx failed: %v", err)
+ }
+ return syscall.Handle(hwnd), nil
+}
+
+func DefWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
+ r, _, _ := _DefWindowProc.Call(uintptr(hwnd), uintptr(msg), wparam, lparam)
+ return r
+}
+
+func DestroyWindow(hwnd syscall.Handle) {
+ _DestroyWindow.Call(uintptr(hwnd))
+}
+
+func DispatchMessage(m *Msg) {
+ _DispatchMessage.Call(uintptr(unsafe.Pointer(m)))
+ issue34474KeepAlive(m)
+}
+
+func EmptyClipboard() error {
+ r, _, err := _EmptyClipboard.Call()
+ if r == 0 {
+ return fmt.Errorf("EmptyClipboard: %v", err)
+ }
+ return nil
+}
+
+func GetClientRect(hwnd syscall.Handle, r *Rect) {
+ _GetClientRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(r)))
+ issue34474KeepAlive(r)
+}
+
+func GetClipboardData(format uint32) (syscall.Handle, error) {
+ r, _, err := _GetClipboardData.Call(uintptr(format))
+ if r == 0 {
+ return 0, fmt.Errorf("GetClipboardData: %v", err)
+ }
+ return syscall.Handle(r), nil
+}
+
+func GetDC(hwnd syscall.Handle) (syscall.Handle, error) {
+ hdc, _, err := _GetDC.Call(uintptr(hwnd))
+ if hdc == 0 {
+ return 0, fmt.Errorf("GetDC failed: %v", err)
+ }
+ return syscall.Handle(hdc), nil
+}
+
+func GetModuleHandle() (syscall.Handle, error) {
+ h, _, err := _GetModuleHandleW.Call(uintptr(0))
+ if h == 0 {
+ return 0, fmt.Errorf("GetModuleHandleW failed: %v", err)
+ }
+ return syscall.Handle(h), nil
+}
+
+func getDeviceCaps(hdc syscall.Handle, index int32) int {
+ c, _, _ := _GetDeviceCaps.Call(uintptr(hdc), uintptr(index))
+ return int(c)
+}
+
+func getDpiForMonitor(hmonitor syscall.Handle, dpiType uint32) int {
+ var dpiX, dpiY uintptr
+ _GetDpiForMonitor.Call(uintptr(hmonitor), uintptr(dpiType), uintptr(unsafe.Pointer(&dpiX)), uintptr(unsafe.Pointer(&dpiY)))
+ return int(dpiX)
+}
+
+// GetSystemDPI returns the effective DPI of the system.
+func GetSystemDPI() int {
+ // Check for GetDpiForMonitor, introduced in Windows 8.1.
+ if _GetDpiForMonitor.Find() == nil {
+ hmon := monitorFromPoint(Point{}, MONITOR_DEFAULTTOPRIMARY)
+ return getDpiForMonitor(hmon, MDT_EFFECTIVE_DPI)
+ } else {
+ // Fall back to the physical device DPI.
+ screenDC, err := GetDC(0)
+ if err != nil {
+ return 96
+ }
+ defer ReleaseDC(screenDC)
+ return getDeviceCaps(screenDC, LOGPIXELSX)
+ }
+}
+
+func GetKeyState(nVirtKey int32) int16 {
+ c, _, _ := _GetKeyState.Call(uintptr(nVirtKey))
+ return int16(c)
+}
+
+func GetMessage(m *Msg, hwnd syscall.Handle, wMsgFilterMin, wMsgFilterMax uint32) int32 {
+ r, _, _ := _GetMessage.Call(uintptr(unsafe.Pointer(m)),
+ uintptr(hwnd),
+ uintptr(wMsgFilterMin),
+ uintptr(wMsgFilterMax))
+ issue34474KeepAlive(m)
+ return int32(r)
+}
+
+func GetMessageTime() time.Duration {
+ r, _, _ := _GetMessageTime.Call()
+ return time.Duration(r) * time.Millisecond
+}
+
+// GetWindowDPI returns the effective DPI of the window.
+func GetWindowDPI(hwnd syscall.Handle) int {
+ // Check for GetDpiForWindow, introduced in Windows 10.
+ if _GetDpiForWindow.Find() == nil {
+ dpi, _, _ := _GetDpiForWindow.Call(uintptr(hwnd))
+ return int(dpi)
+ } else {
+ return GetSystemDPI()
+ }
+}
+
+func GetWindowPlacement(hwnd syscall.Handle) *WindowPlacement {
+ var wp WindowPlacement
+ wp.length = uint32(unsafe.Sizeof(wp))
+ _GetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&wp)))
+ return &wp
+}
+
+func GetMonitorInfo(hwnd syscall.Handle) MonitorInfo {
+ var mi MonitorInfo
+ mi.cbSize = uint32(unsafe.Sizeof(mi))
+ v, _, _ := _MonitorFromWindow.Call(uintptr(hwnd), MONITOR_DEFAULTTOPRIMARY)
+ _GetMonitorInfo.Call(v, uintptr(unsafe.Pointer(&mi)))
+ return mi
+}
+
+func GetWindowLong(hwnd syscall.Handle) (style uintptr) {
+ style, _, _ = _GetWindowLong.Call(uintptr(hwnd), uintptr(GWL_STYLE))
+ return
+}
+
+func SetWindowLong(hwnd syscall.Handle, idx uint32, style uintptr) {
+ _SetWindowLong.Call(uintptr(hwnd), uintptr(idx), style)
+}
+
+func SetWindowPlacement(hwnd syscall.Handle, wp *WindowPlacement) {
+ _SetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wp)))
+}
+
+func SetWindowPos(hwnd syscall.Handle, hwndInsertAfter uint32, x, y, dx, dy int32, style uintptr) {
+ _SetWindowPos.Call(uintptr(hwnd), uintptr(hwndInsertAfter),
+ uintptr(x), uintptr(y),
+ uintptr(dx), uintptr(dy),
+ style,
+ )
+}
+
+func SetWindowText(hwnd syscall.Handle, title string) {
+ wname := syscall.StringToUTF16Ptr(title)
+ _SetWindowText.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wname)))
+}
+
+func GlobalAlloc(size int) (syscall.Handle, error) {
+ r, _, err := _GlobalAlloc.Call(GHND, uintptr(size))
+ if r == 0 {
+ return 0, fmt.Errorf("GlobalAlloc: %v", err)
+ }
+ return syscall.Handle(r), nil
+}
+
+func GlobalFree(h syscall.Handle) {
+ _GlobalFree.Call(uintptr(h))
+}
+
+func GlobalLock(h syscall.Handle) (uintptr, error) {
+ r, _, err := _GlobalLock.Call(uintptr(h))
+ if r == 0 {
+ return 0, fmt.Errorf("GlobalLock: %v", err)
+ }
+ return r, nil
+}
+
+func GlobalUnlock(h syscall.Handle) {
+ _GlobalUnlock.Call(uintptr(h))
+}
+
+func KillTimer(hwnd syscall.Handle, nIDEvent uintptr) error {
+ r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), 0, 0)
+ if r == 0 {
+ return fmt.Errorf("KillTimer failed: %v", err)
+ }
+ return nil
+}
+
+func LoadCursor(curID uint16) (syscall.Handle, error) {
+ h, _, err := _LoadCursor.Call(0, uintptr(curID))
+ if h == 0 {
+ return 0, fmt.Errorf("LoadCursorW failed: %v", err)
+ }
+ return syscall.Handle(h), nil
+}
+
+func LoadImage(hInst syscall.Handle, res uint32, typ uint32, cx, cy int, fuload uint32) (syscall.Handle, error) {
+ h, _, err := _LoadImage.Call(uintptr(hInst), uintptr(res), uintptr(typ), uintptr(cx), uintptr(cy), uintptr(fuload))
+ if h == 0 {
+ return 0, fmt.Errorf("LoadImageW failed: %v", err)
+ }
+ return syscall.Handle(h), nil
+}
+
+func MoveWindow(hwnd syscall.Handle, x, y, width, height int32, repaint bool) {
+ var paint uintptr
+ if repaint {
+ paint = TRUE
+ }
+ _MoveWindow.Call(uintptr(hwnd), uintptr(x), uintptr(y), uintptr(width), uintptr(height), paint)
+}
+
+func monitorFromPoint(pt Point, flags uint32) syscall.Handle {
+ r, _, _ := _MonitorFromPoint.Call(uintptr(pt.X), uintptr(pt.Y), uintptr(flags))
+ return syscall.Handle(r)
+}
+
+func MsgWaitForMultipleObjectsEx(nCount uint32, pHandles uintptr, millis, mask, flags uint32) (uint32, error) {
+ r, _, err := _MsgWaitForMultipleObjectsEx.Call(uintptr(nCount), pHandles, uintptr(millis), uintptr(mask), uintptr(flags))
+ res := uint32(r)
+ if res == 0xFFFFFFFF {
+ return 0, fmt.Errorf("MsgWaitForMultipleObjectsEx failed: %v", err)
+ }
+ return res, nil
+}
+
+func OpenClipboard(hwnd syscall.Handle) error {
+ r, _, err := _OpenClipboard.Call(uintptr(hwnd))
+ if r == 0 {
+ return fmt.Errorf("OpenClipboard: %v", err)
+ }
+ return nil
+}
+
+func PeekMessage(m *Msg, hwnd syscall.Handle, wMsgFilterMin, wMsgFilterMax, wRemoveMsg uint32) bool {
+ r, _, _ := _PeekMessage.Call(uintptr(unsafe.Pointer(m)), uintptr(hwnd), uintptr(wMsgFilterMin), uintptr(wMsgFilterMax), uintptr(wRemoveMsg))
+ issue34474KeepAlive(m)
+ return r != 0
+}
+
+func PostQuitMessage(exitCode uintptr) {
+ _PostQuitMessage.Call(exitCode)
+}
+
+func PostMessage(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) error {
+ r, _, err := _PostMessage.Call(uintptr(hwnd), uintptr(msg), wParam, lParam)
+ if r == 0 {
+ return fmt.Errorf("PostMessage failed: %v", err)
+ }
+ return nil
+}
+
+func ReleaseCapture() bool {
+ r, _, _ := _ReleaseCapture.Call()
+ return r != 0
+}
+
+func RegisterClassEx(cls *WndClassEx) (uint16, error) {
+ a, _, err := _RegisterClassExW.Call(uintptr(unsafe.Pointer(cls)))
+ issue34474KeepAlive(cls)
+ if a == 0 {
+ return 0, fmt.Errorf("RegisterClassExW failed: %v", err)
+ }
+ return uint16(a), nil
+}
+
+func ReleaseDC(hdc syscall.Handle) {
+ _ReleaseDC.Call(uintptr(hdc))
+}
+
+func SetForegroundWindow(hwnd syscall.Handle) {
+ _SetForegroundWindow.Call(uintptr(hwnd))
+}
+
+func SetFocus(hwnd syscall.Handle) {
+ _SetFocus.Call(uintptr(hwnd))
+}
+
+func SetProcessDPIAware() {
+ _SetProcessDPIAware.Call()
+}
+
+func SetCapture(hwnd syscall.Handle) syscall.Handle {
+ r, _, _ := _SetCapture.Call(uintptr(hwnd))
+ return syscall.Handle(r)
+}
+
+func SetClipboardData(format uint32, mem syscall.Handle) error {
+ r, _, err := _SetClipboardData.Call(uintptr(format), uintptr(mem))
+ if r == 0 {
+ return fmt.Errorf("SetClipboardData: %v", err)
+ }
+ return nil
+}
+
+func SetCursor(h syscall.Handle) {
+ _SetCursor.Call(uintptr(h))
+}
+
+func SetTimer(hwnd syscall.Handle, nIDEvent uintptr, uElapse uint32, timerProc uintptr) error {
+ r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), uintptr(uElapse), timerProc)
+ if r == 0 {
+ return fmt.Errorf("SetTimer failed: %v", err)
+ }
+ return nil
+}
+
+func ScreenToClient(hwnd syscall.Handle, p *Point) {
+ _ScreenToClient.Call(uintptr(hwnd), uintptr(unsafe.Pointer(p)))
+ issue34474KeepAlive(p)
+}
+
+func ShowWindow(hwnd syscall.Handle, nCmdShow int32) {
+ _ShowWindow.Call(uintptr(hwnd), uintptr(nCmdShow))
+}
+
+func TranslateMessage(m *Msg) {
+ _TranslateMessage.Call(uintptr(unsafe.Pointer(m)))
+ issue34474KeepAlive(m)
+}
+
+func UnregisterClass(cls uint16, hInst syscall.Handle) {
+ _UnregisterClass.Call(uintptr(cls), uintptr(hInst))
+}
+
+func UpdateWindow(hwnd syscall.Handle) {
+ _UpdateWindow.Call(uintptr(hwnd))
+}
+
+// issue34474KeepAlive calls runtime.KeepAlive as a
+// workaround for golang.org/issue/34474.
+func issue34474KeepAlive(v interface{}) {
+ runtime.KeepAlive(v)
+}
diff --git a/gio/giold/app/internal/wm/Gio.java b/gio/giold/app/internal/wm/Gio.java
new file mode 100644
index 0000000..33e1a68
--- /dev/null
+++ b/gio/giold/app/internal/wm/Gio.java
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package org.gioui;
+
+import android.content.ClipboardManager;
+import android.content.ClipData;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.io.UnsupportedEncodingException;
+
+public final class Gio {
+ private static final Object initLock = new Object();
+ private static boolean jniLoaded;
+ private static final Handler handler = new Handler(Looper.getMainLooper());
+
+ /**
+ * init loads and initializes the Go native library and runs
+ * the Go main function.
+ *
+ * It is exported for use by Android apps that need to run Go code
+ * outside the lifecycle of the Gio activity.
+ */
+ public static synchronized void init(Context appCtx) {
+ synchronized (initLock) {
+ if (jniLoaded) {
+ return;
+ }
+ String dataDir = appCtx.getFilesDir().getAbsolutePath();
+ byte[] dataDirUTF8;
+ try {
+ dataDirUTF8 = dataDir.getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ System.loadLibrary("gio");
+ runGoMain(dataDirUTF8, appCtx);
+ jniLoaded = true;
+ }
+ }
+
+ static private native void runGoMain(byte[] dataDir, Context context);
+
+ static void writeClipboard(Context ctx, String s) {
+ ClipboardManager m = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE);
+ m.setPrimaryClip(ClipData.newPlainText(null, s));
+ }
+
+ static String readClipboard(Context ctx) {
+ ClipboardManager m = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData c = m.getPrimaryClip();
+ if (c == null || c.getItemCount() < 1) {
+ return null;
+ }
+ return c.getItemAt(0).coerceToText(ctx).toString();
+ }
+
+ static void wakeupMainThread() {
+ handler.post(new Runnable() {
+ @Override public void run() {
+ scheduleMainFuncs();
+ }
+ });
+ }
+
+ static private native void scheduleMainFuncs();
+}
diff --git a/gio/giold/app/internal/wm/GioActivity.java b/gio/giold/app/internal/wm/GioActivity.java
new file mode 100644
index 0000000..260d4b6
--- /dev/null
+++ b/gio/giold/app/internal/wm/GioActivity.java
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package org.gioui;
+
+import android.app.Activity;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+public final class GioActivity extends Activity {
+ private GioView view;
+
+ @Override public void onCreate(Bundle state) {
+ super.onCreate(state);
+
+ Window w = getWindow();
+
+ this.view = new GioView(this);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ this.view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+ this.view.setLayoutParams(new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT));
+ setContentView(view);
+ }
+
+ @Override public void onDestroy() {
+ view.destroy();
+ super.onDestroy();
+ }
+
+ @Override public void onStart() {
+ super.onStart();
+ view.start();
+ }
+
+ @Override public void onStop() {
+ view.stop();
+ super.onStop();
+ }
+
+ @Override public void onConfigurationChanged(Configuration c) {
+ super.onConfigurationChanged(c);
+ view.configurationChanged();
+ }
+
+ @Override public void onLowMemory() {
+ super.onLowMemory();
+ view.lowMemory();
+ }
+
+ @Override public void onBackPressed() {
+ if (!view.backPressed())
+ super.onBackPressed();
+ }
+}
diff --git a/gio/giold/app/internal/wm/GioView.java b/gio/giold/app/internal/wm/GioView.java
new file mode 100644
index 0000000..7ed9f05
--- /dev/null
+++ b/gio/giold/app/internal/wm/GioView.java
@@ -0,0 +1,263 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package org.gioui;
+
+import java.lang.Class;
+import java.lang.IllegalAccessException;
+import java.lang.InstantiationException;
+import java.lang.ExceptionInInitializerError;
+import java.lang.SecurityException;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.os.Build;
+import android.text.Editable;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Choreographer;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.WindowInsets;
+import android.view.Surface;
+import android.view.SurfaceView;
+import android.view.SurfaceHolder;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.EditorInfo;
+
+import java.io.UnsupportedEncodingException;
+
+public final class GioView extends SurfaceView implements Choreographer.FrameCallback {
+ private static boolean jniLoaded;
+
+ private final SurfaceHolder.Callback surfCallbacks;
+ private final View.OnFocusChangeListener focusCallback;
+ private final InputMethodManager imm;
+ private final float scrollXScale;
+ private final float scrollYScale;
+
+ private long nhandle;
+
+ public GioView(Context context) {
+ this(context, null);
+ }
+
+ public GioView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ // Late initialization of the Go runtime to wait for a valid context.
+ Gio.init(context.getApplicationContext());
+
+ // Set background color to transparent to avoid a flickering
+ // issue on ChromeOS.
+ setBackgroundColor(Color.argb(0, 0, 0, 0));
+
+ ViewConfiguration conf = ViewConfiguration.get(context);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ scrollXScale = conf.getScaledHorizontalScrollFactor();
+ scrollYScale = conf.getScaledVerticalScrollFactor();
+
+ // The platform focus highlight is not aware of Gio's widgets.
+ setDefaultFocusHighlightEnabled(false);
+ } else {
+ float listItemHeight = 48; // dp
+ float px = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ listItemHeight,
+ getResources().getDisplayMetrics()
+ );
+ scrollXScale = px;
+ scrollYScale = px;
+ }
+
+ nhandle = onCreateView(this);
+ imm = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ focusCallback = new View.OnFocusChangeListener() {
+ @Override public void onFocusChange(View v, boolean focus) {
+ GioView.this.onFocusChange(nhandle, focus);
+ }
+ };
+ setOnFocusChangeListener(focusCallback);
+ surfCallbacks = new SurfaceHolder.Callback() {
+ @Override public void surfaceCreated(SurfaceHolder holder) {
+ // Ignore; surfaceChanged is guaranteed to be called immediately after this.
+ }
+ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ onSurfaceChanged(nhandle, getHolder().getSurface());
+ }
+ @Override public void surfaceDestroyed(SurfaceHolder holder) {
+ onSurfaceDestroyed(nhandle);
+ }
+ };
+ getHolder().addCallback(surfCallbacks);
+ }
+
+ @Override public boolean onKeyDown(int keyCode, KeyEvent event) {
+ onKeyEvent(nhandle, keyCode, event.getUnicodeChar(), event.getEventTime());
+ return false;
+ }
+
+ @Override public boolean onGenericMotionEvent(MotionEvent event) {
+ dispatchMotionEvent(event);
+ return true;
+ }
+
+ @Override public boolean onTouchEvent(MotionEvent event) {
+ // Ask for unbuffered events. Flutter and Chrome do it
+ // so assume it's good for us as well.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ requestUnbufferedDispatch(event);
+ }
+
+ dispatchMotionEvent(event);
+ return true;
+ }
+
+ private void setCursor(int id) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ return;
+ }
+ PointerIcon pointerIcon = PointerIcon.getSystemIcon(getContext(), id);
+ setPointerIcon(pointerIcon);
+ }
+
+ private void dispatchMotionEvent(MotionEvent event) {
+ for (int j = 0; j < event.getHistorySize(); j++) {
+ long time = event.getHistoricalEventTime(j);
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ onTouchEvent(
+ nhandle,
+ event.ACTION_MOVE,
+ event.getPointerId(i),
+ event.getToolType(i),
+ event.getHistoricalX(i, j),
+ event.getHistoricalY(i, j),
+ scrollXScale*event.getHistoricalAxisValue(MotionEvent.AXIS_HSCROLL, i, j),
+ scrollYScale*event.getHistoricalAxisValue(MotionEvent.AXIS_VSCROLL, i, j),
+ event.getButtonState(),
+ time);
+ }
+ }
+ int act = event.getActionMasked();
+ int idx = event.getActionIndex();
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ int pact = event.ACTION_MOVE;
+ if (i == idx) {
+ pact = act;
+ }
+ onTouchEvent(
+ nhandle,
+ pact,
+ event.getPointerId(i),
+ event.getToolType(i),
+ event.getX(i), event.getY(i),
+ scrollXScale*event.getAxisValue(MotionEvent.AXIS_HSCROLL, i),
+ scrollYScale*event.getAxisValue(MotionEvent.AXIS_VSCROLL, i),
+ event.getButtonState(),
+ event.getEventTime());
+ }
+ }
+
+ @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ return new InputConnection(this);
+ }
+
+ void showTextInput() {
+ GioView.this.requestFocus();
+ imm.showSoftInput(GioView.this, 0);
+ }
+
+ void hideTextInput() {
+ imm.hideSoftInputFromWindow(getWindowToken(), 0);
+ }
+
+ @Override protected boolean fitSystemWindows(Rect insets) {
+ onWindowInsets(nhandle, insets.top, insets.right, insets.bottom, insets.left);
+ return true;
+ }
+
+ void postFrameCallback() {
+ Choreographer.getInstance().removeFrameCallback(this);
+ Choreographer.getInstance().postFrameCallback(this);
+ }
+
+ @Override public void doFrame(long nanos) {
+ onFrameCallback(nhandle, nanos);
+ }
+
+ int getDensity() {
+ return getResources().getDisplayMetrics().densityDpi;
+ }
+
+ float getFontScale() {
+ return getResources().getConfiguration().fontScale;
+ }
+
+ void start() {
+ onStartView(nhandle);
+ }
+
+ void stop() {
+ onStopView(nhandle);
+ }
+
+ void destroy() {
+ setOnFocusChangeListener(null);
+ getHolder().removeCallback(surfCallbacks);
+ onDestroyView(nhandle);
+ nhandle = 0;
+ }
+
+ void configurationChanged() {
+ onConfigurationChanged(nhandle);
+ }
+
+ void lowMemory() {
+ onLowMemory();
+ }
+
+ boolean backPressed() {
+ return onBack(nhandle);
+ }
+
+ static private native long onCreateView(GioView view);
+ static private native void onDestroyView(long handle);
+ static private native void onStartView(long handle);
+ static private native void onStopView(long handle);
+ static private native void onSurfaceDestroyed(long handle);
+ static private native void onSurfaceChanged(long handle, Surface surface);
+ static private native void onConfigurationChanged(long handle);
+ static private native void onWindowInsets(long handle, int top, int right, int bottom, int left);
+ static private native void onLowMemory();
+ static private native void onTouchEvent(long handle, int action, int pointerID, int tool, float x, float y, float scrollX, float scrollY, int buttons, long time);
+ static private native void onKeyEvent(long handle, int code, int character, long time);
+ static private native void onFrameCallback(long handle, long nanos);
+ static private native boolean onBack(long handle);
+ static private native void onFocusChange(long handle, boolean focus);
+
+ private static class InputConnection extends BaseInputConnection {
+ private final Editable editable;
+
+ InputConnection(View view) {
+ // Passing false enables "dummy mode", where the BaseInputConnection
+ // attempts to convert IME operations to key events.
+ super(view, false);
+ editable = Editable.Factory.getInstance().newEditable("");
+ }
+
+ @Override public Editable getEditable() {
+ return editable;
+ }
+ }
+}
diff --git a/gio/giold/app/internal/wm/d3d11_windows.go b/gio/giold/app/internal/wm/d3d11_windows.go
new file mode 100644
index 0000000..8368933
--- /dev/null
+++ b/gio/giold/app/internal/wm/d3d11_windows.go
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package wm
+
+import (
+ "fmt"
+ "unsafe"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/internal/d3d11"
+)
+
+type d3d11Context struct {
+ win *window
+ dev *d3d11.Device
+ ctx *d3d11.DeviceContext
+
+ swchain *d3d11.IDXGISwapChain
+ renderTarget *d3d11.RenderTargetView
+ depthView *d3d11.DepthStencilView
+ width, height int
+}
+
+const debug = false
+
+func init() {
+ drivers = append(drivers, gpuAPI{
+ priority: 1,
+ initializer: func(w *window) (Context, error) {
+ hwnd, _, _ := w.HWND()
+ var flags uint32
+ if debug {
+ flags |= d3d11.CREATE_DEVICE_DEBUG
+ }
+ dev, ctx, _, err := d3d11.CreateDevice(
+ d3d11.DRIVER_TYPE_HARDWARE,
+ flags,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("NewContext: %v", err)
+ }
+ swchain, err := d3d11.CreateSwapChain(dev, hwnd)
+ if err != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(ctx), ctx.Vtbl.Release)
+ d3d11.IUnknownRelease(unsafe.Pointer(dev), dev.Vtbl.Release)
+ return nil, err
+ }
+ return &d3d11Context{win: w, dev: dev, ctx: ctx,
+ swchain: swchain}, nil
+ },
+ })
+}
+
+func (c *d3d11Context) API() gpu.API {
+ return gpu.Direct3D11{Device: unsafe.Pointer(c.dev)}
+}
+
+func (c *d3d11Context) Present() error {
+ err := c.swchain.Present(1, 0)
+ if err == nil {
+ return nil
+ }
+ if err, ok := err.(d3d11.ErrorCode); ok {
+ switch err.Code {
+ case d3d11.DXGI_STATUS_OCCLUDED:
+ // Ignore
+ return nil
+ case d3d11.DXGI_ERROR_DEVICE_RESET, d3d11.DXGI_ERROR_DEVICE_REMOVED, d3d11.D3DDDIERR_DEVICEREMOVED:
+ return ErrDeviceLost
+ }
+ }
+ return err
+}
+
+func (c *d3d11Context) MakeCurrent() error {
+ _, width, height := c.win.HWND()
+ if c.renderTarget != nil && width == c.width && height == c.height {
+ c.ctx.OMSetRenderTargets(c.renderTarget, c.depthView)
+ return nil
+ }
+ c.releaseFBO()
+ if err := c.swchain.ResizeBuffers(0, 0, 0, d3d11.DXGI_FORMAT_UNKNOWN,
+ 0); err != nil {
+ return err
+ }
+ c.width = width
+ c.height = height
+
+ desc, err := c.swchain.GetDesc()
+ if err != nil {
+ return err
+ }
+ backBuffer, err := c.swchain.GetBuffer(0, &d3d11.IID_Texture2D)
+ if err != nil {
+ return err
+ }
+ texture := (*d3d11.Resource)(unsafe.Pointer(backBuffer))
+ renderTarget, err := c.dev.CreateRenderTargetView(texture)
+ d3d11.IUnknownRelease(unsafe.Pointer(backBuffer), backBuffer.Vtbl.Release)
+ if err != nil {
+ return err
+ }
+ depthView, err := d3d11.CreateDepthView(c.dev, int(desc.BufferDesc.Width),
+ int(desc.BufferDesc.Height), 24)
+ if err != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(renderTarget),
+ renderTarget.Vtbl.Release)
+ return err
+ }
+ c.renderTarget = renderTarget
+ c.depthView = depthView
+
+ c.ctx.OMSetRenderTargets(c.renderTarget, c.depthView)
+ return nil
+}
+
+func (c *d3d11Context) Lock() {}
+
+func (c *d3d11Context) Unlock() {}
+
+func (c *d3d11Context) Release() {
+ c.releaseFBO()
+ if c.swchain != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(c.swchain), c.swchain.Vtbl.Release)
+ }
+ if c.ctx != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(c.ctx), c.ctx.Vtbl.Release)
+ }
+ if c.dev != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release)
+ }
+ *c = d3d11Context{}
+}
+
+func (c *d3d11Context) releaseFBO() {
+ if c.depthView != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(c.depthView),
+ c.depthView.Vtbl.Release)
+ c.depthView = nil
+ }
+ if c.renderTarget != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(c.renderTarget),
+ c.renderTarget.Vtbl.Release)
+ c.renderTarget = nil
+ }
+}
diff --git a/gio/giold/app/internal/wm/egl_android.go b/gio/giold/app/internal/wm/egl_android.go
new file mode 100644
index 0000000..50e38ad
--- /dev/null
+++ b/gio/giold/app/internal/wm/egl_android.go
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package wm
+
+/*
+#include
+*/
+import "C"
+
+import (
+ "unsafe"
+
+ "realy.lol/gio/internal/egl"
+)
+
+type context struct {
+ win *window
+ *egl.Context
+}
+
+func (w *window) NewContext() (Context, error) {
+ ctx, err := egl.NewContext(nil)
+ if err != nil {
+ return nil, err
+ }
+ return &context{win: w, Context: ctx}, nil
+}
+
+func (c *context) Release() {
+ if c.Context != nil {
+ c.Context.Release()
+ c.Context = nil
+ }
+}
+
+func (c *context) MakeCurrent() error {
+ c.Context.ReleaseSurface()
+ win, width, height := c.win.nativeWindow(c.Context.VisualID())
+ if win == nil {
+ return nil
+ }
+ eglSurf := egl.NativeWindowType(unsafe.Pointer(win))
+ if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
+ return err
+ }
+ if err := c.Context.MakeCurrent(); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (c *context) Lock() {}
+
+func (c *context) Unlock() {}
diff --git a/gio/giold/app/internal/wm/egl_wayland.go b/gio/giold/app/internal/wm/egl_wayland.go
new file mode 100644
index 0000000..0cd5b6c
--- /dev/null
+++ b/gio/giold/app/internal/wm/egl_wayland.go
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build (linux && !android && !nowayland) || freebsd
+// +build linux,!android,!nowayland freebsd
+
+package wm
+
+import (
+ "errors"
+ "unsafe"
+
+ "realy.lol/gio/internal/egl"
+)
+
+/*
+#cgo linux pkg-config: egl wayland-egl
+#cgo freebsd openbsd LDFLAGS: -lwayland-egl
+#cgo CFLAGS: -DEGL_NO_X11
+
+#include
+#include
+#include
+*/
+import "C"
+
+type context struct {
+ win *window
+ *egl.Context
+ eglWin *C.struct_wl_egl_window
+}
+
+func (w *window) NewContext() (Context, error) {
+ disp := egl.NativeDisplayType(unsafe.Pointer(w.display()))
+ ctx, err := egl.NewContext(disp)
+ if err != nil {
+ return nil, err
+ }
+ return &context{Context: ctx, win: w}, nil
+}
+
+func (c *context) Release() {
+ if c.Context != nil {
+ c.Context.Release()
+ c.Context = nil
+ }
+ if c.eglWin != nil {
+ C.wl_egl_window_destroy(c.eglWin)
+ c.eglWin = nil
+ }
+}
+
+func (c *context) MakeCurrent() error {
+ c.Context.ReleaseSurface()
+ if c.eglWin != nil {
+ C.wl_egl_window_destroy(c.eglWin)
+ c.eglWin = nil
+ }
+ surf, width, height := c.win.surface()
+ if surf == nil {
+ return errors.New("wayland: no surface")
+ }
+ eglWin := C.wl_egl_window_create(surf, C.int(width), C.int(height))
+ if eglWin == nil {
+ return errors.New("wayland: wl_egl_window_create failed")
+ }
+ c.eglWin = eglWin
+ eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin)))
+ if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
+ return err
+ }
+ return c.Context.MakeCurrent()
+}
+
+func (c *context) Lock() {}
+
+func (c *context) Unlock() {}
diff --git a/gio/giold/app/internal/wm/egl_windows.go b/gio/giold/app/internal/wm/egl_windows.go
new file mode 100644
index 0000000..ce7645c
--- /dev/null
+++ b/gio/giold/app/internal/wm/egl_windows.go
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package wm
+
+import (
+ "realy.lol/gio/internal/egl"
+)
+
+type glContext struct {
+ win *window
+ *egl.Context
+}
+
+func init() {
+ drivers = append(drivers, gpuAPI{
+ priority: 2,
+ initializer: func(w *window) (Context, error) {
+ disp := egl.NativeDisplayType(w.HDC())
+ ctx, err := egl.NewContext(disp)
+ if err != nil {
+ return nil, err
+ }
+ return &glContext{win: w, Context: ctx}, nil
+ },
+ })
+}
+
+func (c *glContext) Release() {
+ if c.Context != nil {
+ c.Context.Release()
+ c.Context = nil
+ }
+}
+
+func (c *glContext) MakeCurrent() error {
+ c.Context.ReleaseSurface()
+ win, width, height := c.win.HWND()
+ eglSurf := egl.NativeWindowType(win)
+ if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
+ return err
+ }
+ if err := c.Context.MakeCurrent(); err != nil {
+ return err
+ }
+ c.Context.EnableVSync(true)
+ return nil
+}
+
+func (c *glContext) Lock() {}
+
+func (c *glContext) Unlock() {}
diff --git a/gio/giold/app/internal/wm/egl_x11.go b/gio/giold/app/internal/wm/egl_x11.go
new file mode 100644
index 0000000..556cd78
--- /dev/null
+++ b/gio/giold/app/internal/wm/egl_x11.go
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build (linux && !android && !nox11) || freebsd || openbsd
+// +build linux,!android,!nox11 freebsd openbsd
+
+package wm
+
+import (
+ "unsafe"
+
+ "realy.lol/gio/internal/egl"
+)
+
+type x11Context struct {
+ win *x11Window
+ *egl.Context
+}
+
+func (w *x11Window) NewContext() (Context, error) {
+ disp := egl.NativeDisplayType(unsafe.Pointer(w.display()))
+ ctx, err := egl.NewContext(disp)
+ if err != nil {
+ return nil, err
+ }
+ return &x11Context{win: w, Context: ctx}, nil
+}
+
+func (c *x11Context) Release() {
+ if c.Context != nil {
+ c.Context.Release()
+ c.Context = nil
+ }
+}
+
+func (c *x11Context) MakeCurrent() error {
+ c.Context.ReleaseSurface()
+ win, width, height := c.win.window()
+ eglSurf := egl.NativeWindowType(uintptr(win))
+ if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
+ return err
+ }
+ if err := c.Context.MakeCurrent(); err != nil {
+ return err
+ }
+ c.Context.EnableVSync(true)
+ return nil
+}
+
+func (c *x11Context) Lock() {}
+
+func (c *x11Context) Unlock() {}
diff --git a/gio/giold/app/internal/wm/framework_ios.h b/gio/giold/app/internal/wm/framework_ios.h
new file mode 100644
index 0000000..18e5a02
--- /dev/null
+++ b/gio/giold/app/internal/wm/framework_ios.h
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+#include
+
+@interface GioViewController : UIViewController
+@end
diff --git a/gio/giold/app/internal/wm/gl_ios.go b/gio/giold/app/internal/wm/gl_ios.go
new file mode 100644
index 0000000..b3e7a47
--- /dev/null
+++ b/gio/giold/app/internal/wm/gl_ios.go
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build darwin && ios
+// +build darwin,ios
+
+package wm
+
+/*
+#include
+#include
+#include
+
+__attribute__ ((visibility ("hidden"))) int gio_renderbufferStorage(CFTypeRef ctx, CFTypeRef layer, GLenum buffer);
+__attribute__ ((visibility ("hidden"))) int gio_presentRenderbuffer(CFTypeRef ctx, GLenum buffer);
+__attribute__ ((visibility ("hidden"))) int gio_makeCurrent(CFTypeRef ctx);
+__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createContext(void);
+__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createGLLayer(void);
+*/
+import "C"
+
+import (
+ "errors"
+ "fmt"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/internal/gl"
+)
+
+type context struct {
+ owner *window
+ c *gl.Functions
+ ctx C.CFTypeRef
+ layer C.CFTypeRef
+ init bool
+ frameBuffer gl.Framebuffer
+ colorBuffer, depthBuffer gl.Renderbuffer
+}
+
+func init() {
+ layerFactory = func() uintptr {
+ return uintptr(C.gio_createGLLayer())
+ }
+}
+
+func newContext(w *window) (*context, error) {
+ ctx := C.gio_createContext()
+ if ctx == 0 {
+ return nil, fmt.Errorf("failed to create EAGLContext")
+ }
+ c := &context{
+ ctx: ctx,
+ owner: w,
+ layer: C.CFTypeRef(w.contextLayer()),
+ c: new(gl.Functions),
+ }
+ return c, nil
+}
+
+func (c *context) API() gpu.API {
+ return gpu.OpenGL{}
+}
+
+func (c *context) Release() {
+ if c.ctx == 0 {
+ return
+ }
+ C.gio_renderbufferStorage(c.ctx, 0, C.GLenum(gl.RENDERBUFFER))
+ c.c.DeleteFramebuffer(c.frameBuffer)
+ c.c.DeleteRenderbuffer(c.colorBuffer)
+ c.c.DeleteRenderbuffer(c.depthBuffer)
+ C.gio_makeCurrent(0)
+ C.CFRelease(c.ctx)
+ c.ctx = 0
+}
+
+func (c *context) Present() error {
+ if c.layer == 0 {
+ panic("context is not active")
+ }
+ // Discard depth buffer as recommended in
+ // https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/WorkingwithEAGLContexts/WorkingwithEAGLContexts.html
+ c.c.BindFramebuffer(gl.FRAMEBUFFER, c.frameBuffer)
+ c.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT)
+ c.c.BindRenderbuffer(gl.RENDERBUFFER, c.colorBuffer)
+ if C.gio_presentRenderbuffer(c.ctx, C.GLenum(gl.RENDERBUFFER)) == 0 {
+ return errors.New("presentRenderBuffer failed")
+ }
+ return nil
+}
+
+func (c *context) Lock() {}
+
+func (c *context) Unlock() {}
+
+func (c *context) MakeCurrent() error {
+ if C.gio_makeCurrent(c.ctx) == 0 {
+ C.CFRelease(c.ctx)
+ c.ctx = 0
+ return errors.New("[EAGLContext setCurrentContext] failed")
+ }
+ if !c.init {
+ c.init = true
+ c.frameBuffer = c.c.CreateFramebuffer()
+ c.colorBuffer = c.c.CreateRenderbuffer()
+ c.depthBuffer = c.c.CreateRenderbuffer()
+ }
+ if !c.owner.isVisible() {
+ // Make sure any in-flight GL commands are complete.
+ c.c.Finish()
+ return nil
+ }
+ currentRB := gl.Renderbuffer{uint(c.c.GetInteger(gl.RENDERBUFFER_BINDING))}
+ c.c.BindRenderbuffer(gl.RENDERBUFFER, c.colorBuffer)
+ if C.gio_renderbufferStorage(c.ctx, c.layer,
+ C.GLenum(gl.RENDERBUFFER)) == 0 {
+ return errors.New("renderbufferStorage failed")
+ }
+ w := c.c.GetRenderbufferParameteri(gl.RENDERBUFFER, gl.RENDERBUFFER_WIDTH)
+ h := c.c.GetRenderbufferParameteri(gl.RENDERBUFFER, gl.RENDERBUFFER_HEIGHT)
+ c.c.BindRenderbuffer(gl.RENDERBUFFER, c.depthBuffer)
+ c.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h)
+ c.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB)
+ c.c.BindFramebuffer(gl.FRAMEBUFFER, c.frameBuffer)
+ c.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
+ gl.RENDERBUFFER, c.colorBuffer)
+ c.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT,
+ gl.RENDERBUFFER, c.depthBuffer)
+ if st := c.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
+ return fmt.Errorf("framebuffer incomplete, status: %#x\n", st)
+ }
+ return nil
+}
+
+func (w *window) NewContext() (Context, error) {
+ return newContext(w)
+}
diff --git a/gio/giold/app/internal/wm/gl_ios.m b/gio/giold/app/internal/wm/gl_ios.m
new file mode 100644
index 0000000..065ea97
--- /dev/null
+++ b/gio/giold/app/internal/wm/gl_ios.m
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build darwin,ios
+
+@import UIKit;
+@import OpenGLES;
+
+#include "_cgo_export.h"
+
+int gio_renderbufferStorage(CFTypeRef ctxRef, CFTypeRef layerRef, GLenum buffer) {
+ EAGLContext *ctx = (__bridge EAGLContext *)ctxRef;
+ CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef;
+ return (int)[ctx renderbufferStorage:buffer fromDrawable:layer];
+}
+
+int gio_presentRenderbuffer(CFTypeRef ctxRef, GLenum buffer) {
+ EAGLContext *ctx = (__bridge EAGLContext *)ctxRef;
+ return (int)[ctx presentRenderbuffer:buffer];
+}
+
+int gio_makeCurrent(CFTypeRef ctxRef) {
+ EAGLContext *ctx = (__bridge EAGLContext *)ctxRef;
+ return (int)[EAGLContext setCurrentContext:ctx];
+}
+
+CFTypeRef gio_createContext(void) {
+ EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
+ if (ctx == nil) {
+ return nil;
+ }
+ return CFBridgingRetain(ctx);
+}
+
+CFTypeRef gio_createGLLayer(void) {
+ CAEAGLLayer *layer = [[CAEAGLLayer layer] init];
+ if (layer == nil) {
+ return nil;
+ }
+ layer.drawableProperties = @{kEAGLDrawablePropertyColorFormat: kEAGLColorFormatSRGBA8};
+ layer.opaque = YES;
+ layer.anchorPoint = CGPointMake(0, 0);
+ return CFBridgingRetain(layer);
+}
diff --git a/gio/giold/app/internal/wm/gl_js.go b/gio/giold/app/internal/wm/gl_js.go
new file mode 100644
index 0000000..0a931b6
--- /dev/null
+++ b/gio/giold/app/internal/wm/gl_js.go
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package wm
+
+import (
+ "errors"
+ "syscall/js"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/internal/gl"
+ "realy.lol/gio/internal/srgb"
+)
+
+type context struct {
+ ctx js.Value
+ cnv js.Value
+ srgbFBO *srgb.FBO
+}
+
+func newContext(w *window) (*context, error) {
+ args := map[string]interface{}{
+ // Enable low latency rendering.
+ // See https://developers.google.com/web/updates/2019/05/desynchronized.
+ "desynchronized": true,
+ "preserveDrawingBuffer": true,
+ }
+ ctx := w.cnv.Call("getContext", "webgl2", args)
+ if ctx.IsNull() {
+ ctx = w.cnv.Call("getContext", "webgl", args)
+ }
+ if ctx.IsNull() {
+ return nil, errors.New("app: webgl is not supported")
+ }
+ c := &context{
+ ctx: ctx,
+ cnv: w.cnv,
+ }
+ return c, nil
+}
+
+func (c *context) API() gpu.API {
+ return gpu.OpenGL{Context: gl.Context(c.ctx)}
+}
+
+func (c *context) Release() {
+ if c.srgbFBO != nil {
+ c.srgbFBO.Release()
+ c.srgbFBO = nil
+ }
+}
+
+func (c *context) Present() error {
+ if c.srgbFBO != nil {
+ c.srgbFBO.Blit()
+ }
+ if c.srgbFBO != nil {
+ c.srgbFBO.AfterPresent()
+ }
+ if c.ctx.Call("isContextLost").Bool() {
+ return errors.New("context lost")
+ }
+ return nil
+}
+
+func (c *context) Lock() {}
+
+func (c *context) Unlock() {}
+
+func (c *context) MakeCurrent() error {
+ if c.srgbFBO == nil {
+ var err error
+ c.srgbFBO, err = srgb.New(gl.Context(c.ctx))
+ if err != nil {
+ c.Release()
+ c.srgbFBO = nil
+ return err
+ }
+ }
+ w, h := c.cnv.Get("width").Int(), c.cnv.Get("height").Int()
+ if err := c.srgbFBO.Refresh(w, h); err != nil {
+ c.Release()
+ return err
+ }
+ return nil
+}
+
+func (w *window) NewContext() (Context, error) {
+ return newContext(w)
+}
diff --git a/gio/giold/app/internal/wm/gl_macos.go b/gio/giold/app/internal/wm/gl_macos.go
new file mode 100644
index 0000000..a95557b
--- /dev/null
+++ b/gio/giold/app/internal/wm/gl_macos.go
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build darwin && !ios
+// +build darwin,!ios
+
+package wm
+
+import (
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/internal/gl"
+)
+
+/*
+#include
+#include
+#include
+#include
+
+__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createGLView(void);
+__attribute__ ((visibility ("hidden"))) CFTypeRef gio_contextForView(CFTypeRef viewRef);
+__attribute__ ((visibility ("hidden"))) void gio_makeCurrentContext(CFTypeRef ctx);
+__attribute__ ((visibility ("hidden"))) void gio_flushContextBuffer(CFTypeRef ctx);
+__attribute__ ((visibility ("hidden"))) void gio_clearCurrentContext(void);
+__attribute__ ((visibility ("hidden"))) void gio_lockContext(CFTypeRef ctxRef);
+__attribute__ ((visibility ("hidden"))) void gio_unlockContext(CFTypeRef ctxRef);
+*/
+import "C"
+
+type context struct {
+ c *gl.Functions
+ ctx C.CFTypeRef
+ view C.CFTypeRef
+}
+
+func init() {
+ viewFactory = func() C.CFTypeRef {
+ return C.gio_createGLView()
+ }
+}
+
+func newContext(w *window) (*context, error) {
+ view := w.contextView()
+ ctx := C.gio_contextForView(view)
+ c := &context{
+ ctx: ctx,
+ view: view,
+ }
+ return c, nil
+}
+
+func (c *context) API() gpu.API {
+ return gpu.OpenGL{}
+}
+
+func (c *context) Release() {
+ c.Lock()
+ defer c.Unlock()
+ C.gio_clearCurrentContext()
+ // We could release the context with [view clearGLContext]
+ // and rely on [view openGLContext] auto-creating a new context.
+ // However that second context is not properly set up by
+ // OpenGLContextView, so we'll stay on the safe side and keep
+ // the first context around.
+}
+
+func (c *context) Present() error {
+ // Assume the caller already locked the context.
+ C.glFlush()
+ return nil
+}
+
+func (c *context) Lock() {
+ C.gio_lockContext(c.ctx)
+}
+
+func (c *context) Unlock() {
+ C.gio_unlockContext(c.ctx)
+}
+
+func (c *context) MakeCurrent() error {
+ c.Lock()
+ defer c.Unlock()
+ C.gio_makeCurrentContext(c.ctx)
+ return nil
+}
+
+func (w *window) NewContext() (Context, error) {
+ return newContext(w)
+}
diff --git a/gio/giold/app/internal/wm/gl_macos.m b/gio/giold/app/internal/wm/gl_macos.m
new file mode 100644
index 0000000..576aa40
--- /dev/null
+++ b/gio/giold/app/internal/wm/gl_macos.m
@@ -0,0 +1,147 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build darwin,!ios
+
+@import AppKit;
+
+#include
+#include
+#include
+#include "_cgo_export.h"
+
+static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
+ NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil];
+ if (!event.hasPreciseScrollingDeltas) {
+ // dx and dy are in rows and columns.
+ dx *= 10;
+ dy *= 10;
+ }
+ gio_onMouse((__bridge CFTypeRef)view, typ, [NSEvent pressedMouseButtons], p.x, p.y, dx, dy, [event timestamp], [event modifierFlags]);
+}
+
+@interface GioView : NSOpenGLView
+@end
+
+@implementation GioView
+- (instancetype)initWithFrame:(NSRect)frameRect
+ pixelFormat:(NSOpenGLPixelFormat *)format {
+ return [super initWithFrame:frameRect pixelFormat:format];
+}
+- (void)prepareOpenGL {
+ [super prepareOpenGL];
+ // Bind a default VBA to emulate OpenGL ES 2.
+ GLuint defVBA;
+ glGenVertexArrays(1, &defVBA);
+ glBindVertexArray(defVBA);
+ glEnable(GL_FRAMEBUFFER_SRGB);
+}
+- (BOOL)isFlipped {
+ return YES;
+}
+- (void)update {
+ [super update];
+ [self setNeedsDisplay:YES];
+}
+- (void)drawRect:(NSRect)r {
+ gio_onDraw((__bridge CFTypeRef)self);
+}
+- (void)mouseDown:(NSEvent *)event {
+ handleMouse(self, event, GIO_MOUSE_DOWN, 0, 0);
+}
+- (void)mouseUp:(NSEvent *)event {
+ handleMouse(self, event, GIO_MOUSE_UP, 0, 0);
+}
+- (void)middleMouseDown:(NSEvent *)event {
+ handleMouse(self, event, GIO_MOUSE_DOWN, 0, 0);
+}
+- (void)middletMouseUp:(NSEvent *)event {
+ handleMouse(self, event, GIO_MOUSE_UP, 0, 0);
+}
+- (void)rightMouseDown:(NSEvent *)event {
+ handleMouse(self, event, GIO_MOUSE_DOWN, 0, 0);
+}
+- (void)rightMouseUp:(NSEvent *)event {
+ handleMouse(self, event, GIO_MOUSE_UP, 0, 0);
+}
+- (void)mouseMoved:(NSEvent *)event {
+ handleMouse(self, event, GIO_MOUSE_MOVE, 0, 0);
+}
+- (void)mouseDragged:(NSEvent *)event {
+ handleMouse(self, event, GIO_MOUSE_MOVE, 0, 0);
+}
+- (void)scrollWheel:(NSEvent *)event {
+ CGFloat dx = -event.scrollingDeltaX;
+ CGFloat dy = -event.scrollingDeltaY;
+ handleMouse(self, event, GIO_MOUSE_SCROLL, dx, dy);
+}
+- (void)keyDown:(NSEvent *)event {
+ NSString *keys = [event charactersIgnoringModifiers];
+ gio_onKeys((__bridge CFTypeRef)self, (char *)[keys UTF8String], [event timestamp], [event modifierFlags], true);
+ [self interpretKeyEvents:[NSArray arrayWithObject:event]];
+}
+- (void)keyUp:(NSEvent *)event {
+ NSString *keys = [event charactersIgnoringModifiers];
+ gio_onKeys((__bridge CFTypeRef)self, (char *)[keys UTF8String], [event timestamp], [event modifierFlags], false);
+}
+- (void)insertText:(id)string {
+ const char *utf8 = [string UTF8String];
+ gio_onText((__bridge CFTypeRef)self, (char *)utf8);
+}
+- (void)doCommandBySelector:(SEL)sel {
+ // Don't pass commands up the responder chain.
+ // They will end up in a beep.
+}
+@end
+
+CFTypeRef gio_createGLView(void) {
+ @autoreleasepool {
+ NSOpenGLPixelFormatAttribute attr[] = {
+ NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
+ NSOpenGLPFAColorSize, 24,
+ NSOpenGLPFADepthSize, 16,
+ NSOpenGLPFAAccelerated,
+ // Opt-in to automatic GPU switching. CGL-only property.
+ kCGLPFASupportsAutomaticGraphicsSwitching,
+ NSOpenGLPFAAllowOfflineRenderers,
+ 0
+ };
+ id pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
+
+ NSRect frame = NSMakeRect(0, 0, 0, 0);
+ GioView* view = [[GioView alloc] initWithFrame:frame pixelFormat:pixFormat];
+
+ [view setWantsBestResolutionOpenGLSurface:YES];
+ [view setWantsLayer:YES]; // The default in Mojave.
+
+ return CFBridgingRetain(view);
+ }
+}
+
+void gio_setNeedsDisplay(CFTypeRef viewRef) {
+ NSOpenGLView *view = (__bridge NSOpenGLView *)viewRef;
+ [view setNeedsDisplay:YES];
+}
+
+CFTypeRef gio_contextForView(CFTypeRef viewRef) {
+ NSOpenGLView *view = (__bridge NSOpenGLView *)viewRef;
+ return (__bridge CFTypeRef)view.openGLContext;
+}
+
+void gio_clearCurrentContext(void) {
+ [NSOpenGLContext clearCurrentContext];
+}
+
+void gio_makeCurrentContext(CFTypeRef ctxRef) {
+ NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef;
+ [ctx makeCurrentContext];
+}
+
+void gio_lockContext(CFTypeRef ctxRef) {
+ NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef;
+ CGLLockContext([ctx CGLContextObj]);
+}
+
+void gio_unlockContext(CFTypeRef ctxRef) {
+ NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef;
+ CGLUnlockContext([ctx CGLContextObj]);
+}
diff --git a/gio/giold/app/internal/wm/os_android.c b/gio/giold/app/internal/wm/os_android.c
new file mode 100644
index 0000000..8a2c62d
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_android.c
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+#include
+#include "_cgo_export.h"
+
+jint gio_jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version) {
+ return (*vm)->GetEnv(vm, (void **)env, version);
+}
+
+jint gio_jni_GetJavaVM(JNIEnv *env, JavaVM **jvm) {
+ return (*env)->GetJavaVM(env, jvm);
+}
+
+jint gio_jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) {
+ return (*vm)->AttachCurrentThread(vm, p_env, thr_args);
+}
+
+jint gio_jni_DetachCurrentThread(JavaVM *vm) {
+ return (*vm)->DetachCurrentThread(vm);
+}
+
+jobject gio_jni_NewGlobalRef(JNIEnv *env, jobject obj) {
+ return (*env)->NewGlobalRef(env, obj);
+}
+
+void gio_jni_DeleteGlobalRef(JNIEnv *env, jobject obj) {
+ (*env)->DeleteGlobalRef(env, obj);
+}
+
+jclass gio_jni_GetObjectClass(JNIEnv *env, jobject obj) {
+ return (*env)->GetObjectClass(env, obj);
+}
+
+jmethodID gio_jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
+ return (*env)->GetMethodID(env, clazz, name, sig);
+}
+
+jmethodID gio_jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) {
+ return (*env)->GetStaticMethodID(env, clazz, name, sig);
+}
+
+jfloat gio_jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID) {
+ return (*env)->CallFloatMethod(env, obj, methodID);
+}
+
+jint gio_jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID) {
+ return (*env)->CallIntMethod(env, obj, methodID);
+}
+
+void gio_jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args) {
+ (*env)->CallStaticVoidMethodA(env, cls, methodID, args);
+}
+
+void gio_jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args) {
+ (*env)->CallVoidMethodA(env, obj, methodID, args);
+}
+
+jbyte *gio_jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr) {
+ return (*env)->GetByteArrayElements(env, arr, NULL);
+}
+
+void gio_jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes) {
+ (*env)->ReleaseByteArrayElements(env, arr, bytes, JNI_ABORT);
+}
+
+jsize gio_jni_GetArrayLength(JNIEnv *env, jbyteArray arr) {
+ return (*env)->GetArrayLength(env, arr);
+}
+
+jstring gio_jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len) {
+ return (*env)->NewString(env, unicodeChars, len);
+}
+
+jsize gio_jni_GetStringLength(JNIEnv *env, jstring str) {
+ return (*env)->GetStringLength(env, str);
+}
+
+const jchar *gio_jni_GetStringChars(JNIEnv *env, jstring str) {
+ return (*env)->GetStringChars(env, str, NULL);
+}
+
+jthrowable gio_jni_ExceptionOccurred(JNIEnv *env) {
+ return (*env)->ExceptionOccurred(env);
+}
+
+void gio_jni_ExceptionClear(JNIEnv *env) {
+ (*env)->ExceptionClear(env);
+}
+
+jobject gio_jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) {
+ return (*env)->CallObjectMethodA(env, obj, method, args);
+}
+
+jobject gio_jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) {
+ return (*env)->CallStaticObjectMethodA(env, cls, method, args);
+}
diff --git a/gio/giold/app/internal/wm/os_android.go b/gio/giold/app/internal/wm/os_android.go
new file mode 100644
index 0000000..a690c42
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_android.go
@@ -0,0 +1,785 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package wm
+
+/*
+#cgo CFLAGS: -Werror
+#cgo LDFLAGS: -landroid
+
+#include
+#include
+#include
+#include
+#include
+
+__attribute__ ((visibility ("hidden"))) jint gio_jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version);
+__attribute__ ((visibility ("hidden"))) jint gio_jni_GetJavaVM(JNIEnv *env, JavaVM **jvm);
+__attribute__ ((visibility ("hidden"))) jint gio_jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args);
+__attribute__ ((visibility ("hidden"))) jint gio_jni_DetachCurrentThread(JavaVM *vm);
+
+__attribute__ ((visibility ("hidden"))) jobject gio_jni_NewGlobalRef(JNIEnv *env, jobject obj);
+__attribute__ ((visibility ("hidden"))) void gio_jni_DeleteGlobalRef(JNIEnv *env, jobject obj);
+__attribute__ ((visibility ("hidden"))) jclass gio_jni_GetObjectClass(JNIEnv *env, jobject obj);
+__attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
+__attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
+__attribute__ ((visibility ("hidden"))) jfloat gio_jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID);
+__attribute__ ((visibility ("hidden"))) jint gio_jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID);
+__attribute__ ((visibility ("hidden"))) void gio_jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);
+__attribute__ ((visibility ("hidden"))) void gio_jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
+__attribute__ ((visibility ("hidden"))) jbyte *gio_jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr);
+__attribute__ ((visibility ("hidden"))) void gio_jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes);
+__attribute__ ((visibility ("hidden"))) jsize gio_jni_GetArrayLength(JNIEnv *env, jbyteArray arr);
+__attribute__ ((visibility ("hidden"))) jstring gio_jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len);
+__attribute__ ((visibility ("hidden"))) jsize gio_jni_GetStringLength(JNIEnv *env, jstring str);
+__attribute__ ((visibility ("hidden"))) const jchar *gio_jni_GetStringChars(JNIEnv *env, jstring str);
+__attribute__ ((visibility ("hidden"))) jthrowable gio_jni_ExceptionOccurred(JNIEnv *env);
+__attribute__ ((visibility ("hidden"))) void gio_jni_ExceptionClear(JNIEnv *env);
+__attribute__ ((visibility ("hidden"))) jobject gio_jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args);
+__attribute__ ((visibility ("hidden"))) jobject gio_jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args);
+*/
+import "C"
+
+import (
+ "errors"
+ "fmt"
+ "image"
+ "reflect"
+ "runtime"
+ "runtime/debug"
+ "sync"
+ "time"
+ "unicode/utf16"
+ "unsafe"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/unit"
+)
+
+type window struct {
+ callbacks Callbacks
+
+ view C.jobject
+
+ dpi int
+ fontScale float32
+ insets system.Insets
+
+ stage system.Stage
+ started bool
+
+ state, newState windowState
+
+ // mu protects the fields following it.
+ mu sync.Mutex
+ win *C.ANativeWindow
+ animating bool
+}
+
+// windowState tracks the View or Activity specific state lost when Android
+// re-creates our Activity.
+type windowState struct {
+ cursor *pointer.CursorName
+}
+
+// gioView hold cached JNI methods for GioView.
+var gioView struct {
+ once sync.Once
+ getDensity C.jmethodID
+ getFontScale C.jmethodID
+ showTextInput C.jmethodID
+ hideTextInput C.jmethodID
+ postFrameCallback C.jmethodID
+ setCursor C.jmethodID
+}
+
+// ViewEvent is sent whenever the Window's underlying Android view
+// changes.
+type ViewEvent struct {
+ // View is a JNI global reference to the android.view.View
+ // instance backing the Window. The reference is valid until
+ // the next ViewEvent is received.
+ // A zero View means that there is currently no view attached.
+ View uintptr
+}
+
+type jvalue uint64 // The largest JNI type fits in 64 bits.
+
+var dataDirChan = make(chan string, 1)
+
+var android struct {
+ // mu protects all fields of this structure. However, once a
+ // non-nil jvm is returned from javaVM, all the other fields may
+ // be accessed unlocked.
+ mu sync.Mutex
+ jvm *C.JavaVM
+
+ // appCtx is the global Android App context.
+ appCtx C.jobject
+ // gioCls is the class of the Gio class.
+ gioCls C.jclass
+
+ mwriteClipboard C.jmethodID
+ mreadClipboard C.jmethodID
+ mwakeupMainThread C.jmethodID
+}
+
+// view maps from GioView JNI refenreces to windows.
+var views = make(map[C.jlong]*window)
+
+// windows maps from Callbacks to windows
+var windows = make(map[Callbacks]*window)
+
+var mainWindow = newWindowRendezvous()
+
+var mainFuncs = make(chan func(env *C.JNIEnv), 1)
+
+func getMethodID(env *C.JNIEnv, class C.jclass,
+ method, sig string) C.jmethodID {
+ m := C.CString(method)
+ defer C.free(unsafe.Pointer(m))
+ s := C.CString(sig)
+ defer C.free(unsafe.Pointer(s))
+ jm := C.gio_jni_GetMethodID(env, class, m, s)
+ if err := exception(env); err != nil {
+ panic(err)
+ }
+ return jm
+}
+
+func getStaticMethodID(env *C.JNIEnv, class C.jclass,
+ method, sig string) C.jmethodID {
+ m := C.CString(method)
+ defer C.free(unsafe.Pointer(m))
+ s := C.CString(sig)
+ defer C.free(unsafe.Pointer(s))
+ jm := C.gio_jni_GetStaticMethodID(env, class, m, s)
+ if err := exception(env); err != nil {
+ panic(err)
+ }
+ return jm
+}
+
+//export Java_org_gioui_Gio_runGoMain
+func Java_org_gioui_Gio_runGoMain(env *C.JNIEnv, class C.jclass,
+ jdataDir C.jbyteArray, context C.jobject) {
+ initJVM(env, class, context)
+ dirBytes := C.gio_jni_GetByteArrayElements(env, jdataDir)
+ if dirBytes == nil {
+ panic("runGoMain: GetByteArrayElements failed")
+ }
+ n := C.gio_jni_GetArrayLength(env, jdataDir)
+ dataDir := C.GoStringN((*C.char)(unsafe.Pointer(dirBytes)), n)
+ dataDirChan <- dataDir
+ C.gio_jni_ReleaseByteArrayElements(env, jdataDir, dirBytes)
+
+ runMain()
+}
+
+func initJVM(env *C.JNIEnv, gio C.jclass, ctx C.jobject) {
+ android.mu.Lock()
+ defer android.mu.Unlock()
+ if res := C.gio_jni_GetJavaVM(env, &android.jvm); res != 0 {
+ panic("gio: GetJavaVM failed")
+ }
+ android.appCtx = C.gio_jni_NewGlobalRef(env, ctx)
+ android.gioCls = C.jclass(C.gio_jni_NewGlobalRef(env, C.jobject(gio)))
+ android.mwriteClipboard = getStaticMethodID(env, gio, "writeClipboard",
+ "(Landroid/content/Context;Ljava/lang/String;)V")
+ android.mreadClipboard = getStaticMethodID(env, gio, "readClipboard",
+ "(Landroid/content/Context;)Ljava/lang/String;")
+ android.mwakeupMainThread = getStaticMethodID(env, gio, "wakeupMainThread",
+ "()V")
+}
+
+func JavaVM() uintptr {
+ jvm := javaVM()
+ return uintptr(unsafe.Pointer(jvm))
+}
+
+func javaVM() *C.JavaVM {
+ android.mu.Lock()
+ defer android.mu.Unlock()
+ return android.jvm
+}
+
+func AppContext() uintptr {
+ android.mu.Lock()
+ defer android.mu.Unlock()
+ return uintptr(android.appCtx)
+}
+
+func GetDataDir() string {
+ return <-dataDirChan
+}
+
+//export Java_org_gioui_GioView_onCreateView
+func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass,
+ view C.jobject) C.jlong {
+ gioView.once.Do(func() {
+ m := &gioView
+ m.getDensity = getMethodID(env, class, "getDensity", "()I")
+ m.getFontScale = getMethodID(env, class, "getFontScale", "()F")
+ m.showTextInput = getMethodID(env, class, "showTextInput", "()V")
+ m.hideTextInput = getMethodID(env, class, "hideTextInput", "()V")
+ m.postFrameCallback = getMethodID(env, class, "postFrameCallback",
+ "()V")
+ m.setCursor = getMethodID(env, class, "setCursor", "(I)V")
+ })
+ view = C.gio_jni_NewGlobalRef(env, view)
+ wopts := <-mainWindow.out
+ w, ok := windows[wopts.window]
+ if !ok {
+ w = &window{
+ callbacks: wopts.window,
+ }
+ windows[wopts.window] = w
+ }
+ w.callbacks.SetDriver(w)
+ w.view = view
+ handle := C.jlong(view)
+ views[handle] = w
+ w.loadConfig(env, class)
+ applyStateDiff(env, view, windowState{}, w.state)
+ w.setStage(system.StagePaused)
+ w.callbacks.Event(ViewEvent{View: uintptr(view)})
+ return handle
+}
+
+//export Java_org_gioui_GioView_onDestroyView
+func Java_org_gioui_GioView_onDestroyView(env *C.JNIEnv, class C.jclass,
+ handle C.jlong) {
+ w := views[handle]
+ w.callbacks.Event(ViewEvent{View: 0})
+ w.callbacks.SetDriver(nil)
+ delete(views, handle)
+ C.gio_jni_DeleteGlobalRef(env, w.view)
+ w.view = 0
+}
+
+//export Java_org_gioui_GioView_onStopView
+func Java_org_gioui_GioView_onStopView(env *C.JNIEnv, class C.jclass,
+ handle C.jlong) {
+ w := views[handle]
+ w.started = false
+ w.setStage(system.StagePaused)
+}
+
+//export Java_org_gioui_GioView_onStartView
+func Java_org_gioui_GioView_onStartView(env *C.JNIEnv, class C.jclass,
+ handle C.jlong) {
+ w := views[handle]
+ w.started = true
+ if w.aNativeWindow() != nil {
+ w.setVisible()
+ }
+}
+
+//export Java_org_gioui_GioView_onSurfaceDestroyed
+func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass,
+ handle C.jlong) {
+ w := views[handle]
+ w.mu.Lock()
+ w.win = nil
+ w.mu.Unlock()
+ w.setStage(system.StagePaused)
+}
+
+//export Java_org_gioui_GioView_onSurfaceChanged
+func Java_org_gioui_GioView_onSurfaceChanged(env *C.JNIEnv, class C.jclass,
+ handle C.jlong, surf C.jobject) {
+ w := views[handle]
+ w.mu.Lock()
+ w.win = C.ANativeWindow_fromSurface(env, surf)
+ w.mu.Unlock()
+ if w.started {
+ w.setVisible()
+ }
+}
+
+//export Java_org_gioui_GioView_onLowMemory
+func Java_org_gioui_GioView_onLowMemory() {
+ runtime.GC()
+ debug.FreeOSMemory()
+}
+
+//export Java_org_gioui_GioView_onConfigurationChanged
+func Java_org_gioui_GioView_onConfigurationChanged(env *C.JNIEnv,
+ class C.jclass, view C.jlong) {
+ w := views[view]
+ w.loadConfig(env, class)
+ if w.stage >= system.StageRunning {
+ w.draw(true)
+ }
+}
+
+//export Java_org_gioui_GioView_onFrameCallback
+func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass,
+ view C.jlong, nanos C.jlong) {
+ w, exist := views[view]
+ if !exist {
+ return
+ }
+ if w.stage < system.StageRunning {
+ return
+ }
+ w.mu.Lock()
+ anim := w.animating
+ w.mu.Unlock()
+ if anim {
+ runInJVM(javaVM(), func(env *C.JNIEnv) {
+ callVoidMethod(env, w.view, gioView.postFrameCallback)
+ })
+ w.draw(false)
+ }
+}
+
+//export Java_org_gioui_GioView_onBack
+func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass,
+ view C.jlong) C.jboolean {
+ w := views[view]
+ ev := &system.CommandEvent{Type: system.CommandBack}
+ w.callbacks.Event(ev)
+ if ev.Cancel {
+ return C.JNI_TRUE
+ }
+ return C.JNI_FALSE
+}
+
+//export Java_org_gioui_GioView_onFocusChange
+func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass,
+ view C.jlong, focus C.jboolean) {
+ w := views[view]
+ w.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE})
+}
+
+//export Java_org_gioui_GioView_onWindowInsets
+func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass,
+ view C.jlong, top, right, bottom, left C.jint) {
+ w := views[view]
+ w.insets = system.Insets{
+ Top: unit.Px(float32(top)),
+ Right: unit.Px(float32(right)),
+ Bottom: unit.Px(float32(bottom)),
+ Left: unit.Px(float32(left)),
+ }
+ if w.stage >= system.StageRunning {
+ w.draw(true)
+ }
+}
+
+func (w *window) setVisible() {
+ win := w.aNativeWindow()
+ width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win)
+ if width == 0 || height == 0 {
+ return
+ }
+ w.setStage(system.StageRunning)
+ w.draw(true)
+}
+
+func (w *window) setStage(stage system.Stage) {
+ if stage == w.stage {
+ return
+ }
+ w.stage = stage
+ w.callbacks.Event(system.StageEvent{stage})
+}
+
+func (w *window) nativeWindow(visID int) (*C.ANativeWindow, int, int) {
+ win := w.aNativeWindow()
+ var width, height int
+ if win != nil {
+ if C.ANativeWindow_setBuffersGeometry(win, 0, 0,
+ C.int32_t(visID)) != 0 {
+ panic(errors.New("ANativeWindow_setBuffersGeometry failed"))
+ }
+ w, h := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win)
+ width, height = int(w), int(h)
+ }
+ return win, width, height
+}
+
+func (w *window) aNativeWindow() *C.ANativeWindow {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ return w.win
+}
+
+func (w *window) loadConfig(env *C.JNIEnv, class C.jclass) {
+ dpi := int(C.gio_jni_CallIntMethod(env, w.view, gioView.getDensity))
+ w.fontScale = float32(C.gio_jni_CallFloatMethod(env, w.view,
+ gioView.getFontScale))
+ switch dpi {
+ case C.ACONFIGURATION_DENSITY_NONE,
+ C.ACONFIGURATION_DENSITY_DEFAULT,
+ C.ACONFIGURATION_DENSITY_ANY:
+ // Assume standard density.
+ w.dpi = C.ACONFIGURATION_DENSITY_MEDIUM
+ default:
+ w.dpi = int(dpi)
+ }
+}
+
+func (w *window) SetAnimating(anim bool) {
+ w.mu.Lock()
+ w.animating = anim
+ w.mu.Unlock()
+ if anim {
+ runOnMain(func(env *C.JNIEnv) {
+ if w.view == 0 {
+ // View was destroyed while switching to main thread.
+ return
+ }
+ callVoidMethod(env, w.view, gioView.postFrameCallback)
+ })
+ }
+}
+
+func (w *window) draw(sync bool) {
+ win := w.aNativeWindow()
+ width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win)
+ if width == 0 || height == 0 {
+ return
+ }
+ const inchPrDp = 1.0 / 160
+ ppdp := float32(w.dpi) * inchPrDp
+ w.callbacks.Event(FrameEvent{
+ FrameEvent: system.FrameEvent{
+ Now: time.Now(),
+ Size: image.Point{
+ X: int(width),
+ Y: int(height),
+ },
+ Insets: w.insets,
+ Metric: unit.Metric{
+ PxPerDp: ppdp,
+ PxPerSp: w.fontScale * ppdp,
+ },
+ },
+ Sync: sync,
+ })
+}
+
+type keyMapper func(devId, keyCode C.int32_t) rune
+
+func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) {
+ if jvm == nil {
+ panic("nil JVM")
+ }
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+ var env *C.JNIEnv
+ if res := C.gio_jni_GetEnv(jvm, &env, C.JNI_VERSION_1_6); res != C.JNI_OK {
+ if res != C.JNI_EDETACHED {
+ panic(fmt.Errorf("JNI GetEnv failed with error %d", res))
+ }
+ if C.gio_jni_AttachCurrentThread(jvm, &env, nil) != C.JNI_OK {
+ panic(errors.New("runInJVM: AttachCurrentThread failed"))
+ }
+ defer C.gio_jni_DetachCurrentThread(jvm)
+ }
+
+ f(env)
+}
+
+func convertKeyCode(code C.jint) (string, bool) {
+ var n string
+ switch code {
+ case C.AKEYCODE_DPAD_UP:
+ n = key.NameUpArrow
+ case C.AKEYCODE_DPAD_DOWN:
+ n = key.NameDownArrow
+ case C.AKEYCODE_DPAD_LEFT:
+ n = key.NameLeftArrow
+ case C.AKEYCODE_DPAD_RIGHT:
+ n = key.NameRightArrow
+ case C.AKEYCODE_FORWARD_DEL:
+ n = key.NameDeleteForward
+ case C.AKEYCODE_DEL:
+ n = key.NameDeleteBackward
+ case C.AKEYCODE_NUMPAD_ENTER:
+ n = key.NameEnter
+ case C.AKEYCODE_ENTER:
+ n = key.NameEnter
+ default:
+ return "", false
+ }
+ return n, true
+}
+
+//export Java_org_gioui_GioView_onKeyEvent
+func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass,
+ handle C.jlong, keyCode, r C.jint, t C.jlong) {
+ w := views[handle]
+ if n, ok := convertKeyCode(keyCode); ok {
+ w.callbacks.Event(key.Event{Name: n})
+ }
+ if r != 0 {
+ w.callbacks.Event(key.EditEvent{Text: string(rune(r))})
+ }
+}
+
+//export Java_org_gioui_GioView_onTouchEvent
+func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass,
+ handle C.jlong, action, pointerID, tool C.jint,
+ x, y, scrollX, scrollY C.jfloat, jbtns C.jint, t C.jlong) {
+ w := views[handle]
+ var typ pointer.Type
+ switch action {
+ case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN:
+ typ = pointer.Press
+ case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP:
+ typ = pointer.Release
+ case C.AMOTION_EVENT_ACTION_CANCEL:
+ typ = pointer.Cancel
+ case C.AMOTION_EVENT_ACTION_MOVE:
+ typ = pointer.Move
+ case C.AMOTION_EVENT_ACTION_SCROLL:
+ typ = pointer.Scroll
+ default:
+ return
+ }
+ var src pointer.Source
+ var btns pointer.Buttons
+ if jbtns&C.AMOTION_EVENT_BUTTON_PRIMARY != 0 {
+ btns |= pointer.ButtonPrimary
+ }
+ if jbtns&C.AMOTION_EVENT_BUTTON_SECONDARY != 0 {
+ btns |= pointer.ButtonSecondary
+ }
+ if jbtns&C.AMOTION_EVENT_BUTTON_TERTIARY != 0 {
+ btns |= pointer.ButtonTertiary
+ }
+ switch tool {
+ case C.AMOTION_EVENT_TOOL_TYPE_FINGER:
+ src = pointer.Touch
+ case C.AMOTION_EVENT_TOOL_TYPE_MOUSE:
+ src = pointer.Mouse
+ case C.AMOTION_EVENT_TOOL_TYPE_UNKNOWN:
+ // For example, triggered via 'adb shell input tap'.
+ // Instead of discarding it, treat it as a touch event.
+ src = pointer.Touch
+ default:
+ return
+ }
+ w.callbacks.Event(pointer.Event{
+ Type: typ,
+ Source: src,
+ Buttons: btns,
+ PointerID: pointer.ID(pointerID),
+ Time: time.Duration(t) * time.Millisecond,
+ Position: f32.Point{X: float32(x), Y: float32(y)},
+ Scroll: f32.Pt(float32(scrollX), float32(scrollY)),
+ })
+}
+
+func (w *window) ShowTextInput(show bool) {
+ runOnMain(func(env *C.JNIEnv) {
+ if w.view == 0 {
+ return
+ }
+ if show {
+ callVoidMethod(env, w.view, gioView.showTextInput)
+ } else {
+ callVoidMethod(env, w.view, gioView.hideTextInput)
+ }
+ })
+}
+
+func javaString(env *C.JNIEnv, str string) C.jstring {
+ if str == "" {
+ return 0
+ }
+ utf16Chars := utf16.Encode([]rune(str))
+ return C.gio_jni_NewString(env, (*C.jchar)(unsafe.Pointer(&utf16Chars[0])),
+ C.int(len(utf16Chars)))
+}
+
+func varArgs(args []jvalue) *C.jvalue {
+ if len(args) == 0 {
+ return nil
+ }
+ return (*C.jvalue)(unsafe.Pointer(&args[0]))
+}
+
+func callStaticVoidMethod(env *C.JNIEnv, cls C.jclass, method C.jmethodID,
+ args ...jvalue) error {
+ C.gio_jni_CallStaticVoidMethodA(env, cls, method, varArgs(args))
+ return exception(env)
+}
+
+func callStaticObjectMethod(env *C.JNIEnv, cls C.jclass, method C.jmethodID,
+ args ...jvalue) (C.jobject, error) {
+ res := C.gio_jni_CallStaticObjectMethodA(env, cls, method, varArgs(args))
+ return res, exception(env)
+}
+
+func callVoidMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID,
+ args ...jvalue) error {
+ C.gio_jni_CallVoidMethodA(env, obj, method, varArgs(args))
+ return exception(env)
+}
+
+func callObjectMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID,
+ args ...jvalue) (C.jobject, error) {
+ res := C.gio_jni_CallObjectMethodA(env, obj, method, varArgs(args))
+ return res, exception(env)
+}
+
+// exception returns an error corresponding to the pending
+// exception, or nil if no exception is pending. The pending
+// exception is cleared.
+func exception(env *C.JNIEnv) error {
+ thr := C.gio_jni_ExceptionOccurred(env)
+ if thr == 0 {
+ return nil
+ }
+ C.gio_jni_ExceptionClear(env)
+ cls := getObjectClass(env, C.jobject(thr))
+ toString := getMethodID(env, cls, "toString", "()Ljava/lang/String;")
+ msg, err := callObjectMethod(env, C.jobject(thr), toString)
+ if err != nil {
+ return err
+ }
+ return errors.New(goString(env, C.jstring(msg)))
+}
+
+func getObjectClass(env *C.JNIEnv, obj C.jobject) C.jclass {
+ if obj == 0 {
+ panic("null object")
+ }
+ cls := C.gio_jni_GetObjectClass(env, C.jobject(obj))
+ if err := exception(env); err != nil {
+ // GetObjectClass should never fail.
+ panic(err)
+ }
+ return cls
+}
+
+// goString converts the JVM jstring to a Go string.
+func goString(env *C.JNIEnv, str C.jstring) string {
+ if str == 0 {
+ return ""
+ }
+ strlen := C.gio_jni_GetStringLength(env, C.jstring(str))
+ chars := C.gio_jni_GetStringChars(env, C.jstring(str))
+ var utf16Chars []uint16
+ hdr := (*reflect.SliceHeader)(unsafe.Pointer(&utf16Chars))
+ hdr.Data = uintptr(unsafe.Pointer(chars))
+ hdr.Cap = int(strlen)
+ hdr.Len = int(strlen)
+ utf8 := utf16.Decode(utf16Chars)
+ return string(utf8)
+}
+
+func Main() {
+}
+
+func NewWindow(window Callbacks, opts *Options) error {
+ mainWindow.in <- windowAndOptions{window, opts}
+ return <-mainWindow.errs
+}
+
+func (w *window) WriteClipboard(s string) {
+ runOnMain(func(env *C.JNIEnv) {
+ jstr := javaString(env, s)
+ callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard,
+ jvalue(android.appCtx), jvalue(jstr))
+ })
+}
+
+func (w *window) ReadClipboard() {
+ runOnMain(func(env *C.JNIEnv) {
+ c, err := callStaticObjectMethod(env, android.gioCls,
+ android.mreadClipboard,
+ jvalue(android.appCtx))
+ if err != nil {
+ return
+ }
+ content := goString(env, C.jstring(c))
+ w.callbacks.Event(clipboard.Event{Text: content})
+ })
+}
+
+func (w *window) Option(opts *Options) {}
+
+func (w *window) SetCursor(name pointer.CursorName) {
+ w.setState(func(state *windowState) {
+ state.cursor = &name
+ })
+}
+
+// setState adjust the window state on the main thread.
+func (w *window) setState(f func(state *windowState)) {
+ runOnMain(func(env *C.JNIEnv) {
+ f(&w.newState)
+ if w.view == 0 {
+ // No View attached. The state will be applied at next onCreateView.
+ return
+ }
+ old := w.state
+ state := w.newState
+ applyStateDiff(env, w.view, old, state)
+ w.state = state
+ })
+}
+
+func applyStateDiff(env *C.JNIEnv, view C.jobject, old, state windowState) {
+ if state.cursor != nil && old.cursor != state.cursor {
+ setCursor(env, view, *state.cursor)
+ }
+}
+
+func setCursor(env *C.JNIEnv, view C.jobject, name pointer.CursorName) {
+ var curID int
+ switch name {
+ default:
+ fallthrough
+ case pointer.CursorDefault:
+ curID = 1000 // TYPE_ARROW
+ case pointer.CursorText:
+ curID = 1008 // TYPE_TEXT
+ case pointer.CursorPointer:
+ curID = 1002 // TYPE_HAND
+ case pointer.CursorCrossHair:
+ curID = 1007 // TYPE_CROSSHAIR
+ case pointer.CursorColResize:
+ curID = 1014 // TYPE_HORIZONTAL_DOUBLE_ARROW
+ case pointer.CursorRowResize:
+ curID = 1015 // TYPE_VERTICAL_DOUBLE_ARROW
+ case pointer.CursorNone:
+ curID = 0 // TYPE_NULL
+ }
+ callVoidMethod(env, view, gioView.setCursor, jvalue(curID))
+}
+
+// Close the window. Not implemented for Android.
+func (w *window) Close() {}
+
+// runOnMain runs a function on the Java main thread.
+func runOnMain(f func(env *C.JNIEnv)) {
+ go func() {
+ mainFuncs <- f
+ runInJVM(javaVM(), func(env *C.JNIEnv) {
+ callStaticVoidMethod(env, android.gioCls, android.mwakeupMainThread)
+ })
+ }()
+}
+
+//export Java_org_gioui_Gio_scheduleMainFuncs
+func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {
+ for {
+ select {
+ case f := <-mainFuncs:
+ f(env)
+ default:
+ return
+ }
+ }
+}
+
+func (_ ViewEvent) ImplementsEvent() {}
diff --git a/gio/giold/app/internal/wm/os_darwin.go b/gio/giold/app/internal/wm/os_darwin.go
new file mode 100644
index 0000000..9bd7a17
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_darwin.go
@@ -0,0 +1,225 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package wm
+
+/*
+#include
+
+__attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void);
+__attribute__ ((visibility ("hidden"))) NSUInteger gio_nsstringLength(CFTypeRef str);
+__attribute__ ((visibility ("hidden"))) void gio_nsstringGetCharacters(CFTypeRef str, unichar *chars, NSUInteger loc, NSUInteger length);
+__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(void);
+__attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl);
+__attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl);
+__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl);
+__attribute__ ((visibility ("hidden"))) void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did);
+__attribute__ ((visibility ("hidden"))) void gio_hideCursor();
+__attribute__ ((visibility ("hidden"))) void gio_showCursor();
+__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID);
+__attribute__ ((visibility ("hidden"))) bool gio_isMainThread();
+*/
+import "C"
+import (
+ "errors"
+ "sync"
+ "sync/atomic"
+ "time"
+ "unicode/utf16"
+ "unsafe"
+
+ "realy.lol/gio/io/pointer"
+)
+
+// displayLink is the state for a display link (CVDisplayLinkRef on macOS,
+// CADisplayLink on iOS). It runs a state-machine goroutine that keeps the
+// display link running for a while after being stopped to avoid the thread
+// start/stop overhead and because the CVDisplayLink sometimes fails to
+// start, stop and start again within a short duration.
+type displayLink struct {
+ callback func()
+ // states is for starting or stopping the display link.
+ states chan bool
+ // done is closed when the display link is destroyed.
+ done chan struct{}
+ // dids receives the display id when the callback owner is moved
+ // to a different screen.
+ dids chan uint64
+ // running tracks the desired state of the link. running is accessed
+ // with atomic.
+ running uint32
+}
+
+// displayLinks maps CFTypeRefs to *displayLinks.
+var displayLinks sync.Map
+
+var mainFuncs = make(chan func(), 1)
+
+// runOnMain runs the function on the main thread.
+func runOnMain(f func()) {
+ if C.gio_isMainThread() {
+ f()
+ return
+ }
+ go func() {
+ mainFuncs <- f
+ C.gio_wakeupMainThread()
+ }()
+}
+
+//export gio_dispatchMainFuncs
+func gio_dispatchMainFuncs() {
+ for {
+ select {
+ case f := <-mainFuncs:
+ f()
+ default:
+ return
+ }
+ }
+}
+
+// nsstringToString converts a NSString to a Go string, and
+// releases the original string.
+func nsstringToString(str C.CFTypeRef) string {
+ if str == 0 {
+ return ""
+ }
+ defer C.CFRelease(str)
+ n := C.gio_nsstringLength(str)
+ if n == 0 {
+ return ""
+ }
+ chars := make([]uint16, n)
+ C.gio_nsstringGetCharacters(str, (*C.unichar)(unsafe.Pointer(&chars[0])), 0,
+ n)
+ utf8 := utf16.Decode(chars)
+ return string(utf8)
+}
+
+func NewDisplayLink(callback func()) (*displayLink, error) {
+ d := &displayLink{
+ callback: callback,
+ done: make(chan struct{}),
+ states: make(chan bool),
+ dids: make(chan uint64),
+ }
+ dl := C.gio_createDisplayLink()
+ if dl == 0 {
+ return nil, errors.New("app: failed to create display link")
+ }
+ go d.run(dl)
+ return d, nil
+}
+
+func (d *displayLink) run(dl C.CFTypeRef) {
+ defer C.gio_releaseDisplayLink(dl)
+ displayLinks.Store(dl, d)
+ defer displayLinks.Delete(dl)
+ var stopTimer *time.Timer
+ var tchan <-chan time.Time
+ started := false
+ for {
+ select {
+ case <-tchan:
+ tchan = nil
+ started = false
+ C.gio_stopDisplayLink(dl)
+ case start := <-d.states:
+ switch {
+ case !start && tchan == nil:
+ // stopTimeout is the delay before stopping the display link to
+ // avoid the overhead of frequently starting and stopping the
+ // link thread.
+ const stopTimeout = 500 * time.Millisecond
+ if stopTimer == nil {
+ stopTimer = time.NewTimer(stopTimeout)
+ } else {
+ // stopTimer is always drained when tchan == nil.
+ stopTimer.Reset(stopTimeout)
+ }
+ tchan = stopTimer.C
+ atomic.StoreUint32(&d.running, 0)
+ case start:
+ if tchan != nil && !stopTimer.Stop() {
+ <-tchan
+ }
+ tchan = nil
+ atomic.StoreUint32(&d.running, 1)
+ if !started {
+ started = true
+ C.gio_startDisplayLink(dl)
+ }
+ }
+ case did := <-d.dids:
+ C.gio_setDisplayLinkDisplay(dl, C.uint64_t(did))
+ case <-d.done:
+ return
+ }
+ }
+}
+
+func (d *displayLink) Start() {
+ d.states <- true
+}
+
+func (d *displayLink) Stop() {
+ d.states <- false
+}
+
+func (d *displayLink) Close() {
+ close(d.done)
+}
+
+func (d *displayLink) SetDisplayID(did uint64) {
+ d.dids <- did
+}
+
+//export gio_onFrameCallback
+func gio_onFrameCallback(dl C.CFTypeRef) {
+ if d, exists := displayLinks.Load(dl); exists {
+ d := d.(*displayLink)
+ if atomic.LoadUint32(&d.running) != 0 {
+ d.callback()
+ }
+ }
+}
+
+// windowSetCursor updates the cursor from the current one to a new one
+// and returns the new one.
+func windowSetCursor(from, to pointer.CursorName) pointer.CursorName {
+ if from == to {
+ return to
+ }
+ var curID int
+ switch to {
+ default:
+ to = pointer.CursorDefault
+ fallthrough
+ case pointer.CursorDefault:
+ curID = 1
+ case pointer.CursorText:
+ curID = 2
+ case pointer.CursorPointer:
+ curID = 3
+ case pointer.CursorCrossHair:
+ curID = 4
+ case pointer.CursorColResize:
+ curID = 5
+ case pointer.CursorRowResize:
+ curID = 6
+ case pointer.CursorGrab:
+ curID = 7
+ case pointer.CursorNone:
+ runOnMain(func() {
+ C.gio_hideCursor()
+ })
+ return to
+ }
+ runOnMain(func() {
+ if from == pointer.CursorNone {
+ C.gio_showCursor()
+ }
+ C.gio_setCursor(C.NSUInteger(curID))
+ })
+ return to
+}
diff --git a/gio/giold/app/internal/wm/os_darwin.m b/gio/giold/app/internal/wm/os_darwin.m
new file mode 100644
index 0000000..8d37371
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_darwin.m
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+@import Dispatch;
+@import Foundation;
+
+#include "_cgo_export.h"
+
+void gio_wakeupMainThread(void) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ gio_dispatchMainFuncs();
+ });
+}
+
+bool gio_isMainThread() {
+ return [NSThread isMainThread];
+}
+
+NSUInteger gio_nsstringLength(CFTypeRef cstr) {
+ NSString *str = (__bridge NSString *)cstr;
+ return [str length];
+}
+
+void gio_nsstringGetCharacters(CFTypeRef cstr, unichar *chars, NSUInteger loc, NSUInteger length) {
+ NSString *str = (__bridge NSString *)cstr;
+ [str getCharacters:chars range:NSMakeRange(loc, length)];
+}
diff --git a/gio/giold/app/internal/wm/os_ios.go b/gio/giold/app/internal/wm/os_ios.go
new file mode 100644
index 0000000..62d854f
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_ios.go
@@ -0,0 +1,326 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build darwin && ios
+// +build darwin,ios
+
+package wm
+
+/*
+#cgo CFLAGS: -DGLES_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c
+
+#include
+#include
+#include
+
+struct drawParams {
+ CGFloat dpi, sdpi;
+ CGFloat width, height;
+ CGFloat top, right, bottom, left;
+};
+
+__attribute__ ((visibility ("hidden"))) void gio_showTextInput(CFTypeRef viewRef);
+__attribute__ ((visibility ("hidden"))) void gio_hideTextInput(CFTypeRef viewRef);
+__attribute__ ((visibility ("hidden"))) void gio_addLayerToView(CFTypeRef viewRef, CFTypeRef layerRef);
+__attribute__ ((visibility ("hidden"))) void gio_updateView(CFTypeRef viewRef, CFTypeRef layerRef);
+__attribute__ ((visibility ("hidden"))) void gio_removeLayer(CFTypeRef layerRef);
+__attribute__ ((visibility ("hidden"))) struct drawParams gio_viewDrawParams(CFTypeRef viewRef);
+__attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void);
+__attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length);
+*/
+import "C"
+
+import (
+ "image"
+ "runtime"
+ "runtime/debug"
+ "sync/atomic"
+ "time"
+ "unicode/utf16"
+ "unsafe"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/unit"
+)
+
+type window struct {
+ view C.CFTypeRef
+ w Callbacks
+ displayLink *displayLink
+
+ layer C.CFTypeRef
+ visible atomic.Value
+ cursor pointer.CursorName
+
+ pointerMap []C.CFTypeRef
+}
+
+var mainWindow = newWindowRendezvous()
+
+var layerFactory func() uintptr
+
+var views = make(map[C.CFTypeRef]*window)
+
+func init() {
+ // Darwin requires UI operations happen on the main thread only.
+ runtime.LockOSThread()
+}
+
+//export onCreate
+func onCreate(view C.CFTypeRef) {
+ w := &window{
+ view: view,
+ }
+ dl, err := NewDisplayLink(func() {
+ w.draw(false)
+ })
+ if err != nil {
+ panic(err)
+ }
+ w.displayLink = dl
+ wopts := <-mainWindow.out
+ w.w = wopts.window
+ w.w.SetDriver(w)
+ w.visible.Store(false)
+ w.layer = C.CFTypeRef(layerFactory())
+ C.gio_addLayerToView(view, w.layer)
+ views[view] = w
+ w.w.Event(system.StageEvent{Stage: system.StagePaused})
+}
+
+//export gio_onDraw
+func gio_onDraw(view C.CFTypeRef) {
+ w := views[view]
+ w.draw(true)
+}
+
+func (w *window) draw(sync bool) {
+ params := C.gio_viewDrawParams(w.view)
+ if params.width == 0 || params.height == 0 {
+ return
+ }
+ wasVisible := w.isVisible()
+ w.visible.Store(true)
+ C.gio_updateView(w.view, w.layer)
+ if !wasVisible {
+ w.w.Event(system.StageEvent{Stage: system.StageRunning})
+ }
+ const inchPrDp = 1.0 / 163
+ w.w.Event(FrameEvent{
+ FrameEvent: system.FrameEvent{
+ Now: time.Now(),
+ Size: image.Point{
+ X: int(params.width + .5),
+ Y: int(params.height + .5),
+ },
+ Insets: system.Insets{
+ Top: unit.Px(float32(params.top)),
+ Right: unit.Px(float32(params.right)),
+ Bottom: unit.Px(float32(params.bottom)),
+ Left: unit.Px(float32(params.left)),
+ },
+ Metric: unit.Metric{
+ PxPerDp: float32(params.dpi) * inchPrDp,
+ PxPerSp: float32(params.sdpi) * inchPrDp,
+ },
+ },
+ Sync: sync,
+ })
+}
+
+//export onStop
+func onStop(view C.CFTypeRef) {
+ w := views[view]
+ w.visible.Store(false)
+ w.w.Event(system.StageEvent{Stage: system.StagePaused})
+}
+
+//export onDestroy
+func onDestroy(view C.CFTypeRef) {
+ w := views[view]
+ delete(views, view)
+ w.w.Event(system.DestroyEvent{})
+ w.displayLink.Close()
+ C.gio_removeLayer(w.layer)
+ C.CFRelease(w.layer)
+ w.layer = 0
+ w.view = 0
+}
+
+//export onFocus
+func onFocus(view C.CFTypeRef, focus int) {
+ w := views[view]
+ w.w.Event(key.FocusEvent{Focus: focus != 0})
+}
+
+//export onLowMemory
+func onLowMemory() {
+ runtime.GC()
+ debug.FreeOSMemory()
+}
+
+//export onUpArrow
+func onUpArrow(view C.CFTypeRef) {
+ views[view].onKeyCommand(key.NameUpArrow)
+}
+
+//export onDownArrow
+func onDownArrow(view C.CFTypeRef) {
+ views[view].onKeyCommand(key.NameDownArrow)
+}
+
+//export onLeftArrow
+func onLeftArrow(view C.CFTypeRef) {
+ views[view].onKeyCommand(key.NameLeftArrow)
+}
+
+//export onRightArrow
+func onRightArrow(view C.CFTypeRef) {
+ views[view].onKeyCommand(key.NameRightArrow)
+}
+
+//export onDeleteBackward
+func onDeleteBackward(view C.CFTypeRef) {
+ views[view].onKeyCommand(key.NameDeleteBackward)
+}
+
+//export onText
+func onText(view C.CFTypeRef, str *C.char) {
+ w := views[view]
+ w.w.Event(key.EditEvent{
+ Text: C.GoString(str),
+ })
+}
+
+//export onTouch
+func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger,
+ x, y C.CGFloat, ti C.double) {
+ var typ pointer.Type
+ switch phase {
+ case C.UITouchPhaseBegan:
+ typ = pointer.Press
+ case C.UITouchPhaseMoved:
+ typ = pointer.Move
+ case C.UITouchPhaseEnded:
+ typ = pointer.Release
+ case C.UITouchPhaseCancelled:
+ typ = pointer.Cancel
+ default:
+ return
+ }
+ w := views[view]
+ t := time.Duration(float64(ti) * float64(time.Second))
+ p := f32.Point{X: float32(x), Y: float32(y)}
+ w.w.Event(pointer.Event{
+ Type: typ,
+ Source: pointer.Touch,
+ PointerID: w.lookupTouch(last != 0, touchRef),
+ Position: p,
+ Time: t,
+ })
+}
+
+func (w *window) ReadClipboard() {
+ runOnMain(func() {
+ content := nsstringToString(C.gio_readClipboard())
+ w.w.Event(clipboard.Event{Text: content})
+ })
+}
+
+func (w *window) WriteClipboard(s string) {
+ u16 := utf16.Encode([]rune(s))
+ runOnMain(func() {
+ var chars *C.unichar
+ if len(u16) > 0 {
+ chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
+ }
+ C.gio_writeClipboard(chars, C.NSUInteger(len(u16)))
+ })
+}
+
+func (w *window) Option(opts *Options) {}
+
+func (w *window) SetAnimating(anim bool) {
+ v := w.view
+ if v == 0 {
+ return
+ }
+ if anim {
+ w.displayLink.Start()
+ } else {
+ w.displayLink.Stop()
+ }
+}
+
+func (w *window) SetCursor(name pointer.CursorName) {
+ w.cursor = windowSetCursor(w.cursor, name)
+}
+
+func (w *window) onKeyCommand(name string) {
+ w.w.Event(key.Event{
+ Name: name,
+ })
+}
+
+// lookupTouch maps an UITouch pointer value to an index. If
+// last is set, the map is cleared.
+func (w *window) lookupTouch(last bool, touch C.CFTypeRef) pointer.ID {
+ id := -1
+ for i, ref := range w.pointerMap {
+ if ref == touch {
+ id = i
+ break
+ }
+ }
+ if id == -1 {
+ id = len(w.pointerMap)
+ w.pointerMap = append(w.pointerMap, touch)
+ }
+ if last {
+ w.pointerMap = w.pointerMap[:0]
+ }
+ return pointer.ID(id)
+}
+
+func (w *window) contextLayer() uintptr {
+ return uintptr(w.layer)
+}
+
+func (w *window) isVisible() bool {
+ return w.visible.Load().(bool)
+}
+
+func (w *window) ShowTextInput(show bool) {
+ v := w.view
+ if v == 0 {
+ return
+ }
+ C.CFRetain(v)
+ runOnMain(func() {
+ defer C.CFRelease(v)
+ if show {
+ C.gio_showTextInput(w.view)
+ } else {
+ C.gio_hideTextInput(w.view)
+ }
+ })
+}
+
+// Close the window. Not implemented for iOS.
+func (w *window) Close() {}
+
+func NewWindow(win Callbacks, opts *Options) error {
+ mainWindow.in <- windowAndOptions{win, opts}
+ return <-mainWindow.errs
+}
+
+func Main() {
+}
+
+//export gio_runMain
+func gio_runMain() {
+ runMain()
+}
diff --git a/gio/giold/app/internal/wm/os_ios.m b/gio/giold/app/internal/wm/os_ios.m
new file mode 100644
index 0000000..f1e556d
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_ios.m
@@ -0,0 +1,340 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build darwin,ios
+
+@import UIKit;
+
+#include
+#include "_cgo_export.h"
+#include "framework_ios.h"
+
+@interface GioView: UIView
+@end
+
+@implementation GioViewController
+
+CGFloat _keyboardHeight;
+
+- (void)loadView {
+ gio_runMain();
+
+ CGRect zeroFrame = CGRectMake(0, 0, 0, 0);
+ self.view = [[UIView alloc] initWithFrame:zeroFrame];
+ self.view.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
+ UIView *drawView = [[GioView alloc] initWithFrame:zeroFrame];
+ [self.view addSubview: drawView];
+#ifndef TARGET_OS_TV
+ drawView.multipleTouchEnabled = YES;
+#endif
+ drawView.preservesSuperviewLayoutMargins = YES;
+ drawView.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
+ onCreate((__bridge CFTypeRef)drawView);
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(keyboardWillChange:)
+ name:UIKeyboardWillShowNotification
+ object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(keyboardWillChange:)
+ name:UIKeyboardWillChangeFrameNotification
+ object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(keyboardWillHide:)
+ name:UIKeyboardWillHideNotification
+ object:nil];
+ [[NSNotificationCenter defaultCenter] addObserver: self
+ selector: @selector(applicationDidEnterBackground:)
+ name: UIApplicationDidEnterBackgroundNotification
+ object: nil];
+ [[NSNotificationCenter defaultCenter] addObserver: self
+ selector: @selector(applicationWillEnterForeground:)
+ name: UIApplicationWillEnterForegroundNotification
+ object: nil];
+}
+
+- (void)applicationWillEnterForeground:(UIApplication *)application {
+ UIView *drawView = self.view.subviews[0];
+ if (drawView != nil) {
+ gio_onDraw((__bridge CFTypeRef)drawView);
+ }
+}
+
+- (void)applicationDidEnterBackground:(UIApplication *)application {
+ UIView *drawView = self.view.subviews[0];
+ if (drawView != nil) {
+ onStop((__bridge CFTypeRef)drawView);
+ }
+}
+
+- (void)viewDidDisappear:(BOOL)animated {
+ [super viewDidDisappear:animated];
+ CFTypeRef viewRef = (__bridge CFTypeRef)self.view.subviews[0];
+ onDestroy(viewRef);
+}
+
+- (void)viewDidLayoutSubviews {
+ [super viewDidLayoutSubviews];
+ UIView *view = self.view.subviews[0];
+ CGRect frame = self.view.bounds;
+ // Adjust view bounds to make room for the keyboard.
+ frame.size.height -= _keyboardHeight;
+ view.frame = frame;
+ gio_onDraw((__bridge CFTypeRef)view);
+}
+
+- (void)didReceiveMemoryWarning {
+ onLowMemory();
+ [super didReceiveMemoryWarning];
+}
+
+- (void)keyboardWillChange:(NSNotification *)note {
+ NSDictionary *userInfo = note.userInfo;
+ CGRect f = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
+ _keyboardHeight = f.size.height;
+ [self.view setNeedsLayout];
+}
+
+- (void)keyboardWillHide:(NSNotification *)note {
+ _keyboardHeight = 0.0;
+ [self.view setNeedsLayout];
+}
+@end
+
+static void handleTouches(int last, UIView *view, NSSet *touches, UIEvent *event) {
+ CGFloat scale = view.contentScaleFactor;
+ NSUInteger i = 0;
+ NSUInteger n = [touches count];
+ CFTypeRef viewRef = (__bridge CFTypeRef)view;
+ for (UITouch *touch in touches) {
+ CFTypeRef touchRef = (__bridge CFTypeRef)touch;
+ i++;
+ NSArray *coalescedTouches = [event coalescedTouchesForTouch:touch];
+ NSUInteger j = 0;
+ NSUInteger m = [coalescedTouches count];
+ for (UITouch *coalescedTouch in [event coalescedTouchesForTouch:touch]) {
+ CGPoint loc = [coalescedTouch locationInView:view];
+ j++;
+ int lastTouch = last && i == n && j == m;
+ onTouch(lastTouch, viewRef, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]);
+ }
+ }
+}
+
+@implementation GioView
+NSArray *_keyCommands;
++ (void)onFrameCallback:(CADisplayLink *)link {
+ gio_onFrameCallback((__bridge CFTypeRef)link);
+}
+
+- (void)willMoveToWindow:(UIWindow *)newWindow {
+ if (self.window != nil) {
+ [[NSNotificationCenter defaultCenter] removeObserver:self
+ name:UIWindowDidBecomeKeyNotification
+ object:self.window];
+ [[NSNotificationCenter defaultCenter] removeObserver:self
+ name:UIWindowDidResignKeyNotification
+ object:self.window];
+ }
+ self.contentScaleFactor = newWindow.screen.nativeScale;
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(onWindowDidBecomeKey:)
+ name:UIWindowDidBecomeKeyNotification
+ object:newWindow];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(onWindowDidResignKey:)
+ name:UIWindowDidResignKeyNotification
+ object:newWindow];
+}
+
+- (void)onWindowDidBecomeKey:(NSNotification *)note {
+ if (self.isFirstResponder) {
+ onFocus((__bridge CFTypeRef)self, YES);
+ }
+}
+
+- (void)onWindowDidResignKey:(NSNotification *)note {
+ if (self.isFirstResponder) {
+ onFocus((__bridge CFTypeRef)self, NO);
+ }
+}
+
+- (void)dealloc {
+}
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+ handleTouches(0, self, touches, event);
+}
+
+- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
+ handleTouches(0, self, touches, event);
+}
+
+- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
+ handleTouches(1, self, touches, event);
+}
+
+- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
+ handleTouches(1, self, touches, event);
+}
+
+- (void)insertText:(NSString *)text {
+ onText((__bridge CFTypeRef)self, (char *)text.UTF8String);
+}
+
+- (BOOL)canBecomeFirstResponder {
+ return YES;
+}
+
+- (BOOL)hasText {
+ return YES;
+}
+
+- (void)deleteBackward {
+ onDeleteBackward((__bridge CFTypeRef)self);
+}
+
+- (void)onUpArrow {
+ onUpArrow((__bridge CFTypeRef)self);
+}
+
+- (void)onDownArrow {
+ onDownArrow((__bridge CFTypeRef)self);
+}
+
+- (void)onLeftArrow {
+ onLeftArrow((__bridge CFTypeRef)self);
+}
+
+- (void)onRightArrow {
+ onRightArrow((__bridge CFTypeRef)self);
+}
+
+- (NSArray *)keyCommands {
+ if (_keyCommands == nil) {
+ _keyCommands = @[
+ [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow
+ modifierFlags:0
+ action:@selector(onUpArrow)],
+ [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow
+ modifierFlags:0
+ action:@selector(onDownArrow)],
+ [UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow
+ modifierFlags:0
+ action:@selector(onLeftArrow)],
+ [UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow
+ modifierFlags:0
+ action:@selector(onRightArrow)]
+ ];
+ }
+ return _keyCommands;
+}
+@end
+
+void gio_writeClipboard(unichar *chars, NSUInteger length) {
+ @autoreleasepool {
+ NSString *s = [NSString string];
+ if (length > 0) {
+ s = [NSString stringWithCharacters:chars length:length];
+ }
+ UIPasteboard *p = UIPasteboard.generalPasteboard;
+ p.string = s;
+ }
+}
+
+CFTypeRef gio_readClipboard(void) {
+ @autoreleasepool {
+ UIPasteboard *p = UIPasteboard.generalPasteboard;
+ return (__bridge_retained CFTypeRef)p.string;
+ }
+}
+
+void gio_showTextInput(CFTypeRef viewRef) {
+ UIView *view = (__bridge UIView *)viewRef;
+ [view becomeFirstResponder];
+}
+
+void gio_hideTextInput(CFTypeRef viewRef) {
+ UIView *view = (__bridge UIView *)viewRef;
+ [view resignFirstResponder];
+}
+
+void gio_addLayerToView(CFTypeRef viewRef, CFTypeRef layerRef) {
+ UIView *view = (__bridge UIView *)viewRef;
+ CALayer *layer = (__bridge CALayer *)layerRef;
+ [view.layer addSublayer:layer];
+}
+
+void gio_updateView(CFTypeRef viewRef, CFTypeRef layerRef) {
+ UIView *view = (__bridge UIView *)viewRef;
+ CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef;
+ layer.contentsScale = view.contentScaleFactor;
+ layer.bounds = view.bounds;
+}
+
+void gio_removeLayer(CFTypeRef layerRef) {
+ CALayer *layer = (__bridge CALayer *)layerRef;
+ [layer removeFromSuperlayer];
+}
+
+struct drawParams gio_viewDrawParams(CFTypeRef viewRef) {
+ UIView *v = (__bridge UIView *)viewRef;
+ struct drawParams params;
+ CGFloat scale = v.layer.contentsScale;
+ // Use 163 as the standard ppi on iOS.
+ params.dpi = 163*scale;
+ params.sdpi = params.dpi;
+ UIEdgeInsets insets = v.layoutMargins;
+ if (@available(iOS 11.0, tvOS 11.0, *)) {
+ UIFontMetrics *metrics = [UIFontMetrics defaultMetrics];
+ params.sdpi = [metrics scaledValueForValue:params.sdpi];
+ insets = v.safeAreaInsets;
+ }
+ params.width = v.bounds.size.width*scale;
+ params.height = v.bounds.size.height*scale;
+ params.top = insets.top*scale;
+ params.right = insets.right*scale;
+ params.bottom = insets.bottom*scale;
+ params.left = insets.left*scale;
+ return params;
+}
+
+CFTypeRef gio_createDisplayLink(void) {
+ CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:[GioView class] selector:@selector(onFrameCallback:)];
+ dl.paused = YES;
+ NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
+ [dl addToRunLoop:runLoop forMode:[runLoop currentMode]];
+ return (__bridge_retained CFTypeRef)dl;
+}
+
+int gio_startDisplayLink(CFTypeRef dlref) {
+ CADisplayLink *dl = (__bridge CADisplayLink *)dlref;
+ dl.paused = NO;
+ return 0;
+}
+
+int gio_stopDisplayLink(CFTypeRef dlref) {
+ CADisplayLink *dl = (__bridge CADisplayLink *)dlref;
+ dl.paused = YES;
+ return 0;
+}
+
+void gio_releaseDisplayLink(CFTypeRef dlref) {
+ CADisplayLink *dl = (__bridge CADisplayLink *)dlref;
+ [dl invalidate];
+ CFRelease(dlref);
+}
+
+void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did) {
+ // Nothing to do on iOS.
+}
+
+void gio_hideCursor() {
+ // Not supported.
+}
+
+void gio_showCursor() {
+ // Not supported.
+}
+
+void gio_setCursor(NSUInteger curID) {
+ // Not supported.
+}
diff --git a/gio/giold/app/internal/wm/os_js.go b/gio/giold/app/internal/wm/os_js.go
new file mode 100644
index 0000000..f30f7bc
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_js.go
@@ -0,0 +1,656 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package wm
+
+import (
+ "image"
+ "strings"
+ "sync"
+ "syscall/js"
+ "time"
+ "unicode"
+ "unicode/utf8"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/unit"
+)
+
+type window struct {
+ window js.Value
+ document js.Value
+ clipboard js.Value
+ cnv js.Value
+ tarea js.Value
+ w Callbacks
+ redraw js.Func
+ clipboardCallback js.Func
+ requestAnimationFrame js.Value
+ browserHistory js.Value
+ visualViewport js.Value
+ cleanfuncs []func()
+ touches []js.Value
+ composing bool
+ requestFocus bool
+
+ chanAnimation chan struct{}
+ chanRedraw chan struct{}
+
+ mu sync.Mutex
+ size f32.Point
+ inset f32.Point
+ scale float32
+ animating bool
+ // animRequested tracks whether a requestAnimationFrame callback
+ // is pending.
+ animRequested bool
+}
+
+func NewWindow(win Callbacks, opts *Options) error {
+ doc := js.Global().Get("document")
+ cont := getContainer(doc)
+ cnv := createCanvas(doc)
+ cont.Call("appendChild", cnv)
+ tarea := createTextArea(doc)
+ cont.Call("appendChild", tarea)
+ w := &window{
+ cnv: cnv,
+ document: doc,
+ tarea: tarea,
+ window: js.Global().Get("window"),
+ clipboard: js.Global().Get("navigator").Get("clipboard"),
+ }
+ w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
+ w.browserHistory = w.window.Get("history")
+ w.visualViewport = w.window.Get("visualViewport")
+ if w.visualViewport.IsUndefined() {
+ w.visualViewport = w.window
+ }
+ w.chanAnimation = make(chan struct{}, 1)
+ w.chanRedraw = make(chan struct{}, 1)
+ w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
+ w.chanAnimation <- struct{}{}
+ return nil
+ })
+ w.clipboardCallback = w.funcOf(func(this js.Value,
+ args []js.Value) interface{} {
+ content := args[0].String()
+ win.Event(clipboard.Event{Text: content})
+ return nil
+ })
+ w.addEventListeners()
+ w.addHistory()
+ w.Option(opts)
+ w.w = win
+
+ go func() {
+ defer w.cleanup()
+ w.w.SetDriver(w)
+ w.blur()
+ w.w.Event(system.StageEvent{Stage: system.StageRunning})
+ w.resize()
+ w.draw(true)
+ for {
+ select {
+ case <-w.chanAnimation:
+ w.animCallback()
+ case <-w.chanRedraw:
+ w.draw(true)
+ }
+ }
+ }()
+ return nil
+}
+
+func getContainer(doc js.Value) js.Value {
+ cont := doc.Call("getElementById", "giowindow")
+ if !cont.IsNull() {
+ return cont
+ }
+ cont = doc.Call("createElement", "DIV")
+ doc.Get("body").Call("appendChild", cont)
+ return cont
+}
+
+func createTextArea(doc js.Value) js.Value {
+ tarea := doc.Call("createElement", "input")
+ style := tarea.Get("style")
+ style.Set("width", "1px")
+ style.Set("height", "1px")
+ style.Set("opacity", "0")
+ style.Set("border", "0")
+ style.Set("padding", "0")
+ tarea.Set("autocomplete", "off")
+ tarea.Set("autocorrect", "off")
+ tarea.Set("autocapitalize", "off")
+ tarea.Set("spellcheck", false)
+ return tarea
+}
+
+func createCanvas(doc js.Value) js.Value {
+ cnv := doc.Call("createElement", "canvas")
+ style := cnv.Get("style")
+ style.Set("position", "fixed")
+ style.Set("width", "100%")
+ style.Set("height", "100%")
+ return cnv
+}
+
+func (w *window) cleanup() {
+ // Cleanup in the opposite order of
+ // construction.
+ for i := len(w.cleanfuncs) - 1; i >= 0; i-- {
+ w.cleanfuncs[i]()
+ }
+ w.cleanfuncs = nil
+}
+
+func (w *window) addEventListeners() {
+ w.addEventListener(w.visualViewport, "resize",
+ func(this js.Value, args []js.Value) interface{} {
+ w.resize()
+ w.chanRedraw <- struct{}{}
+ return nil
+ })
+ w.addEventListener(w.window, "contextmenu",
+ func(this js.Value, args []js.Value) interface{} {
+ args[0].Call("preventDefault")
+ return nil
+ })
+ w.addEventListener(w.window, "popstate",
+ func(this js.Value, args []js.Value) interface{} {
+ ev := &system.CommandEvent{Type: system.CommandBack}
+ w.w.Event(ev)
+ if ev.Cancel {
+ return w.browserHistory.Call("forward")
+ }
+
+ return w.browserHistory.Call("back")
+ })
+ w.addEventListener(w.document, "visibilitychange",
+ func(this js.Value, args []js.Value) interface{} {
+ ev := system.StageEvent{}
+ switch w.document.Get("visibilityState").String() {
+ case "hidden", "prerender", "unloaded":
+ ev.Stage = system.StagePaused
+ default:
+ ev.Stage = system.StageRunning
+ }
+ w.w.Event(ev)
+ return nil
+ })
+ w.addEventListener(w.cnv, "mousemove",
+ func(this js.Value, args []js.Value) interface{} {
+ w.pointerEvent(pointer.Move, 0, 0, args[0])
+ return nil
+ })
+ w.addEventListener(w.cnv, "mousedown",
+ func(this js.Value, args []js.Value) interface{} {
+ w.pointerEvent(pointer.Press, 0, 0, args[0])
+ if w.requestFocus {
+ w.focus()
+ w.requestFocus = false
+ }
+ return nil
+ })
+ w.addEventListener(w.cnv, "mouseup",
+ func(this js.Value, args []js.Value) interface{} {
+ w.pointerEvent(pointer.Release, 0, 0, args[0])
+ return nil
+ })
+ w.addEventListener(w.cnv, "wheel",
+ func(this js.Value, args []js.Value) interface{} {
+ e := args[0]
+ dx, dy := e.Get("deltaX").Float(), e.Get("deltaY").Float()
+ mode := e.Get("deltaMode").Int()
+ switch mode {
+ case 0x01: // DOM_DELTA_LINE
+ dx *= 10
+ dy *= 10
+ case 0x02: // DOM_DELTA_PAGE
+ dx *= 120
+ dy *= 120
+ }
+ w.pointerEvent(pointer.Scroll, float32(dx), float32(dy), e)
+ return nil
+ })
+ w.addEventListener(w.cnv, "touchstart",
+ func(this js.Value, args []js.Value) interface{} {
+ w.touchEvent(pointer.Press, args[0])
+ if w.requestFocus {
+ w.focus() // iOS can only focus inside a Touch event.
+ w.requestFocus = false
+ }
+ return nil
+ })
+ w.addEventListener(w.cnv, "touchend",
+ func(this js.Value, args []js.Value) interface{} {
+ w.touchEvent(pointer.Release, args[0])
+ return nil
+ })
+ w.addEventListener(w.cnv, "touchmove",
+ func(this js.Value, args []js.Value) interface{} {
+ w.touchEvent(pointer.Move, args[0])
+ return nil
+ })
+ w.addEventListener(w.cnv, "touchcancel",
+ func(this js.Value, args []js.Value) interface{} {
+ // Cancel all touches even if only one touch was cancelled.
+ for i := range w.touches {
+ w.touches[i] = js.Null()
+ }
+ w.touches = w.touches[:0]
+ w.w.Event(pointer.Event{
+ Type: pointer.Cancel,
+ Source: pointer.Touch,
+ })
+ return nil
+ })
+ w.addEventListener(w.tarea, "focus",
+ func(this js.Value, args []js.Value) interface{} {
+ w.w.Event(key.FocusEvent{Focus: true})
+ return nil
+ })
+ w.addEventListener(w.tarea, "blur",
+ func(this js.Value, args []js.Value) interface{} {
+ w.w.Event(key.FocusEvent{Focus: false})
+ w.blur()
+ return nil
+ })
+ w.addEventListener(w.tarea, "keydown",
+ func(this js.Value, args []js.Value) interface{} {
+ w.keyEvent(args[0], key.Press)
+ return nil
+ })
+ w.addEventListener(w.tarea, "keyup",
+ func(this js.Value, args []js.Value) interface{} {
+ w.keyEvent(args[0], key.Release)
+ return nil
+ })
+ w.addEventListener(w.tarea, "compositionstart",
+ func(this js.Value, args []js.Value) interface{} {
+ w.composing = true
+ return nil
+ })
+ w.addEventListener(w.tarea, "compositionend",
+ func(this js.Value, args []js.Value) interface{} {
+ w.composing = false
+ w.flushInput()
+ return nil
+ })
+ w.addEventListener(w.tarea, "input",
+ func(this js.Value, args []js.Value) interface{} {
+ if w.composing {
+ return nil
+ }
+ w.flushInput()
+ return nil
+ })
+ w.addEventListener(w.tarea, "paste",
+ func(this js.Value, args []js.Value) interface{} {
+ if w.clipboard.IsUndefined() {
+ return nil
+ }
+ // Prevents duplicated-paste, since "paste" is already handled through Clipboard API.
+ args[0].Call("preventDefault")
+ return nil
+ })
+}
+
+func (w *window) addHistory() {
+ w.browserHistory.Call("pushState", nil, nil,
+ w.window.Get("location").Get("href"))
+}
+
+func (w *window) flushInput() {
+ val := w.tarea.Get("value").String()
+ w.tarea.Set("value", "")
+ w.w.Event(key.EditEvent{Text: string(val)})
+}
+
+func (w *window) blur() {
+ w.tarea.Call("blur")
+ w.requestFocus = false
+}
+
+func (w *window) focus() {
+ w.tarea.Call("focus")
+ w.requestFocus = true
+}
+
+func (w *window) keyEvent(e js.Value, ks key.State) {
+ k := e.Get("key").String()
+ if n, ok := translateKey(k); ok {
+ cmd := key.Event{
+ Name: n,
+ Modifiers: modifiersFor(e),
+ State: ks,
+ }
+ w.w.Event(cmd)
+ }
+}
+
+// modifiersFor returns the modifier set for a DOM MouseEvent or
+// KeyEvent.
+func modifiersFor(e js.Value) key.Modifiers {
+ var mods key.Modifiers
+ if e.Get("getModifierState").IsUndefined() {
+ // Some browsers doesn't support getModifierState.
+ return mods
+ }
+ if e.Call("getModifierState", "Alt").Bool() {
+ mods |= key.ModAlt
+ }
+ if e.Call("getModifierState", "Control").Bool() {
+ mods |= key.ModCtrl
+ }
+ if e.Call("getModifierState", "Shift").Bool() {
+ mods |= key.ModShift
+ }
+ return mods
+}
+
+func (w *window) touchEvent(typ pointer.Type, e js.Value) {
+ e.Call("preventDefault")
+ t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond
+ changedTouches := e.Get("changedTouches")
+ n := changedTouches.Length()
+ rect := w.cnv.Call("getBoundingClientRect")
+ w.mu.Lock()
+ scale := w.scale
+ w.mu.Unlock()
+ var mods key.Modifiers
+ if e.Get("shiftKey").Bool() {
+ mods |= key.ModShift
+ }
+ if e.Get("altKey").Bool() {
+ mods |= key.ModAlt
+ }
+ if e.Get("ctrlKey").Bool() {
+ mods |= key.ModCtrl
+ }
+ for i := 0; i < n; i++ {
+ touch := changedTouches.Index(i)
+ pid := w.touchIDFor(touch)
+ x, y := touch.Get("clientX").Float(), touch.Get("clientY").Float()
+ x -= rect.Get("left").Float()
+ y -= rect.Get("top").Float()
+ pos := f32.Point{
+ X: float32(x) * scale,
+ Y: float32(y) * scale,
+ }
+ w.w.Event(pointer.Event{
+ Type: typ,
+ Source: pointer.Touch,
+ Position: pos,
+ PointerID: pid,
+ Time: t,
+ Modifiers: mods,
+ })
+ }
+}
+
+func (w *window) touchIDFor(touch js.Value) pointer.ID {
+ id := touch.Get("identifier")
+ for i, id2 := range w.touches {
+ if id2.Equal(id) {
+ return pointer.ID(i)
+ }
+ }
+ pid := pointer.ID(len(w.touches))
+ w.touches = append(w.touches, id)
+ return pid
+}
+
+func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
+ e.Call("preventDefault")
+ x, y := e.Get("clientX").Float(), e.Get("clientY").Float()
+ rect := w.cnv.Call("getBoundingClientRect")
+ x -= rect.Get("left").Float()
+ y -= rect.Get("top").Float()
+ w.mu.Lock()
+ scale := w.scale
+ w.mu.Unlock()
+ pos := f32.Point{
+ X: float32(x) * scale,
+ Y: float32(y) * scale,
+ }
+ scroll := f32.Point{
+ X: dx * scale,
+ Y: dy * scale,
+ }
+ t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond
+ jbtns := e.Get("buttons").Int()
+ var btns pointer.Buttons
+ if jbtns&1 != 0 {
+ btns |= pointer.ButtonPrimary
+ }
+ if jbtns&2 != 0 {
+ btns |= pointer.ButtonSecondary
+ }
+ if jbtns&4 != 0 {
+ btns |= pointer.ButtonTertiary
+ }
+ w.w.Event(pointer.Event{
+ Type: typ,
+ Source: pointer.Mouse,
+ Buttons: btns,
+ Position: pos,
+ Scroll: scroll,
+ Time: t,
+ Modifiers: modifiersFor(e),
+ })
+}
+
+func (w *window) addEventListener(this js.Value, event string,
+ f func(this js.Value, args []js.Value) interface{}) {
+ jsf := w.funcOf(f)
+ this.Call("addEventListener", event, jsf)
+ w.cleanfuncs = append(w.cleanfuncs, func() {
+ this.Call("removeEventListener", event, jsf)
+ })
+}
+
+// funcOf is like js.FuncOf but adds the js.Func to a list of
+// functions to be released during cleanup.
+func (w *window) funcOf(f func(this js.Value,
+ args []js.Value) interface{}) js.Func {
+ jsf := js.FuncOf(f)
+ w.cleanfuncs = append(w.cleanfuncs, jsf.Release)
+ return jsf
+}
+
+func (w *window) animCallback() {
+ w.mu.Lock()
+ anim := w.animating
+ w.animRequested = anim
+ if anim {
+ w.requestAnimationFrame.Invoke(w.redraw)
+ }
+ w.mu.Unlock()
+ if anim {
+ w.draw(false)
+ }
+}
+
+func (w *window) SetAnimating(anim bool) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ w.animating = anim
+ if anim && !w.animRequested {
+ w.animRequested = true
+ w.requestAnimationFrame.Invoke(w.redraw)
+ }
+}
+
+func (w *window) ReadClipboard() {
+ if w.clipboard.IsUndefined() {
+ return
+ }
+ if w.clipboard.Get("readText").IsUndefined() {
+ return
+ }
+ w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback)
+}
+
+func (w *window) WriteClipboard(s string) {
+ if w.clipboard.IsUndefined() {
+ return
+ }
+ if w.clipboard.Get("writeText").IsUndefined() {
+ return
+ }
+ w.clipboard.Call("writeText", s)
+}
+
+func (w *window) Option(opts *Options) {
+ if o := opts.WindowMode; o != nil {
+ w.windowMode(*o)
+ }
+}
+
+func (w *window) SetCursor(name pointer.CursorName) {
+ style := w.cnv.Get("style")
+ style.Set("cursor", string(name))
+}
+
+func (w *window) ShowTextInput(show bool) {
+ // Run in a goroutine to avoid a deadlock if the
+ // focus change result in an event.
+ go func() {
+ if show {
+ w.focus()
+ } else {
+ w.blur()
+ }
+ }()
+}
+
+// Close the window. Not implemented for js.
+func (w *window) Close() {}
+
+func (w *window) resize() {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ w.scale = float32(w.window.Get("devicePixelRatio").Float())
+
+ rect := w.cnv.Call("getBoundingClientRect")
+ w.size.X = float32(rect.Get("width").Float()) * w.scale
+ w.size.Y = float32(rect.Get("height").Float()) * w.scale
+
+ if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() {
+ w.inset.X = w.size.X - float32(vx.Float())*w.scale
+ w.inset.Y = w.size.Y - float32(vy.Float())*w.scale
+ }
+
+ if w.size.X == 0 || w.size.Y == 0 {
+ return
+ }
+
+ w.cnv.Set("width", int(w.size.X+.5))
+ w.cnv.Set("height", int(w.size.Y+.5))
+}
+
+func (w *window) draw(sync bool) {
+ width, height, insets, metric := w.config()
+ if metric == (unit.Metric{}) || width == 0 || height == 0 {
+ return
+ }
+
+ w.w.Event(FrameEvent{
+ FrameEvent: system.FrameEvent{
+ Now: time.Now(),
+ Size: image.Point{
+ X: width,
+ Y: height,
+ },
+ Insets: insets,
+ Metric: metric,
+ },
+ Sync: sync,
+ })
+}
+
+func (w *window) config() (int, int, system.Insets, unit.Metric) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ return int(w.size.X + .5), int(w.size.Y + .5), system.Insets{
+ Bottom: unit.Px(w.inset.Y),
+ Right: unit.Px(w.inset.X),
+ }, unit.Metric{
+ PxPerDp: w.scale,
+ PxPerSp: w.scale,
+ }
+}
+
+func (w *window) windowMode(mode WindowMode) {
+ switch mode {
+ case Windowed:
+ if fs := w.document.Get("fullscreenElement"); !fs.Truthy() {
+ return // Browser is already Windowed.
+ }
+ if !w.document.Get("exitFullscreen").Truthy() {
+ return // Browser doesn't support such feature.
+ }
+ w.document.Call("exitFullscreen")
+ case Fullscreen:
+ elem := w.document.Get("documentElement")
+ if !elem.Get("requestFullscreen").Truthy() {
+ return // Browser doesn't support such feature.
+ }
+ elem.Call("requestFullscreen")
+ }
+}
+
+func Main() {
+ select {}
+}
+
+func translateKey(k string) (string, bool) {
+ var n string
+ switch k {
+ case "ArrowUp":
+ n = key.NameUpArrow
+ case "ArrowDown":
+ n = key.NameDownArrow
+ case "ArrowLeft":
+ n = key.NameLeftArrow
+ case "ArrowRight":
+ n = key.NameRightArrow
+ case "Escape":
+ n = key.NameEscape
+ case "Enter":
+ n = key.NameReturn
+ case "Backspace":
+ n = key.NameDeleteBackward
+ case "Delete":
+ n = key.NameDeleteForward
+ case "Home":
+ n = key.NameHome
+ case "End":
+ n = key.NameEnd
+ case "PageUp":
+ n = key.NamePageUp
+ case "PageDown":
+ n = key.NamePageDown
+ case "Tab":
+ n = key.NameTab
+ case " ":
+ n = key.NameSpace
+ case "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12":
+ n = k
+ default:
+ r, s := utf8.DecodeRuneInString(k)
+ // If there is exactly one printable character, return that.
+ if s == len(k) && unicode.IsPrint(r) {
+ return strings.ToUpper(k), true
+ }
+ return "", false
+ }
+ return n, true
+}
diff --git a/gio/giold/app/internal/wm/os_macos.go b/gio/giold/app/internal/wm/os_macos.go
new file mode 100644
index 0000000..f93a5b8
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_macos.go
@@ -0,0 +1,516 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build darwin && !ios
+// +build darwin,!ios
+
+package wm
+
+import (
+ "errors"
+ "image"
+ "runtime"
+ "time"
+ "unicode"
+ "unicode/utf16"
+ "unsafe"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/unit"
+
+ _ "realy.lol/gio/internal/cocoainit"
+)
+
+/*
+#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c
+
+#include
+
+#define GIO_MOUSE_MOVE 1
+#define GIO_MOUSE_UP 2
+#define GIO_MOUSE_DOWN 3
+#define GIO_MOUSE_SCROLL 4
+
+__attribute__ ((visibility ("hidden"))) void gio_main(void);
+__attribute__ ((visibility ("hidden"))) CGFloat gio_viewWidth(CFTypeRef viewRef);
+__attribute__ ((visibility ("hidden"))) CGFloat gio_viewHeight(CFTypeRef viewRef);
+__attribute__ ((visibility ("hidden"))) CGFloat gio_getViewBackingScale(CFTypeRef viewRef);
+__attribute__ ((visibility ("hidden"))) CGFloat gio_getScreenBackingScale(void);
+__attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void);
+__attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length);
+__attribute__ ((visibility ("hidden"))) void gio_setNeedsDisplay(CFTypeRef viewRef);
+__attribute__ ((visibility ("hidden"))) void gio_toggleFullScreen(CFTypeRef windowRef);
+__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
+__attribute__ ((visibility ("hidden"))) void gio_makeKeyAndOrderFront(CFTypeRef windowRef);
+__attribute__ ((visibility ("hidden"))) NSPoint gio_cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft);
+__attribute__ ((visibility ("hidden"))) void gio_close(CFTypeRef windowRef);
+__attribute__ ((visibility ("hidden"))) void gio_setSize(CFTypeRef windowRef, CGFloat width, CGFloat height);
+__attribute__ ((visibility ("hidden"))) void gio_setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height);
+__attribute__ ((visibility ("hidden"))) void gio_setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height);
+__attribute__ ((visibility ("hidden"))) void gio_setTitle(CFTypeRef windowRef, const char *title);
+*/
+import "C"
+
+func init() {
+ // Darwin requires that UI operations happen on the main thread only.
+ runtime.LockOSThread()
+}
+
+type window struct {
+ view C.CFTypeRef
+ window C.CFTypeRef
+ w Callbacks
+ stage system.Stage
+ displayLink *displayLink
+ cursor pointer.CursorName
+
+ scale float32
+ mode WindowMode
+}
+
+// viewMap is the mapping from Cocoa NSViews to Go windows.
+var viewMap = make(map[C.CFTypeRef]*window)
+
+var viewFactory func() C.CFTypeRef
+
+// launched is closed when applicationDidFinishLaunching is called.
+var launched = make(chan struct{})
+
+// nextTopLeft is the offset to use for the next window's call to
+// cascadeTopLeftFromPoint.
+var nextTopLeft C.NSPoint
+
+// mustView is like lookupView, except that it panics
+// if the view isn't mapped.
+func mustView(view C.CFTypeRef) *window {
+ w, ok := lookupView(view)
+ if !ok {
+ panic("no window for view")
+ }
+ return w
+}
+
+func lookupView(view C.CFTypeRef) (*window, bool) {
+ w, exists := viewMap[view]
+ if !exists {
+ return nil, false
+ }
+ return w, true
+}
+
+func deleteView(view C.CFTypeRef) {
+ delete(viewMap, view)
+}
+
+func insertView(view C.CFTypeRef, w *window) {
+ viewMap[view] = w
+}
+
+func (w *window) contextView() C.CFTypeRef {
+ return w.view
+}
+
+func (w *window) ReadClipboard() {
+ runOnMain(func() {
+ content := nsstringToString(C.gio_readClipboard())
+ w.w.Event(clipboard.Event{Text: content})
+ })
+}
+
+func (w *window) WriteClipboard(s string) {
+ u16 := utf16.Encode([]rune(s))
+ runOnMain(func() {
+ var chars *C.unichar
+ if len(u16) > 0 {
+ chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
+ }
+ C.gio_writeClipboard(chars, C.NSUInteger(len(u16)))
+ })
+}
+
+func (w *window) Option(opts *Options) {
+ w.runOnMain(func() {
+ screenScale := float32(C.gio_getScreenBackingScale())
+ cfg := configFor(screenScale)
+ val := func(v unit.Value) float32 {
+ return float32(cfg.Px(v)) / screenScale
+ }
+ if o := opts.Size; o != nil {
+ width := val(o.Width)
+ height := val(o.Height)
+ if width > 0 || height > 0 {
+ C.gio_setSize(w.window, C.CGFloat(width), C.CGFloat(height))
+ }
+ }
+ if o := opts.MinSize; o != nil {
+ width := val(o.Width)
+ height := val(o.Height)
+ if width > 0 || height > 0 {
+ C.gio_setMinSize(w.window, C.CGFloat(width), C.CGFloat(height))
+ }
+ }
+ if o := opts.MaxSize; o != nil {
+ width := val(o.Width)
+ height := val(o.Height)
+ if width > 0 || height > 0 {
+ C.gio_setMaxSize(w.window, C.CGFloat(width), C.CGFloat(height))
+ }
+ }
+ if o := opts.Title; o != nil {
+ title := C.CString(*o)
+ defer C.free(unsafe.Pointer(title))
+ C.gio_setTitle(w.window, title)
+ }
+ if o := opts.WindowMode; o != nil {
+ w.SetWindowMode(*o)
+ }
+ })
+}
+
+func (w *window) SetWindowMode(mode WindowMode) {
+ switch mode {
+ case w.mode:
+ case Windowed, Fullscreen:
+ C.gio_toggleFullScreen(w.window)
+ w.mode = mode
+ }
+}
+
+func (w *window) SetCursor(name pointer.CursorName) {
+ w.cursor = windowSetCursor(w.cursor, name)
+}
+
+func (w *window) ShowTextInput(show bool) {}
+
+func (w *window) SetAnimating(anim bool) {
+ if anim {
+ w.displayLink.Start()
+ } else {
+ w.displayLink.Stop()
+ }
+}
+
+func (w *window) runOnMain(f func()) {
+ runOnMain(func() {
+ // Make sure the view is still valid. The window might've been closed
+ // during the switch to the main thread.
+ if w.view != 0 {
+ f()
+ }
+ })
+}
+
+func (w *window) Close() {
+ w.runOnMain(func() {
+ C.gio_close(w.window)
+ })
+}
+
+func (w *window) setStage(stage system.Stage) {
+ if stage == w.stage {
+ return
+ }
+ w.stage = stage
+ w.w.Event(system.StageEvent{Stage: stage})
+}
+
+//export gio_onKeys
+func gio_onKeys(view C.CFTypeRef, cstr *C.char, ti C.double, mods C.NSUInteger,
+ keyDown C.bool) {
+ str := C.GoString(cstr)
+ kmods := convertMods(mods)
+ ks := key.Release
+ if keyDown {
+ ks = key.Press
+ }
+ w := mustView(view)
+ for _, k := range str {
+ if n, ok := convertKey(k); ok {
+ w.w.Event(key.Event{
+ Name: n,
+ Modifiers: kmods,
+ State: ks,
+ })
+ }
+ }
+}
+
+//export gio_onText
+func gio_onText(view C.CFTypeRef, cstr *C.char) {
+ str := C.GoString(cstr)
+ w := mustView(view)
+ w.w.Event(key.EditEvent{Text: str})
+}
+
+//export gio_onMouse
+func gio_onMouse(view C.CFTypeRef, cdir C.int, cbtns C.NSUInteger,
+ x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) {
+ var typ pointer.Type
+ switch cdir {
+ case C.GIO_MOUSE_MOVE:
+ typ = pointer.Move
+ case C.GIO_MOUSE_UP:
+ typ = pointer.Release
+ case C.GIO_MOUSE_DOWN:
+ typ = pointer.Press
+ case C.GIO_MOUSE_SCROLL:
+ typ = pointer.Scroll
+ default:
+ panic("invalid direction")
+ }
+ var btns pointer.Buttons
+ if cbtns&(1<<0) != 0 {
+ btns |= pointer.ButtonPrimary
+ }
+ if cbtns&(1<<1) != 0 {
+ btns |= pointer.ButtonSecondary
+ }
+ if cbtns&(1<<2) != 0 {
+ btns |= pointer.ButtonTertiary
+ }
+ t := time.Duration(float64(ti)*float64(time.Second) + .5)
+ w := mustView(view)
+ xf, yf := float32(x)*w.scale, float32(y)*w.scale
+ dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale
+ w.w.Event(pointer.Event{
+ Type: typ,
+ Source: pointer.Mouse,
+ Time: t,
+ Buttons: btns,
+ Position: f32.Point{X: xf, Y: yf},
+ Scroll: f32.Point{X: dxf, Y: dyf},
+ Modifiers: convertMods(mods),
+ })
+}
+
+//export gio_onDraw
+func gio_onDraw(view C.CFTypeRef) {
+ w := mustView(view)
+ w.draw()
+}
+
+//export gio_onFocus
+func gio_onFocus(view C.CFTypeRef, focus C.int) {
+ w := mustView(view)
+ w.w.Event(key.FocusEvent{Focus: focus == 1})
+ w.SetCursor(w.cursor)
+}
+
+//export gio_onChangeScreen
+func gio_onChangeScreen(view C.CFTypeRef, did uint64) {
+ w := mustView(view)
+ w.displayLink.SetDisplayID(did)
+}
+
+func (w *window) draw() {
+ w.scale = float32(C.gio_getViewBackingScale(w.view))
+ wf, hf := float32(C.gio_viewWidth(w.view)), float32(C.gio_viewHeight(w.view))
+ if wf == 0 || hf == 0 {
+ return
+ }
+ width := int(wf*w.scale + .5)
+ height := int(hf*w.scale + .5)
+ cfg := configFor(w.scale)
+ w.setStage(system.StageRunning)
+ w.w.Event(FrameEvent{
+ FrameEvent: system.FrameEvent{
+ Now: time.Now(),
+ Size: image.Point{
+ X: width,
+ Y: height,
+ },
+ Metric: cfg,
+ },
+ Sync: true,
+ })
+}
+
+func configFor(scale float32) unit.Metric {
+ return unit.Metric{
+ PxPerDp: scale,
+ PxPerSp: scale,
+ }
+}
+
+//export gio_onClose
+func gio_onClose(view C.CFTypeRef) {
+ w := mustView(view)
+ w.displayLink.Close()
+ deleteView(view)
+ w.w.Event(system.DestroyEvent{})
+ C.CFRelease(w.view)
+ w.view = 0
+ C.CFRelease(w.window)
+ w.window = 0
+}
+
+//export gio_onHide
+func gio_onHide(view C.CFTypeRef) {
+ w := mustView(view)
+ w.setStage(system.StagePaused)
+}
+
+//export gio_onShow
+func gio_onShow(view C.CFTypeRef) {
+ w := mustView(view)
+ w.setStage(system.StageRunning)
+}
+
+//export gio_onAppHide
+func gio_onAppHide() {
+ for _, w := range viewMap {
+ w.setStage(system.StagePaused)
+ }
+}
+
+//export gio_onAppShow
+func gio_onAppShow() {
+ for _, w := range viewMap {
+ w.setStage(system.StageRunning)
+ }
+}
+
+//export gio_onFinishLaunching
+func gio_onFinishLaunching() {
+ close(launched)
+}
+
+func NewWindow(win Callbacks, opts *Options) error {
+ <-launched
+ errch := make(chan error)
+ runOnMain(func() {
+ w, err := newWindow(opts)
+ if err != nil {
+ errch <- err
+ return
+ }
+ errch <- nil
+ win.SetDriver(w)
+ w.w = win
+ w.window = C.gio_createWindow(w.view, nil, 0, 0, 0, 0, 0, 0)
+ w.Option(opts)
+ if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
+ // cascadeTopLeftFromPoint treats (0, 0) as a no-op,
+ // and just returns the offset we need for the first window.
+ nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft)
+ }
+ nextTopLeft = C.gio_cascadeTopLeftFromPoint(w.window, nextTopLeft)
+ C.gio_makeKeyAndOrderFront(w.window)
+ })
+ return <-errch
+}
+
+func newWindow(opts *Options) (*window, error) {
+ view := viewFactory()
+ if view == 0 {
+ return nil, errors.New("CreateWindow: failed to create view")
+ }
+ scale := float32(C.gio_getViewBackingScale(view))
+ w := &window{
+ view: view,
+ scale: scale,
+ }
+ dl, err := NewDisplayLink(func() {
+ w.runOnMain(func() {
+ C.gio_setNeedsDisplay(w.view)
+ })
+ })
+ w.displayLink = dl
+ if err != nil {
+ C.CFRelease(view)
+ return nil, err
+ }
+ insertView(view, w)
+ return w, nil
+}
+
+func Main() {
+ C.gio_main()
+}
+
+func convertKey(k rune) (string, bool) {
+ var n string
+ switch k {
+ case 0x1b:
+ n = key.NameEscape
+ case C.NSLeftArrowFunctionKey:
+ n = key.NameLeftArrow
+ case C.NSRightArrowFunctionKey:
+ n = key.NameRightArrow
+ case C.NSUpArrowFunctionKey:
+ n = key.NameUpArrow
+ case C.NSDownArrowFunctionKey:
+ n = key.NameDownArrow
+ case 0xd:
+ n = key.NameReturn
+ case 0x3:
+ n = key.NameEnter
+ case C.NSHomeFunctionKey:
+ n = key.NameHome
+ case C.NSEndFunctionKey:
+ n = key.NameEnd
+ case 0x7f:
+ n = key.NameDeleteBackward
+ case C.NSDeleteFunctionKey:
+ n = key.NameDeleteForward
+ case C.NSPageUpFunctionKey:
+ n = key.NamePageUp
+ case C.NSPageDownFunctionKey:
+ n = key.NamePageDown
+ case C.NSF1FunctionKey:
+ n = "F1"
+ case C.NSF2FunctionKey:
+ n = "F2"
+ case C.NSF3FunctionKey:
+ n = "F3"
+ case C.NSF4FunctionKey:
+ n = "F4"
+ case C.NSF5FunctionKey:
+ n = "F5"
+ case C.NSF6FunctionKey:
+ n = "F6"
+ case C.NSF7FunctionKey:
+ n = "F7"
+ case C.NSF8FunctionKey:
+ n = "F8"
+ case C.NSF9FunctionKey:
+ n = "F9"
+ case C.NSF10FunctionKey:
+ n = "F10"
+ case C.NSF11FunctionKey:
+ n = "F11"
+ case C.NSF12FunctionKey:
+ n = "F12"
+ case 0x09, 0x19:
+ n = key.NameTab
+ case 0x20:
+ n = key.NameSpace
+ default:
+ k = unicode.ToUpper(k)
+ if !unicode.IsPrint(k) {
+ return "", false
+ }
+ n = string(k)
+ }
+ return n, true
+}
+
+func convertMods(mods C.NSUInteger) key.Modifiers {
+ var kmods key.Modifiers
+ if mods&C.NSAlternateKeyMask != 0 {
+ kmods |= key.ModAlt
+ }
+ if mods&C.NSControlKeyMask != 0 {
+ kmods |= key.ModCtrl
+ }
+ if mods&C.NSCommandKeyMask != 0 {
+ kmods |= key.ModCommand
+ }
+ if mods&C.NSShiftKeyMask != 0 {
+ kmods |= key.ModShift
+ }
+ return kmods
+}
diff --git a/gio/giold/app/internal/wm/os_macos.m b/gio/giold/app/internal/wm/os_macos.m
new file mode 100644
index 0000000..7980d53
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_macos.m
@@ -0,0 +1,272 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build darwin,!ios
+
+@import AppKit;
+
+#include "_cgo_export.h"
+
+@interface GioAppDelegate : NSObject
+@end
+
+@interface GioWindowDelegate : NSObject
+@end
+
+@implementation GioWindowDelegate
+- (void)windowWillMiniaturize:(NSNotification *)notification {
+ NSWindow *window = (NSWindow *)[notification object];
+ gio_onHide((__bridge CFTypeRef)window.contentView);
+}
+- (void)windowDidDeminiaturize:(NSNotification *)notification {
+ NSWindow *window = (NSWindow *)[notification object];
+ gio_onShow((__bridge CFTypeRef)window.contentView);
+}
+- (void)windowDidChangeScreen:(NSNotification *)notification {
+ NSWindow *window = (NSWindow *)[notification object];
+ CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue];
+ CFTypeRef view = (__bridge CFTypeRef)window.contentView;
+ gio_onChangeScreen(view, dispID);
+}
+- (void)windowDidBecomeKey:(NSNotification *)notification {
+ NSWindow *window = (NSWindow *)[notification object];
+ gio_onFocus((__bridge CFTypeRef)window.contentView, 1);
+}
+- (void)windowDidResignKey:(NSNotification *)notification {
+ NSWindow *window = (NSWindow *)[notification object];
+ gio_onFocus((__bridge CFTypeRef)window.contentView, 0);
+}
+- (void)windowWillClose:(NSNotification *)notification {
+ NSWindow *window = (NSWindow *)[notification object];
+ window.delegate = nil;
+ gio_onClose((__bridge CFTypeRef)window.contentView);
+}
+@end
+
+// Delegates are weakly referenced from their peers. Nothing
+// else holds a strong reference to our window delegate, so
+// keep a single global reference instead.
+static GioWindowDelegate *globalWindowDel;
+
+void gio_writeClipboard(unichar *chars, NSUInteger length) {
+ @autoreleasepool {
+ NSString *s = [NSString string];
+ if (length > 0) {
+ s = [NSString stringWithCharacters:chars length:length];
+ }
+ NSPasteboard *p = NSPasteboard.generalPasteboard;
+ [p declareTypes:@[NSPasteboardTypeString] owner:nil];
+ [p setString:s forType:NSPasteboardTypeString];
+ }
+}
+
+CFTypeRef gio_readClipboard(void) {
+ @autoreleasepool {
+ NSPasteboard *p = NSPasteboard.generalPasteboard;
+ NSString *content = [p stringForType:NSPasteboardTypeString];
+ return (__bridge_retained CFTypeRef)content;
+ }
+}
+
+CGFloat gio_viewHeight(CFTypeRef viewRef) {
+ NSView *view = (__bridge NSView *)viewRef;
+ return [view bounds].size.height;
+}
+
+CGFloat gio_viewWidth(CFTypeRef viewRef) {
+ NSView *view = (__bridge NSView *)viewRef;
+ return [view bounds].size.width;
+}
+
+CGFloat gio_getScreenBackingScale(void) {
+ return [NSScreen.mainScreen backingScaleFactor];
+}
+
+CGFloat gio_getViewBackingScale(CFTypeRef viewRef) {
+ NSView *view = (__bridge NSView *)viewRef;
+ return [view.window backingScaleFactor];
+}
+
+void gio_hideCursor() {
+ @autoreleasepool {
+ [NSCursor hide];
+ }
+}
+
+void gio_showCursor() {
+ @autoreleasepool {
+ [NSCursor unhide];
+ }
+}
+
+void gio_setCursor(NSUInteger curID) {
+ @autoreleasepool {
+ switch (curID) {
+ case 1:
+ [NSCursor.arrowCursor set];
+ break;
+ case 2:
+ [NSCursor.IBeamCursor set];
+ break;
+ case 3:
+ [NSCursor.pointingHandCursor set];
+ break;
+ case 4:
+ [NSCursor.crosshairCursor set];
+ break;
+ case 5:
+ [NSCursor.resizeLeftRightCursor set];
+ break;
+ case 6:
+ [NSCursor.resizeUpDownCursor set];
+ break;
+ case 7:
+ [NSCursor.openHandCursor set];
+ break;
+ default:
+ [NSCursor.arrowCursor set];
+ break;
+ }
+ }
+}
+
+static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) {
+ gio_onFrameCallback(dl);
+ return kCVReturnSuccess;
+}
+
+CFTypeRef gio_createDisplayLink(void) {
+ CVDisplayLinkRef dl;
+ CVDisplayLinkCreateWithActiveCGDisplays(&dl);
+ CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, nil);
+ return dl;
+}
+
+int gio_startDisplayLink(CFTypeRef dl) {
+ return CVDisplayLinkStart((CVDisplayLinkRef)dl);
+}
+
+int gio_stopDisplayLink(CFTypeRef dl) {
+ return CVDisplayLinkStop((CVDisplayLinkRef)dl);
+}
+
+void gio_releaseDisplayLink(CFTypeRef dl) {
+ CVDisplayLinkRelease((CVDisplayLinkRef)dl);
+}
+
+void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did) {
+ CVDisplayLinkSetCurrentCGDisplay((CVDisplayLinkRef)dl, (CGDirectDisplayID)did);
+}
+
+NSPoint gio_cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) {
+ NSWindow *window = (__bridge NSWindow *)windowRef;
+ return [window cascadeTopLeftFromPoint:topLeft];
+}
+
+void gio_makeKeyAndOrderFront(CFTypeRef windowRef) {
+ NSWindow *window = (__bridge NSWindow *)windowRef;
+ [window makeKeyAndOrderFront:nil];
+}
+
+void gio_toggleFullScreen(CFTypeRef windowRef) {
+ NSWindow *window = (__bridge NSWindow *)windowRef;
+ [window toggleFullScreen:nil];
+}
+
+CFTypeRef gio_createWindow(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight) {
+ @autoreleasepool {
+ NSRect rect = NSMakeRect(0, 0, width, height);
+ NSUInteger styleMask = NSTitledWindowMask |
+ NSResizableWindowMask |
+ NSMiniaturizableWindowMask |
+ NSClosableWindowMask;
+
+ NSWindow* window = [[NSWindow alloc] initWithContentRect:rect
+ styleMask:styleMask
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ if (minWidth > 0 || minHeight > 0) {
+ window.contentMinSize = NSMakeSize(minWidth, minHeight);
+ }
+ if (maxWidth > 0 || maxHeight > 0) {
+ window.contentMaxSize = NSMakeSize(maxWidth, maxHeight);
+ }
+ [window setAcceptsMouseMovedEvents:YES];
+ if (title != nil) {
+ window.title = [NSString stringWithUTF8String: title];
+ }
+ NSView *view = (__bridge NSView *)viewRef;
+ [window setContentView:view];
+ [window makeFirstResponder:view];
+ window.releasedWhenClosed = NO;
+ window.delegate = globalWindowDel;
+ return (__bridge_retained CFTypeRef)window;
+ }
+}
+
+void gio_close(CFTypeRef windowRef) {
+ NSWindow* window = (__bridge NSWindow *)windowRef;
+ [window performClose:nil];
+}
+
+void gio_setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
+ NSWindow* window = (__bridge NSWindow *)windowRef;
+ NSSize size = NSMakeSize(width, height);
+ [window setContentSize:size];
+}
+
+void gio_setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
+ NSWindow* window = (__bridge NSWindow *)windowRef;
+ window.contentMinSize = NSMakeSize(width, height);
+}
+
+void gio_setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
+ NSWindow* window = (__bridge NSWindow *)windowRef;
+ window.contentMaxSize = NSMakeSize(width, height);
+}
+
+void gio_setTitle(CFTypeRef windowRef, const char *title) {
+ NSWindow* window = (__bridge NSWindow *)windowRef;
+ window.title = [NSString stringWithUTF8String: title];
+}
+
+@implementation GioAppDelegate
+- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
+ [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
+ gio_onFinishLaunching();
+}
+- (void)applicationDidHide:(NSNotification *)aNotification {
+ gio_onAppHide();
+}
+- (void)applicationWillUnhide:(NSNotification *)notification {
+ gio_onAppShow();
+}
+@end
+
+void gio_main() {
+ @autoreleasepool {
+ [NSApplication sharedApplication];
+ GioAppDelegate *del = [[GioAppDelegate alloc] init];
+ [NSApp setDelegate:del];
+ [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
+
+ NSMenuItem *mainMenu = [NSMenuItem new];
+
+ NSMenu *menu = [NSMenu new];
+ NSMenuItem *hideMenuItem = [[NSMenuItem alloc] initWithTitle:@"Hide"
+ action:@selector(hide:)
+ keyEquivalent:@"h"];
+ [menu addItem:hideMenuItem];
+ NSMenuItem *quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit"
+ action:@selector(terminate:)
+ keyEquivalent:@"q"];
+ [menu addItem:quitMenuItem];
+ [mainMenu setSubmenu:menu];
+ NSMenu *menuBar = [NSMenu new];
+ [menuBar addItem:mainMenu];
+ [NSApp setMainMenu:menuBar];
+
+ globalWindowDel = [[GioWindowDelegate alloc] init];
+
+ [NSApp run];
+ }
+}
diff --git a/gio/giold/app/internal/wm/os_unix.go b/gio/giold/app/internal/wm/os_unix.go
new file mode 100644
index 0000000..8143100
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_unix.go
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build linux,!android freebsd openbsd
+
+package wm
+
+import (
+ "errors"
+)
+
+func Main() {
+ select {}
+}
+
+type windowDriver func(Callbacks, *Options) error
+
+// Instead of creating files with build tags for each combination of wayland +/- x11
+// let each driver initialize these variables with their own version of createWindow.
+var wlDriver, x11Driver windowDriver
+
+func NewWindow(window Callbacks, opts *Options) error {
+ var errFirst error
+ for _, d := range []windowDriver{x11Driver, wlDriver} {
+ if d == nil {
+ continue
+ }
+ err := d(window, opts)
+ if err == nil {
+ return nil
+ }
+ if errFirst == nil {
+ errFirst = err
+ }
+ }
+ if errFirst != nil {
+ return errFirst
+ }
+ return errors.New("app: no window driver available")
+}
diff --git a/gio/giold/app/internal/wm/os_wayland.c b/gio/giold/app/internal/wm/os_wayland.c
new file mode 100644
index 0000000..5c1c075
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_wayland.c
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build linux,!android,!nowayland freebsd
+
+#include
+#include "wayland_xdg_shell.h"
+#include "wayland_text_input.h"
+#include "_cgo_export.h"
+
+const struct wl_registry_listener gio_registry_listener = {
+ // Cast away const parameter.
+ .global = (void (*)(void *, struct wl_registry *, uint32_t, const char *, uint32_t))gio_onRegistryGlobal,
+ .global_remove = gio_onRegistryGlobalRemove
+};
+
+const struct wl_surface_listener gio_surface_listener = {
+ .enter = gio_onSurfaceEnter,
+ .leave = gio_onSurfaceLeave,
+};
+
+const struct xdg_surface_listener gio_xdg_surface_listener = {
+ .configure = gio_onXdgSurfaceConfigure,
+};
+
+const struct xdg_toplevel_listener gio_xdg_toplevel_listener = {
+ .configure = gio_onToplevelConfigure,
+ .close = gio_onToplevelClose,
+};
+
+static void xdg_wm_base_handle_ping(void *data, struct xdg_wm_base *wm, uint32_t serial) {
+ xdg_wm_base_pong(wm, serial);
+}
+
+const struct xdg_wm_base_listener gio_xdg_wm_base_listener = {
+ .ping = xdg_wm_base_handle_ping,
+};
+
+const struct wl_callback_listener gio_callback_listener = {
+ .done = gio_onFrameDone,
+};
+
+const struct wl_output_listener gio_output_listener = {
+ // Cast away const parameter.
+ .geometry = (void (*)(void *, struct wl_output *, int32_t, int32_t, int32_t, int32_t, int32_t, const char *, const char *, int32_t))gio_onOutputGeometry,
+ .mode = gio_onOutputMode,
+ .done = gio_onOutputDone,
+ .scale = gio_onOutputScale,
+};
+
+const struct wl_seat_listener gio_seat_listener = {
+ .capabilities = gio_onSeatCapabilities,
+ // Cast away const parameter.
+ .name = (void (*)(void *, struct wl_seat *, const char *))gio_onSeatName,
+};
+
+const struct wl_pointer_listener gio_pointer_listener = {
+ .enter = gio_onPointerEnter,
+ .leave = gio_onPointerLeave,
+ .motion = gio_onPointerMotion,
+ .button = gio_onPointerButton,
+ .axis = gio_onPointerAxis,
+ .frame = gio_onPointerFrame,
+ .axis_source = gio_onPointerAxisSource,
+ .axis_stop = gio_onPointerAxisStop,
+ .axis_discrete = gio_onPointerAxisDiscrete,
+};
+
+const struct wl_touch_listener gio_touch_listener = {
+ .down = gio_onTouchDown,
+ .up = gio_onTouchUp,
+ .motion = gio_onTouchMotion,
+ .frame = gio_onTouchFrame,
+ .cancel = gio_onTouchCancel,
+};
+
+const struct wl_keyboard_listener gio_keyboard_listener = {
+ .keymap = gio_onKeyboardKeymap,
+ .enter = gio_onKeyboardEnter,
+ .leave = gio_onKeyboardLeave,
+ .key = gio_onKeyboardKey,
+ .modifiers = gio_onKeyboardModifiers,
+ .repeat_info = gio_onKeyboardRepeatInfo
+};
+
+const struct zwp_text_input_v3_listener gio_zwp_text_input_v3_listener = {
+ .enter = gio_onTextInputEnter,
+ .leave = gio_onTextInputLeave,
+ // Cast away const parameter.
+ .preedit_string = (void (*)(void *, struct zwp_text_input_v3 *, const char *, int32_t, int32_t))gio_onTextInputPreeditString,
+ .commit_string = (void (*)(void *, struct zwp_text_input_v3 *, const char *))gio_onTextInputCommitString,
+ .delete_surrounding_text = gio_onTextInputDeleteSurroundingText,
+ .done = gio_onTextInputDone
+};
+
+const struct wl_data_device_listener gio_data_device_listener = {
+ .data_offer = gio_onDataDeviceOffer,
+ .enter = gio_onDataDeviceEnter,
+ .leave = gio_onDataDeviceLeave,
+ .motion = gio_onDataDeviceMotion,
+ .drop = gio_onDataDeviceDrop,
+ .selection = gio_onDataDeviceSelection,
+};
+
+const struct wl_data_offer_listener gio_data_offer_listener = {
+ .offer = (void (*)(void *, struct wl_data_offer *, const char *))gio_onDataOfferOffer,
+ .source_actions = gio_onDataOfferSourceActions,
+ .action = gio_onDataOfferAction,
+};
+
+const struct wl_data_source_listener gio_data_source_listener = {
+ .target = (void (*)(void *, struct wl_data_source *, const char *))gio_onDataSourceTarget,
+ .send = (void (*)(void *, struct wl_data_source *, const char *, int32_t))gio_onDataSourceSend,
+ .cancelled = gio_onDataSourceCancelled,
+ .dnd_drop_performed = gio_onDataSourceDNDDropPerformed,
+ .dnd_finished = gio_onDataSourceDNDFinished,
+ .action = gio_onDataSourceAction,
+};
diff --git a/gio/giold/app/internal/wm/os_wayland.go b/gio/giold/app/internal/wm/os_wayland.go
new file mode 100644
index 0000000..f59bc1e
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_wayland.go
@@ -0,0 +1,1694 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build (linux && !android && !nowayland) || freebsd
+// +build linux,!android,!nowayland freebsd
+
+package wm
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "image"
+ "io"
+ "io/ioutil"
+ "math"
+ "os"
+ "os/exec"
+ "strconv"
+ "sync"
+ "time"
+ "unsafe"
+
+ syscall "golang.org/x/sys/unix"
+
+ "realy.lol/gio/app/internal/xkb"
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/fling"
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/unit"
+)
+
+// Use wayland-scanner to generate glue code for the xdg-shell and xdg-decoration extensions.
+//go:generate wayland-scanner client-header /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml wayland_xdg_shell.h
+//go:generate wayland-scanner private-code /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml wayland_xdg_shell.c
+
+//go:generate wayland-scanner client-header /usr/share/wayland-protocols/unstable/text-input/text-input-unstable-v3.xml wayland_text_input.h
+//go:generate wayland-scanner private-code /usr/share/wayland-protocols/unstable/text-input/text-input-unstable-v3.xml wayland_text_input.c
+
+//go:generate wayland-scanner client-header /usr/share/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml wayland_xdg_decoration.h
+//go:generate wayland-scanner private-code /usr/share/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml wayland_xdg_decoration.c
+
+//go:generate sed -i "1s;^;// +build linux,!android,!nowayland freebsd\\n\\n;" wayland_xdg_shell.c
+//go:generate sed -i "1s;^;// +build linux,!android,!nowayland freebsd\\n\\n;" wayland_xdg_decoration.c
+//go:generate sed -i "1s;^;// +build linux,!android,!nowayland freebsd\\n\\n;" wayland_text_input.c
+
+/*
+#cgo linux pkg-config: wayland-client wayland-cursor
+#cgo freebsd openbsd LDFLAGS: -lwayland-client -lwayland-cursor
+#cgo freebsd CFLAGS: -I/usr/local/include
+#cgo freebsd LDFLAGS: -L/usr/local/lib
+
+#include
+#include
+#include
+#include "wayland_text_input.h"
+#include "wayland_xdg_shell.h"
+#include "wayland_xdg_decoration.h"
+
+extern const struct wl_registry_listener gio_registry_listener;
+extern const struct wl_surface_listener gio_surface_listener;
+extern const struct xdg_surface_listener gio_xdg_surface_listener;
+extern const struct xdg_toplevel_listener gio_xdg_toplevel_listener;
+extern const struct xdg_wm_base_listener gio_xdg_wm_base_listener;
+extern const struct wl_callback_listener gio_callback_listener;
+extern const struct wl_output_listener gio_output_listener;
+extern const struct wl_seat_listener gio_seat_listener;
+extern const struct wl_pointer_listener gio_pointer_listener;
+extern const struct wl_touch_listener gio_touch_listener;
+extern const struct wl_keyboard_listener gio_keyboard_listener;
+extern const struct zwp_text_input_v3_listener gio_zwp_text_input_v3_listener;
+extern const struct wl_data_device_listener gio_data_device_listener;
+extern const struct wl_data_offer_listener gio_data_offer_listener;
+extern const struct wl_data_source_listener gio_data_source_listener;
+*/
+import "C"
+
+type wlDisplay struct {
+ disp *C.struct_wl_display
+ reg *C.struct_wl_registry
+ compositor *C.struct_wl_compositor
+ wm *C.struct_xdg_wm_base
+ imm *C.struct_zwp_text_input_manager_v3
+ shm *C.struct_wl_shm
+ dataDeviceManager *C.struct_wl_data_device_manager
+ decor *C.struct_zxdg_decoration_manager_v1
+ seat *wlSeat
+ xkb *xkb.Context
+ outputMap map[C.uint32_t]*C.struct_wl_output
+ outputConfig map[*C.struct_wl_output]*wlOutput
+
+ // Notification pipe fds.
+ notify struct {
+ read, write int
+ }
+
+ repeat repeatState
+}
+
+type wlSeat struct {
+ disp *wlDisplay
+ seat *C.struct_wl_seat
+ name C.uint32_t
+ pointer *C.struct_wl_pointer
+ touch *C.struct_wl_touch
+ keyboard *C.struct_wl_keyboard
+ im *C.struct_zwp_text_input_v3
+
+ // The most recent input serial.
+ serial C.uint32_t
+
+ pointerFocus *window
+ keyboardFocus *window
+ touchFoci map[C.int32_t]*window
+
+ // Clipboard support.
+ dataDev *C.struct_wl_data_device
+ // offers is a map from active wl_data_offers to
+ // the list of mime types they support.
+ offers map[*C.struct_wl_data_offer][]string
+ // clipboard is the wl_data_offer for the clipboard.
+ clipboard *C.struct_wl_data_offer
+ // mimeType is the chosen mime type of clipboard.
+ mimeType string
+ // source represents the clipboard content of the most recent
+ // clipboard write, if any.
+ source *C.struct_wl_data_source
+ // content is the data belonging to source.
+ content []byte
+}
+
+type repeatState struct {
+ rate int
+ delay time.Duration
+
+ key uint32
+ win Callbacks
+ stopC chan struct{}
+
+ start time.Duration
+ last time.Duration
+ mu sync.Mutex
+ now time.Duration
+}
+
+type window struct {
+ w Callbacks
+ disp *wlDisplay
+ surf *C.struct_wl_surface
+ wmSurf *C.struct_xdg_surface
+ topLvl *C.struct_xdg_toplevel
+ decor *C.struct_zxdg_toplevel_decoration_v1
+ ppdp, ppsp float32
+ scroll struct {
+ time time.Duration
+ steps image.Point
+ dist f32.Point
+ }
+ pointerBtns pointer.Buttons
+ lastPos f32.Point
+ lastTouch f32.Point
+
+ cursor struct {
+ theme *C.struct_wl_cursor_theme
+ cursor *C.struct_wl_cursor
+ surf *C.struct_wl_surface
+ }
+
+ fling struct {
+ yExtrapolation fling.Extrapolation
+ xExtrapolation fling.Extrapolation
+ anim fling.Animation
+ start bool
+ dir f32.Point
+ }
+
+ stage system.Stage
+ dead bool
+ lastFrameCallback *C.struct_wl_callback
+
+ mu sync.Mutex
+ animating bool
+ opts *Options
+ needAck bool
+ // The most recent configure serial waiting to be ack'ed.
+ serial C.uint32_t
+ width int
+ height int
+ newScale bool
+ scale int
+ // readClipboard tracks whether a ClipboardEvent is requested.
+ readClipboard bool
+ // writeClipboard is set whenever a clipboard write is requested.
+ writeClipboard *string
+}
+
+type poller struct {
+ pollfds [2]syscall.PollFd
+ // buf is scratch space for draining the notification pipe.
+ buf [100]byte
+}
+
+type wlOutput struct {
+ width int
+ height int
+ physWidth int
+ physHeight int
+ transform C.int32_t
+ scale int
+ windows []*window
+}
+
+// callbackMap maps Wayland native handles to corresponding Go
+// references. It is necessary because the the Wayland client API
+// forces the use of callbacks and storing pointers to Go values
+// in C is forbidden.
+var callbackMap sync.Map
+
+// clipboardMimeTypes is a list of supported clipboard mime types, in
+// order of preference.
+var clipboardMimeTypes = []string{"text/plain;charset=utf8", "UTF8_STRING",
+ "text/plain", "TEXT", "STRING"}
+
+func init() {
+ wlDriver = newWLWindow
+}
+
+func newWLWindow(window Callbacks, opts *Options) error {
+ d, err := newWLDisplay()
+ if err != nil {
+ return err
+ }
+ w, err := d.createNativeWindow(opts)
+ if err != nil {
+ d.destroy()
+ return err
+ }
+ w.w = window
+ go func() {
+ defer d.destroy()
+ defer w.destroy()
+ w.w.SetDriver(w)
+ if err := w.loop(); err != nil {
+ panic(err)
+ }
+ }()
+ return nil
+}
+
+func (d *wlDisplay) writeClipboard(content []byte) error {
+ s := d.seat
+ if s == nil {
+ return nil
+ }
+ // Clear old offer.
+ if s.source != nil {
+ C.wl_data_source_destroy(s.source)
+ s.source = nil
+ s.content = nil
+ }
+ if d.dataDeviceManager == nil || s.dataDev == nil {
+ return nil
+ }
+ s.content = content
+ s.source = C.wl_data_device_manager_create_data_source(d.dataDeviceManager)
+ C.wl_data_source_add_listener(s.source, &C.gio_data_source_listener,
+ unsafe.Pointer(s.seat))
+ for _, mime := range clipboardMimeTypes {
+ C.wl_data_source_offer(s.source, C.CString(mime))
+ }
+ C.wl_data_device_set_selection(s.dataDev, s.source, s.serial)
+ return nil
+}
+
+func (d *wlDisplay) readClipboard() (io.ReadCloser, error) {
+ s := d.seat
+ if s == nil {
+ return nil, nil
+ }
+ if s.clipboard == nil {
+ return nil, nil
+ }
+ r, w, err := os.Pipe()
+ if err != nil {
+ return nil, err
+ }
+ // wl_data_offer_receive performs and implicit dup(2) of the write end
+ // of the pipe. Close our version.
+ defer w.Close()
+ cmimeType := C.CString(s.mimeType)
+ defer C.free(unsafe.Pointer(cmimeType))
+ C.wl_data_offer_receive(s.clipboard, cmimeType, C.int(w.Fd()))
+ return r, nil
+}
+
+func (d *wlDisplay) createNativeWindow(opts *Options) (*window, error) {
+ if d.compositor == nil {
+ return nil, errors.New("wayland: no compositor available")
+ }
+ if d.wm == nil {
+ return nil, errors.New("wayland: no xdg_wm_base available")
+ }
+ if d.shm == nil {
+ return nil, errors.New("wayland: no wl_shm available")
+ }
+ if len(d.outputMap) == 0 {
+ return nil, errors.New("wayland: no outputs available")
+ }
+ var scale int
+ for _, conf := range d.outputConfig {
+ if s := conf.scale; s > scale {
+ scale = s
+ }
+ }
+ ppdp := detectUIScale()
+
+ w := &window{
+ disp: d,
+ scale: scale,
+ newScale: scale != 1,
+ ppdp: ppdp,
+ ppsp: ppdp,
+ }
+ w.surf = C.wl_compositor_create_surface(d.compositor)
+ if w.surf == nil {
+ w.destroy()
+ return nil, errors.New("wayland: wl_compositor_create_surface failed")
+ }
+ callbackStore(unsafe.Pointer(w.surf), w)
+ w.wmSurf = C.xdg_wm_base_get_xdg_surface(d.wm, w.surf)
+ if w.wmSurf == nil {
+ w.destroy()
+ return nil, errors.New("wayland: xdg_wm_base_get_xdg_surface failed")
+ }
+ w.topLvl = C.xdg_surface_get_toplevel(w.wmSurf)
+ if w.topLvl == nil {
+ w.destroy()
+ return nil, errors.New("wayland: xdg_surface_get_toplevel failed")
+ }
+ w.cursor.theme = C.wl_cursor_theme_load(nil, 32, d.shm)
+ if w.cursor.theme == nil {
+ w.destroy()
+ return nil, errors.New("wayland: wl_cursor_theme_load failed")
+ }
+ cname := C.CString("left_ptr")
+ defer C.free(unsafe.Pointer(cname))
+ w.cursor.cursor = C.wl_cursor_theme_get_cursor(w.cursor.theme, cname)
+ if w.cursor.cursor == nil {
+ w.destroy()
+ return nil, errors.New("wayland: wl_cursor_theme_get_cursor failed")
+ }
+ w.cursor.surf = C.wl_compositor_create_surface(d.compositor)
+ if w.cursor.surf == nil {
+ w.destroy()
+ return nil, errors.New("wayland: wl_compositor_create_surface failed")
+ }
+ C.xdg_wm_base_add_listener(d.wm, &C.gio_xdg_wm_base_listener,
+ unsafe.Pointer(w.surf))
+ C.wl_surface_add_listener(w.surf, &C.gio_surface_listener,
+ unsafe.Pointer(w.surf))
+ C.xdg_surface_add_listener(w.wmSurf, &C.gio_xdg_surface_listener,
+ unsafe.Pointer(w.surf))
+ C.xdg_toplevel_add_listener(w.topLvl, &C.gio_xdg_toplevel_listener,
+ unsafe.Pointer(w.surf))
+
+ w.setOptions(opts)
+
+ if d.decor != nil {
+ // Request server side decorations.
+ w.decor = C.zxdg_decoration_manager_v1_get_toplevel_decoration(d.decor,
+ w.topLvl)
+ C.zxdg_toplevel_decoration_v1_set_mode(w.decor,
+ C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE)
+ }
+ w.updateOpaqueRegion()
+ C.wl_surface_commit(w.surf)
+ return w, nil
+}
+
+func callbackDelete(k unsafe.Pointer) {
+ callbackMap.Delete(k)
+}
+
+func callbackStore(k unsafe.Pointer, v interface{}) {
+ callbackMap.Store(k, v)
+}
+
+func callbackLoad(k unsafe.Pointer) interface{} {
+ v, exists := callbackMap.Load(k)
+ if !exists {
+ panic("missing callback entry")
+ }
+ return v
+}
+
+//export gio_onSeatCapabilities
+func gio_onSeatCapabilities(data unsafe.Pointer, seat *C.struct_wl_seat,
+ caps C.uint32_t) {
+ s := callbackLoad(data).(*wlSeat)
+ s.updateCaps(caps)
+}
+
+// flushOffers remove all wl_data_offers that isn't the clipboard
+// content.
+func (s *wlSeat) flushOffers() {
+ for o := range s.offers {
+ if o == s.clipboard {
+ continue
+ }
+ // We're only interested in clipboard offers.
+ delete(s.offers, o)
+ callbackDelete(unsafe.Pointer(o))
+ C.wl_data_offer_destroy(o)
+ }
+}
+
+func (s *wlSeat) destroy() {
+ if s.source != nil {
+ C.wl_data_source_destroy(s.source)
+ s.source = nil
+ }
+ if s.im != nil {
+ C.zwp_text_input_v3_destroy(s.im)
+ s.im = nil
+ }
+ if s.pointer != nil {
+ C.wl_pointer_release(s.pointer)
+ }
+ if s.touch != nil {
+ C.wl_touch_release(s.touch)
+ }
+ if s.keyboard != nil {
+ C.wl_keyboard_release(s.keyboard)
+ }
+ s.clipboard = nil
+ s.flushOffers()
+ if s.dataDev != nil {
+ C.wl_data_device_release(s.dataDev)
+ }
+ if s.seat != nil {
+ callbackDelete(unsafe.Pointer(s.seat))
+ C.wl_seat_release(s.seat)
+ }
+}
+
+func (s *wlSeat) updateCaps(caps C.uint32_t) {
+ if s.im == nil && s.disp.imm != nil {
+ s.im = C.zwp_text_input_manager_v3_get_text_input(s.disp.imm, s.seat)
+ C.zwp_text_input_v3_add_listener(s.im,
+ &C.gio_zwp_text_input_v3_listener, unsafe.Pointer(s.seat))
+ }
+ switch {
+ case s.pointer == nil && caps&C.WL_SEAT_CAPABILITY_POINTER != 0:
+ s.pointer = C.wl_seat_get_pointer(s.seat)
+ C.wl_pointer_add_listener(s.pointer, &C.gio_pointer_listener,
+ unsafe.Pointer(s.seat))
+ case s.pointer != nil && caps&C.WL_SEAT_CAPABILITY_POINTER == 0:
+ C.wl_pointer_release(s.pointer)
+ s.pointer = nil
+ }
+ switch {
+ case s.touch == nil && caps&C.WL_SEAT_CAPABILITY_TOUCH != 0:
+ s.touch = C.wl_seat_get_touch(s.seat)
+ C.wl_touch_add_listener(s.touch, &C.gio_touch_listener,
+ unsafe.Pointer(s.seat))
+ case s.touch != nil && caps&C.WL_SEAT_CAPABILITY_TOUCH == 0:
+ C.wl_touch_release(s.touch)
+ s.touch = nil
+ }
+ switch {
+ case s.keyboard == nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD != 0:
+ s.keyboard = C.wl_seat_get_keyboard(s.seat)
+ C.wl_keyboard_add_listener(s.keyboard, &C.gio_keyboard_listener,
+ unsafe.Pointer(s.seat))
+ case s.keyboard != nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD == 0:
+ C.wl_keyboard_release(s.keyboard)
+ s.keyboard = nil
+ }
+}
+
+//export gio_onSeatName
+func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) {
+}
+
+//export gio_onXdgSurfaceConfigure
+func gio_onXdgSurfaceConfigure(data unsafe.Pointer,
+ wmSurf *C.struct_xdg_surface, serial C.uint32_t) {
+ w := callbackLoad(data).(*window)
+ w.mu.Lock()
+ w.serial = serial
+ w.needAck = true
+ w.mu.Unlock()
+ w.setStage(system.StageRunning)
+ w.draw(true)
+}
+
+//export gio_onToplevelClose
+func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
+ w := callbackLoad(data).(*window)
+ w.dead = true
+}
+
+//export gio_onToplevelConfigure
+func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel,
+ width, height C.int32_t, states *C.struct_wl_array) {
+ w := callbackLoad(data).(*window)
+ if width != 0 && height != 0 {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ w.width = int(width)
+ w.height = int(height)
+ w.updateOpaqueRegion()
+ }
+}
+
+//export gio_onOutputMode
+func gio_onOutputMode(data unsafe.Pointer, output *C.struct_wl_output,
+ flags C.uint32_t, width, height, refresh C.int32_t) {
+ if flags&C.WL_OUTPUT_MODE_CURRENT == 0 {
+ return
+ }
+ d := callbackLoad(data).(*wlDisplay)
+ c := d.outputConfig[output]
+ c.width = int(width)
+ c.height = int(height)
+}
+
+//export gio_onOutputGeometry
+func gio_onOutputGeometry(data unsafe.Pointer, output *C.struct_wl_output,
+ x, y, physWidth, physHeight, subpixel C.int32_t, make, model *C.char,
+ transform C.int32_t) {
+ d := callbackLoad(data).(*wlDisplay)
+ c := d.outputConfig[output]
+ c.transform = transform
+ c.physWidth = int(physWidth)
+ c.physHeight = int(physHeight)
+}
+
+//export gio_onOutputScale
+func gio_onOutputScale(data unsafe.Pointer, output *C.struct_wl_output,
+ scale C.int32_t) {
+ d := callbackLoad(data).(*wlDisplay)
+ c := d.outputConfig[output]
+ c.scale = int(scale)
+}
+
+//export gio_onOutputDone
+func gio_onOutputDone(data unsafe.Pointer, output *C.struct_wl_output) {
+ d := callbackLoad(data).(*wlDisplay)
+ conf := d.outputConfig[output]
+ for _, w := range conf.windows {
+ w.draw(true)
+ }
+}
+
+//export gio_onSurfaceEnter
+func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface,
+ output *C.struct_wl_output) {
+ w := callbackLoad(data).(*window)
+ conf := w.disp.outputConfig[output]
+ var found bool
+ for _, w2 := range conf.windows {
+ if w2 == w {
+ found = true
+ break
+ }
+ }
+ if !found {
+ conf.windows = append(conf.windows, w)
+ }
+ w.updateOutputs()
+}
+
+//export gio_onSurfaceLeave
+func gio_onSurfaceLeave(data unsafe.Pointer, surf *C.struct_wl_surface,
+ output *C.struct_wl_output) {
+ w := callbackLoad(data).(*window)
+ conf := w.disp.outputConfig[output]
+ for i, w2 := range conf.windows {
+ if w2 == w {
+ conf.windows = append(conf.windows[:i], conf.windows[i+1:]...)
+ break
+ }
+ }
+ w.updateOutputs()
+}
+
+//export gio_onRegistryGlobal
+func gio_onRegistryGlobal(data unsafe.Pointer, reg *C.struct_wl_registry,
+ name C.uint32_t, cintf *C.char, version C.uint32_t) {
+ d := callbackLoad(data).(*wlDisplay)
+ switch C.GoString(cintf) {
+ case "wl_compositor":
+ d.compositor = (*C.struct_wl_compositor)(C.wl_registry_bind(reg, name,
+ &C.wl_compositor_interface, 3))
+ case "wl_output":
+ output := (*C.struct_wl_output)(C.wl_registry_bind(reg, name,
+ &C.wl_output_interface, 2))
+ C.wl_output_add_listener(output, &C.gio_output_listener,
+ unsafe.Pointer(d.disp))
+ d.outputMap[name] = output
+ d.outputConfig[output] = new(wlOutput)
+ case "wl_seat":
+ if d.seat != nil {
+ break
+ }
+ s := (*C.struct_wl_seat)(C.wl_registry_bind(reg, name,
+ &C.wl_seat_interface, 5))
+ if s == nil {
+ // No support for v5 protocol.
+ break
+ }
+ d.seat = &wlSeat{
+ disp: d,
+ name: name,
+ seat: s,
+ offers: make(map[*C.struct_wl_data_offer][]string),
+ touchFoci: make(map[C.int32_t]*window),
+ }
+ callbackStore(unsafe.Pointer(s), d.seat)
+ C.wl_seat_add_listener(s, &C.gio_seat_listener, unsafe.Pointer(s))
+ if d.dataDeviceManager == nil {
+ break
+ }
+ d.seat.dataDev = C.wl_data_device_manager_get_data_device(d.dataDeviceManager,
+ s)
+ if d.seat.dataDev == nil {
+ break
+ }
+ callbackStore(unsafe.Pointer(d.seat.dataDev), d.seat)
+ C.wl_data_device_add_listener(d.seat.dataDev,
+ &C.gio_data_device_listener, unsafe.Pointer(d.seat.dataDev))
+ case "wl_shm":
+ d.shm = (*C.struct_wl_shm)(C.wl_registry_bind(reg, name,
+ &C.wl_shm_interface, 1))
+ case "xdg_wm_base":
+ d.wm = (*C.struct_xdg_wm_base)(C.wl_registry_bind(reg, name,
+ &C.xdg_wm_base_interface, 1))
+ case "zxdg_decoration_manager_v1":
+ d.decor = (*C.struct_zxdg_decoration_manager_v1)(C.wl_registry_bind(reg,
+ name, &C.zxdg_decoration_manager_v1_interface, 1))
+ // TODO: Implement and test text-input support.
+ /*case "zwp_text_input_manager_v3":
+ d.imm = (*C.struct_zwp_text_input_manager_v3)(C.wl_registry_bind(reg, name, &C.zwp_text_input_manager_v3_interface, 1))*/
+ case "wl_data_device_manager":
+ d.dataDeviceManager = (*C.struct_wl_data_device_manager)(C.wl_registry_bind(reg,
+ name, &C.wl_data_device_manager_interface, 3))
+ }
+}
+
+//export gio_onDataOfferOffer
+func gio_onDataOfferOffer(data unsafe.Pointer, offer *C.struct_wl_data_offer,
+ mime *C.char) {
+ s := callbackLoad(data).(*wlSeat)
+ s.offers[offer] = append(s.offers[offer], C.GoString(mime))
+}
+
+//export gio_onDataOfferSourceActions
+func gio_onDataOfferSourceActions(data unsafe.Pointer,
+ offer *C.struct_wl_data_offer, acts C.uint32_t) {
+}
+
+//export gio_onDataOfferAction
+func gio_onDataOfferAction(data unsafe.Pointer, offer *C.struct_wl_data_offer,
+ act C.uint32_t) {
+}
+
+//export gio_onDataDeviceOffer
+func gio_onDataDeviceOffer(data unsafe.Pointer,
+ dataDev *C.struct_wl_data_device, id *C.struct_wl_data_offer) {
+ s := callbackLoad(data).(*wlSeat)
+ callbackStore(unsafe.Pointer(id), s)
+ C.wl_data_offer_add_listener(id, &C.gio_data_offer_listener,
+ unsafe.Pointer(id))
+ s.offers[id] = nil
+}
+
+//export gio_onDataDeviceEnter
+func gio_onDataDeviceEnter(data unsafe.Pointer,
+ dataDev *C.struct_wl_data_device, serial C.uint32_t,
+ surf *C.struct_wl_surface, x, y C.wl_fixed_t, id *C.struct_wl_data_offer) {
+ s := callbackLoad(data).(*wlSeat)
+ s.serial = serial
+ s.flushOffers()
+}
+
+//export gio_onDataDeviceLeave
+func gio_onDataDeviceLeave(data unsafe.Pointer,
+ dataDev *C.struct_wl_data_device) {
+}
+
+//export gio_onDataDeviceMotion
+func gio_onDataDeviceMotion(data unsafe.Pointer,
+ dataDev *C.struct_wl_data_device, t C.uint32_t, x, y C.wl_fixed_t) {
+}
+
+//export gio_onDataDeviceDrop
+func gio_onDataDeviceDrop(data unsafe.Pointer,
+ dataDev *C.struct_wl_data_device) {
+}
+
+//export gio_onDataDeviceSelection
+func gio_onDataDeviceSelection(data unsafe.Pointer,
+ dataDev *C.struct_wl_data_device, id *C.struct_wl_data_offer) {
+ s := callbackLoad(data).(*wlSeat)
+ defer s.flushOffers()
+ s.clipboard = nil
+loop:
+ for _, want := range clipboardMimeTypes {
+ for _, got := range s.offers[id] {
+ if want != got {
+ continue
+ }
+ s.clipboard = id
+ s.mimeType = got
+ break loop
+ }
+ }
+}
+
+//export gio_onRegistryGlobalRemove
+func gio_onRegistryGlobalRemove(data unsafe.Pointer, reg *C.struct_wl_registry,
+ name C.uint32_t) {
+ d := callbackLoad(data).(*wlDisplay)
+ if s := d.seat; s != nil && name == s.name {
+ s.destroy()
+ d.seat = nil
+ }
+ if output, exists := d.outputMap[name]; exists {
+ C.wl_output_destroy(output)
+ delete(d.outputMap, name)
+ delete(d.outputConfig, output)
+ }
+}
+
+//export gio_onTouchDown
+func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch,
+ serial, t C.uint32_t, surf *C.struct_wl_surface, id C.int32_t,
+ x, y C.wl_fixed_t) {
+ s := callbackLoad(data).(*wlSeat)
+ s.serial = serial
+ w := callbackLoad(unsafe.Pointer(surf)).(*window)
+ s.touchFoci[id] = w
+ w.lastTouch = f32.Point{
+ X: fromFixed(x) * float32(w.scale),
+ Y: fromFixed(y) * float32(w.scale),
+ }
+ w.w.Event(pointer.Event{
+ Type: pointer.Press,
+ Source: pointer.Touch,
+ Position: w.lastTouch,
+ PointerID: pointer.ID(id),
+ Time: time.Duration(t) * time.Millisecond,
+ Modifiers: w.disp.xkb.Modifiers(),
+ })
+}
+
+//export gio_onTouchUp
+func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch,
+ serial, t C.uint32_t, id C.int32_t) {
+ s := callbackLoad(data).(*wlSeat)
+ s.serial = serial
+ w := s.touchFoci[id]
+ delete(s.touchFoci, id)
+ w.w.Event(pointer.Event{
+ Type: pointer.Release,
+ Source: pointer.Touch,
+ Position: w.lastTouch,
+ PointerID: pointer.ID(id),
+ Time: time.Duration(t) * time.Millisecond,
+ Modifiers: w.disp.xkb.Modifiers(),
+ })
+}
+
+//export gio_onTouchMotion
+func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch,
+ t C.uint32_t, id C.int32_t, x, y C.wl_fixed_t) {
+ s := callbackLoad(data).(*wlSeat)
+ w := s.touchFoci[id]
+ w.lastTouch = f32.Point{
+ X: fromFixed(x) * float32(w.scale),
+ Y: fromFixed(y) * float32(w.scale),
+ }
+ w.w.Event(pointer.Event{
+ Type: pointer.Move,
+ Position: w.lastTouch,
+ Source: pointer.Touch,
+ PointerID: pointer.ID(id),
+ Time: time.Duration(t) * time.Millisecond,
+ Modifiers: w.disp.xkb.Modifiers(),
+ })
+}
+
+//export gio_onTouchFrame
+func gio_onTouchFrame(data unsafe.Pointer, touch *C.struct_wl_touch) {
+}
+
+//export gio_onTouchCancel
+func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) {
+ s := callbackLoad(data).(*wlSeat)
+ for id, w := range s.touchFoci {
+ delete(s.touchFoci, id)
+ w.w.Event(pointer.Event{
+ Type: pointer.Cancel,
+ Source: pointer.Touch,
+ })
+ }
+}
+
+//export gio_onPointerEnter
+func gio_onPointerEnter(data unsafe.Pointer, pointer *C.struct_wl_pointer,
+ serial C.uint32_t, surf *C.struct_wl_surface, x, y C.wl_fixed_t) {
+ s := callbackLoad(data).(*wlSeat)
+ s.serial = serial
+ w := callbackLoad(unsafe.Pointer(surf)).(*window)
+ s.pointerFocus = w
+ w.setCursor(pointer, serial)
+ w.lastPos = f32.Point{X: fromFixed(x), Y: fromFixed(y)}
+}
+
+//export gio_onPointerLeave
+func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer,
+ serial C.uint32_t, surface *C.struct_wl_surface) {
+ s := callbackLoad(data).(*wlSeat)
+ s.serial = serial
+}
+
+//export gio_onPointerMotion
+func gio_onPointerMotion(data unsafe.Pointer, p *C.struct_wl_pointer,
+ t C.uint32_t, x, y C.wl_fixed_t) {
+ s := callbackLoad(data).(*wlSeat)
+ w := s.pointerFocus
+ w.resetFling()
+ w.onPointerMotion(x, y, t)
+}
+
+//export gio_onPointerButton
+func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer,
+ serial, t, wbtn, state C.uint32_t) {
+ s := callbackLoad(data).(*wlSeat)
+ s.serial = serial
+ w := s.pointerFocus
+ // From linux-event-codes.h.
+ const (
+ BTN_LEFT = 0x110
+ BTN_RIGHT = 0x111
+ BTN_MIDDLE = 0x112
+ )
+ var btn pointer.Buttons
+ switch wbtn {
+ case BTN_LEFT:
+ btn = pointer.ButtonPrimary
+ case BTN_RIGHT:
+ btn = pointer.ButtonSecondary
+ case BTN_MIDDLE:
+ btn = pointer.ButtonTertiary
+ default:
+ return
+ }
+ var typ pointer.Type
+ switch state {
+ case 0:
+ w.pointerBtns &^= btn
+ typ = pointer.Release
+ case 1:
+ w.pointerBtns |= btn
+ typ = pointer.Press
+ }
+ w.flushScroll()
+ w.resetFling()
+ w.w.Event(pointer.Event{
+ Type: typ,
+ Source: pointer.Mouse,
+ Buttons: w.pointerBtns,
+ Position: w.lastPos,
+ Time: time.Duration(t) * time.Millisecond,
+ Modifiers: w.disp.xkb.Modifiers(),
+ })
+}
+
+//export gio_onPointerAxis
+func gio_onPointerAxis(data unsafe.Pointer, p *C.struct_wl_pointer,
+ t, axis C.uint32_t, value C.wl_fixed_t) {
+ s := callbackLoad(data).(*wlSeat)
+ w := s.pointerFocus
+ v := fromFixed(value)
+ w.resetFling()
+ if w.scroll.dist == (f32.Point{}) {
+ w.scroll.time = time.Duration(t) * time.Millisecond
+ }
+ switch axis {
+ case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL:
+ w.scroll.dist.X += v
+ case C.WL_POINTER_AXIS_VERTICAL_SCROLL:
+ w.scroll.dist.Y += v
+ }
+}
+
+//export gio_onPointerFrame
+func gio_onPointerFrame(data unsafe.Pointer, p *C.struct_wl_pointer) {
+ s := callbackLoad(data).(*wlSeat)
+ w := s.pointerFocus
+ w.flushScroll()
+ w.flushFling()
+}
+
+func (w *window) flushFling() {
+ if !w.fling.start {
+ return
+ }
+ w.fling.start = false
+ estx, esty := w.fling.xExtrapolation.Estimate(), w.fling.yExtrapolation.Estimate()
+ w.fling.xExtrapolation = fling.Extrapolation{}
+ w.fling.yExtrapolation = fling.Extrapolation{}
+ vel := float32(math.Sqrt(float64(estx.Velocity*estx.Velocity + esty.Velocity*esty.Velocity)))
+ _, _, c := w.config()
+ if !w.fling.anim.Start(c, time.Now(), vel) {
+ return
+ }
+ invDist := 1 / vel
+ w.fling.dir.X = estx.Velocity * invDist
+ w.fling.dir.Y = esty.Velocity * invDist
+ // Wake up the window loop.
+ w.disp.wakeup()
+}
+
+//export gio_onPointerAxisSource
+func gio_onPointerAxisSource(data unsafe.Pointer, pointer *C.struct_wl_pointer,
+ source C.uint32_t) {
+}
+
+//export gio_onPointerAxisStop
+func gio_onPointerAxisStop(data unsafe.Pointer, p *C.struct_wl_pointer,
+ t, axis C.uint32_t) {
+ s := callbackLoad(data).(*wlSeat)
+ w := s.pointerFocus
+ w.fling.start = true
+}
+
+//export gio_onPointerAxisDiscrete
+func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer,
+ axis C.uint32_t, discrete C.int32_t) {
+ s := callbackLoad(data).(*wlSeat)
+ w := s.pointerFocus
+ w.resetFling()
+ switch axis {
+ case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL:
+ w.scroll.steps.X += int(discrete)
+ case C.WL_POINTER_AXIS_VERTICAL_SCROLL:
+ w.scroll.steps.Y += int(discrete)
+ }
+}
+
+func (w *window) ReadClipboard() {
+ w.mu.Lock()
+ w.readClipboard = true
+ w.mu.Unlock()
+ w.disp.wakeup()
+}
+
+func (w *window) WriteClipboard(s string) {
+ w.mu.Lock()
+ w.writeClipboard = &s
+ w.mu.Unlock()
+ w.disp.wakeup()
+}
+
+func (w *window) Option(opts *Options) {
+ w.mu.Lock()
+ w.opts = opts
+ w.mu.Unlock()
+ w.disp.wakeup()
+}
+
+func (w *window) setOptions(opts *Options) {
+ _, _, cfg := w.config()
+ if o := opts.Size; o != nil {
+ w.width = cfg.Px(o.Width)
+ w.height = cfg.Px(o.Height)
+ }
+ if o := opts.Title; o != nil {
+ title := C.CString(*o)
+ C.xdg_toplevel_set_title(w.topLvl, title)
+ C.free(unsafe.Pointer(title))
+ }
+}
+
+func (w *window) SetCursor(name pointer.CursorName) {
+ if name == pointer.CursorNone {
+ C.wl_pointer_set_cursor(w.disp.seat.pointer, w.serial, nil, 0, 0)
+ return
+ }
+ switch name {
+ default:
+ fallthrough
+ case pointer.CursorDefault:
+ name = "left_ptr"
+ case pointer.CursorText:
+ name = "xterm"
+ case pointer.CursorPointer:
+ name = "hand1"
+ case pointer.CursorCrossHair:
+ name = "crosshair"
+ case pointer.CursorRowResize:
+ name = "top_side"
+ case pointer.CursorColResize:
+ name = "left_side"
+ case pointer.CursorGrab:
+ name = "hand1"
+ }
+ cname := C.CString(string(name))
+ defer C.free(unsafe.Pointer(cname))
+ c := C.wl_cursor_theme_get_cursor(w.cursor.theme, cname)
+ if c == nil {
+ return
+ }
+ w.cursor.cursor = c
+ w.setCursor(w.disp.seat.pointer, w.serial)
+}
+
+func (w *window) setCursor(pointer *C.struct_wl_pointer, serial C.uint32_t) {
+ // Get images[0].
+ img := *w.cursor.cursor.images
+ buf := C.wl_cursor_image_get_buffer(img)
+ if buf == nil {
+ return
+ }
+ C.wl_pointer_set_cursor(pointer, serial, w.cursor.surf,
+ C.int32_t(img.hotspot_x), C.int32_t(img.hotspot_y))
+ C.wl_surface_attach(w.cursor.surf, buf, 0, 0)
+ C.wl_surface_damage(w.cursor.surf, 0, 0, C.int32_t(img.width),
+ C.int32_t(img.height))
+ C.wl_surface_commit(w.cursor.surf)
+}
+
+func (w *window) resetFling() {
+ w.fling.start = false
+ w.fling.anim = fling.Animation{}
+}
+
+//export gio_onKeyboardKeymap
+func gio_onKeyboardKeymap(data unsafe.Pointer, keyboard *C.struct_wl_keyboard,
+ format C.uint32_t, fd C.int32_t, size C.uint32_t) {
+ defer syscall.Close(int(fd))
+ s := callbackLoad(data).(*wlSeat)
+ s.disp.repeat.Stop(0)
+ s.disp.xkb.DestroyKeymapState()
+ if format != C.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 {
+ return
+ }
+ if err := s.disp.xkb.LoadKeymap(int(format), int(fd),
+ int(size)); err != nil {
+ // TODO: Do better.
+ panic(err)
+ }
+}
+
+//export gio_onKeyboardEnter
+func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard,
+ serial C.uint32_t, surf *C.struct_wl_surface, keys *C.struct_wl_array) {
+ s := callbackLoad(data).(*wlSeat)
+ s.serial = serial
+ w := callbackLoad(unsafe.Pointer(surf)).(*window)
+ s.keyboardFocus = w
+ s.disp.repeat.Stop(0)
+ w.w.Event(key.FocusEvent{Focus: true})
+}
+
+//export gio_onKeyboardLeave
+func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard,
+ serial C.uint32_t, surf *C.struct_wl_surface) {
+ s := callbackLoad(data).(*wlSeat)
+ s.serial = serial
+ s.disp.repeat.Stop(0)
+ w := s.keyboardFocus
+ w.w.Event(key.FocusEvent{Focus: false})
+}
+
+//export gio_onKeyboardKey
+func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard,
+ serial, timestamp, keyCode, state C.uint32_t) {
+ s := callbackLoad(data).(*wlSeat)
+ s.serial = serial
+ w := s.keyboardFocus
+ t := time.Duration(timestamp) * time.Millisecond
+ s.disp.repeat.Stop(t)
+ w.resetFling()
+ kc := mapXKBKeycode(uint32(keyCode))
+ ks := mapXKBKeyState(uint32(state))
+ for _, e := range w.disp.xkb.DispatchKey(kc, ks) {
+ w.w.Event(e)
+ }
+ if state != C.WL_KEYBOARD_KEY_STATE_PRESSED {
+ return
+ }
+ if w.disp.xkb.IsRepeatKey(kc) {
+ w.disp.repeat.Start(w, kc, t)
+ }
+}
+
+func mapXKBKeycode(keyCode uint32) uint32 {
+ // According to the xkb_v1 spec: "to determine the xkb keycode, clients must add 8 to the key event keycode."
+ return keyCode + 8
+}
+
+func mapXKBKeyState(state uint32) key.State {
+ switch state {
+ case C.WL_KEYBOARD_KEY_STATE_RELEASED:
+ return key.Release
+ default:
+ return key.Press
+ }
+}
+
+func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) {
+ if r.rate <= 0 {
+ return
+ }
+ stopC := make(chan struct{})
+ r.start = t
+ r.last = 0
+ r.now = 0
+ r.stopC = stopC
+ r.key = keyCode
+ r.win = w.w
+ rate, delay := r.rate, r.delay
+ go func() {
+ timer := time.NewTimer(delay)
+ for {
+ select {
+ case <-timer.C:
+ case <-stopC:
+ close(stopC)
+ return
+ }
+ r.Advance(delay)
+ w.disp.wakeup()
+ delay = time.Second / time.Duration(rate)
+ timer.Reset(delay)
+ }
+ }()
+}
+
+func (r *repeatState) Stop(t time.Duration) {
+ if r.stopC == nil {
+ return
+ }
+ r.stopC <- struct{}{}
+ <-r.stopC
+ r.stopC = nil
+ t -= r.start
+ if r.now > t {
+ r.now = t
+ }
+}
+
+func (r *repeatState) Advance(dt time.Duration) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.now += dt
+}
+
+func (r *repeatState) Repeat(d *wlDisplay) {
+ if r.rate <= 0 {
+ return
+ }
+ r.mu.Lock()
+ now := r.now
+ r.mu.Unlock()
+ for {
+ var delay time.Duration
+ if r.last < r.delay {
+ delay = r.delay
+ } else {
+ delay = time.Second / time.Duration(r.rate)
+ }
+ if r.last+delay > now {
+ break
+ }
+ for _, e := range d.xkb.DispatchKey(r.key, key.Press) {
+ r.win.Event(e)
+ }
+ r.last += delay
+ }
+}
+
+//export gio_onFrameDone
+func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback,
+ t C.uint32_t) {
+ C.wl_callback_destroy(callback)
+ w := callbackLoad(data).(*window)
+ if w.lastFrameCallback == callback {
+ w.lastFrameCallback = nil
+ w.draw(false)
+ }
+}
+
+func (w *window) loop() error {
+ var p poller
+ for {
+ if err := w.disp.dispatch(&p); err != nil {
+ return err
+ }
+ if w.dead {
+ w.w.Event(system.DestroyEvent{})
+ break
+ }
+ w.process()
+ }
+ return nil
+}
+
+func (w *window) process() {
+ w.mu.Lock()
+ readClipboard := w.readClipboard
+ writeClipboard := w.writeClipboard
+ opts := w.opts
+ w.readClipboard = false
+ w.writeClipboard = nil
+ w.opts = nil
+ w.mu.Unlock()
+ if readClipboard {
+ r, err := w.disp.readClipboard()
+ // Send empty responses on unavailable clipboards or errors.
+ if r == nil || err != nil {
+ w.w.Event(clipboard.Event{})
+ return
+ }
+ // Don't let slow clipboard transfers block event loop.
+ go func() {
+ defer r.Close()
+ data, _ := ioutil.ReadAll(r)
+ w.w.Event(clipboard.Event{Text: string(data)})
+ }()
+ }
+ if writeClipboard != nil {
+ w.disp.writeClipboard([]byte(*writeClipboard))
+ }
+ if opts != nil {
+ w.setOptions(opts)
+ }
+ // pass false to skip unnecessary drawing.
+ w.draw(false)
+}
+
+func (d *wlDisplay) dispatch(p *poller) error {
+ dispfd := C.wl_display_get_fd(d.disp)
+ // Poll for events and notifications.
+ pollfds := append(p.pollfds[:0],
+ syscall.PollFd{Fd: int32(dispfd),
+ Events: syscall.POLLIN | syscall.POLLERR},
+ syscall.PollFd{Fd: int32(d.notify.read),
+ Events: syscall.POLLIN | syscall.POLLERR},
+ )
+ dispFd := &pollfds[0]
+ if ret, err := C.wl_display_flush(d.disp); ret < 0 {
+ if err != syscall.EAGAIN {
+ return fmt.Errorf("wayland: wl_display_flush failed: %v", err)
+ }
+ // EAGAIN means the output buffer was full. Poll for
+ // POLLOUT to know when we can write again.
+ dispFd.Events |= syscall.POLLOUT
+ }
+ if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
+ return fmt.Errorf("wayland: poll failed: %v", err)
+ }
+ // Clear notifications.
+ for {
+ _, err := syscall.Read(d.notify.read, p.buf[:])
+ if err == syscall.EAGAIN {
+ break
+ }
+ if err != nil {
+ return fmt.Errorf("wayland: read from notify pipe failed: %v", err)
+ }
+ }
+ // Handle events
+ switch {
+ case dispFd.Revents&syscall.POLLIN != 0:
+ if ret, err := C.wl_display_dispatch(d.disp); ret < 0 {
+ return fmt.Errorf("wayland: wl_display_dispatch failed: %v", err)
+ }
+ case dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0:
+ return errors.New("wayland: display file descriptor gone")
+ }
+ d.repeat.Repeat(d)
+ return nil
+}
+
+func (w *window) SetAnimating(anim bool) {
+ w.mu.Lock()
+ w.animating = anim
+ w.mu.Unlock()
+ w.disp.wakeup()
+}
+
+// Wakeup wakes up the event loop through the notification pipe.
+func (d *wlDisplay) wakeup() {
+ oneByte := make([]byte, 1)
+ if _, err := syscall.Write(d.notify.write,
+ oneByte); err != nil && err != syscall.EAGAIN {
+ panic(fmt.Errorf("failed to write to pipe: %v", err))
+ }
+}
+
+func (w *window) destroy() {
+ if w.cursor.surf != nil {
+ C.wl_surface_destroy(w.cursor.surf)
+ }
+ if w.cursor.theme != nil {
+ C.wl_cursor_theme_destroy(w.cursor.theme)
+ }
+ if w.topLvl != nil {
+ C.xdg_toplevel_destroy(w.topLvl)
+ }
+ if w.surf != nil {
+ C.wl_surface_destroy(w.surf)
+ }
+ if w.wmSurf != nil {
+ C.xdg_surface_destroy(w.wmSurf)
+ }
+ if w.decor != nil {
+ C.zxdg_toplevel_decoration_v1_destroy(w.decor)
+ }
+ callbackDelete(unsafe.Pointer(w.surf))
+}
+
+//export gio_onKeyboardModifiers
+func gio_onKeyboardModifiers(data unsafe.Pointer,
+ keyboard *C.struct_wl_keyboard,
+ serial, depressed, latched, locked, group C.uint32_t) {
+ s := callbackLoad(data).(*wlSeat)
+ s.serial = serial
+ d := s.disp
+ d.repeat.Stop(0)
+ if d.xkb == nil {
+ return
+ }
+ d.xkb.UpdateMask(uint32(depressed), uint32(latched), uint32(locked),
+ uint32(group), uint32(group), uint32(group))
+}
+
+//export gio_onKeyboardRepeatInfo
+func gio_onKeyboardRepeatInfo(data unsafe.Pointer,
+ keyboard *C.struct_wl_keyboard, rate, delay C.int32_t) {
+ s := callbackLoad(data).(*wlSeat)
+ d := s.disp
+ d.repeat.Stop(0)
+ d.repeat.rate = int(rate)
+ d.repeat.delay = time.Duration(delay) * time.Millisecond
+}
+
+//export gio_onTextInputEnter
+func gio_onTextInputEnter(data unsafe.Pointer, im *C.struct_zwp_text_input_v3,
+ surf *C.struct_wl_surface) {
+}
+
+//export gio_onTextInputLeave
+func gio_onTextInputLeave(data unsafe.Pointer, im *C.struct_zwp_text_input_v3,
+ surf *C.struct_wl_surface) {
+}
+
+//export gio_onTextInputPreeditString
+func gio_onTextInputPreeditString(data unsafe.Pointer,
+ im *C.struct_zwp_text_input_v3, ctxt *C.char, begin, end C.int32_t) {
+}
+
+//export gio_onTextInputCommitString
+func gio_onTextInputCommitString(data unsafe.Pointer,
+ im *C.struct_zwp_text_input_v3, ctxt *C.char) {
+}
+
+//export gio_onTextInputDeleteSurroundingText
+func gio_onTextInputDeleteSurroundingText(data unsafe.Pointer,
+ im *C.struct_zwp_text_input_v3, before, after C.uint32_t) {
+}
+
+//export gio_onTextInputDone
+func gio_onTextInputDone(data unsafe.Pointer, im *C.struct_zwp_text_input_v3,
+ serial C.uint32_t) {
+ s := callbackLoad(data).(*wlSeat)
+ s.serial = serial
+}
+
+//export gio_onDataSourceTarget
+func gio_onDataSourceTarget(data unsafe.Pointer,
+ source *C.struct_wl_data_source, mime *C.char) {
+}
+
+//export gio_onDataSourceSend
+func gio_onDataSourceSend(data unsafe.Pointer, source *C.struct_wl_data_source,
+ mime *C.char, fd C.int32_t) {
+ s := callbackLoad(data).(*wlSeat)
+ content := s.content
+ go func() {
+ defer syscall.Close(int(fd))
+ syscall.Write(int(fd), content)
+ }()
+}
+
+//export gio_onDataSourceCancelled
+func gio_onDataSourceCancelled(data unsafe.Pointer,
+ source *C.struct_wl_data_source) {
+ s := callbackLoad(data).(*wlSeat)
+ if s.source == source {
+ s.content = nil
+ s.source = nil
+ }
+ C.wl_data_source_destroy(source)
+}
+
+//export gio_onDataSourceDNDDropPerformed
+func gio_onDataSourceDNDDropPerformed(data unsafe.Pointer,
+ source *C.struct_wl_data_source) {
+}
+
+//export gio_onDataSourceDNDFinished
+func gio_onDataSourceDNDFinished(data unsafe.Pointer,
+ source *C.struct_wl_data_source) {
+}
+
+//export gio_onDataSourceAction
+func gio_onDataSourceAction(data unsafe.Pointer,
+ source *C.struct_wl_data_source, act C.uint32_t) {
+}
+
+func (w *window) flushScroll() {
+ var fling f32.Point
+ if w.fling.anim.Active() {
+ dist := float32(w.fling.anim.Tick(time.Now()))
+ fling = w.fling.dir.Mul(dist)
+ }
+ // The Wayland reported scroll distance for
+ // discrete scroll axes is only 10 pixels, where
+ // 100 seems more appropriate.
+ const discreteScale = 10
+ if w.scroll.steps.X != 0 {
+ w.scroll.dist.X *= discreteScale
+ }
+ if w.scroll.steps.Y != 0 {
+ w.scroll.dist.Y *= discreteScale
+ }
+ total := w.scroll.dist.Add(fling)
+ if total == (f32.Point{}) {
+ return
+ }
+ w.w.Event(pointer.Event{
+ Type: pointer.Scroll,
+ Source: pointer.Mouse,
+ Buttons: w.pointerBtns,
+ Position: w.lastPos,
+ Scroll: total,
+ Time: w.scroll.time,
+ Modifiers: w.disp.xkb.Modifiers(),
+ })
+ if w.scroll.steps == (image.Point{}) {
+ w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X)
+ w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y)
+ }
+ w.scroll.dist = f32.Point{}
+ w.scroll.steps = image.Point{}
+}
+
+func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
+ w.flushScroll()
+ w.lastPos = f32.Point{
+ X: fromFixed(x) * float32(w.scale),
+ Y: fromFixed(y) * float32(w.scale),
+ }
+ w.w.Event(pointer.Event{
+ Type: pointer.Move,
+ Position: w.lastPos,
+ Buttons: w.pointerBtns,
+ Source: pointer.Mouse,
+ Time: time.Duration(t) * time.Millisecond,
+ Modifiers: w.disp.xkb.Modifiers(),
+ })
+}
+
+func (w *window) updateOpaqueRegion() {
+ reg := C.wl_compositor_create_region(w.disp.compositor)
+ C.wl_region_add(reg, 0, 0, C.int32_t(w.width), C.int32_t(w.height))
+ C.wl_surface_set_opaque_region(w.surf, reg)
+ C.wl_region_destroy(reg)
+}
+
+func (w *window) updateOutputs() {
+ scale := 1
+ var found bool
+ for _, conf := range w.disp.outputConfig {
+ for _, w2 := range conf.windows {
+ if w2 == w {
+ found = true
+ if conf.scale > scale {
+ scale = conf.scale
+ }
+ }
+ }
+ }
+ w.mu.Lock()
+ if found && scale != w.scale {
+ w.scale = scale
+ w.newScale = true
+ }
+ w.mu.Unlock()
+ if !found {
+ w.setStage(system.StagePaused)
+ } else {
+ w.setStage(system.StageRunning)
+ w.draw(true)
+ }
+}
+
+func (w *window) config() (int, int, unit.Metric) {
+ width, height := w.width*w.scale, w.height*w.scale
+ return width, height, unit.Metric{
+ PxPerDp: w.ppdp * float32(w.scale),
+ PxPerSp: w.ppsp * float32(w.scale),
+ }
+}
+
+func (w *window) draw(sync bool) {
+ w.flushScroll()
+ w.mu.Lock()
+ anim := w.animating || w.fling.anim.Active()
+ dead := w.dead
+ w.mu.Unlock()
+ if dead || (!anim && !sync) {
+ return
+ }
+ width, height, cfg := w.config()
+ if cfg == (unit.Metric{}) {
+ return
+ }
+ if anim && w.lastFrameCallback == nil {
+ w.lastFrameCallback = C.wl_surface_frame(w.surf)
+ // Use the surface as listener data for gio_onFrameDone.
+ C.wl_callback_add_listener(w.lastFrameCallback,
+ &C.gio_callback_listener, unsafe.Pointer(w.surf))
+ }
+ w.w.Event(FrameEvent{
+ FrameEvent: system.FrameEvent{
+ Now: time.Now(),
+ Size: image.Point{
+ X: width,
+ Y: height,
+ },
+ Metric: cfg,
+ },
+ Sync: sync,
+ })
+}
+
+func (w *window) setStage(s system.Stage) {
+ if s == w.stage {
+ return
+ }
+ w.stage = s
+ w.w.Event(system.StageEvent{Stage: s})
+}
+
+func (w *window) display() *C.struct_wl_display {
+ return w.disp.disp
+}
+
+func (w *window) surface() (*C.struct_wl_surface, int, int) {
+ if w.needAck {
+ C.xdg_surface_ack_configure(w.wmSurf, w.serial)
+ w.needAck = false
+ }
+ width, height, scale := w.width, w.height, w.scale
+ if w.newScale {
+ C.wl_surface_set_buffer_scale(w.surf, C.int32_t(scale))
+ w.newScale = false
+ }
+ return w.surf, width * scale, height * scale
+}
+
+func (w *window) ShowTextInput(show bool) {}
+
+// Close the window. Not implemented for Wayland.
+func (w *window) Close() {}
+
+// detectUIScale reports the system UI scale, or 1.0 if it fails.
+func detectUIScale() float32 {
+ // TODO: What about other window environments?
+ out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface",
+ "text-scaling-factor").Output()
+ if err != nil {
+ return 1.0
+ }
+ scale, err := strconv.ParseFloat(string(bytes.TrimSpace(out)), 32)
+ if err != nil {
+ return 1.0
+ }
+ return float32(scale)
+}
+
+func newWLDisplay() (*wlDisplay, error) {
+ d := &wlDisplay{
+ outputMap: make(map[C.uint32_t]*C.struct_wl_output),
+ outputConfig: make(map[*C.struct_wl_output]*wlOutput),
+ }
+ pipe := make([]int, 2)
+ if err := syscall.Pipe2(pipe,
+ syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil {
+ return nil, fmt.Errorf("wayland: failed to create pipe: %v", err)
+ }
+ d.notify.read = pipe[0]
+ d.notify.write = pipe[1]
+ xkb, err := xkb.New()
+ if err != nil {
+ d.destroy()
+ return nil, fmt.Errorf("wayland: %v", err)
+ }
+ d.xkb = xkb
+ d.disp, err = C.wl_display_connect(nil)
+ if d.disp == nil {
+ d.destroy()
+ return nil, fmt.Errorf("wayland: wl_display_connect failed: %v", err)
+ }
+ callbackMap.Store(unsafe.Pointer(d.disp), d)
+ d.reg = C.wl_display_get_registry(d.disp)
+ if d.reg == nil {
+ d.destroy()
+ return nil, errors.New("wayland: wl_display_get_registry failed")
+ }
+ C.wl_registry_add_listener(d.reg, &C.gio_registry_listener,
+ unsafe.Pointer(d.disp))
+ // Wait for the server to register all its globals to the
+ // registry listener (gio_onRegistryGlobal).
+ C.wl_display_roundtrip(d.disp)
+ // Configuration listeners are added to outputs by gio_onRegistryGlobal.
+ // We need another roundtrip to get the initial output configurations
+ // through the gio_onOutput* callbacks.
+ C.wl_display_roundtrip(d.disp)
+ return d, nil
+}
+
+func (d *wlDisplay) destroy() {
+ if d.notify.write != 0 {
+ syscall.Close(d.notify.write)
+ d.notify.write = 0
+ }
+ if d.notify.read != 0 {
+ syscall.Close(d.notify.read)
+ d.notify.read = 0
+ }
+ d.repeat.Stop(0)
+ if d.xkb != nil {
+ d.xkb.Destroy()
+ d.xkb = nil
+ }
+ if d.seat != nil {
+ d.seat.destroy()
+ d.seat = nil
+ }
+ if d.imm != nil {
+ C.zwp_text_input_manager_v3_destroy(d.imm)
+ }
+ if d.decor != nil {
+ C.zxdg_decoration_manager_v1_destroy(d.decor)
+ }
+ if d.shm != nil {
+ C.wl_shm_destroy(d.shm)
+ }
+ if d.compositor != nil {
+ C.wl_compositor_destroy(d.compositor)
+ }
+ if d.wm != nil {
+ C.xdg_wm_base_destroy(d.wm)
+ }
+ for _, output := range d.outputMap {
+ C.wl_output_destroy(output)
+ }
+ if d.reg != nil {
+ C.wl_registry_destroy(d.reg)
+ }
+ if d.disp != nil {
+ C.wl_display_disconnect(d.disp)
+ callbackDelete(unsafe.Pointer(d.disp))
+ }
+}
+
+// fromFixed converts a Wayland wl_fixed_t 23.8 number to float32.
+func fromFixed(v C.wl_fixed_t) float32 {
+ // Convert to float64 to avoid overflow.
+ // From wayland-util.h.
+ b := ((1023 + 44) << 52) + (1 << 51) + uint64(v)
+ f := math.Float64frombits(b) - (3 << 43)
+ return float32(f)
+}
diff --git a/gio/giold/app/internal/wm/os_windows.go b/gio/giold/app/internal/wm/os_windows.go
new file mode 100644
index 0000000..ba83a87
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_windows.go
@@ -0,0 +1,805 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package wm
+
+import (
+ "errors"
+ "fmt"
+ "image"
+ "reflect"
+ "runtime"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+ "unicode"
+ "unsafe"
+
+ syscall "golang.org/x/sys/windows"
+
+ "realy.lol/gio/app/internal/windows"
+ "realy.lol/gio/unit"
+ gowindows "golang.org/x/sys/windows"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/system"
+)
+
+type winConstraints struct {
+ minWidth, minHeight int32
+ maxWidth, maxHeight int32
+}
+
+type winDeltas struct {
+ width int32
+ height int32
+}
+
+type window struct {
+ hwnd syscall.Handle
+ hdc syscall.Handle
+ w Callbacks
+ width int
+ height int
+ stage system.Stage
+ pointerBtns pointer.Buttons
+
+ // cursorIn tracks whether the cursor was inside the window according
+ // to the most recent WM_SETCURSOR.
+ cursorIn bool
+ cursor syscall.Handle
+
+ // placement saves the previous window position when in full screen mode.
+ placement *windows.WindowPlacement
+
+ mu sync.Mutex
+ animating bool
+
+ minmax winConstraints
+ deltas winDeltas
+ opts *Options
+}
+
+const (
+ _WM_REDRAW = windows.WM_USER + iota
+ _WM_CURSOR
+ _WM_OPTION
+)
+
+type gpuAPI struct {
+ priority int
+ initializer func(w *window) (Context, error)
+}
+
+// drivers is the list of potential Context implementations.
+var drivers []gpuAPI
+
+// winMap maps win32 HWNDs to *windows.
+var winMap sync.Map
+
+// iconID is the ID of the icon in the resource file.
+const iconID = 1
+
+var resources struct {
+ once sync.Once
+ // handle is the module handle from GetModuleHandle.
+ handle syscall.Handle
+ // class is the Gio window class from RegisterClassEx.
+ class uint16
+ // cursor is the arrow cursor resource.
+ cursor syscall.Handle
+}
+
+func Main() {
+ select {}
+}
+
+func NewWindow(window Callbacks, opts *Options) error {
+ cerr := make(chan error)
+ go func() {
+ // GetMessage and PeekMessage can filter on a window HWND, but
+ // then thread-specific messages such as WM_QUIT are ignored.
+ // Instead lock the thread so window messages arrive through
+ // unfiltered GetMessage calls.
+ runtime.LockOSThread()
+ w, err := createNativeWindow(opts)
+ if err != nil {
+ cerr <- err
+ return
+ }
+ defer w.destroy()
+ cerr <- nil
+ winMap.Store(w.hwnd, w)
+ defer winMap.Delete(w.hwnd)
+ w.w = window
+ w.w.SetDriver(w)
+ defer w.w.Event(system.DestroyEvent{})
+ w.Option(opts)
+ windows.ShowWindow(w.hwnd, windows.SW_SHOWDEFAULT)
+ windows.SetForegroundWindow(w.hwnd)
+ windows.SetFocus(w.hwnd)
+ // Since the window class for the cursor is null,
+ // set it here to show the cursor.
+ w.SetCursor(pointer.CursorDefault)
+ if err := w.loop(); err != nil {
+ panic(err)
+ }
+ }()
+ return <-cerr
+}
+
+// initResources initializes the resources global.
+func initResources() error {
+ windows.SetProcessDPIAware()
+ hInst, err := windows.GetModuleHandle()
+ if err != nil {
+ return err
+ }
+ resources.handle = hInst
+ c, err := windows.LoadCursor(windows.IDC_ARROW)
+ if err != nil {
+ return err
+ }
+ resources.cursor = c
+ icon, _ := windows.LoadImage(hInst, iconID, windows.IMAGE_ICON, 0, 0,
+ windows.LR_DEFAULTSIZE|windows.LR_SHARED)
+ wcls := windows.WndClassEx{
+ CbSize: uint32(unsafe.Sizeof(windows.WndClassEx{})),
+ Style: windows.CS_HREDRAW | windows.CS_VREDRAW | windows.CS_OWNDC,
+ LpfnWndProc: syscall.NewCallback(windowProc),
+ HInstance: hInst,
+ HIcon: icon,
+ LpszClassName: syscall.StringToUTF16Ptr("GioWindow"),
+ }
+ cls, err := windows.RegisterClassEx(&wcls)
+ if err != nil {
+ return err
+ }
+ resources.class = cls
+ return nil
+}
+
+func getWindowConstraints(cfg unit.Metric, opts *Options) winConstraints {
+ var minmax winConstraints
+ if o := opts.MinSize; o != nil {
+ minmax.minWidth = int32(cfg.Px(o.Width))
+ minmax.minHeight = int32(cfg.Px(o.Height))
+ }
+ if o := opts.MaxSize; o != nil {
+ minmax.maxWidth = int32(cfg.Px(o.Width))
+ minmax.maxHeight = int32(cfg.Px(o.Height))
+ }
+ return minmax
+}
+
+func createNativeWindow(opts *Options) (*window, error) {
+ var resErr error
+ resources.once.Do(func() {
+ resErr = initResources()
+ })
+ if resErr != nil {
+ return nil, resErr
+ }
+ dpi := windows.GetSystemDPI()
+ cfg := configForDPI(dpi)
+ dwStyle := uint32(windows.WS_OVERLAPPEDWINDOW)
+ dwExStyle := uint32(windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE)
+
+ hwnd, err := windows.CreateWindowEx(dwExStyle,
+ resources.class,
+ "",
+ dwStyle|windows.WS_CLIPSIBLINGS|windows.WS_CLIPCHILDREN,
+ windows.CW_USEDEFAULT, windows.CW_USEDEFAULT,
+ windows.CW_USEDEFAULT, windows.CW_USEDEFAULT,
+ 0,
+ 0,
+ resources.handle,
+ 0)
+ if err != nil {
+ return nil, err
+ }
+ w := &window{
+ hwnd: hwnd,
+ minmax: getWindowConstraints(cfg, opts),
+ opts: opts,
+ }
+ w.hdc, err = windows.GetDC(hwnd)
+ if err != nil {
+ return nil, err
+ }
+ return w, nil
+}
+
+func windowProc(hwnd syscall.Handle, msg uint32,
+ wParam, lParam uintptr) uintptr {
+ win, exists := winMap.Load(hwnd)
+ if !exists {
+ return windows.DefWindowProc(hwnd, msg, wParam, lParam)
+ }
+
+ w := win.(*window)
+
+ switch msg {
+ case windows.WM_UNICHAR:
+ if wParam == windows.UNICODE_NOCHAR {
+ // Tell the system that we accept WM_UNICHAR messages.
+ return windows.TRUE
+ }
+ fallthrough
+ case windows.WM_CHAR:
+ if r := rune(wParam); unicode.IsPrint(r) {
+ w.w.Event(key.EditEvent{Text: string(r)})
+ }
+ // The message is processed.
+ return windows.TRUE
+ case windows.WM_DPICHANGED:
+ // Let Windows know we're prepared for runtime DPI changes.
+ return windows.TRUE
+ case windows.WM_ERASEBKGND:
+ // Avoid flickering between GPU content and background color.
+ return windows.TRUE
+ case windows.WM_KEYDOWN, windows.WM_KEYUP, windows.WM_SYSKEYDOWN, windows.WM_SYSKEYUP:
+ if n, ok := convertKeyCode(wParam); ok {
+ e := key.Event{
+ Name: n,
+ Modifiers: getModifiers(),
+ State: key.Press,
+ }
+ if msg == windows.WM_KEYUP || msg == windows.WM_SYSKEYUP {
+ e.State = key.Release
+ }
+
+ w.w.Event(e)
+
+ if (wParam == windows.VK_F10) && (msg == windows.WM_SYSKEYDOWN || msg == windows.WM_SYSKEYUP) {
+ // Reserve F10 for ourselves, and don't let it open the system menu. Other Windows programs
+ // such as cmd.exe and graphical debuggers also reserve F10.
+ return 0
+ }
+ }
+ case windows.WM_LBUTTONDOWN:
+ w.pointerButton(pointer.ButtonPrimary, true, lParam, getModifiers())
+ case windows.WM_LBUTTONUP:
+ w.pointerButton(pointer.ButtonPrimary, false, lParam, getModifiers())
+ case windows.WM_RBUTTONDOWN:
+ w.pointerButton(pointer.ButtonSecondary, true, lParam, getModifiers())
+ case windows.WM_RBUTTONUP:
+ w.pointerButton(pointer.ButtonSecondary, false, lParam, getModifiers())
+ case windows.WM_MBUTTONDOWN:
+ w.pointerButton(pointer.ButtonTertiary, true, lParam, getModifiers())
+ case windows.WM_MBUTTONUP:
+ w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers())
+ case windows.WM_CANCELMODE:
+ w.w.Event(pointer.Event{
+ Type: pointer.Cancel,
+ })
+ case windows.WM_SETFOCUS:
+ w.w.Event(key.FocusEvent{Focus: true})
+ case windows.WM_KILLFOCUS:
+ w.w.Event(key.FocusEvent{Focus: false})
+ case windows.WM_MOUSEMOVE:
+ x, y := coordsFromlParam(lParam)
+ p := f32.Point{X: float32(x), Y: float32(y)}
+ w.w.Event(pointer.Event{
+ Type: pointer.Move,
+ Source: pointer.Mouse,
+ Position: p,
+ Buttons: w.pointerBtns,
+ Time: windows.GetMessageTime(),
+ })
+ case windows.WM_MOUSEWHEEL:
+ w.scrollEvent(wParam, lParam, false)
+ case windows.WM_MOUSEHWHEEL:
+ w.scrollEvent(wParam, lParam, true)
+ case windows.WM_DESTROY:
+ windows.PostQuitMessage(0)
+ case windows.WM_PAINT:
+ w.draw(true)
+ case windows.WM_SIZE:
+ switch wParam {
+ case windows.SIZE_MINIMIZED:
+ w.setStage(system.StagePaused)
+ case windows.SIZE_MAXIMIZED, windows.SIZE_RESTORED:
+ w.setStage(system.StageRunning)
+ }
+ case windows.WM_GETMINMAXINFO:
+ mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam)))
+ if w.minmax.minWidth > 0 || w.minmax.minHeight > 0 {
+ mm.PtMinTrackSize = windows.Point{
+ X: w.minmax.minWidth + w.deltas.width,
+ Y: w.minmax.minHeight + w.deltas.height,
+ }
+ }
+ if w.minmax.maxWidth > 0 || w.minmax.maxHeight > 0 {
+ mm.PtMaxTrackSize = windows.Point{
+ X: w.minmax.maxWidth + w.deltas.width,
+ Y: w.minmax.maxHeight + w.deltas.height,
+ }
+ }
+ case windows.WM_SETCURSOR:
+ w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT
+ fallthrough
+ case _WM_CURSOR:
+ if w.cursorIn {
+ windows.SetCursor(w.cursor)
+ return windows.TRUE
+ }
+ case _WM_OPTION:
+ w.setOptions()
+ }
+
+ return windows.DefWindowProc(hwnd, msg, wParam, lParam)
+}
+
+func getModifiers() key.Modifiers {
+ var kmods key.Modifiers
+ if windows.GetKeyState(windows.VK_LWIN)&0x1000 != 0 || windows.GetKeyState(windows.VK_RWIN)&0x1000 != 0 {
+ kmods |= key.ModSuper
+ }
+ if windows.GetKeyState(windows.VK_MENU)&0x1000 != 0 {
+ kmods |= key.ModAlt
+ }
+ if windows.GetKeyState(windows.VK_CONTROL)&0x1000 != 0 {
+ kmods |= key.ModCtrl
+ }
+ if windows.GetKeyState(windows.VK_SHIFT)&0x1000 != 0 {
+ kmods |= key.ModShift
+ }
+ return kmods
+}
+
+func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr,
+ kmods key.Modifiers) {
+ var typ pointer.Type
+ if press {
+ typ = pointer.Press
+ if w.pointerBtns == 0 {
+ windows.SetCapture(w.hwnd)
+ }
+ w.pointerBtns |= btn
+ } else {
+ typ = pointer.Release
+ w.pointerBtns &^= btn
+ if w.pointerBtns == 0 {
+ windows.ReleaseCapture()
+ }
+ }
+ x, y := coordsFromlParam(lParam)
+ p := f32.Point{X: float32(x), Y: float32(y)}
+ w.w.Event(pointer.Event{
+ Type: typ,
+ Source: pointer.Mouse,
+ Position: p,
+ Buttons: w.pointerBtns,
+ Time: windows.GetMessageTime(),
+ Modifiers: kmods,
+ })
+}
+
+func coordsFromlParam(lParam uintptr) (int, int) {
+ x := int(int16(lParam & 0xffff))
+ y := int(int16((lParam >> 16) & 0xffff))
+ return x, y
+}
+
+func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool) {
+ x, y := coordsFromlParam(lParam)
+ // The WM_MOUSEWHEEL coordinates are in screen coordinates, in contrast
+ // to other mouse events.
+ np := windows.Point{X: int32(x), Y: int32(y)}
+ windows.ScreenToClient(w.hwnd, &np)
+ p := f32.Point{X: float32(np.X), Y: float32(np.Y)}
+ dist := float32(int16(wParam >> 16))
+ var sp f32.Point
+ if horizontal {
+ sp.X = dist
+ } else {
+ sp.Y = -dist
+ }
+ w.w.Event(pointer.Event{
+ Type: pointer.Scroll,
+ Source: pointer.Mouse,
+ Position: p,
+ Buttons: w.pointerBtns,
+ Scroll: sp,
+ Time: windows.GetMessageTime(),
+ })
+}
+
+// Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/
+func (w *window) loop() error {
+ msg := new(windows.Msg)
+loop:
+ for {
+ w.mu.Lock()
+ anim := w.animating
+ w.mu.Unlock()
+ if anim && !windows.PeekMessage(msg, 0, 0, 0, windows.PM_NOREMOVE) {
+ w.draw(false)
+ continue
+ }
+ switch ret := windows.GetMessage(msg, 0, 0, 0); ret {
+ case -1:
+ return errors.New("GetMessage failed")
+ case 0:
+ // WM_QUIT received.
+ break loop
+ }
+ windows.TranslateMessage(msg)
+ windows.DispatchMessage(msg)
+ }
+ return nil
+}
+
+func (w *window) SetAnimating(anim bool) {
+ w.mu.Lock()
+ w.animating = anim
+ w.mu.Unlock()
+ if anim {
+ w.postRedraw()
+ }
+}
+
+func (w *window) postRedraw() {
+ if err := windows.PostMessage(w.hwnd, _WM_REDRAW, 0, 0); err != nil {
+ panic(err)
+ }
+}
+
+func (w *window) setStage(s system.Stage) {
+ w.stage = s
+ w.w.Event(system.StageEvent{Stage: s})
+}
+
+func (w *window) draw(sync bool) {
+ var r windows.Rect
+ windows.GetClientRect(w.hwnd, &r)
+ w.width = int(r.Right - r.Left)
+ w.height = int(r.Bottom - r.Top)
+ if w.width == 0 || w.height == 0 {
+ return
+ }
+ dpi := windows.GetWindowDPI(w.hwnd)
+ cfg := configForDPI(dpi)
+ w.minmax = getWindowConstraints(cfg, w.opts)
+ w.w.Event(FrameEvent{
+ FrameEvent: system.FrameEvent{
+ Now: time.Now(),
+ Size: image.Point{
+ X: w.width,
+ Y: w.height,
+ },
+ Metric: cfg,
+ },
+ Sync: sync,
+ })
+}
+
+func (w *window) destroy() {
+ if w.hdc != 0 {
+ windows.ReleaseDC(w.hdc)
+ w.hdc = 0
+ }
+ if w.hwnd != 0 {
+ windows.DestroyWindow(w.hwnd)
+ w.hwnd = 0
+ }
+}
+
+func (w *window) NewContext() (Context, error) {
+ sort.Slice(drivers, func(i, j int) bool {
+ return drivers[i].priority < drivers[j].priority
+ })
+ var errs []string
+ for _, b := range drivers {
+ ctx, err := b.initializer(w)
+ if err == nil {
+ return ctx, nil
+ }
+ errs = append(errs, err.Error())
+ }
+ if len(errs) > 0 {
+ return nil, fmt.Errorf("NewContext: failed to create a GPU device, tried: %s",
+ strings.Join(errs, ", "))
+ }
+ return nil, errors.New("NewContext: no available GPU drivers")
+}
+
+func (w *window) ReadClipboard() {
+ w.readClipboard()
+}
+
+func (w *window) readClipboard() error {
+ if err := windows.OpenClipboard(w.hwnd); err != nil {
+ return err
+ }
+ defer windows.CloseClipboard()
+ mem, err := windows.GetClipboardData(windows.CF_UNICODETEXT)
+ if err != nil {
+ return err
+ }
+ ptr, err := windows.GlobalLock(mem)
+ if err != nil {
+ return err
+ }
+ defer windows.GlobalUnlock(mem)
+ content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr)))
+ go func() {
+ w.w.Event(clipboard.Event{Text: content})
+ }()
+ return nil
+}
+
+func (w *window) Option(opts *Options) {
+ w.mu.Lock()
+ w.opts = opts
+ w.mu.Unlock()
+ if err := windows.PostMessage(w.hwnd, _WM_OPTION, 0, 0); err != nil {
+ panic(err)
+ }
+}
+
+func (w *window) setOptions() {
+ w.mu.Lock()
+ opts := w.opts
+ w.mu.Unlock()
+ if o := opts.Size; o != nil {
+ dpi := windows.GetSystemDPI()
+ cfg := configForDPI(dpi)
+ width := int32(cfg.Px(o.Width))
+ height := int32(cfg.Px(o.Height))
+
+ // Include the window decorations.
+ wr := windows.Rect{
+ Right: width,
+ Bottom: height,
+ }
+ dwStyle := uint32(windows.WS_OVERLAPPEDWINDOW)
+ dwExStyle := uint32(windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE)
+ windows.AdjustWindowRectEx(&wr, dwStyle, 0, dwExStyle)
+
+ dw, dh := width, height
+ width = wr.Right - wr.Left
+ height = wr.Bottom - wr.Top
+ w.deltas.width = width - dw
+ w.deltas.height = height - dh
+
+ w.opts.Size = o
+ windows.MoveWindow(w.hwnd, 0, 0, width, height, true)
+ }
+ if o := opts.MinSize; o != nil {
+ w.opts.MinSize = o
+ }
+ if o := opts.MaxSize; o != nil {
+ w.opts.MaxSize = o
+ }
+ if o := opts.Title; o != nil {
+ windows.SetWindowText(w.hwnd, *opts.Title)
+ }
+ if o := opts.WindowMode; o != nil {
+ w.SetWindowMode(*o)
+ }
+}
+
+func (w *window) SetWindowMode(mode WindowMode) {
+ // https://devblogs.microsoft.com/oldnewthing/20100412-00/?p=14353
+ switch mode {
+ case Windowed:
+ if w.placement == nil {
+ return
+ }
+ windows.SetWindowPlacement(w.hwnd, w.placement)
+ w.placement = nil
+ style := windows.GetWindowLong(w.hwnd)
+ windows.SetWindowLong(w.hwnd, windows.GWL_STYLE,
+ style|windows.WS_OVERLAPPEDWINDOW)
+ windows.SetWindowPos(w.hwnd, windows.HWND_TOPMOST,
+ 0, 0, 0, 0,
+ windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED,
+ )
+ case Fullscreen:
+ if w.placement != nil {
+ return
+ }
+ w.placement = windows.GetWindowPlacement(w.hwnd)
+ style := windows.GetWindowLong(w.hwnd)
+ windows.SetWindowLong(w.hwnd, windows.GWL_STYLE,
+ style&^windows.WS_OVERLAPPEDWINDOW)
+ mi := windows.GetMonitorInfo(w.hwnd)
+ windows.SetWindowPos(w.hwnd, 0,
+ mi.Monitor.Left, mi.Monitor.Top,
+ mi.Monitor.Right-mi.Monitor.Left,
+ mi.Monitor.Bottom-mi.Monitor.Top,
+ windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED,
+ )
+ }
+}
+
+func (w *window) WriteClipboard(s string) {
+ w.writeClipboard(s)
+}
+
+func (w *window) writeClipboard(s string) error {
+ if err := windows.OpenClipboard(w.hwnd); err != nil {
+ return err
+ }
+ defer windows.CloseClipboard()
+ if err := windows.EmptyClipboard(); err != nil {
+ return err
+ }
+ u16, err := gowindows.UTF16FromString(s)
+ if err != nil {
+ return err
+ }
+ n := len(u16) * int(unsafe.Sizeof(u16[0]))
+ mem, err := windows.GlobalAlloc(n)
+ if err != nil {
+ return err
+ }
+ ptr, err := windows.GlobalLock(mem)
+ if err != nil {
+ windows.GlobalFree(mem)
+ return err
+ }
+ var u16v []uint16
+ hdr := (*reflect.SliceHeader)(unsafe.Pointer(&u16v))
+ hdr.Data = ptr
+ hdr.Cap = len(u16)
+ hdr.Len = len(u16)
+ copy(u16v, u16)
+ windows.GlobalUnlock(mem)
+ if err := windows.SetClipboardData(windows.CF_UNICODETEXT,
+ mem); err != nil {
+ windows.GlobalFree(mem)
+ return err
+ }
+ return nil
+}
+
+func (w *window) SetCursor(name pointer.CursorName) {
+ c, err := loadCursor(name)
+ if err != nil {
+ c = resources.cursor
+ }
+ w.cursor = c
+ if err := windows.PostMessage(w.hwnd, _WM_CURSOR, 0, 0); err != nil {
+ panic(err)
+ }
+}
+
+func loadCursor(name pointer.CursorName) (syscall.Handle, error) {
+ var curID uint16
+ switch name {
+ default:
+ fallthrough
+ case pointer.CursorDefault:
+ return resources.cursor, nil
+ case pointer.CursorText:
+ curID = windows.IDC_IBEAM
+ case pointer.CursorPointer:
+ curID = windows.IDC_HAND
+ case pointer.CursorCrossHair:
+ curID = windows.IDC_CROSS
+ case pointer.CursorColResize:
+ curID = windows.IDC_SIZEWE
+ case pointer.CursorRowResize:
+ curID = windows.IDC_SIZENS
+ case pointer.CursorGrab:
+ curID = windows.IDC_SIZEALL
+ case pointer.CursorNone:
+ return 0, nil
+ }
+ return windows.LoadCursor(curID)
+}
+
+func (w *window) ShowTextInput(show bool) {}
+
+func (w *window) HDC() syscall.Handle {
+ return w.hdc
+}
+
+func (w *window) HWND() (syscall.Handle, int, int) {
+ return w.hwnd, w.width, w.height
+}
+
+func (w *window) Close() {
+ windows.PostMessage(w.hwnd, windows.WM_CLOSE, 0, 0)
+}
+
+func convertKeyCode(code uintptr) (string, bool) {
+ if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' {
+ return string(rune(code)), true
+ }
+ var r string
+ switch code {
+ case windows.VK_ESCAPE:
+ r = key.NameEscape
+ case windows.VK_LEFT:
+ r = key.NameLeftArrow
+ case windows.VK_RIGHT:
+ r = key.NameRightArrow
+ case windows.VK_RETURN:
+ r = key.NameReturn
+ case windows.VK_UP:
+ r = key.NameUpArrow
+ case windows.VK_DOWN:
+ r = key.NameDownArrow
+ case windows.VK_HOME:
+ r = key.NameHome
+ case windows.VK_END:
+ r = key.NameEnd
+ case windows.VK_BACK:
+ r = key.NameDeleteBackward
+ case windows.VK_DELETE:
+ r = key.NameDeleteForward
+ case windows.VK_PRIOR:
+ r = key.NamePageUp
+ case windows.VK_NEXT:
+ r = key.NamePageDown
+ case windows.VK_F1:
+ r = "F1"
+ case windows.VK_F2:
+ r = "F2"
+ case windows.VK_F3:
+ r = "F3"
+ case windows.VK_F4:
+ r = "F4"
+ case windows.VK_F5:
+ r = "F5"
+ case windows.VK_F6:
+ r = "F6"
+ case windows.VK_F7:
+ r = "F7"
+ case windows.VK_F8:
+ r = "F8"
+ case windows.VK_F9:
+ r = "F9"
+ case windows.VK_F10:
+ r = "F10"
+ case windows.VK_F11:
+ r = "F11"
+ case windows.VK_F12:
+ r = "F12"
+ case windows.VK_TAB:
+ r = key.NameTab
+ case windows.VK_SPACE:
+ r = key.NameSpace
+ case windows.VK_OEM_1:
+ r = ";"
+ case windows.VK_OEM_PLUS:
+ r = "+"
+ case windows.VK_OEM_COMMA:
+ r = ","
+ case windows.VK_OEM_MINUS:
+ r = "-"
+ case windows.VK_OEM_PERIOD:
+ r = "."
+ case windows.VK_OEM_2:
+ r = "/"
+ case windows.VK_OEM_3:
+ r = "`"
+ case windows.VK_OEM_4:
+ r = "["
+ case windows.VK_OEM_5, windows.VK_OEM_102:
+ r = "\\"
+ case windows.VK_OEM_6:
+ r = "]"
+ case windows.VK_OEM_7:
+ r = "'"
+ default:
+ return "", false
+ }
+ return r, true
+}
+
+func configForDPI(dpi int) unit.Metric {
+ const inchPrDp = 1.0 / 96.0
+ ppdp := float32(dpi) * inchPrDp
+ return unit.Metric{
+ PxPerDp: ppdp,
+ PxPerSp: ppdp,
+ }
+}
diff --git a/gio/giold/app/internal/wm/os_x11.go b/gio/giold/app/internal/wm/os_x11.go
new file mode 100644
index 0000000..f17f36f
--- /dev/null
+++ b/gio/giold/app/internal/wm/os_x11.go
@@ -0,0 +1,799 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build (linux && !android && !nox11) || freebsd || openbsd
+// +build linux,!android,!nox11 freebsd openbsd
+
+package wm
+
+/*
+#cgo openbsd CFLAGS: -I/usr/X11R6/include -I/usr/local/include
+#cgo openbsd LDFLAGS: -L/usr/X11R6/lib -L/usr/local/lib
+#cgo freebsd openbsd LDFLAGS: -lX11 -lxkbcommon -lxkbcommon-x11 -lX11-xcb -lXcursor -lXfixes
+#cgo linux pkg-config: x11 xkbcommon xkbcommon-x11 x11-xcb xcursor xfixes
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+*/
+import "C"
+import (
+ "errors"
+ "fmt"
+ "image"
+ "os"
+ "path/filepath"
+ "strconv"
+ "sync"
+ "time"
+ "unsafe"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/unit"
+
+ syscall "golang.org/x/sys/unix"
+
+ "realy.lol/gio/app/internal/xkb"
+)
+
+type x11Window struct {
+ w Callbacks
+ x *C.Display
+ xkb *xkb.Context
+ xkbEventBase C.int
+ xw C.Window
+
+ atoms struct {
+ // "UTF8_STRING".
+ utf8string C.Atom
+ // "text/plain;charset=utf-8".
+ plaintext C.Atom
+ // "TARGETS"
+ targets C.Atom
+ // "CLIPBOARD".
+ clipboard C.Atom
+ // "CLIPBOARD_CONTENT", the clipboard destination property.
+ clipboardContent C.Atom
+ // "WM_DELETE_WINDOW"
+ evDelWindow C.Atom
+ // "ATOM"
+ atom C.Atom
+ // "GTK_TEXT_BUFFER_CONTENTS"
+ gtk_text_buffer_contents C.Atom
+ // "_NET_WM_NAME"
+ wmName C.Atom
+ // "_NET_WM_STATE"
+ wmState C.Atom
+ // _NET_WM_STATE_FULLSCREEN"
+ wmStateFullscreen C.Atom
+ }
+ stage system.Stage
+ cfg unit.Metric
+ width int
+ height int
+ notify struct {
+ read, write int
+ }
+ dead bool
+
+ mu sync.Mutex
+ animating bool
+ opts *Options
+
+ pointerBtns pointer.Buttons
+
+ clipboard struct {
+ read bool
+ write *string
+ content []byte
+ }
+ cursor pointer.CursorName
+ mode WindowMode
+}
+
+func (w *x11Window) SetAnimating(anim bool) {
+ w.mu.Lock()
+ w.animating = anim
+ w.mu.Unlock()
+ if anim {
+ w.wakeup()
+ }
+}
+
+func (w *x11Window) ReadClipboard() {
+ w.mu.Lock()
+ w.clipboard.read = true
+ w.mu.Unlock()
+ w.wakeup()
+}
+
+func (w *x11Window) WriteClipboard(s string) {
+ w.mu.Lock()
+ w.clipboard.write = &s
+ w.mu.Unlock()
+ w.wakeup()
+}
+
+func (w *x11Window) Option(opts *Options) {
+ w.mu.Lock()
+ w.opts = opts
+ w.mu.Unlock()
+ w.wakeup()
+}
+
+func (w *x11Window) setOptions() {
+ w.mu.Lock()
+ opts := w.opts
+ w.opts = nil
+ w.mu.Unlock()
+ if opts == nil {
+ return
+ }
+ var shints C.XSizeHints
+ if o := opts.MinSize; o != nil {
+ shints.min_width = C.int(w.cfg.Px(o.Width))
+ shints.min_height = C.int(w.cfg.Px(o.Height))
+ shints.flags = C.PMinSize
+ }
+ if o := opts.MaxSize; o != nil {
+ shints.max_width = C.int(w.cfg.Px(o.Width))
+ shints.max_height = C.int(w.cfg.Px(o.Height))
+ shints.flags = shints.flags | C.PMaxSize
+ }
+ if shints.flags != 0 {
+ C.XSetWMNormalHints(w.x, w.xw, &shints)
+ }
+
+ var title string
+ if o := opts.Title; o != nil {
+ title = *o
+ }
+ ctitle := C.CString(title)
+ defer C.free(unsafe.Pointer(ctitle))
+ C.XStoreName(w.x, w.xw, ctitle)
+ // set _NET_WM_NAME as well for UTF-8 support in window title.
+ C.XSetTextProperty(w.x, w.xw,
+ &C.XTextProperty{
+ value: (*C.uchar)(unsafe.Pointer(ctitle)),
+ encoding: w.atoms.utf8string,
+ format: 8,
+ nitems: C.ulong(len(title)),
+ },
+ w.atoms.wmName)
+
+ if o := opts.WindowMode; o != nil {
+ w.SetWindowMode(*o)
+ }
+}
+
+func (w *x11Window) SetCursor(name pointer.CursorName) {
+ switch name {
+ case pointer.CursorNone:
+ w.cursor = name
+ C.XFixesHideCursor(w.x, w.xw)
+ return
+ case pointer.CursorGrab:
+ name = "hand1"
+ }
+ if w.cursor == pointer.CursorNone {
+ C.XFixesShowCursor(w.x, w.xw)
+ }
+ cname := C.CString(string(name))
+ defer C.free(unsafe.Pointer(cname))
+ c := C.XcursorLibraryLoadCursor(w.x, cname)
+ if c == 0 {
+ name = pointer.CursorDefault
+ }
+ w.cursor = name
+ // If c if null (i.e. name was not found),
+ // XDefineCursor will use the default cursor.
+ C.XDefineCursor(w.x, w.xw, c)
+}
+
+func (w *x11Window) SetWindowMode(mode WindowMode) {
+ switch mode {
+ case w.mode:
+ return
+ case Windowed:
+ C.XDeleteProperty(w.x, w.xw, w.atoms.wmStateFullscreen)
+ case Fullscreen:
+ C.XChangeProperty(w.x, w.xw, w.atoms.wmState, C.XA_ATOM,
+ 32, C.PropModeReplace,
+ (*C.uchar)(unsafe.Pointer(&w.atoms.wmStateFullscreen)), 1,
+ )
+ default:
+ return
+ }
+ w.mode = mode
+ // "A Client wishing to change the state of a window MUST send
+ // a _NET_WM_STATE client message to the root window (see below)."
+ var xev C.XEvent
+ ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev))
+ *ev = C.XClientMessageEvent{
+ _type: C.ClientMessage,
+ display: w.x,
+ window: w.xw,
+ message_type: w.atoms.wmState,
+ format: 32,
+ }
+ arr := (*[5]C.long)(unsafe.Pointer(&ev.data))
+ arr[0] = 2 // _NET_WM_STATE_TOGGLE
+ arr[1] = C.long(w.atoms.wmStateFullscreen)
+ arr[2] = 0
+ arr[3] = 1 // application
+ arr[4] = 0
+ C.XSendEvent(
+ w.x,
+ C.XDefaultRootWindow(w.x), // MUST be the root window
+ C.False,
+ C.SubstructureNotifyMask|C.SubstructureRedirectMask,
+ &xev,
+ )
+}
+
+func (w *x11Window) ShowTextInput(show bool) {}
+
+// Close the window.
+func (w *x11Window) Close() {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ var xev C.XEvent
+ ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev))
+ *ev = C.XClientMessageEvent{
+ _type: C.ClientMessage,
+ display: w.x,
+ window: w.xw,
+ message_type: w.atom("WM_PROTOCOLS", true),
+ format: 32,
+ }
+ arr := (*[5]C.long)(unsafe.Pointer(&ev.data))
+ arr[0] = C.long(w.atoms.evDelWindow)
+ arr[1] = C.CurrentTime
+ C.XSendEvent(w.x, w.xw, C.False, C.NoEventMask, &xev)
+}
+
+var x11OneByte = make([]byte, 1)
+
+func (w *x11Window) wakeup() {
+ if _, err := syscall.Write(w.notify.write,
+ x11OneByte); err != nil && err != syscall.EAGAIN {
+ panic(fmt.Errorf("failed to write to pipe: %v", err))
+ }
+}
+
+func (w *x11Window) display() *C.Display {
+ return w.x
+}
+
+func (w *x11Window) window() (C.Window, int, int) {
+ return w.xw, w.width, w.height
+}
+
+func (w *x11Window) setStage(s system.Stage) {
+ if s == w.stage {
+ return
+ }
+ w.stage = s
+ w.w.Event(system.StageEvent{Stage: s})
+}
+
+func (w *x11Window) loop() {
+ h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
+ xfd := C.XConnectionNumber(w.x)
+
+ // Poll for events and notifications.
+ pollfds := []syscall.PollFd{
+ {Fd: int32(xfd), Events: syscall.POLLIN | syscall.POLLERR},
+ {Fd: int32(w.notify.read), Events: syscall.POLLIN | syscall.POLLERR},
+ }
+ xEvents := &pollfds[0].Revents
+ // Plenty of room for a backlog of notifications.
+ buf := make([]byte, 100)
+
+loop:
+ for !w.dead {
+ var syn, anim bool
+ // Check for pending draw events before checking animation or blocking.
+ // This fixes an issue on Xephyr where on startup XPending() > 0 but
+ // poll will still block. This also prevents no-op calls to poll.
+ if syn = h.handleEvents(); !syn {
+ w.mu.Lock()
+ anim = w.animating
+ w.mu.Unlock()
+ if !anim {
+ // Clear poll events.
+ *xEvents = 0
+ // Wait for X event or gio notification.
+ if _, err := syscall.Poll(pollfds,
+ -1); err != nil && err != syscall.EINTR {
+ panic(fmt.Errorf("x11 loop: poll failed: %w", err))
+ }
+ switch {
+ case *xEvents&syscall.POLLIN != 0:
+ syn = h.handleEvents()
+ if w.dead {
+ break loop
+ }
+ case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
+ break loop
+ }
+ }
+ }
+ w.setOptions()
+ // Clear notifications.
+ for {
+ _, err := syscall.Read(w.notify.read, buf)
+ if err == syscall.EAGAIN {
+ break
+ }
+ if err != nil {
+ panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w",
+ err))
+ }
+ }
+
+ if anim || syn {
+ w.w.Event(FrameEvent{
+ FrameEvent: system.FrameEvent{
+ Now: time.Now(),
+ Size: image.Point{
+ X: w.width,
+ Y: w.height,
+ },
+ Metric: w.cfg,
+ },
+ Sync: syn,
+ })
+ }
+ w.mu.Lock()
+ readClipboard := w.clipboard.read
+ writeClipboard := w.clipboard.write
+ w.clipboard.read = false
+ w.clipboard.write = nil
+ w.mu.Unlock()
+ if readClipboard {
+ C.XDeleteProperty(w.x, w.xw, w.atoms.clipboardContent)
+ C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string,
+ w.atoms.clipboardContent, w.xw, C.CurrentTime)
+ }
+ if writeClipboard != nil {
+ w.clipboard.content = []byte(*writeClipboard)
+ C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime)
+ }
+ }
+ w.w.Event(system.DestroyEvent{Err: nil})
+}
+
+func (w *x11Window) destroy() {
+ if w.notify.write != 0 {
+ syscall.Close(w.notify.write)
+ w.notify.write = 0
+ }
+ if w.notify.read != 0 {
+ syscall.Close(w.notify.read)
+ w.notify.read = 0
+ }
+ if w.xkb != nil {
+ w.xkb.Destroy()
+ w.xkb = nil
+ }
+ C.XDestroyWindow(w.x, w.xw)
+ C.XCloseDisplay(w.x)
+}
+
+// atom is a wrapper around XInternAtom. Callers should cache the result
+// in order to limit round-trips to the X server.
+//
+func (w *x11Window) atom(name string, onlyIfExists bool) C.Atom {
+ cname := C.CString(name)
+ defer C.free(unsafe.Pointer(cname))
+ flag := C.Bool(C.False)
+ if onlyIfExists {
+ flag = C.True
+ }
+ return C.XInternAtom(w.x, cname, flag)
+}
+
+// x11EventHandler wraps static variables for the main event loop.
+// Its sole purpose is to prevent heap allocation and reduce clutter
+// in x11window.loop.
+//
+type x11EventHandler struct {
+ w *x11Window
+ text []byte
+ xev *C.XEvent
+}
+
+// handleEvents returns true if the window needs to be redrawn.
+//
+func (h *x11EventHandler) handleEvents() bool {
+ w := h.w
+ xev := h.xev
+ redraw := false
+ for C.XPending(w.x) != 0 {
+ C.XNextEvent(w.x, xev)
+ if C.XFilterEvent(xev, C.None) == C.True {
+ continue
+ }
+ switch _type := (*C.XAnyEvent)(unsafe.Pointer(xev))._type; _type {
+ case h.w.xkbEventBase:
+ xkbEvent := (*C.XkbAnyEvent)(unsafe.Pointer(xev))
+ switch xkbEvent.xkb_type {
+ case C.XkbNewKeyboardNotify, C.XkbMapNotify:
+ if err := h.w.updateXkbKeymap(); err != nil {
+ panic(err)
+ }
+ case C.XkbStateNotify:
+ state := (*C.XkbStateNotifyEvent)(unsafe.Pointer(xev))
+ h.w.xkb.UpdateMask(uint32(state.base_mods),
+ uint32(state.latched_mods), uint32(state.locked_mods),
+ uint32(state.base_group), uint32(state.latched_group),
+ uint32(state.locked_group))
+ }
+ case C.KeyPress, C.KeyRelease:
+ ks := key.Press
+ if _type == C.KeyRelease {
+ ks = key.Release
+ }
+ kevt := (*C.XKeyPressedEvent)(unsafe.Pointer(xev))
+ for _, e := range h.w.xkb.DispatchKey(uint32(kevt.keycode), ks) {
+ w.w.Event(e)
+ }
+ case C.ButtonPress, C.ButtonRelease:
+ bevt := (*C.XButtonEvent)(unsafe.Pointer(xev))
+ ev := pointer.Event{
+ Type: pointer.Press,
+ Source: pointer.Mouse,
+ Position: f32.Point{
+ X: float32(bevt.x),
+ Y: float32(bevt.y),
+ },
+ Time: time.Duration(bevt.time) * time.Millisecond,
+ Modifiers: w.xkb.Modifiers(),
+ }
+ if bevt._type == C.ButtonRelease {
+ ev.Type = pointer.Release
+ }
+ var btn pointer.Buttons
+ const scrollScale = 10
+ switch bevt.button {
+ case C.Button1:
+ btn = pointer.ButtonPrimary
+ case C.Button2:
+ btn = pointer.ButtonTertiary
+ case C.Button3:
+ btn = pointer.ButtonSecondary
+ case C.Button4:
+ // scroll up
+ ev.Type = pointer.Scroll
+ ev.Scroll.Y = -scrollScale
+ case C.Button5:
+ // scroll down
+ ev.Type = pointer.Scroll
+ ev.Scroll.Y = +scrollScale
+ case 6:
+ // http://xahlee.info/linux/linux_x11_mouse_button_number.html
+ // scroll left
+ ev.Type = pointer.Scroll
+ ev.Scroll.X = -scrollScale * 2
+ case 7:
+ // scroll right
+ ev.Type = pointer.Scroll
+ ev.Scroll.X = +scrollScale * 2
+ default:
+ continue
+ }
+ switch _type {
+ case C.ButtonPress:
+ w.pointerBtns |= btn
+ case C.ButtonRelease:
+ w.pointerBtns |= btn
+ }
+ ev.Buttons = w.pointerBtns
+ w.w.Event(ev)
+ w.pointerBtns = 0
+ case C.MotionNotify:
+ mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
+ w.w.Event(pointer.Event{
+ Type: pointer.Move,
+ Source: pointer.Mouse,
+ Buttons: w.pointerBtns,
+ Position: f32.Point{
+ X: float32(mevt.x),
+ Y: float32(mevt.y),
+ },
+ Time: time.Duration(mevt.time) * time.Millisecond,
+ Modifiers: w.xkb.Modifiers(),
+ })
+ case C.Expose: // update
+ // redraw only on the last expose event
+ redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0
+ case C.FocusIn:
+ w.w.Event(key.FocusEvent{Focus: true})
+ case C.FocusOut:
+ w.w.Event(key.FocusEvent{Focus: false})
+ case C.ConfigureNotify: // window configuration change
+ cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev))
+ w.width = int(cevt.width)
+ w.height = int(cevt.height)
+ // redraw will be done by a later expose event
+ case C.SelectionNotify:
+ cevt := (*C.XSelectionEvent)(unsafe.Pointer(xev))
+ prop := w.atoms.clipboardContent
+ if cevt.property != prop {
+ break
+ }
+ if cevt.selection != w.atoms.clipboard {
+ break
+ }
+ var text C.XTextProperty
+ if st := C.XGetTextProperty(w.x, w.xw, &text, prop); st == 0 {
+ // Failed; ignore.
+ break
+ }
+ if text.format != 8 || text.encoding != w.atoms.utf8string {
+ // Ignore non-utf-8 encoded strings.
+ break
+ }
+ str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)),
+ C.int(text.nitems))
+ w.w.Event(clipboard.Event{Text: str})
+ case C.SelectionRequest:
+ cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
+ if cevt.selection != w.atoms.clipboard || cevt.property == C.None {
+ // Unsupported clipboard or obsolete requestor.
+ break
+ }
+ notify := func() {
+ var xev C.XEvent
+ ev := (*C.XSelectionEvent)(unsafe.Pointer(&xev))
+ *ev = C.XSelectionEvent{
+ _type: C.SelectionNotify,
+ display: cevt.display,
+ requestor: cevt.requestor,
+ selection: cevt.selection,
+ target: cevt.target,
+ property: cevt.property,
+ time: cevt.time,
+ }
+ C.XSendEvent(w.x, cevt.requestor, 0, 0, &xev)
+ }
+ switch cevt.target {
+ case w.atoms.targets:
+ // The requestor wants the supported clipboard
+ // formats. First write the targets...
+ formats := [...]C.long{
+ C.long(w.atoms.targets),
+ C.long(w.atoms.utf8string),
+ C.long(w.atoms.plaintext),
+ // GTK clients need this.
+ C.long(w.atoms.gtk_text_buffer_contents),
+ }
+ C.XChangeProperty(w.x, cevt.requestor, cevt.property,
+ w.atoms.atom,
+ 32 /* bitwidth of formats */, C.PropModeReplace,
+ (*C.uchar)(unsafe.Pointer(&formats)), C.int(len(formats)),
+ )
+ // ...then notify the requestor.
+ notify()
+ case w.atoms.plaintext, w.atoms.utf8string, w.atoms.gtk_text_buffer_contents:
+ content := w.clipboard.content
+ var ptr *C.uchar
+ if len(content) > 0 {
+ ptr = (*C.uchar)(unsafe.Pointer(&content[0]))
+ }
+ C.XChangeProperty(w.x, cevt.requestor, cevt.property,
+ cevt.target,
+ 8 /* bitwidth */, C.PropModeReplace,
+ ptr, C.int(len(content)),
+ )
+ notify()
+ }
+ case C.ClientMessage: // extensions
+ cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev))
+ switch *(*C.long)(unsafe.Pointer(&cevt.data)) {
+ case C.long(w.atoms.evDelWindow):
+ w.dead = true
+ return false
+ }
+ }
+ }
+ return redraw
+}
+
+var (
+ x11Threads sync.Once
+)
+
+func init() {
+ x11Driver = newX11Window
+}
+
+func newX11Window(gioWin Callbacks, opts *Options) error {
+ var err error
+
+ pipe := make([]int, 2)
+ if err := syscall.Pipe2(pipe,
+ syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil {
+ return fmt.Errorf("NewX11Window: failed to create pipe: %w", err)
+ }
+
+ x11Threads.Do(func() {
+ if C.XInitThreads() == 0 {
+ err = errors.New("x11: threads init failed")
+ }
+ C.XrmInitialize()
+ })
+ if err != nil {
+ return err
+ }
+ dpy := C.XOpenDisplay(nil)
+ if dpy == nil {
+ return errors.New("x11: cannot connect to the X server")
+ }
+ var major, minor C.int = C.XkbMajorVersion, C.XkbMinorVersion
+ var xkbEventBase C.int
+ if C.XkbQueryExtension(dpy, nil, &xkbEventBase, nil, &major,
+ &minor) != C.True {
+ C.XCloseDisplay(dpy)
+ return errors.New("x11: XkbQueryExtension failed")
+ }
+ const bits = C.uint(C.XkbNewKeyboardNotifyMask | C.XkbMapNotifyMask | C.XkbStateNotifyMask)
+ if C.XkbSelectEvents(dpy, C.XkbUseCoreKbd, bits, bits) != C.True {
+ C.XCloseDisplay(dpy)
+ return errors.New("x11: XkbSelectEvents failed")
+ }
+ xkb, err := xkb.New()
+ if err != nil {
+ C.XCloseDisplay(dpy)
+ return fmt.Errorf("x11: %v", err)
+ }
+
+ ppsp := x11DetectUIScale(dpy)
+ cfg := unit.Metric{PxPerDp: ppsp, PxPerSp: ppsp}
+ swa := C.XSetWindowAttributes{
+ event_mask: C.ExposureMask | C.FocusChangeMask | // update
+ C.KeyPressMask | C.KeyReleaseMask | // keyboard
+ C.ButtonPressMask | C.ButtonReleaseMask | // mouse clicks
+ C.PointerMotionMask | // mouse movement
+ C.StructureNotifyMask, // resize
+ background_pixmap: C.None,
+ override_redirect: C.False,
+ }
+ var width, height int
+ if o := opts.Size; o != nil {
+ width = cfg.Px(o.Width)
+ height = cfg.Px(o.Height)
+ }
+ win := C.XCreateWindow(dpy, C.XDefaultRootWindow(dpy),
+ 0, 0, C.uint(width), C.uint(height),
+ 0, C.CopyFromParent, C.InputOutput, nil,
+ C.CWEventMask|C.CWBackPixmap|C.CWOverrideRedirect, &swa)
+
+ w := &x11Window{
+ w: gioWin, x: dpy, xw: win,
+ width: width,
+ height: height,
+ cfg: cfg,
+ xkb: xkb,
+ xkbEventBase: xkbEventBase,
+ }
+ w.notify.read = pipe[0]
+ w.notify.write = pipe[1]
+
+ if err := w.updateXkbKeymap(); err != nil {
+ w.destroy()
+ return err
+ }
+
+ var hints C.XWMHints
+ hints.input = C.True
+ hints.flags = C.InputHint
+ C.XSetWMHints(dpy, win, &hints)
+
+ name := C.CString(filepath.Base(os.Args[0]))
+ defer C.free(unsafe.Pointer(name))
+ wmhints := C.XClassHint{name, name}
+ C.XSetClassHint(dpy, win, &wmhints)
+
+ w.atoms.utf8string = w.atom("UTF8_STRING", false)
+ w.atoms.plaintext = w.atom("text/plain;charset=utf-8", false)
+ w.atoms.gtk_text_buffer_contents = w.atom("GTK_TEXT_BUFFER_CONTENTS", false)
+ w.atoms.evDelWindow = w.atom("WM_DELETE_WINDOW", false)
+ w.atoms.clipboard = w.atom("CLIPBOARD", false)
+ w.atoms.clipboardContent = w.atom("CLIPBOARD_CONTENT", false)
+ w.atoms.atom = w.atom("ATOM", false)
+ w.atoms.targets = w.atom("TARGETS", false)
+ w.atoms.wmName = w.atom("_NET_WM_NAME", false)
+ w.atoms.wmState = w.atom("_NET_WM_STATE", false)
+ w.atoms.wmStateFullscreen = w.atom("_NET_WM_STATE_FULLSCREEN", false)
+
+ // extensions
+ C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1)
+
+ w.Option(opts)
+
+ // make the window visible on the screen
+ C.XMapWindow(dpy, win)
+
+ go func() {
+ w.w.SetDriver(w)
+ w.setStage(system.StageRunning)
+ w.loop()
+ w.destroy()
+ }()
+ return nil
+}
+
+// detectUIScale reports the system UI scale, or 1.0 if it fails.
+func x11DetectUIScale(dpy *C.Display) float32 {
+ // default fixed DPI value used in most desktop UI toolkits
+ const defaultDesktopDPI = 96
+ var scale float32 = 1.0
+
+ // Get actual DPI from X resource Xft.dpi (set by GTK and Qt).
+ // This value is entirely based on user preferences and conflates both
+ // screen (UI) scaling and font scale.
+ rms := C.XResourceManagerString(dpy)
+ if rms != nil {
+ db := C.XrmGetStringDatabase(rms)
+ if db != nil {
+ var (
+ t *C.char
+ v C.XrmValue
+ )
+ if C.XrmGetResource(db,
+ (*C.char)(unsafe.Pointer(&[]byte("Xft.dpi\x00")[0])),
+ (*C.char)(unsafe.Pointer(&[]byte("Xft.Dpi\x00")[0])), &t,
+ &v) != C.False {
+ if t != nil && C.GoString(t) == "String" {
+ f, err := strconv.ParseFloat(C.GoString(v.addr), 32)
+ if err == nil {
+ scale = float32(f) / defaultDesktopDPI
+ }
+ }
+ }
+ C.XrmDestroyDatabase(db)
+ }
+ }
+
+ return scale
+}
+
+func (w *x11Window) updateXkbKeymap() error {
+ w.xkb.DestroyKeymapState()
+ ctx := (*C.struct_xkb_context)(unsafe.Pointer(w.xkb.Ctx))
+ xcb := C.XGetXCBConnection(w.x)
+ if xcb == nil {
+ return errors.New("x11: XGetXCBConnection failed")
+ }
+ xkbDevID := C.xkb_x11_get_core_keyboard_device_id(xcb)
+ if xkbDevID == -1 {
+ return errors.New("x11: xkb_x11_get_core_keyboard_device_id failed")
+ }
+ keymap := C.xkb_x11_keymap_new_from_device(ctx, xcb, xkbDevID,
+ C.XKB_KEYMAP_COMPILE_NO_FLAGS)
+ if keymap == nil {
+ return errors.New("x11: xkb_x11_keymap_new_from_device failed")
+ }
+ state := C.xkb_x11_state_new_from_device(keymap, xcb, xkbDevID)
+ if state == nil {
+ C.xkb_keymap_unref(keymap)
+ return errors.New("x11: xkb_x11_keymap_new_from_device failed")
+ }
+ w.xkb.SetKeymap(unsafe.Pointer(keymap), unsafe.Pointer(state))
+ return nil
+}
diff --git a/gio/giold/app/internal/wm/runmain.go b/gio/giold/app/internal/wm/runmain.go
new file mode 100644
index 0000000..4617217
--- /dev/null
+++ b/gio/giold/app/internal/wm/runmain.go
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build android darwin,ios
+
+package wm
+
+// Android only supports non-Java programs as c-shared libraries.
+// Unfortunately, Go does not run a program's main function in
+// library mode. To make Gio programs simpler and uniform, we'll
+// link to the main function here and call it from Java.
+
+import (
+ "sync"
+ _ "unsafe" // for go:linkname
+)
+
+//go:linkname mainMain main.main
+func mainMain()
+
+var runMainOnce sync.Once
+
+func runMain() {
+ runMainOnce.Do(func() {
+ // Indirect call, since the linker does not know the address of main when
+ // laying down this package.
+ fn := mainMain
+ fn()
+ })
+}
diff --git a/gio/giold/app/internal/wm/wayland_text_input.c b/gio/giold/app/internal/wm/wayland_text_input.c
new file mode 100644
index 0000000..de01dd5
--- /dev/null
+++ b/gio/giold/app/internal/wm/wayland_text_input.c
@@ -0,0 +1,98 @@
+// +build linux,!android,!nowayland freebsd
+
+/* Generated by wayland-scanner 1.21.0 */
+
+/*
+ * Copyright Ā© 2012, 2013 Intel Corporation
+ * Copyright Ā© 2015, 2016 Jan Arne Petersen
+ * Copyright Ā© 2017, 2018 Red Hat, Inc.
+ * Copyright Ā© 2018 Purism SPC
+ *
+ * Permission to use, copy, modify, distribute, and sell this
+ * software and its documentation for any purpose is hereby granted
+ * without fee, provided that the above copyright notice appear in
+ * all copies and that both that copyright notice and this permission
+ * notice appear in supporting documentation, and that the name of
+ * the copyright holders not be used in advertising or publicity
+ * pertaining to distribution of the software without specific,
+ * written prior permission. The copyright holders make no
+ * representations about the suitability of this software for any
+ * purpose. It is provided "as is" without express or implied
+ * warranty.
+ *
+ * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+ * THIS SOFTWARE.
+ */
+
+#include
+#include
+#include "wayland-util.h"
+
+#ifndef __has_attribute
+# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
+#endif
+
+#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
+#define WL_PRIVATE __attribute__ ((visibility("hidden")))
+#else
+#define WL_PRIVATE
+#endif
+
+extern const struct wl_interface wl_seat_interface;
+extern const struct wl_interface wl_surface_interface;
+extern const struct wl_interface zwp_text_input_v3_interface;
+
+static const struct wl_interface *text_input_unstable_v3_types[] = {
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ &wl_surface_interface,
+ &wl_surface_interface,
+ &zwp_text_input_v3_interface,
+ &wl_seat_interface,
+};
+
+static const struct wl_message zwp_text_input_v3_requests[] = {
+ { "destroy", "", text_input_unstable_v3_types + 0 },
+ { "enable", "", text_input_unstable_v3_types + 0 },
+ { "disable", "", text_input_unstable_v3_types + 0 },
+ { "set_surrounding_text", "sii", text_input_unstable_v3_types + 0 },
+ { "set_text_change_cause", "u", text_input_unstable_v3_types + 0 },
+ { "set_content_type", "uu", text_input_unstable_v3_types + 0 },
+ { "set_cursor_rectangle", "iiii", text_input_unstable_v3_types + 0 },
+ { "commit", "", text_input_unstable_v3_types + 0 },
+};
+
+static const struct wl_message zwp_text_input_v3_events[] = {
+ { "enter", "o", text_input_unstable_v3_types + 4 },
+ { "leave", "o", text_input_unstable_v3_types + 5 },
+ { "preedit_string", "?sii", text_input_unstable_v3_types + 0 },
+ { "commit_string", "?s", text_input_unstable_v3_types + 0 },
+ { "delete_surrounding_text", "uu", text_input_unstable_v3_types + 0 },
+ { "done", "u", text_input_unstable_v3_types + 0 },
+};
+
+WL_PRIVATE const struct wl_interface zwp_text_input_v3_interface = {
+ "zwp_text_input_v3", 1,
+ 8, zwp_text_input_v3_requests,
+ 6, zwp_text_input_v3_events,
+};
+
+static const struct wl_message zwp_text_input_manager_v3_requests[] = {
+ { "destroy", "", text_input_unstable_v3_types + 0 },
+ { "get_text_input", "no", text_input_unstable_v3_types + 6 },
+};
+
+WL_PRIVATE const struct wl_interface zwp_text_input_manager_v3_interface = {
+ "zwp_text_input_manager_v3", 1,
+ 2, zwp_text_input_manager_v3_requests,
+ 0, NULL,
+};
+
diff --git a/gio/giold/app/internal/wm/wayland_text_input.h b/gio/giold/app/internal/wm/wayland_text_input.h
new file mode 100644
index 0000000..b1bb886
--- /dev/null
+++ b/gio/giold/app/internal/wm/wayland_text_input.h
@@ -0,0 +1,838 @@
+/* Generated by wayland-scanner 1.21.0 */
+
+#ifndef TEXT_INPUT_UNSTABLE_V3_CLIENT_PROTOCOL_H
+#define TEXT_INPUT_UNSTABLE_V3_CLIENT_PROTOCOL_H
+
+#include
+#include
+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_text_input_unstable_v3 The text_input_unstable_v3 protocol
+ * Protocol for composing text
+ *
+ * @section page_desc_text_input_unstable_v3 Description
+ *
+ * This protocol allows compositors to act as input methods and to send text
+ * to applications. A text input object is used to manage state of what are
+ * typically text entry fields in the application.
+ *
+ * This document adheres to the RFC 2119 when using words like "must",
+ * "should", "may", etc.
+ *
+ * Warning! The protocol described in this file is experimental and
+ * backward incompatible changes may be made. Backward compatible changes
+ * may be added together with the corresponding interface version bump.
+ * Backward incompatible changes are done by bumping the version number in
+ * the protocol and interface names and resetting the interface version.
+ * Once the protocol is to be declared stable, the 'z' prefix and the
+ * version number in the protocol and interface names are removed and the
+ * interface version number is reset.
+ *
+ * @section page_ifaces_text_input_unstable_v3 Interfaces
+ * - @subpage page_iface_zwp_text_input_v3 - text input
+ * - @subpage page_iface_zwp_text_input_manager_v3 - text input manager
+ * @section page_copyright_text_input_unstable_v3 Copyright
+ *
+ *
+ * Copyright Ā© 2012, 2013 Intel Corporation
+ * Copyright Ā© 2015, 2016 Jan Arne Petersen
+ * Copyright Ā© 2017, 2018 Red Hat, Inc.
+ * Copyright Ā© 2018 Purism SPC
+ *
+ * Permission to use, copy, modify, distribute, and sell this
+ * software and its documentation for any purpose is hereby granted
+ * without fee, provided that the above copyright notice appear in
+ * all copies and that both that copyright notice and this permission
+ * notice appear in supporting documentation, and that the name of
+ * the copyright holders not be used in advertising or publicity
+ * pertaining to distribution of the software without specific,
+ * written prior permission. The copyright holders make no
+ * representations about the suitability of this software for any
+ * purpose. It is provided "as is" without express or implied
+ * warranty.
+ *
+ * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+ * THIS SOFTWARE.
+ *
+ */
+struct wl_seat;
+struct wl_surface;
+struct zwp_text_input_manager_v3;
+struct zwp_text_input_v3;
+
+#ifndef ZWP_TEXT_INPUT_V3_INTERFACE
+#define ZWP_TEXT_INPUT_V3_INTERFACE
+/**
+ * @page page_iface_zwp_text_input_v3 zwp_text_input_v3
+ * @section page_iface_zwp_text_input_v3_desc Description
+ *
+ * The zwp_text_input_v3 interface represents text input and input methods
+ * associated with a seat. It provides enter/leave events to follow the
+ * text input focus for a seat.
+ *
+ * Requests are used to enable/disable the text-input object and set
+ * state information like surrounding and selected text or the content type.
+ * The information about the entered text is sent to the text-input object
+ * via the preedit_string and commit_string events.
+ *
+ * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices
+ * must not point to middle bytes inside a code point: they must either
+ * point to the first byte of a code point or to the end of the buffer.
+ * Lengths must be measured between two valid indices.
+ *
+ * Focus moving throughout surfaces will result in the emission of
+ * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused
+ * surface must commit zwp_text_input_v3.enable and
+ * zwp_text_input_v3.disable requests as the keyboard focus moves across
+ * editable and non-editable elements of the UI. Those two requests are not
+ * expected to be paired with each other, the compositor must be able to
+ * handle consecutive series of the same request.
+ *
+ * State is sent by the state requests (set_surrounding_text,
+ * set_content_type and set_cursor_rectangle) and a commit request. After an
+ * enter event or disable request all state information is invalidated and
+ * needs to be resent by the client.
+ * @section page_iface_zwp_text_input_v3_api API
+ * See @ref iface_zwp_text_input_v3.
+ */
+/**
+ * @defgroup iface_zwp_text_input_v3 The zwp_text_input_v3 interface
+ *
+ * The zwp_text_input_v3 interface represents text input and input methods
+ * associated with a seat. It provides enter/leave events to follow the
+ * text input focus for a seat.
+ *
+ * Requests are used to enable/disable the text-input object and set
+ * state information like surrounding and selected text or the content type.
+ * The information about the entered text is sent to the text-input object
+ * via the preedit_string and commit_string events.
+ *
+ * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices
+ * must not point to middle bytes inside a code point: they must either
+ * point to the first byte of a code point or to the end of the buffer.
+ * Lengths must be measured between two valid indices.
+ *
+ * Focus moving throughout surfaces will result in the emission of
+ * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused
+ * surface must commit zwp_text_input_v3.enable and
+ * zwp_text_input_v3.disable requests as the keyboard focus moves across
+ * editable and non-editable elements of the UI. Those two requests are not
+ * expected to be paired with each other, the compositor must be able to
+ * handle consecutive series of the same request.
+ *
+ * State is sent by the state requests (set_surrounding_text,
+ * set_content_type and set_cursor_rectangle) and a commit request. After an
+ * enter event or disable request all state information is invalidated and
+ * needs to be resent by the client.
+ */
+extern const struct wl_interface zwp_text_input_v3_interface;
+#endif
+#ifndef ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE
+#define ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE
+/**
+ * @page page_iface_zwp_text_input_manager_v3 zwp_text_input_manager_v3
+ * @section page_iface_zwp_text_input_manager_v3_desc Description
+ *
+ * A factory for text-input objects. This object is a global singleton.
+ * @section page_iface_zwp_text_input_manager_v3_api API
+ * See @ref iface_zwp_text_input_manager_v3.
+ */
+/**
+ * @defgroup iface_zwp_text_input_manager_v3 The zwp_text_input_manager_v3 interface
+ *
+ * A factory for text-input objects. This object is a global singleton.
+ */
+extern const struct wl_interface zwp_text_input_manager_v3_interface;
+#endif
+
+#ifndef ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM
+#define ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM
+/**
+ * @ingroup iface_zwp_text_input_v3
+ * text change reason
+ *
+ * Reason for the change of surrounding text or cursor posision.
+ */
+enum zwp_text_input_v3_change_cause {
+ /**
+ * input method caused the change
+ */
+ ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD = 0,
+ /**
+ * something else than the input method caused the change
+ */
+ ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER = 1,
+};
+#endif /* ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM */
+
+#ifndef ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM
+#define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM
+/**
+ * @ingroup iface_zwp_text_input_v3
+ * content hint
+ *
+ * Content hint is a bitmask to allow to modify the behavior of the text
+ * input.
+ */
+enum zwp_text_input_v3_content_hint {
+ /**
+ * no special behavior
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE = 0x0,
+ /**
+ * suggest word completions
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION = 0x1,
+ /**
+ * suggest word corrections
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK = 0x2,
+ /**
+ * switch to uppercase letters at the start of a sentence
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION = 0x4,
+ /**
+ * prefer lowercase letters
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE = 0x8,
+ /**
+ * prefer uppercase letters
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE = 0x10,
+ /**
+ * prefer casing for titles and headings (can be language dependent)
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE = 0x20,
+ /**
+ * characters should be hidden
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT = 0x40,
+ /**
+ * typed text should not be stored
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA = 0x80,
+ /**
+ * just Latin characters should be entered
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN = 0x100,
+ /**
+ * the text input is multiline
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE = 0x200,
+};
+#endif /* ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM */
+
+#ifndef ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM
+#define ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM
+/**
+ * @ingroup iface_zwp_text_input_v3
+ * content purpose
+ *
+ * The content purpose allows to specify the primary purpose of a text
+ * input.
+ *
+ * This allows an input method to show special purpose input panels with
+ * extra characters or to disallow some characters.
+ */
+enum zwp_text_input_v3_content_purpose {
+ /**
+ * default input, allowing all characters
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL = 0,
+ /**
+ * allow only alphabetic characters
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA = 1,
+ /**
+ * allow only digits
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS = 2,
+ /**
+ * input a number (including decimal separator and sign)
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER = 3,
+ /**
+ * input a phone number
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE = 4,
+ /**
+ * input an URL
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL = 5,
+ /**
+ * input an email address
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL = 6,
+ /**
+ * input a name of a person
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME = 7,
+ /**
+ * input a password (combine with sensitive_data hint)
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD = 8,
+ /**
+ * input is a numeric password (combine with sensitive_data hint)
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN = 9,
+ /**
+ * input a date
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE = 10,
+ /**
+ * input a time
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME = 11,
+ /**
+ * input a date and time
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME = 12,
+ /**
+ * input for a terminal
+ */
+ ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL = 13,
+};
+#endif /* ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM */
+
+/**
+ * @ingroup iface_zwp_text_input_v3
+ * @struct zwp_text_input_v3_listener
+ */
+struct zwp_text_input_v3_listener {
+ /**
+ * enter event
+ *
+ * Notification that this seat's text-input focus is on a certain
+ * surface.
+ *
+ * If client has created multiple text input objects, compositor
+ * must send this event to all of them.
+ *
+ * When the seat has the keyboard capability the text-input focus
+ * follows the keyboard focus. This event sets the current surface
+ * for the text-input object.
+ */
+ void (*enter)(void *data,
+ struct zwp_text_input_v3 *zwp_text_input_v3,
+ struct wl_surface *surface);
+ /**
+ * leave event
+ *
+ * Notification that this seat's text-input focus is no longer on
+ * a certain surface. The client should reset any preedit string
+ * previously set.
+ *
+ * The leave notification clears the current surface. It is sent
+ * before the enter notification for the new focus. After leave
+ * event, compositor must ignore requests from any text input
+ * instances until next enter event.
+ *
+ * When the seat has the keyboard capability the text-input focus
+ * follows the keyboard focus.
+ */
+ void (*leave)(void *data,
+ struct zwp_text_input_v3 *zwp_text_input_v3,
+ struct wl_surface *surface);
+ /**
+ * pre-edit
+ *
+ * Notify when a new composing text (pre-edit) should be set at
+ * the current cursor position. Any previously set composing text
+ * must be removed. Any previously existing selected text must be
+ * removed.
+ *
+ * The argument text contains the pre-edit string buffer.
+ *
+ * The parameters cursor_begin and cursor_end are counted in bytes
+ * relative to the beginning of the submitted text buffer. Cursor
+ * should be hidden when both are equal to -1.
+ *
+ * They could be represented by the client as a line if both values
+ * are the same, or as a text highlight otherwise.
+ *
+ * Values set with this event are double-buffered. They must be
+ * applied and reset to initial on the next zwp_text_input_v3.done
+ * event.
+ *
+ * The initial value of text is an empty string, and cursor_begin,
+ * cursor_end and cursor_hidden are all 0.
+ */
+ void (*preedit_string)(void *data,
+ struct zwp_text_input_v3 *zwp_text_input_v3,
+ const char *text,
+ int32_t cursor_begin,
+ int32_t cursor_end);
+ /**
+ * text commit
+ *
+ * Notify when text should be inserted into the editor widget.
+ * The text to commit could be either just a single character after
+ * a key press or the result of some composing (pre-edit).
+ *
+ * Values set with this event are double-buffered. They must be
+ * applied and reset to initial on the next zwp_text_input_v3.done
+ * event.
+ *
+ * The initial value of text is an empty string.
+ */
+ void (*commit_string)(void *data,
+ struct zwp_text_input_v3 *zwp_text_input_v3,
+ const char *text);
+ /**
+ * delete surrounding text
+ *
+ * Notify when the text around the current cursor position should
+ * be deleted.
+ *
+ * Before_length and after_length are the number of bytes before
+ * and after the current cursor index (excluding the selection) to
+ * delete.
+ *
+ * If a preedit text is present, in effect before_length is counted
+ * from the beginning of it, and after_length from its end (see
+ * done event sequence).
+ *
+ * Values set with this event are double-buffered. They must be
+ * applied and reset to initial on the next zwp_text_input_v3.done
+ * event.
+ *
+ * The initial values of both before_length and after_length are 0.
+ * @param before_length length of text before current cursor position
+ * @param after_length length of text after current cursor position
+ */
+ void (*delete_surrounding_text)(void *data,
+ struct zwp_text_input_v3 *zwp_text_input_v3,
+ uint32_t before_length,
+ uint32_t after_length);
+ /**
+ * apply changes
+ *
+ * Instruct the application to apply changes to state requested
+ * by the preedit_string, commit_string and delete_surrounding_text
+ * events. The state relating to these events is double-buffered,
+ * and each one modifies the pending state. This event replaces the
+ * current state with the pending state.
+ *
+ * The application must proceed by evaluating the changes in the
+ * following order:
+ *
+ * 1. Replace existing preedit string with the cursor. 2. Delete
+ * requested surrounding text. 3. Insert commit string with the
+ * cursor at its end. 4. Calculate surrounding text to send. 5.
+ * Insert new preedit text in cursor position. 6. Place cursor
+ * inside preedit text.
+ *
+ * The serial number reflects the last state of the
+ * zwp_text_input_v3 object known to the compositor. The value of
+ * the serial argument must be equal to the number of commit
+ * requests already issued on that object.
+ *
+ * When the client receives a done event with a serial different
+ * than the number of past commit requests, it must proceed with
+ * evaluating and applying the changes as normal, except it should
+ * not change the current state of the zwp_text_input_v3 object.
+ * All pending state requests (set_surrounding_text,
+ * set_content_type and set_cursor_rectangle) on the
+ * zwp_text_input_v3 object should be sent and committed after
+ * receiving a zwp_text_input_v3.done event with a matching serial.
+ */
+ void (*done)(void *data,
+ struct zwp_text_input_v3 *zwp_text_input_v3,
+ uint32_t serial);
+};
+
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+static inline int
+zwp_text_input_v3_add_listener(struct zwp_text_input_v3 *zwp_text_input_v3,
+ const struct zwp_text_input_v3_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) zwp_text_input_v3,
+ (void (**)(void)) listener, data);
+}
+
+#define ZWP_TEXT_INPUT_V3_DESTROY 0
+#define ZWP_TEXT_INPUT_V3_ENABLE 1
+#define ZWP_TEXT_INPUT_V3_DISABLE 2
+#define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT 3
+#define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE 4
+#define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE 5
+#define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE 6
+#define ZWP_TEXT_INPUT_V3_COMMIT 7
+
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_ENTER_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_LEAVE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_PREEDIT_STRING_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_COMMIT_STRING_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_DELETE_SURROUNDING_TEXT_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_DONE_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_ENABLE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_DISABLE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_v3
+ */
+#define ZWP_TEXT_INPUT_V3_COMMIT_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_text_input_v3 */
+static inline void
+zwp_text_input_v3_set_user_data(struct zwp_text_input_v3 *zwp_text_input_v3, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) zwp_text_input_v3, user_data);
+}
+
+/** @ingroup iface_zwp_text_input_v3 */
+static inline void *
+zwp_text_input_v3_get_user_data(struct zwp_text_input_v3 *zwp_text_input_v3)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) zwp_text_input_v3);
+}
+
+static inline uint32_t
+zwp_text_input_v3_get_version(struct zwp_text_input_v3 *zwp_text_input_v3)
+{
+ return wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3);
+}
+
+/**
+ * @ingroup iface_zwp_text_input_v3
+ *
+ * Destroy the wp_text_input object. Also disables all surfaces enabled
+ * through this wp_text_input object.
+ */
+static inline void
+zwp_text_input_v3_destroy(struct zwp_text_input_v3 *zwp_text_input_v3)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3,
+ ZWP_TEXT_INPUT_V3_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_zwp_text_input_v3
+ *
+ * Requests text input on the surface previously obtained from the enter
+ * event.
+ *
+ * This request must be issued every time the active text input changes
+ * to a new one, including within the current surface. Use
+ * zwp_text_input_v3.disable when there is no longer any input focus on
+ * the current surface.
+ *
+ * Clients must not enable more than one text input on the single seat
+ * and should disable the current text input before enabling the new one.
+ * At most one instance of text input may be in enabled state per instance,
+ * Requests to enable the another text input when some text input is active
+ * must be ignored by compositor.
+ *
+ * This request resets all state associated with previous enable, disable,
+ * set_surrounding_text, set_text_change_cause, set_content_type, and
+ * set_cursor_rectangle requests, as well as the state associated with
+ * preedit_string, commit_string, and delete_surrounding_text events.
+ *
+ * The set_surrounding_text, set_content_type and set_cursor_rectangle
+ * requests must follow if the text input supports the necessary
+ * functionality.
+ *
+ * State set with this request is double-buffered. It will get applied on
+ * the next zwp_text_input_v3.commit request, and stay valid until the
+ * next committed enable or disable request.
+ *
+ * The changes must be applied by the compositor after issuing a
+ * zwp_text_input_v3.commit request.
+ */
+static inline void
+zwp_text_input_v3_enable(struct zwp_text_input_v3 *zwp_text_input_v3)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3,
+ ZWP_TEXT_INPUT_V3_ENABLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0);
+}
+
+/**
+ * @ingroup iface_zwp_text_input_v3
+ *
+ * Explicitly disable text input on the current surface (typically when
+ * there is no focus on any text entry inside the surface).
+ *
+ * State set with this request is double-buffered. It will get applied on
+ * the next zwp_text_input_v3.commit request.
+ */
+static inline void
+zwp_text_input_v3_disable(struct zwp_text_input_v3 *zwp_text_input_v3)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3,
+ ZWP_TEXT_INPUT_V3_DISABLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0);
+}
+
+/**
+ * @ingroup iface_zwp_text_input_v3
+ *
+ * Sets the surrounding plain text around the input, excluding the preedit
+ * text.
+ *
+ * The client should notify the compositor of any changes in any of the
+ * values carried with this request, including changes caused by handling
+ * incoming text-input events as well as changes caused by other
+ * mechanisms like keyboard typing.
+ *
+ * If the client is unaware of the text around the cursor, it should not
+ * issue this request, to signify lack of support to the compositor.
+ *
+ * Text is UTF-8 encoded, and should include the cursor position, the
+ * complete selection and additional characters before and after them.
+ * There is a maximum length of wayland messages, so text can not be
+ * longer than 4000 bytes.
+ *
+ * Cursor is the byte offset of the cursor within text buffer.
+ *
+ * Anchor is the byte offset of the selection anchor within text buffer.
+ * If there is no selected text, anchor is the same as cursor.
+ *
+ * If any preedit text is present, it is replaced with a cursor for the
+ * purpose of this event.
+ *
+ * Values set with this request are double-buffered. They will get applied
+ * on the next zwp_text_input_v3.commit request, and stay valid until the
+ * next committed enable or disable request.
+ *
+ * The initial state for affected fields is empty, meaning that the text
+ * input does not support sending surrounding text. If the empty values
+ * get applied, subsequent attempts to change them may have no effect.
+ */
+static inline void
+zwp_text_input_v3_set_surrounding_text(struct zwp_text_input_v3 *zwp_text_input_v3, const char *text, int32_t cursor, int32_t anchor)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3,
+ ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, text, cursor, anchor);
+}
+
+/**
+ * @ingroup iface_zwp_text_input_v3
+ *
+ * Tells the compositor why the text surrounding the cursor changed.
+ *
+ * Whenever the client detects an external change in text, cursor, or
+ * anchor posision, it must issue this request to the compositor. This
+ * request is intended to give the input method a chance to update the
+ * preedit text in an appropriate way, e.g. by removing it when the user
+ * starts typing with a keyboard.
+ *
+ * cause describes the source of the change.
+ *
+ * The value set with this request is double-buffered. It must be applied
+ * and reset to initial at the next zwp_text_input_v3.commit request.
+ *
+ * The initial value of cause is input_method.
+ */
+static inline void
+zwp_text_input_v3_set_text_change_cause(struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t cause)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3,
+ ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, cause);
+}
+
+/**
+ * @ingroup iface_zwp_text_input_v3
+ *
+ * Sets the content purpose and content hint. While the purpose is the
+ * basic purpose of an input field, the hint flags allow to modify some of
+ * the behavior.
+ *
+ * Values set with this request are double-buffered. They will get applied
+ * on the next zwp_text_input_v3.commit request.
+ * Subsequent attempts to update them may have no effect. The values
+ * remain valid until the next committed enable or disable request.
+ *
+ * The initial value for hint is none, and the initial value for purpose
+ * is normal.
+ */
+static inline void
+zwp_text_input_v3_set_content_type(struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t hint, uint32_t purpose)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3,
+ ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, hint, purpose);
+}
+
+/**
+ * @ingroup iface_zwp_text_input_v3
+ *
+ * Marks an area around the cursor as a x, y, width, height rectangle in
+ * surface local coordinates.
+ *
+ * Allows the compositor to put a window with word suggestions near the
+ * cursor, without obstructing the text being input.
+ *
+ * If the client is unaware of the position of edited text, it should not
+ * issue this request, to signify lack of support to the compositor.
+ *
+ * Values set with this request are double-buffered. They will get applied
+ * on the next zwp_text_input_v3.commit request, and stay valid until the
+ * next committed enable or disable request.
+ *
+ * The initial values describing a cursor rectangle are empty. That means
+ * the text input does not support describing the cursor area. If the
+ * empty values get applied, subsequent attempts to change them may have
+ * no effect.
+ */
+static inline void
+zwp_text_input_v3_set_cursor_rectangle(struct zwp_text_input_v3 *zwp_text_input_v3, int32_t x, int32_t y, int32_t width, int32_t height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3,
+ ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0, x, y, width, height);
+}
+
+/**
+ * @ingroup iface_zwp_text_input_v3
+ *
+ * Atomically applies state changes recently sent to the compositor.
+ *
+ * The commit request establishes and updates the state of the client, and
+ * must be issued after any changes to apply them.
+ *
+ * Text input state (enabled status, content purpose, content hint,
+ * surrounding text and change cause, cursor rectangle) is conceptually
+ * double-buffered within the context of a text input, i.e. between a
+ * committed enable request and the following committed enable or disable
+ * request.
+ *
+ * Protocol requests modify the pending state, as opposed to the current
+ * state in use by the input method. A commit request atomically applies
+ * all pending state, replacing the current state. After commit, the new
+ * pending state is as documented for each related request.
+ *
+ * Requests are applied in the order of arrival.
+ *
+ * Neither current nor pending state are modified unless noted otherwise.
+ *
+ * The compositor must count the number of commit requests coming from
+ * each zwp_text_input_v3 object and use the count as the serial in done
+ * events.
+ */
+static inline void
+zwp_text_input_v3_commit(struct zwp_text_input_v3 *zwp_text_input_v3)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_v3,
+ ZWP_TEXT_INPUT_V3_COMMIT, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3), 0);
+}
+
+#define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY 0
+#define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT 1
+
+
+/**
+ * @ingroup iface_zwp_text_input_manager_v3
+ */
+#define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_zwp_text_input_manager_v3
+ */
+#define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT_SINCE_VERSION 1
+
+/** @ingroup iface_zwp_text_input_manager_v3 */
+static inline void
+zwp_text_input_manager_v3_set_user_data(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) zwp_text_input_manager_v3, user_data);
+}
+
+/** @ingroup iface_zwp_text_input_manager_v3 */
+static inline void *
+zwp_text_input_manager_v3_get_user_data(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) zwp_text_input_manager_v3);
+}
+
+static inline uint32_t
+zwp_text_input_manager_v3_get_version(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3)
+{
+ return wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3);
+}
+
+/**
+ * @ingroup iface_zwp_text_input_manager_v3
+ *
+ * Destroy the wp_text_input_manager object.
+ */
+static inline void
+zwp_text_input_manager_v3_destroy(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_manager_v3,
+ ZWP_TEXT_INPUT_MANAGER_V3_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_zwp_text_input_manager_v3
+ *
+ * Creates a new text-input object for a given seat.
+ */
+static inline struct zwp_text_input_v3 *
+zwp_text_input_manager_v3_get_text_input(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3, struct wl_seat *seat)
+{
+ struct wl_proxy *id;
+
+ id = wl_proxy_marshal_flags((struct wl_proxy *) zwp_text_input_manager_v3,
+ ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT, &zwp_text_input_v3_interface, wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3), 0, NULL, seat);
+
+ return (struct zwp_text_input_v3 *) id;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/gio/giold/app/internal/wm/wayland_xdg_decoration.c b/gio/giold/app/internal/wm/wayland_xdg_decoration.c
new file mode 100644
index 0000000..78f9328
--- /dev/null
+++ b/gio/giold/app/internal/wm/wayland_xdg_decoration.c
@@ -0,0 +1,77 @@
+// +build linux,!android,!nowayland freebsd
+
+/* Generated by wayland-scanner 1.21.0 */
+
+/*
+ * Copyright Ā© 2018 Simon Ser
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include
+#include
+#include "wayland-util.h"
+
+#ifndef __has_attribute
+# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
+#endif
+
+#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
+#define WL_PRIVATE __attribute__ ((visibility("hidden")))
+#else
+#define WL_PRIVATE
+#endif
+
+extern const struct wl_interface xdg_toplevel_interface;
+extern const struct wl_interface zxdg_toplevel_decoration_v1_interface;
+
+static const struct wl_interface *xdg_decoration_unstable_v1_types[] = {
+ NULL,
+ &zxdg_toplevel_decoration_v1_interface,
+ &xdg_toplevel_interface,
+};
+
+static const struct wl_message zxdg_decoration_manager_v1_requests[] = {
+ { "destroy", "", xdg_decoration_unstable_v1_types + 0 },
+ { "get_toplevel_decoration", "no", xdg_decoration_unstable_v1_types + 1 },
+};
+
+WL_PRIVATE const struct wl_interface zxdg_decoration_manager_v1_interface = {
+ "zxdg_decoration_manager_v1", 1,
+ 2, zxdg_decoration_manager_v1_requests,
+ 0, NULL,
+};
+
+static const struct wl_message zxdg_toplevel_decoration_v1_requests[] = {
+ { "destroy", "", xdg_decoration_unstable_v1_types + 0 },
+ { "set_mode", "u", xdg_decoration_unstable_v1_types + 0 },
+ { "unset_mode", "", xdg_decoration_unstable_v1_types + 0 },
+};
+
+static const struct wl_message zxdg_toplevel_decoration_v1_events[] = {
+ { "configure", "u", xdg_decoration_unstable_v1_types + 0 },
+};
+
+WL_PRIVATE const struct wl_interface zxdg_toplevel_decoration_v1_interface = {
+ "zxdg_toplevel_decoration_v1", 1,
+ 3, zxdg_toplevel_decoration_v1_requests,
+ 1, zxdg_toplevel_decoration_v1_events,
+};
+
diff --git a/gio/giold/app/internal/wm/wayland_xdg_decoration.h b/gio/giold/app/internal/wm/wayland_xdg_decoration.h
new file mode 100644
index 0000000..286c236
--- /dev/null
+++ b/gio/giold/app/internal/wm/wayland_xdg_decoration.h
@@ -0,0 +1,378 @@
+/* Generated by wayland-scanner 1.21.0 */
+
+#ifndef XDG_DECORATION_UNSTABLE_V1_CLIENT_PROTOCOL_H
+#define XDG_DECORATION_UNSTABLE_V1_CLIENT_PROTOCOL_H
+
+#include
+#include
+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_xdg_decoration_unstable_v1 The xdg_decoration_unstable_v1 protocol
+ * @section page_ifaces_xdg_decoration_unstable_v1 Interfaces
+ * - @subpage page_iface_zxdg_decoration_manager_v1 - window decoration manager
+ * - @subpage page_iface_zxdg_toplevel_decoration_v1 - decoration object for a toplevel surface
+ * @section page_copyright_xdg_decoration_unstable_v1 Copyright
+ *
+ *
+ * Copyright Ā© 2018 Simon Ser
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ */
+struct xdg_toplevel;
+struct zxdg_decoration_manager_v1;
+struct zxdg_toplevel_decoration_v1;
+
+#ifndef ZXDG_DECORATION_MANAGER_V1_INTERFACE
+#define ZXDG_DECORATION_MANAGER_V1_INTERFACE
+/**
+ * @page page_iface_zxdg_decoration_manager_v1 zxdg_decoration_manager_v1
+ * @section page_iface_zxdg_decoration_manager_v1_desc Description
+ *
+ * This interface allows a compositor to announce support for server-side
+ * decorations.
+ *
+ * A window decoration is a set of window controls as deemed appropriate by
+ * the party managing them, such as user interface components used to move,
+ * resize and change a window's state.
+ *
+ * A client can use this protocol to request being decorated by a supporting
+ * compositor.
+ *
+ * If compositor and client do not negotiate the use of a server-side
+ * decoration using this protocol, clients continue to self-decorate as they
+ * see fit.
+ *
+ * Warning! The protocol described in this file is experimental and
+ * backward incompatible changes may be made. Backward compatible changes
+ * may be added together with the corresponding interface version bump.
+ * Backward incompatible changes are done by bumping the version number in
+ * the protocol and interface names and resetting the interface version.
+ * Once the protocol is to be declared stable, the 'z' prefix and the
+ * version number in the protocol and interface names are removed and the
+ * interface version number is reset.
+ * @section page_iface_zxdg_decoration_manager_v1_api API
+ * See @ref iface_zxdg_decoration_manager_v1.
+ */
+/**
+ * @defgroup iface_zxdg_decoration_manager_v1 The zxdg_decoration_manager_v1 interface
+ *
+ * This interface allows a compositor to announce support for server-side
+ * decorations.
+ *
+ * A window decoration is a set of window controls as deemed appropriate by
+ * the party managing them, such as user interface components used to move,
+ * resize and change a window's state.
+ *
+ * A client can use this protocol to request being decorated by a supporting
+ * compositor.
+ *
+ * If compositor and client do not negotiate the use of a server-side
+ * decoration using this protocol, clients continue to self-decorate as they
+ * see fit.
+ *
+ * Warning! The protocol described in this file is experimental and
+ * backward incompatible changes may be made. Backward compatible changes
+ * may be added together with the corresponding interface version bump.
+ * Backward incompatible changes are done by bumping the version number in
+ * the protocol and interface names and resetting the interface version.
+ * Once the protocol is to be declared stable, the 'z' prefix and the
+ * version number in the protocol and interface names are removed and the
+ * interface version number is reset.
+ */
+extern const struct wl_interface zxdg_decoration_manager_v1_interface;
+#endif
+#ifndef ZXDG_TOPLEVEL_DECORATION_V1_INTERFACE
+#define ZXDG_TOPLEVEL_DECORATION_V1_INTERFACE
+/**
+ * @page page_iface_zxdg_toplevel_decoration_v1 zxdg_toplevel_decoration_v1
+ * @section page_iface_zxdg_toplevel_decoration_v1_desc Description
+ *
+ * The decoration object allows the compositor to toggle server-side window
+ * decorations for a toplevel surface. The client can request to switch to
+ * another mode.
+ *
+ * The xdg_toplevel_decoration object must be destroyed before its
+ * xdg_toplevel.
+ * @section page_iface_zxdg_toplevel_decoration_v1_api API
+ * See @ref iface_zxdg_toplevel_decoration_v1.
+ */
+/**
+ * @defgroup iface_zxdg_toplevel_decoration_v1 The zxdg_toplevel_decoration_v1 interface
+ *
+ * The decoration object allows the compositor to toggle server-side window
+ * decorations for a toplevel surface. The client can request to switch to
+ * another mode.
+ *
+ * The xdg_toplevel_decoration object must be destroyed before its
+ * xdg_toplevel.
+ */
+extern const struct wl_interface zxdg_toplevel_decoration_v1_interface;
+#endif
+
+#define ZXDG_DECORATION_MANAGER_V1_DESTROY 0
+#define ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION 1
+
+
+/**
+ * @ingroup iface_zxdg_decoration_manager_v1
+ */
+#define ZXDG_DECORATION_MANAGER_V1_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_zxdg_decoration_manager_v1
+ */
+#define ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION_SINCE_VERSION 1
+
+/** @ingroup iface_zxdg_decoration_manager_v1 */
+static inline void
+zxdg_decoration_manager_v1_set_user_data(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) zxdg_decoration_manager_v1, user_data);
+}
+
+/** @ingroup iface_zxdg_decoration_manager_v1 */
+static inline void *
+zxdg_decoration_manager_v1_get_user_data(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) zxdg_decoration_manager_v1);
+}
+
+static inline uint32_t
+zxdg_decoration_manager_v1_get_version(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1)
+{
+ return wl_proxy_get_version((struct wl_proxy *) zxdg_decoration_manager_v1);
+}
+
+/**
+ * @ingroup iface_zxdg_decoration_manager_v1
+ *
+ * Destroy the decoration manager. This doesn't destroy objects created
+ * with the manager.
+ */
+static inline void
+zxdg_decoration_manager_v1_destroy(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zxdg_decoration_manager_v1,
+ ZXDG_DECORATION_MANAGER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_decoration_manager_v1), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_zxdg_decoration_manager_v1
+ *
+ * Create a new decoration object associated with the given toplevel.
+ *
+ * Creating an xdg_toplevel_decoration from an xdg_toplevel which has a
+ * buffer attached or committed is a client error, and any attempts by a
+ * client to attach or manipulate a buffer prior to the first
+ * xdg_toplevel_decoration.configure event must also be treated as
+ * errors.
+ */
+static inline struct zxdg_toplevel_decoration_v1 *
+zxdg_decoration_manager_v1_get_toplevel_decoration(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1, struct xdg_toplevel *toplevel)
+{
+ struct wl_proxy *id;
+
+ id = wl_proxy_marshal_flags((struct wl_proxy *) zxdg_decoration_manager_v1,
+ ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION, &zxdg_toplevel_decoration_v1_interface, wl_proxy_get_version((struct wl_proxy *) zxdg_decoration_manager_v1), 0, NULL, toplevel);
+
+ return (struct zxdg_toplevel_decoration_v1 *) id;
+}
+
+#ifndef ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM
+#define ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM
+enum zxdg_toplevel_decoration_v1_error {
+ /**
+ * xdg_toplevel has a buffer attached before configure
+ */
+ ZXDG_TOPLEVEL_DECORATION_V1_ERROR_UNCONFIGURED_BUFFER = 0,
+ /**
+ * xdg_toplevel already has a decoration object
+ */
+ ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ALREADY_CONSTRUCTED = 1,
+ /**
+ * xdg_toplevel destroyed before the decoration object
+ */
+ ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ORPHANED = 2,
+};
+#endif /* ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM */
+
+#ifndef ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM
+#define ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM
+/**
+ * @ingroup iface_zxdg_toplevel_decoration_v1
+ * window decoration modes
+ *
+ * These values describe window decoration modes.
+ */
+enum zxdg_toplevel_decoration_v1_mode {
+ /**
+ * no server-side window decoration
+ */
+ ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE = 1,
+ /**
+ * server-side window decoration
+ */
+ ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE = 2,
+};
+#endif /* ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM */
+
+/**
+ * @ingroup iface_zxdg_toplevel_decoration_v1
+ * @struct zxdg_toplevel_decoration_v1_listener
+ */
+struct zxdg_toplevel_decoration_v1_listener {
+ /**
+ * suggest a surface change
+ *
+ * The configure event asks the client to change its decoration
+ * mode. The configured state should not be applied immediately.
+ * Clients must send an ack_configure in response to this event.
+ * See xdg_surface.configure and xdg_surface.ack_configure for
+ * details.
+ *
+ * A configure event can be sent at any time. The specified mode
+ * must be obeyed by the client.
+ * @param mode the decoration mode
+ */
+ void (*configure)(void *data,
+ struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1,
+ uint32_t mode);
+};
+
+/**
+ * @ingroup iface_zxdg_toplevel_decoration_v1
+ */
+static inline int
+zxdg_toplevel_decoration_v1_add_listener(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1,
+ const struct zxdg_toplevel_decoration_v1_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) zxdg_toplevel_decoration_v1,
+ (void (**)(void)) listener, data);
+}
+
+#define ZXDG_TOPLEVEL_DECORATION_V1_DESTROY 0
+#define ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE 1
+#define ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE 2
+
+/**
+ * @ingroup iface_zxdg_toplevel_decoration_v1
+ */
+#define ZXDG_TOPLEVEL_DECORATION_V1_CONFIGURE_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_zxdg_toplevel_decoration_v1
+ */
+#define ZXDG_TOPLEVEL_DECORATION_V1_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_zxdg_toplevel_decoration_v1
+ */
+#define ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE_SINCE_VERSION 1
+/**
+ * @ingroup iface_zxdg_toplevel_decoration_v1
+ */
+#define ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE_SINCE_VERSION 1
+
+/** @ingroup iface_zxdg_toplevel_decoration_v1 */
+static inline void
+zxdg_toplevel_decoration_v1_set_user_data(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) zxdg_toplevel_decoration_v1, user_data);
+}
+
+/** @ingroup iface_zxdg_toplevel_decoration_v1 */
+static inline void *
+zxdg_toplevel_decoration_v1_get_user_data(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) zxdg_toplevel_decoration_v1);
+}
+
+static inline uint32_t
+zxdg_toplevel_decoration_v1_get_version(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1)
+{
+ return wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1);
+}
+
+/**
+ * @ingroup iface_zxdg_toplevel_decoration_v1
+ *
+ * Switch back to a mode without any server-side decorations at the next
+ * commit.
+ */
+static inline void
+zxdg_toplevel_decoration_v1_destroy(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zxdg_toplevel_decoration_v1,
+ ZXDG_TOPLEVEL_DECORATION_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_zxdg_toplevel_decoration_v1
+ *
+ * Set the toplevel surface decoration mode. This informs the compositor
+ * that the client prefers the provided decoration mode.
+ *
+ * After requesting a decoration mode, the compositor will respond by
+ * emitting an xdg_surface.configure event. The client should then update
+ * its content, drawing it without decorations if the received mode is
+ * server-side decorations. The client must also acknowledge the configure
+ * when committing the new content (see xdg_surface.ack_configure).
+ *
+ * The compositor can decide not to use the client's mode and enforce a
+ * different mode instead.
+ *
+ * Clients whose decoration mode depend on the xdg_toplevel state may send
+ * a set_mode request in response to an xdg_surface.configure event and wait
+ * for the next xdg_surface.configure event to prevent unwanted state.
+ * Such clients are responsible for preventing configure loops and must
+ * make sure not to send multiple successive set_mode requests with the
+ * same decoration mode.
+ */
+static inline void
+zxdg_toplevel_decoration_v1_set_mode(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, uint32_t mode)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zxdg_toplevel_decoration_v1,
+ ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1), 0, mode);
+}
+
+/**
+ * @ingroup iface_zxdg_toplevel_decoration_v1
+ *
+ * Unset the toplevel surface decoration mode. This informs the compositor
+ * that the client doesn't prefer a particular decoration mode.
+ *
+ * This request has the same semantics as set_mode.
+ */
+static inline void
+zxdg_toplevel_decoration_v1_unset_mode(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) zxdg_toplevel_decoration_v1,
+ ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1), 0);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/gio/giold/app/internal/wm/wayland_xdg_shell.c b/gio/giold/app/internal/wm/wayland_xdg_shell.c
new file mode 100644
index 0000000..4ed2659
--- /dev/null
+++ b/gio/giold/app/internal/wm/wayland_xdg_shell.c
@@ -0,0 +1,185 @@
+// +build linux,!android,!nowayland freebsd
+
+/* Generated by wayland-scanner 1.21.0 */
+
+/*
+ * Copyright Ā© 2008-2013 Kristian HĆøgsberg
+ * Copyright Ā© 2013 Rafael Antognolli
+ * Copyright Ā© 2013 Jasper St. Pierre
+ * Copyright Ā© 2010-2013 Intel Corporation
+ * Copyright Ā© 2015-2017 Samsung Electronics Co., Ltd
+ * Copyright Ā© 2015-2017 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include
+#include
+#include "wayland-util.h"
+
+#ifndef __has_attribute
+# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */
+#endif
+
+#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4)
+#define WL_PRIVATE __attribute__ ((visibility("hidden")))
+#else
+#define WL_PRIVATE
+#endif
+
+extern const struct wl_interface wl_output_interface;
+extern const struct wl_interface wl_seat_interface;
+extern const struct wl_interface wl_surface_interface;
+extern const struct wl_interface xdg_popup_interface;
+extern const struct wl_interface xdg_positioner_interface;
+extern const struct wl_interface xdg_surface_interface;
+extern const struct wl_interface xdg_toplevel_interface;
+
+static const struct wl_interface *xdg_shell_types[] = {
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ &xdg_positioner_interface,
+ &xdg_surface_interface,
+ &wl_surface_interface,
+ &xdg_toplevel_interface,
+ &xdg_popup_interface,
+ &xdg_surface_interface,
+ &xdg_positioner_interface,
+ &xdg_toplevel_interface,
+ &wl_seat_interface,
+ NULL,
+ NULL,
+ NULL,
+ &wl_seat_interface,
+ NULL,
+ &wl_seat_interface,
+ NULL,
+ NULL,
+ &wl_output_interface,
+ &wl_seat_interface,
+ NULL,
+ &xdg_positioner_interface,
+ NULL,
+};
+
+static const struct wl_message xdg_wm_base_requests[] = {
+ { "destroy", "", xdg_shell_types + 0 },
+ { "create_positioner", "n", xdg_shell_types + 4 },
+ { "get_xdg_surface", "no", xdg_shell_types + 5 },
+ { "pong", "u", xdg_shell_types + 0 },
+};
+
+static const struct wl_message xdg_wm_base_events[] = {
+ { "ping", "u", xdg_shell_types + 0 },
+};
+
+WL_PRIVATE const struct wl_interface xdg_wm_base_interface = {
+ "xdg_wm_base", 5,
+ 4, xdg_wm_base_requests,
+ 1, xdg_wm_base_events,
+};
+
+static const struct wl_message xdg_positioner_requests[] = {
+ { "destroy", "", xdg_shell_types + 0 },
+ { "set_size", "ii", xdg_shell_types + 0 },
+ { "set_anchor_rect", "iiii", xdg_shell_types + 0 },
+ { "set_anchor", "u", xdg_shell_types + 0 },
+ { "set_gravity", "u", xdg_shell_types + 0 },
+ { "set_constraint_adjustment", "u", xdg_shell_types + 0 },
+ { "set_offset", "ii", xdg_shell_types + 0 },
+ { "set_reactive", "3", xdg_shell_types + 0 },
+ { "set_parent_size", "3ii", xdg_shell_types + 0 },
+ { "set_parent_configure", "3u", xdg_shell_types + 0 },
+};
+
+WL_PRIVATE const struct wl_interface xdg_positioner_interface = {
+ "xdg_positioner", 5,
+ 10, xdg_positioner_requests,
+ 0, NULL,
+};
+
+static const struct wl_message xdg_surface_requests[] = {
+ { "destroy", "", xdg_shell_types + 0 },
+ { "get_toplevel", "n", xdg_shell_types + 7 },
+ { "get_popup", "n?oo", xdg_shell_types + 8 },
+ { "set_window_geometry", "iiii", xdg_shell_types + 0 },
+ { "ack_configure", "u", xdg_shell_types + 0 },
+};
+
+static const struct wl_message xdg_surface_events[] = {
+ { "configure", "u", xdg_shell_types + 0 },
+};
+
+WL_PRIVATE const struct wl_interface xdg_surface_interface = {
+ "xdg_surface", 5,
+ 5, xdg_surface_requests,
+ 1, xdg_surface_events,
+};
+
+static const struct wl_message xdg_toplevel_requests[] = {
+ { "destroy", "", xdg_shell_types + 0 },
+ { "set_parent", "?o", xdg_shell_types + 11 },
+ { "set_title", "s", xdg_shell_types + 0 },
+ { "set_app_id", "s", xdg_shell_types + 0 },
+ { "show_window_menu", "ouii", xdg_shell_types + 12 },
+ { "move", "ou", xdg_shell_types + 16 },
+ { "resize", "ouu", xdg_shell_types + 18 },
+ { "set_max_size", "ii", xdg_shell_types + 0 },
+ { "set_min_size", "ii", xdg_shell_types + 0 },
+ { "set_maximized", "", xdg_shell_types + 0 },
+ { "unset_maximized", "", xdg_shell_types + 0 },
+ { "set_fullscreen", "?o", xdg_shell_types + 21 },
+ { "unset_fullscreen", "", xdg_shell_types + 0 },
+ { "set_minimized", "", xdg_shell_types + 0 },
+};
+
+static const struct wl_message xdg_toplevel_events[] = {
+ { "configure", "iia", xdg_shell_types + 0 },
+ { "close", "", xdg_shell_types + 0 },
+ { "configure_bounds", "4ii", xdg_shell_types + 0 },
+ { "wm_capabilities", "5a", xdg_shell_types + 0 },
+};
+
+WL_PRIVATE const struct wl_interface xdg_toplevel_interface = {
+ "xdg_toplevel", 5,
+ 14, xdg_toplevel_requests,
+ 4, xdg_toplevel_events,
+};
+
+static const struct wl_message xdg_popup_requests[] = {
+ { "destroy", "", xdg_shell_types + 0 },
+ { "grab", "ou", xdg_shell_types + 22 },
+ { "reposition", "3ou", xdg_shell_types + 24 },
+};
+
+static const struct wl_message xdg_popup_events[] = {
+ { "configure", "iiii", xdg_shell_types + 0 },
+ { "popup_done", "", xdg_shell_types + 0 },
+ { "repositioned", "3u", xdg_shell_types + 0 },
+};
+
+WL_PRIVATE const struct wl_interface xdg_popup_interface = {
+ "xdg_popup", 5,
+ 3, xdg_popup_requests,
+ 3, xdg_popup_events,
+};
+
diff --git a/gio/giold/app/internal/wm/wayland_xdg_shell.h b/gio/giold/app/internal/wm/wayland_xdg_shell.h
new file mode 100644
index 0000000..aa14e2e
--- /dev/null
+++ b/gio/giold/app/internal/wm/wayland_xdg_shell.h
@@ -0,0 +1,2280 @@
+/* Generated by wayland-scanner 1.21.0 */
+
+#ifndef XDG_SHELL_CLIENT_PROTOCOL_H
+#define XDG_SHELL_CLIENT_PROTOCOL_H
+
+#include
+#include
+#include "wayland-client.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @page page_xdg_shell The xdg_shell protocol
+ * @section page_ifaces_xdg_shell Interfaces
+ * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces
+ * - @subpage page_iface_xdg_positioner - child surface positioner
+ * - @subpage page_iface_xdg_surface - desktop user interface surface base interface
+ * - @subpage page_iface_xdg_toplevel - toplevel surface
+ * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus
+ * @section page_copyright_xdg_shell Copyright
+ *
+ *
+ * Copyright Ā© 2008-2013 Kristian HĆøgsberg
+ * Copyright Ā© 2013 Rafael Antognolli
+ * Copyright Ā© 2013 Jasper St. Pierre
+ * Copyright Ā© 2010-2013 Intel Corporation
+ * Copyright Ā© 2015-2017 Samsung Electronics Co., Ltd
+ * Copyright Ā© 2015-2017 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ */
+struct wl_output;
+struct wl_seat;
+struct wl_surface;
+struct xdg_popup;
+struct xdg_positioner;
+struct xdg_surface;
+struct xdg_toplevel;
+struct xdg_wm_base;
+
+#ifndef XDG_WM_BASE_INTERFACE
+#define XDG_WM_BASE_INTERFACE
+/**
+ * @page page_iface_xdg_wm_base xdg_wm_base
+ * @section page_iface_xdg_wm_base_desc Description
+ *
+ * The xdg_wm_base interface is exposed as a global object enabling clients
+ * to turn their wl_surfaces into windows in a desktop environment. It
+ * defines the basic functionality needed for clients and the compositor to
+ * create windows that can be dragged, resized, maximized, etc, as well as
+ * creating transient windows such as popup menus.
+ * @section page_iface_xdg_wm_base_api API
+ * See @ref iface_xdg_wm_base.
+ */
+/**
+ * @defgroup iface_xdg_wm_base The xdg_wm_base interface
+ *
+ * The xdg_wm_base interface is exposed as a global object enabling clients
+ * to turn their wl_surfaces into windows in a desktop environment. It
+ * defines the basic functionality needed for clients and the compositor to
+ * create windows that can be dragged, resized, maximized, etc, as well as
+ * creating transient windows such as popup menus.
+ */
+extern const struct wl_interface xdg_wm_base_interface;
+#endif
+#ifndef XDG_POSITIONER_INTERFACE
+#define XDG_POSITIONER_INTERFACE
+/**
+ * @page page_iface_xdg_positioner xdg_positioner
+ * @section page_iface_xdg_positioner_desc Description
+ *
+ * The xdg_positioner provides a collection of rules for the placement of a
+ * child surface relative to a parent surface. Rules can be defined to ensure
+ * the child surface remains within the visible area's borders, and to
+ * specify how the child surface changes its position, such as sliding along
+ * an axis, or flipping around a rectangle. These positioner-created rules are
+ * constrained by the requirement that a child surface must intersect with or
+ * be at least partially adjacent to its parent surface.
+ *
+ * See the various requests for details about possible rules.
+ *
+ * At the time of the request, the compositor makes a copy of the rules
+ * specified by the xdg_positioner. Thus, after the request is complete the
+ * xdg_positioner object can be destroyed or reused; further changes to the
+ * object will have no effect on previous usages.
+ *
+ * For an xdg_positioner object to be considered complete, it must have a
+ * non-zero size set by set_size, and a non-zero anchor rectangle set by
+ * set_anchor_rect. Passing an incomplete xdg_positioner object when
+ * positioning a surface raises an invalid_positioner error.
+ * @section page_iface_xdg_positioner_api API
+ * See @ref iface_xdg_positioner.
+ */
+/**
+ * @defgroup iface_xdg_positioner The xdg_positioner interface
+ *
+ * The xdg_positioner provides a collection of rules for the placement of a
+ * child surface relative to a parent surface. Rules can be defined to ensure
+ * the child surface remains within the visible area's borders, and to
+ * specify how the child surface changes its position, such as sliding along
+ * an axis, or flipping around a rectangle. These positioner-created rules are
+ * constrained by the requirement that a child surface must intersect with or
+ * be at least partially adjacent to its parent surface.
+ *
+ * See the various requests for details about possible rules.
+ *
+ * At the time of the request, the compositor makes a copy of the rules
+ * specified by the xdg_positioner. Thus, after the request is complete the
+ * xdg_positioner object can be destroyed or reused; further changes to the
+ * object will have no effect on previous usages.
+ *
+ * For an xdg_positioner object to be considered complete, it must have a
+ * non-zero size set by set_size, and a non-zero anchor rectangle set by
+ * set_anchor_rect. Passing an incomplete xdg_positioner object when
+ * positioning a surface raises an invalid_positioner error.
+ */
+extern const struct wl_interface xdg_positioner_interface;
+#endif
+#ifndef XDG_SURFACE_INTERFACE
+#define XDG_SURFACE_INTERFACE
+/**
+ * @page page_iface_xdg_surface xdg_surface
+ * @section page_iface_xdg_surface_desc Description
+ *
+ * An interface that may be implemented by a wl_surface, for
+ * implementations that provide a desktop-style user interface.
+ *
+ * It provides a base set of functionality required to construct user
+ * interface elements requiring management by the compositor, such as
+ * toplevel windows, menus, etc. The types of functionality are split into
+ * xdg_surface roles.
+ *
+ * Creating an xdg_surface does not set the role for a wl_surface. In order
+ * to map an xdg_surface, the client must create a role-specific object
+ * using, e.g., get_toplevel, get_popup. The wl_surface for any given
+ * xdg_surface can have at most one role, and may not be assigned any role
+ * not based on xdg_surface.
+ *
+ * A role must be assigned before any other requests are made to the
+ * xdg_surface object.
+ *
+ * The client must call wl_surface.commit on the corresponding wl_surface
+ * for the xdg_surface state to take effect.
+ *
+ * Creating an xdg_surface from a wl_surface which has a buffer attached or
+ * committed is a client error, and any attempts by a client to attach or
+ * manipulate a buffer prior to the first xdg_surface.configure call must
+ * also be treated as errors.
+ *
+ * After creating a role-specific object and setting it up, the client must
+ * perform an initial commit without any buffer attached. The compositor
+ * will reply with an xdg_surface.configure event. The client must
+ * acknowledge it and is then allowed to attach a buffer to map the surface.
+ *
+ * Mapping an xdg_surface-based role surface is defined as making it
+ * possible for the surface to be shown by the compositor. Note that
+ * a mapped surface is not guaranteed to be visible once it is mapped.
+ *
+ * For an xdg_surface to be mapped by the compositor, the following
+ * conditions must be met:
+ * (1) the client has assigned an xdg_surface-based role to the surface
+ * (2) the client has set and committed the xdg_surface state and the
+ * role-dependent state to the surface
+ * (3) the client has committed a buffer to the surface
+ *
+ * A newly-unmapped surface is considered to have met condition (1) out
+ * of the 3 required conditions for mapping a surface if its role surface
+ * has not been destroyed, i.e. the client must perform the initial commit
+ * again before attaching a buffer.
+ * @section page_iface_xdg_surface_api API
+ * See @ref iface_xdg_surface.
+ */
+/**
+ * @defgroup iface_xdg_surface The xdg_surface interface
+ *
+ * An interface that may be implemented by a wl_surface, for
+ * implementations that provide a desktop-style user interface.
+ *
+ * It provides a base set of functionality required to construct user
+ * interface elements requiring management by the compositor, such as
+ * toplevel windows, menus, etc. The types of functionality are split into
+ * xdg_surface roles.
+ *
+ * Creating an xdg_surface does not set the role for a wl_surface. In order
+ * to map an xdg_surface, the client must create a role-specific object
+ * using, e.g., get_toplevel, get_popup. The wl_surface for any given
+ * xdg_surface can have at most one role, and may not be assigned any role
+ * not based on xdg_surface.
+ *
+ * A role must be assigned before any other requests are made to the
+ * xdg_surface object.
+ *
+ * The client must call wl_surface.commit on the corresponding wl_surface
+ * for the xdg_surface state to take effect.
+ *
+ * Creating an xdg_surface from a wl_surface which has a buffer attached or
+ * committed is a client error, and any attempts by a client to attach or
+ * manipulate a buffer prior to the first xdg_surface.configure call must
+ * also be treated as errors.
+ *
+ * After creating a role-specific object and setting it up, the client must
+ * perform an initial commit without any buffer attached. The compositor
+ * will reply with an xdg_surface.configure event. The client must
+ * acknowledge it and is then allowed to attach a buffer to map the surface.
+ *
+ * Mapping an xdg_surface-based role surface is defined as making it
+ * possible for the surface to be shown by the compositor. Note that
+ * a mapped surface is not guaranteed to be visible once it is mapped.
+ *
+ * For an xdg_surface to be mapped by the compositor, the following
+ * conditions must be met:
+ * (1) the client has assigned an xdg_surface-based role to the surface
+ * (2) the client has set and committed the xdg_surface state and the
+ * role-dependent state to the surface
+ * (3) the client has committed a buffer to the surface
+ *
+ * A newly-unmapped surface is considered to have met condition (1) out
+ * of the 3 required conditions for mapping a surface if its role surface
+ * has not been destroyed, i.e. the client must perform the initial commit
+ * again before attaching a buffer.
+ */
+extern const struct wl_interface xdg_surface_interface;
+#endif
+#ifndef XDG_TOPLEVEL_INTERFACE
+#define XDG_TOPLEVEL_INTERFACE
+/**
+ * @page page_iface_xdg_toplevel xdg_toplevel
+ * @section page_iface_xdg_toplevel_desc Description
+ *
+ * This interface defines an xdg_surface role which allows a surface to,
+ * among other things, set window-like properties such as maximize,
+ * fullscreen, and minimize, set application-specific metadata like title and
+ * id, and well as trigger user interactive operations such as interactive
+ * resize and move.
+ *
+ * Unmapping an xdg_toplevel means that the surface cannot be shown
+ * by the compositor until it is explicitly mapped again.
+ * All active operations (e.g., move, resize) are canceled and all
+ * attributes (e.g. title, state, stacking, ...) are discarded for
+ * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to
+ * the state it had right after xdg_surface.get_toplevel. The client
+ * can re-map the toplevel by perfoming a commit without any buffer
+ * attached, waiting for a configure event and handling it as usual (see
+ * xdg_surface description).
+ *
+ * Attaching a null buffer to a toplevel unmaps the surface.
+ * @section page_iface_xdg_toplevel_api API
+ * See @ref iface_xdg_toplevel.
+ */
+/**
+ * @defgroup iface_xdg_toplevel The xdg_toplevel interface
+ *
+ * This interface defines an xdg_surface role which allows a surface to,
+ * among other things, set window-like properties such as maximize,
+ * fullscreen, and minimize, set application-specific metadata like title and
+ * id, and well as trigger user interactive operations such as interactive
+ * resize and move.
+ *
+ * Unmapping an xdg_toplevel means that the surface cannot be shown
+ * by the compositor until it is explicitly mapped again.
+ * All active operations (e.g., move, resize) are canceled and all
+ * attributes (e.g. title, state, stacking, ...) are discarded for
+ * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to
+ * the state it had right after xdg_surface.get_toplevel. The client
+ * can re-map the toplevel by perfoming a commit without any buffer
+ * attached, waiting for a configure event and handling it as usual (see
+ * xdg_surface description).
+ *
+ * Attaching a null buffer to a toplevel unmaps the surface.
+ */
+extern const struct wl_interface xdg_toplevel_interface;
+#endif
+#ifndef XDG_POPUP_INTERFACE
+#define XDG_POPUP_INTERFACE
+/**
+ * @page page_iface_xdg_popup xdg_popup
+ * @section page_iface_xdg_popup_desc Description
+ *
+ * A popup surface is a short-lived, temporary surface. It can be used to
+ * implement for example menus, popovers, tooltips and other similar user
+ * interface concepts.
+ *
+ * A popup can be made to take an explicit grab. See xdg_popup.grab for
+ * details.
+ *
+ * When the popup is dismissed, a popup_done event will be sent out, and at
+ * the same time the surface will be unmapped. See the xdg_popup.popup_done
+ * event for details.
+ *
+ * Explicitly destroying the xdg_popup object will also dismiss the popup and
+ * unmap the surface. Clients that want to dismiss the popup when another
+ * surface of their own is clicked should dismiss the popup using the destroy
+ * request.
+ *
+ * A newly created xdg_popup will be stacked on top of all previously created
+ * xdg_popup surfaces associated with the same xdg_toplevel.
+ *
+ * The parent of an xdg_popup must be mapped (see the xdg_surface
+ * description) before the xdg_popup itself.
+ *
+ * The client must call wl_surface.commit on the corresponding wl_surface
+ * for the xdg_popup state to take effect.
+ * @section page_iface_xdg_popup_api API
+ * See @ref iface_xdg_popup.
+ */
+/**
+ * @defgroup iface_xdg_popup The xdg_popup interface
+ *
+ * A popup surface is a short-lived, temporary surface. It can be used to
+ * implement for example menus, popovers, tooltips and other similar user
+ * interface concepts.
+ *
+ * A popup can be made to take an explicit grab. See xdg_popup.grab for
+ * details.
+ *
+ * When the popup is dismissed, a popup_done event will be sent out, and at
+ * the same time the surface will be unmapped. See the xdg_popup.popup_done
+ * event for details.
+ *
+ * Explicitly destroying the xdg_popup object will also dismiss the popup and
+ * unmap the surface. Clients that want to dismiss the popup when another
+ * surface of their own is clicked should dismiss the popup using the destroy
+ * request.
+ *
+ * A newly created xdg_popup will be stacked on top of all previously created
+ * xdg_popup surfaces associated with the same xdg_toplevel.
+ *
+ * The parent of an xdg_popup must be mapped (see the xdg_surface
+ * description) before the xdg_popup itself.
+ *
+ * The client must call wl_surface.commit on the corresponding wl_surface
+ * for the xdg_popup state to take effect.
+ */
+extern const struct wl_interface xdg_popup_interface;
+#endif
+
+#ifndef XDG_WM_BASE_ERROR_ENUM
+#define XDG_WM_BASE_ERROR_ENUM
+enum xdg_wm_base_error {
+ /**
+ * given wl_surface has another role
+ */
+ XDG_WM_BASE_ERROR_ROLE = 0,
+ /**
+ * xdg_wm_base was destroyed before children
+ */
+ XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1,
+ /**
+ * the client tried to map or destroy a non-topmost popup
+ */
+ XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2,
+ /**
+ * the client specified an invalid popup parent surface
+ */
+ XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3,
+ /**
+ * the client provided an invalid surface state
+ */
+ XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4,
+ /**
+ * the client provided an invalid positioner
+ */
+ XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5,
+ /**
+ * the client didnāt respond to a ping event in time
+ */
+ XDG_WM_BASE_ERROR_UNRESPONSIVE = 6,
+};
+#endif /* XDG_WM_BASE_ERROR_ENUM */
+
+/**
+ * @ingroup iface_xdg_wm_base
+ * @struct xdg_wm_base_listener
+ */
+struct xdg_wm_base_listener {
+ /**
+ * check if the client is alive
+ *
+ * The ping event asks the client if it's still alive. Pass the
+ * serial specified in the event back to the compositor by sending
+ * a "pong" request back with the specified serial. See
+ * xdg_wm_base.pong.
+ *
+ * Compositors can use this to determine if the client is still
+ * alive. It's unspecified what will happen if the client doesn't
+ * respond to the ping request, or in what timeframe. Clients
+ * should try to respond in a reasonable amount of time. The
+ * āunresponsiveā error is provided for compositors that wish
+ * to disconnect unresponsive clients.
+ *
+ * A compositor is free to ping in any way it wants, but a client
+ * must always respond to any xdg_wm_base object it created.
+ * @param serial pass this to the pong request
+ */
+ void (*ping)(void *data,
+ struct xdg_wm_base *xdg_wm_base,
+ uint32_t serial);
+};
+
+/**
+ * @ingroup iface_xdg_wm_base
+ */
+static inline int
+xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base,
+ const struct xdg_wm_base_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) xdg_wm_base,
+ (void (**)(void)) listener, data);
+}
+
+#define XDG_WM_BASE_DESTROY 0
+#define XDG_WM_BASE_CREATE_POSITIONER 1
+#define XDG_WM_BASE_GET_XDG_SURFACE 2
+#define XDG_WM_BASE_PONG 3
+
+/**
+ * @ingroup iface_xdg_wm_base
+ */
+#define XDG_WM_BASE_PING_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_xdg_wm_base
+ */
+#define XDG_WM_BASE_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_wm_base
+ */
+#define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_wm_base
+ */
+#define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_wm_base
+ */
+#define XDG_WM_BASE_PONG_SINCE_VERSION 1
+
+/** @ingroup iface_xdg_wm_base */
+static inline void
+xdg_wm_base_set_user_data(struct xdg_wm_base *xdg_wm_base, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) xdg_wm_base, user_data);
+}
+
+/** @ingroup iface_xdg_wm_base */
+static inline void *
+xdg_wm_base_get_user_data(struct xdg_wm_base *xdg_wm_base)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) xdg_wm_base);
+}
+
+static inline uint32_t
+xdg_wm_base_get_version(struct xdg_wm_base *xdg_wm_base)
+{
+ return wl_proxy_get_version((struct wl_proxy *) xdg_wm_base);
+}
+
+/**
+ * @ingroup iface_xdg_wm_base
+ *
+ * Destroy this xdg_wm_base object.
+ *
+ * Destroying a bound xdg_wm_base object while there are surfaces
+ * still alive created by this xdg_wm_base object instance is illegal
+ * and will result in a defunct_surfaces error.
+ */
+static inline void
+xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base,
+ XDG_WM_BASE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_xdg_wm_base
+ *
+ * Create a positioner object. A positioner object is used to position
+ * surfaces relative to some parent surface. See the interface description
+ * and xdg_surface.get_popup for details.
+ */
+static inline struct xdg_positioner *
+xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base)
+{
+ struct wl_proxy *id;
+
+ id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base,
+ XDG_WM_BASE_CREATE_POSITIONER, &xdg_positioner_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL);
+
+ return (struct xdg_positioner *) id;
+}
+
+/**
+ * @ingroup iface_xdg_wm_base
+ *
+ * This creates an xdg_surface for the given surface. While xdg_surface
+ * itself is not a role, the corresponding surface may only be assigned
+ * a role extending xdg_surface, such as xdg_toplevel or xdg_popup. It is
+ * illegal to create an xdg_surface for a wl_surface which already has an
+ * assigned role and this will result in a role error.
+ *
+ * This creates an xdg_surface for the given surface. An xdg_surface is
+ * used as basis to define a role to a given surface, such as xdg_toplevel
+ * or xdg_popup. It also manages functionality shared between xdg_surface
+ * based surface roles.
+ *
+ * See the documentation of xdg_surface for more details about what an
+ * xdg_surface is and how it is used.
+ */
+static inline struct xdg_surface *
+xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface *surface)
+{
+ struct wl_proxy *id;
+
+ id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base,
+ XDG_WM_BASE_GET_XDG_SURFACE, &xdg_surface_interface, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, NULL, surface);
+
+ return (struct xdg_surface *) id;
+}
+
+/**
+ * @ingroup iface_xdg_wm_base
+ *
+ * A client must respond to a ping event with a pong request or
+ * the client may be deemed unresponsive. See xdg_wm_base.ping
+ * and xdg_wm_base.error.unresponsive.
+ */
+static inline void
+xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_wm_base,
+ XDG_WM_BASE_PONG, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_wm_base), 0, serial);
+}
+
+#ifndef XDG_POSITIONER_ERROR_ENUM
+#define XDG_POSITIONER_ERROR_ENUM
+enum xdg_positioner_error {
+ /**
+ * invalid input provided
+ */
+ XDG_POSITIONER_ERROR_INVALID_INPUT = 0,
+};
+#endif /* XDG_POSITIONER_ERROR_ENUM */
+
+#ifndef XDG_POSITIONER_ANCHOR_ENUM
+#define XDG_POSITIONER_ANCHOR_ENUM
+enum xdg_positioner_anchor {
+ XDG_POSITIONER_ANCHOR_NONE = 0,
+ XDG_POSITIONER_ANCHOR_TOP = 1,
+ XDG_POSITIONER_ANCHOR_BOTTOM = 2,
+ XDG_POSITIONER_ANCHOR_LEFT = 3,
+ XDG_POSITIONER_ANCHOR_RIGHT = 4,
+ XDG_POSITIONER_ANCHOR_TOP_LEFT = 5,
+ XDG_POSITIONER_ANCHOR_BOTTOM_LEFT = 6,
+ XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7,
+ XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8,
+};
+#endif /* XDG_POSITIONER_ANCHOR_ENUM */
+
+#ifndef XDG_POSITIONER_GRAVITY_ENUM
+#define XDG_POSITIONER_GRAVITY_ENUM
+enum xdg_positioner_gravity {
+ XDG_POSITIONER_GRAVITY_NONE = 0,
+ XDG_POSITIONER_GRAVITY_TOP = 1,
+ XDG_POSITIONER_GRAVITY_BOTTOM = 2,
+ XDG_POSITIONER_GRAVITY_LEFT = 3,
+ XDG_POSITIONER_GRAVITY_RIGHT = 4,
+ XDG_POSITIONER_GRAVITY_TOP_LEFT = 5,
+ XDG_POSITIONER_GRAVITY_BOTTOM_LEFT = 6,
+ XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7,
+ XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8,
+};
+#endif /* XDG_POSITIONER_GRAVITY_ENUM */
+
+#ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM
+#define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM
+/**
+ * @ingroup iface_xdg_positioner
+ * constraint adjustments
+ *
+ * The constraint adjustment value define ways the compositor will adjust
+ * the position of the surface, if the unadjusted position would result
+ * in the surface being partly constrained.
+ *
+ * Whether a surface is considered 'constrained' is left to the compositor
+ * to determine. For example, the surface may be partly outside the
+ * compositor's defined 'work area', thus necessitating the child surface's
+ * position be adjusted until it is entirely inside the work area.
+ *
+ * The adjustments can be combined, according to a defined precedence: 1)
+ * Flip, 2) Slide, 3) Resize.
+ */
+enum xdg_positioner_constraint_adjustment {
+ /**
+ * don't move the child surface when constrained
+ *
+ * Don't alter the surface position even if it is constrained on
+ * some axis, for example partially outside the edge of an output.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0,
+ /**
+ * move along the x axis until unconstrained
+ *
+ * Slide the surface along the x axis until it is no longer
+ * constrained.
+ *
+ * First try to slide towards the direction of the gravity on the x
+ * axis until either the edge in the opposite direction of the
+ * gravity is unconstrained or the edge in the direction of the
+ * gravity is constrained.
+ *
+ * Then try to slide towards the opposite direction of the gravity
+ * on the x axis until either the edge in the direction of the
+ * gravity is unconstrained or the edge in the opposite direction
+ * of the gravity is constrained.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1,
+ /**
+ * move along the y axis until unconstrained
+ *
+ * Slide the surface along the y axis until it is no longer
+ * constrained.
+ *
+ * First try to slide towards the direction of the gravity on the y
+ * axis until either the edge in the opposite direction of the
+ * gravity is unconstrained or the edge in the direction of the
+ * gravity is constrained.
+ *
+ * Then try to slide towards the opposite direction of the gravity
+ * on the y axis until either the edge in the direction of the
+ * gravity is unconstrained or the edge in the opposite direction
+ * of the gravity is constrained.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2,
+ /**
+ * invert the anchor and gravity on the x axis
+ *
+ * Invert the anchor and gravity on the x axis if the surface is
+ * constrained on the x axis. For example, if the left edge of the
+ * surface is constrained, the gravity is 'left' and the anchor is
+ * 'left', change the gravity to 'right' and the anchor to 'right'.
+ *
+ * If the adjusted position also ends up being constrained, the
+ * resulting position of the flip_x adjustment will be the one
+ * before the adjustment.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4,
+ /**
+ * invert the anchor and gravity on the y axis
+ *
+ * Invert the anchor and gravity on the y axis if the surface is
+ * constrained on the y axis. For example, if the bottom edge of
+ * the surface is constrained, the gravity is 'bottom' and the
+ * anchor is 'bottom', change the gravity to 'top' and the anchor
+ * to 'top'.
+ *
+ * The adjusted position is calculated given the original anchor
+ * rectangle and offset, but with the new flipped anchor and
+ * gravity values.
+ *
+ * If the adjusted position also ends up being constrained, the
+ * resulting position of the flip_y adjustment will be the one
+ * before the adjustment.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8,
+ /**
+ * horizontally resize the surface
+ *
+ * Resize the surface horizontally so that it is completely
+ * unconstrained.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16,
+ /**
+ * vertically resize the surface
+ *
+ * Resize the surface vertically so that it is completely
+ * unconstrained.
+ */
+ XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32,
+};
+#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */
+
+#define XDG_POSITIONER_DESTROY 0
+#define XDG_POSITIONER_SET_SIZE 1
+#define XDG_POSITIONER_SET_ANCHOR_RECT 2
+#define XDG_POSITIONER_SET_ANCHOR 3
+#define XDG_POSITIONER_SET_GRAVITY 4
+#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT 5
+#define XDG_POSITIONER_SET_OFFSET 6
+#define XDG_POSITIONER_SET_REACTIVE 7
+#define XDG_POSITIONER_SET_PARENT_SIZE 8
+#define XDG_POSITIONER_SET_PARENT_CONFIGURE 9
+
+
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION 3
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_PARENT_SIZE_SINCE_VERSION 3
+/**
+ * @ingroup iface_xdg_positioner
+ */
+#define XDG_POSITIONER_SET_PARENT_CONFIGURE_SINCE_VERSION 3
+
+/** @ingroup iface_xdg_positioner */
+static inline void
+xdg_positioner_set_user_data(struct xdg_positioner *xdg_positioner, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) xdg_positioner, user_data);
+}
+
+/** @ingroup iface_xdg_positioner */
+static inline void *
+xdg_positioner_get_user_data(struct xdg_positioner *xdg_positioner)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) xdg_positioner);
+}
+
+static inline uint32_t
+xdg_positioner_get_version(struct xdg_positioner *xdg_positioner)
+{
+ return wl_proxy_get_version((struct wl_proxy *) xdg_positioner);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Notify the compositor that the xdg_positioner will no longer be used.
+ */
+static inline void
+xdg_positioner_destroy(struct xdg_positioner *xdg_positioner)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Set the size of the surface that is to be positioned with the positioner
+ * object. The size is in surface-local coordinates and corresponds to the
+ * window geometry. See xdg_surface.set_window_geometry.
+ *
+ * If a zero or negative size is set the invalid_input error is raised.
+ */
+static inline void
+xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, int32_t height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, width, height);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Specify the anchor rectangle within the parent surface that the child
+ * surface will be placed relative to. The rectangle is relative to the
+ * window geometry as defined by xdg_surface.set_window_geometry of the
+ * parent surface.
+ *
+ * When the xdg_positioner object is used to position a child surface, the
+ * anchor rectangle may not extend outside the window geometry of the
+ * positioned child's parent surface.
+ *
+ * If a negative size is set the invalid_input error is raised.
+ */
+static inline void
+xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y, int32_t width, int32_t height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_ANCHOR_RECT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y, width, height);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Defines the anchor point for the anchor rectangle. The specified anchor
+ * is used derive an anchor point that the child surface will be
+ * positioned relative to. If a corner anchor is set (e.g. 'top_left' or
+ * 'bottom_right'), the anchor point will be at the specified corner;
+ * otherwise, the derived anchor point will be centered on the specified
+ * edge, or in the center of the anchor rectangle if no edge is specified.
+ */
+static inline void
+xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_ANCHOR, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, anchor);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Defines in what direction a surface should be positioned, relative to
+ * the anchor point of the parent surface. If a corner gravity is
+ * specified (e.g. 'bottom_right' or 'top_left'), then the child surface
+ * will be placed towards the specified gravity; otherwise, the child
+ * surface will be centered over the anchor point on any axis that had no
+ * gravity specified. If the gravity is not in the āgravityā enum, an
+ * invalid_input error is raised.
+ */
+static inline void
+xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravity)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_GRAVITY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, gravity);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Specify how the window should be positioned if the originally intended
+ * position caused the surface to be constrained, meaning at least
+ * partially outside positioning boundaries set by the compositor. The
+ * adjustment is set by constructing a bitmask describing the adjustment to
+ * be made when the surface is constrained on that axis.
+ *
+ * If no bit for one axis is set, the compositor will assume that the child
+ * surface should not change its position on that axis when constrained.
+ *
+ * If more than one bit for one axis is set, the order of how adjustments
+ * are applied is specified in the corresponding adjustment descriptions.
+ *
+ * The default adjustment is none.
+ */
+static inline void
+xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, uint32_t constraint_adjustment)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, constraint_adjustment);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Specify the surface position offset relative to the position of the
+ * anchor on the anchor rectangle and the anchor on the surface. For
+ * example if the anchor of the anchor rectangle is at (x, y), the surface
+ * has the gravity bottom|right, and the offset is (ox, oy), the calculated
+ * surface position will be (x + ox, y + oy). The offset position of the
+ * surface is the one used for constraint testing. See
+ * set_constraint_adjustment.
+ *
+ * An example use case is placing a popup menu on top of a user interface
+ * element, while aligning the user interface element of the parent surface
+ * with some user interface element placed somewhere in the popup surface.
+ */
+static inline void
+xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_OFFSET, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, x, y);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * When set reactive, the surface is reconstrained if the conditions used
+ * for constraining changed, e.g. the parent window moved.
+ *
+ * If the conditions changed and the popup was reconstrained, an
+ * xdg_popup.configure event is sent with updated geometry, followed by an
+ * xdg_surface.configure event.
+ */
+static inline void
+xdg_positioner_set_reactive(struct xdg_positioner *xdg_positioner)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_REACTIVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Set the parent window geometry the compositor should use when
+ * positioning the popup. The compositor may use this information to
+ * determine the future state the popup should be constrained using. If
+ * this doesn't match the dimension of the parent the popup is eventually
+ * positioned against, the behavior is undefined.
+ *
+ * The arguments are given in the surface-local coordinate space.
+ */
+static inline void
+xdg_positioner_set_parent_size(struct xdg_positioner *xdg_positioner, int32_t parent_width, int32_t parent_height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_PARENT_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, parent_width, parent_height);
+}
+
+/**
+ * @ingroup iface_xdg_positioner
+ *
+ * Set the serial of an xdg_surface.configure event this positioner will be
+ * used in response to. The compositor may use this information together
+ * with set_parent_size to determine what future state the popup should be
+ * constrained using.
+ */
+static inline void
+xdg_positioner_set_parent_configure(struct xdg_positioner *xdg_positioner, uint32_t serial)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_positioner,
+ XDG_POSITIONER_SET_PARENT_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_positioner), 0, serial);
+}
+
+#ifndef XDG_SURFACE_ERROR_ENUM
+#define XDG_SURFACE_ERROR_ENUM
+enum xdg_surface_error {
+ /**
+ * Surface was not fully constructed
+ */
+ XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1,
+ /**
+ * Surface was already constructed
+ */
+ XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2,
+ /**
+ * Attaching a buffer to an unconfigured surface
+ */
+ XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3,
+ /**
+ * Invalid serial number when acking a configure event
+ */
+ XDG_SURFACE_ERROR_INVALID_SERIAL = 4,
+ /**
+ * Width or height was zero or negative
+ */
+ XDG_SURFACE_ERROR_INVALID_SIZE = 5,
+ /**
+ * Surface was destroyed before its role object
+ */
+ XDG_SURFACE_ERROR_DEFUNCT_ROLE_OBJECT = 6,
+};
+#endif /* XDG_SURFACE_ERROR_ENUM */
+
+/**
+ * @ingroup iface_xdg_surface
+ * @struct xdg_surface_listener
+ */
+struct xdg_surface_listener {
+ /**
+ * suggest a surface change
+ *
+ * The configure event marks the end of a configure sequence. A
+ * configure sequence is a set of one or more events configuring
+ * the state of the xdg_surface, including the final
+ * xdg_surface.configure event.
+ *
+ * Where applicable, xdg_surface surface roles will during a
+ * configure sequence extend this event as a latched state sent as
+ * events before the xdg_surface.configure event. Such events
+ * should be considered to make up a set of atomically applied
+ * configuration states, where the xdg_surface.configure commits
+ * the accumulated state.
+ *
+ * Clients should arrange their surface for the new states, and
+ * then send an ack_configure request with the serial sent in this
+ * configure event at some point before committing the new surface.
+ *
+ * If the client receives multiple configure events before it can
+ * respond to one, it is free to discard all but the last event it
+ * received.
+ * @param serial serial of the configure event
+ */
+ void (*configure)(void *data,
+ struct xdg_surface *xdg_surface,
+ uint32_t serial);
+};
+
+/**
+ * @ingroup iface_xdg_surface
+ */
+static inline int
+xdg_surface_add_listener(struct xdg_surface *xdg_surface,
+ const struct xdg_surface_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) xdg_surface,
+ (void (**)(void)) listener, data);
+}
+
+#define XDG_SURFACE_DESTROY 0
+#define XDG_SURFACE_GET_TOPLEVEL 1
+#define XDG_SURFACE_GET_POPUP 2
+#define XDG_SURFACE_SET_WINDOW_GEOMETRY 3
+#define XDG_SURFACE_ACK_CONFIGURE 4
+
+/**
+ * @ingroup iface_xdg_surface
+ */
+#define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1
+
+/**
+ * @ingroup iface_xdg_surface
+ */
+#define XDG_SURFACE_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_surface
+ */
+#define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_surface
+ */
+#define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_surface
+ */
+#define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_surface
+ */
+#define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1
+
+/** @ingroup iface_xdg_surface */
+static inline void
+xdg_surface_set_user_data(struct xdg_surface *xdg_surface, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) xdg_surface, user_data);
+}
+
+/** @ingroup iface_xdg_surface */
+static inline void *
+xdg_surface_get_user_data(struct xdg_surface *xdg_surface)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) xdg_surface);
+}
+
+static inline uint32_t
+xdg_surface_get_version(struct xdg_surface *xdg_surface)
+{
+ return wl_proxy_get_version((struct wl_proxy *) xdg_surface);
+}
+
+/**
+ * @ingroup iface_xdg_surface
+ *
+ * Destroy the xdg_surface object. An xdg_surface must only be destroyed
+ * after its role object has been destroyed, otherwise
+ * a defunct_role_object error is raised.
+ */
+static inline void
+xdg_surface_destroy(struct xdg_surface *xdg_surface)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface,
+ XDG_SURFACE_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_xdg_surface
+ *
+ * This creates an xdg_toplevel object for the given xdg_surface and gives
+ * the associated wl_surface the xdg_toplevel role.
+ *
+ * See the documentation of xdg_toplevel for more details about what an
+ * xdg_toplevel is and how it is used.
+ */
+static inline struct xdg_toplevel *
+xdg_surface_get_toplevel(struct xdg_surface *xdg_surface)
+{
+ struct wl_proxy *id;
+
+ id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface,
+ XDG_SURFACE_GET_TOPLEVEL, &xdg_toplevel_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL);
+
+ return (struct xdg_toplevel *) id;
+}
+
+/**
+ * @ingroup iface_xdg_surface
+ *
+ * This creates an xdg_popup object for the given xdg_surface and gives
+ * the associated wl_surface the xdg_popup role.
+ *
+ * If null is passed as a parent, a parent surface must be specified using
+ * some other protocol, before committing the initial state.
+ *
+ * See the documentation of xdg_popup for more details about what an
+ * xdg_popup is and how it is used.
+ */
+static inline struct xdg_popup *
+xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *parent, struct xdg_positioner *positioner)
+{
+ struct wl_proxy *id;
+
+ id = wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface,
+ XDG_SURFACE_GET_POPUP, &xdg_popup_interface, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, NULL, parent, positioner);
+
+ return (struct xdg_popup *) id;
+}
+
+/**
+ * @ingroup iface_xdg_surface
+ *
+ * The window geometry of a surface is its "visible bounds" from the
+ * user's perspective. Client-side decorations often have invisible
+ * portions like drop-shadows which should be ignored for the
+ * purposes of aligning, placing and constraining windows.
+ *
+ * The window geometry is double buffered, and will be applied at the
+ * time wl_surface.commit of the corresponding wl_surface is called.
+ *
+ * When maintaining a position, the compositor should treat the (x, y)
+ * coordinate of the window geometry as the top left corner of the window.
+ * A client changing the (x, y) window geometry coordinate should in
+ * general not alter the position of the window.
+ *
+ * Once the window geometry of the surface is set, it is not possible to
+ * unset it, and it will remain the same until set_window_geometry is
+ * called again, even if a new subsurface or buffer is attached.
+ *
+ * If never set, the value is the full bounds of the surface,
+ * including any subsurfaces. This updates dynamically on every
+ * commit. This unset is meant for extremely simple clients.
+ *
+ * The arguments are given in the surface-local coordinate space of
+ * the wl_surface associated with this xdg_surface.
+ *
+ * The width and height must be greater than zero. Setting an invalid size
+ * will raise an invalid_size error. When applied, the effective window
+ * geometry will be the set window geometry clamped to the bounding
+ * rectangle of the combined geometry of the surface of the xdg_surface and
+ * the associated subsurfaces.
+ */
+static inline void
+xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int32_t y, int32_t width, int32_t height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface,
+ XDG_SURFACE_SET_WINDOW_GEOMETRY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, x, y, width, height);
+}
+
+/**
+ * @ingroup iface_xdg_surface
+ *
+ * When a configure event is received, if a client commits the
+ * surface in response to the configure event, then the client
+ * must make an ack_configure request sometime before the commit
+ * request, passing along the serial of the configure event.
+ *
+ * For instance, for toplevel surfaces the compositor might use this
+ * information to move a surface to the top left only when the client has
+ * drawn itself for the maximized or fullscreen state.
+ *
+ * If the client receives multiple configure events before it
+ * can respond to one, it only has to ack the last configure event.
+ * Acking a configure event that was never sent raises an invalid_serial
+ * error.
+ *
+ * A client is not required to commit immediately after sending
+ * an ack_configure request - it may even ack_configure several times
+ * before its next surface commit.
+ *
+ * A client may send multiple ack_configure requests before committing, but
+ * only the last request sent before a commit indicates which configure
+ * event the client really is responding to.
+ *
+ * Sending an ack_configure request consumes the serial number sent with
+ * the request, as well as serial numbers sent by all configure events
+ * sent on this xdg_surface prior to the configure event referenced by
+ * the committed serial.
+ *
+ * It is an error to issue multiple ack_configure requests referencing a
+ * serial from the same configure event, or to issue an ack_configure
+ * request referencing a serial from a configure event issued before the
+ * event identified by the last ack_configure request for the same
+ * xdg_surface. Doing so will raise an invalid_serial error.
+ */
+static inline void
+xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_surface,
+ XDG_SURFACE_ACK_CONFIGURE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_surface), 0, serial);
+}
+
+#ifndef XDG_TOPLEVEL_ERROR_ENUM
+#define XDG_TOPLEVEL_ERROR_ENUM
+enum xdg_toplevel_error {
+ /**
+ * provided value is not a valid variant of the resize_edge enum
+ */
+ XDG_TOPLEVEL_ERROR_INVALID_RESIZE_EDGE = 0,
+ /**
+ * invalid parent toplevel
+ */
+ XDG_TOPLEVEL_ERROR_INVALID_PARENT = 1,
+ /**
+ * client provided an invalid min or max size
+ */
+ XDG_TOPLEVEL_ERROR_INVALID_SIZE = 2,
+};
+#endif /* XDG_TOPLEVEL_ERROR_ENUM */
+
+#ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM
+#define XDG_TOPLEVEL_RESIZE_EDGE_ENUM
+/**
+ * @ingroup iface_xdg_toplevel
+ * edge values for resizing
+ *
+ * These values are used to indicate which edge of a surface
+ * is being dragged in a resize operation.
+ */
+enum xdg_toplevel_resize_edge {
+ XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0,
+ XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1,
+ XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM = 2,
+ XDG_TOPLEVEL_RESIZE_EDGE_LEFT = 4,
+ XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT = 5,
+ XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT = 6,
+ XDG_TOPLEVEL_RESIZE_EDGE_RIGHT = 8,
+ XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9,
+ XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10,
+};
+#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */
+
+#ifndef XDG_TOPLEVEL_STATE_ENUM
+#define XDG_TOPLEVEL_STATE_ENUM
+/**
+ * @ingroup iface_xdg_toplevel
+ * types of state on the surface
+ *
+ * The different state values used on the surface. This is designed for
+ * state values like maximized, fullscreen. It is paired with the
+ * configure event to ensure that both the client and the compositor
+ * setting the state can be synchronized.
+ *
+ * States set in this way are double-buffered. They will get applied on
+ * the next commit.
+ */
+enum xdg_toplevel_state {
+ /**
+ * the surface is maximized
+ * the surface is maximized
+ *
+ * The surface is maximized. The window geometry specified in the
+ * configure event must be obeyed by the client.
+ *
+ * The client should draw without shadow or other decoration
+ * outside of the window geometry.
+ */
+ XDG_TOPLEVEL_STATE_MAXIMIZED = 1,
+ /**
+ * the surface is fullscreen
+ * the surface is fullscreen
+ *
+ * The surface is fullscreen. The window geometry specified in
+ * the configure event is a maximum; the client cannot resize
+ * beyond it. For a surface to cover the whole fullscreened area,
+ * the geometry dimensions must be obeyed by the client. For more
+ * details, see xdg_toplevel.set_fullscreen.
+ */
+ XDG_TOPLEVEL_STATE_FULLSCREEN = 2,
+ /**
+ * the surface is being resized
+ * the surface is being resized
+ *
+ * The surface is being resized. The window geometry specified in
+ * the configure event is a maximum; the client cannot resize
+ * beyond it. Clients that have aspect ratio or cell sizing
+ * configuration can use a smaller size, however.
+ */
+ XDG_TOPLEVEL_STATE_RESIZING = 3,
+ /**
+ * the surface is now activated
+ * the surface is now activated
+ *
+ * Client window decorations should be painted as if the window
+ * is active. Do not assume this means that the window actually has
+ * keyboard or pointer focus.
+ */
+ XDG_TOPLEVEL_STATE_ACTIVATED = 4,
+ /**
+ * the surfaceās left edge is tiled
+ *
+ * The window is currently in a tiled layout and the left edge is
+ * considered to be adjacent to another part of the tiling grid.
+ * @since 2
+ */
+ XDG_TOPLEVEL_STATE_TILED_LEFT = 5,
+ /**
+ * the surfaceās right edge is tiled
+ *
+ * The window is currently in a tiled layout and the right edge
+ * is considered to be adjacent to another part of the tiling grid.
+ * @since 2
+ */
+ XDG_TOPLEVEL_STATE_TILED_RIGHT = 6,
+ /**
+ * the surfaceās top edge is tiled
+ *
+ * The window is currently in a tiled layout and the top edge is
+ * considered to be adjacent to another part of the tiling grid.
+ * @since 2
+ */
+ XDG_TOPLEVEL_STATE_TILED_TOP = 7,
+ /**
+ * the surfaceās bottom edge is tiled
+ *
+ * The window is currently in a tiled layout and the bottom edge
+ * is considered to be adjacent to another part of the tiling grid.
+ * @since 2
+ */
+ XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8,
+};
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2
+#endif /* XDG_TOPLEVEL_STATE_ENUM */
+
+#ifndef XDG_TOPLEVEL_WM_CAPABILITIES_ENUM
+#define XDG_TOPLEVEL_WM_CAPABILITIES_ENUM
+enum xdg_toplevel_wm_capabilities {
+ /**
+ * show_window_menu is available
+ */
+ XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU = 1,
+ /**
+ * set_maximized and unset_maximized are available
+ */
+ XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE = 2,
+ /**
+ * set_fullscreen and unset_fullscreen are available
+ */
+ XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN = 3,
+ /**
+ * set_minimized is available
+ */
+ XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE = 4,
+};
+#endif /* XDG_TOPLEVEL_WM_CAPABILITIES_ENUM */
+
+/**
+ * @ingroup iface_xdg_toplevel
+ * @struct xdg_toplevel_listener
+ */
+struct xdg_toplevel_listener {
+ /**
+ * suggest a surface change
+ *
+ * This configure event asks the client to resize its toplevel
+ * surface or to change its state. The configured state should not
+ * be applied immediately. See xdg_surface.configure for details.
+ *
+ * The width and height arguments specify a hint to the window
+ * about how its surface should be resized in window geometry
+ * coordinates. See set_window_geometry.
+ *
+ * If the width or height arguments are zero, it means the client
+ * should decide its own window dimension. This may happen when the
+ * compositor needs to configure the state of the surface but
+ * doesn't have any information about any previous or expected
+ * dimension.
+ *
+ * The states listed in the event specify how the width/height
+ * arguments should be interpreted, and possibly how it should be
+ * drawn.
+ *
+ * Clients must send an ack_configure in response to this event.
+ * See xdg_surface.configure and xdg_surface.ack_configure for
+ * details.
+ */
+ void (*configure)(void *data,
+ struct xdg_toplevel *xdg_toplevel,
+ int32_t width,
+ int32_t height,
+ struct wl_array *states);
+ /**
+ * surface wants to be closed
+ *
+ * The close event is sent by the compositor when the user wants
+ * the surface to be closed. This should be equivalent to the user
+ * clicking the close button in client-side decorations, if your
+ * application has any.
+ *
+ * This is only a request that the user intends to close the
+ * window. The client may choose to ignore this request, or show a
+ * dialog to ask the user to save their data, etc.
+ */
+ void (*close)(void *data,
+ struct xdg_toplevel *xdg_toplevel);
+ /**
+ * recommended window geometry bounds
+ *
+ * The configure_bounds event may be sent prior to a
+ * xdg_toplevel.configure event to communicate the bounds a window
+ * geometry size is recommended to constrain to.
+ *
+ * The passed width and height are in surface coordinate space. If
+ * width and height are 0, it means bounds is unknown and
+ * equivalent to as if no configure_bounds event was ever sent for
+ * this surface.
+ *
+ * The bounds can for example correspond to the size of a monitor
+ * excluding any panels or other shell components, so that a
+ * surface isn't created in a way that it cannot fit.
+ *
+ * The bounds may change at any point, and in such a case, a new
+ * xdg_toplevel.configure_bounds will be sent, followed by
+ * xdg_toplevel.configure and xdg_surface.configure.
+ * @since 4
+ */
+ void (*configure_bounds)(void *data,
+ struct xdg_toplevel *xdg_toplevel,
+ int32_t width,
+ int32_t height);
+ /**
+ * compositor capabilities
+ *
+ * This event advertises the capabilities supported by the
+ * compositor. If a capability isn't supported, clients should hide
+ * or disable the UI elements that expose this functionality. For
+ * instance, if the compositor doesn't advertise support for
+ * minimized toplevels, a button triggering the set_minimized
+ * request should not be displayed.
+ *
+ * The compositor will ignore requests it doesn't support. For
+ * instance, a compositor which doesn't advertise support for
+ * minimized will ignore set_minimized requests.
+ *
+ * Compositors must send this event once before the first
+ * xdg_surface.configure event. When the capabilities change,
+ * compositors must send this event again and then send an
+ * xdg_surface.configure event.
+ *
+ * The configured state should not be applied immediately. See
+ * xdg_surface.configure for details.
+ *
+ * The capabilities are sent as an array of 32-bit unsigned
+ * integers in native endianness.
+ * @param capabilities array of 32-bit capabilities
+ * @since 5
+ */
+ void (*wm_capabilities)(void *data,
+ struct xdg_toplevel *xdg_toplevel,
+ struct wl_array *capabilities);
+};
+
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+static inline int
+xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel,
+ const struct xdg_toplevel_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) xdg_toplevel,
+ (void (**)(void)) listener, data);
+}
+
+#define XDG_TOPLEVEL_DESTROY 0
+#define XDG_TOPLEVEL_SET_PARENT 1
+#define XDG_TOPLEVEL_SET_TITLE 2
+#define XDG_TOPLEVEL_SET_APP_ID 3
+#define XDG_TOPLEVEL_SHOW_WINDOW_MENU 4
+#define XDG_TOPLEVEL_MOVE 5
+#define XDG_TOPLEVEL_RESIZE 6
+#define XDG_TOPLEVEL_SET_MAX_SIZE 7
+#define XDG_TOPLEVEL_SET_MIN_SIZE 8
+#define XDG_TOPLEVEL_SET_MAXIMIZED 9
+#define XDG_TOPLEVEL_UNSET_MAXIMIZED 10
+#define XDG_TOPLEVEL_SET_FULLSCREEN 11
+#define XDG_TOPLEVEL_UNSET_FULLSCREEN 12
+#define XDG_TOPLEVEL_SET_MINIMIZED 13
+
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION 4
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION 5
+
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_toplevel
+ */
+#define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1
+
+/** @ingroup iface_xdg_toplevel */
+static inline void
+xdg_toplevel_set_user_data(struct xdg_toplevel *xdg_toplevel, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) xdg_toplevel, user_data);
+}
+
+/** @ingroup iface_xdg_toplevel */
+static inline void *
+xdg_toplevel_get_user_data(struct xdg_toplevel *xdg_toplevel)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) xdg_toplevel);
+}
+
+static inline uint32_t
+xdg_toplevel_get_version(struct xdg_toplevel *xdg_toplevel)
+{
+ return wl_proxy_get_version((struct wl_proxy *) xdg_toplevel);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * This request destroys the role surface and unmaps the surface;
+ * see "Unmapping" behavior in interface section for details.
+ */
+static inline void
+xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Set the "parent" of this surface. This surface should be stacked
+ * above the parent surface and all other ancestor surfaces.
+ *
+ * Parent surfaces should be set on dialogs, toolboxes, or other
+ * "auxiliary" surfaces, so that the parent is raised when the dialog
+ * is raised.
+ *
+ * Setting a null parent for a child surface unsets its parent. Setting
+ * a null parent for a surface which currently has no parent is a no-op.
+ *
+ * Only mapped surfaces can have child surfaces. Setting a parent which
+ * is not mapped is equivalent to setting a null parent. If a surface
+ * becomes unmapped, its children's parent is set to the parent of
+ * the now-unmapped surface. If the now-unmapped surface has no parent,
+ * its children's parent is unset. If the now-unmapped surface becomes
+ * mapped again, its parent-child relationship is not restored.
+ *
+ * The parent toplevel must not be one of the child toplevel's
+ * descendants, and the parent must be different from the child toplevel,
+ * otherwise the invalid_parent protocol error is raised.
+ */
+static inline void
+xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel *parent)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_PARENT, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, parent);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Set a short title for the surface.
+ *
+ * This string may be used to identify the surface in a task bar,
+ * window list, or other user interface elements provided by the
+ * compositor.
+ *
+ * The string must be encoded in UTF-8.
+ */
+static inline void
+xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_TITLE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, title);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Set an application identifier for the surface.
+ *
+ * The app ID identifies the general class of applications to which
+ * the surface belongs. The compositor can use this to group multiple
+ * surfaces together, or to determine how to launch a new application.
+ *
+ * For D-Bus activatable applications, the app ID is used as the D-Bus
+ * service name.
+ *
+ * The compositor shell will try to group application surfaces together
+ * by their app ID. As a best practice, it is suggested to select app
+ * ID's that match the basename of the application's .desktop file.
+ * For example, "org.freedesktop.FooViewer" where the .desktop file is
+ * "org.freedesktop.FooViewer.desktop".
+ *
+ * Like other properties, a set_app_id request can be sent after the
+ * xdg_toplevel has been mapped to update the property.
+ *
+ * See the desktop-entry specification [0] for more details on
+ * application identifiers and how they relate to well-known D-Bus
+ * names and .desktop files.
+ *
+ * [0] https://standards.freedesktop.org/desktop-entry-spec/
+ */
+static inline void
+xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_APP_ID, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, app_id);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Clients implementing client-side decorations might want to show
+ * a context menu when right-clicking on the decorations, giving the
+ * user a menu that they can use to maximize or minimize the window.
+ *
+ * This request asks the compositor to pop up such a window menu at
+ * the given position, relative to the local surface coordinates of
+ * the parent surface. There are no guarantees as to what menu items
+ * the window menu contains, or even if a window menu will be drawn
+ * at all.
+ *
+ * This request must be used in response to some sort of user action
+ * like a button press, key press, or touch down event.
+ */
+static inline void
+xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, int32_t x, int32_t y)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SHOW_WINDOW_MENU, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, x, y);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Start an interactive, user-driven move of the surface.
+ *
+ * This request must be used in response to some sort of user action
+ * like a button press, key press, or touch down event. The passed
+ * serial is used to determine the type of interactive move (touch,
+ * pointer, etc).
+ *
+ * The server may ignore move requests depending on the state of
+ * the surface (e.g. fullscreen or maximized), or if the passed serial
+ * is no longer valid.
+ *
+ * If triggered, the surface will lose the focus of the device
+ * (wl_pointer, wl_touch, etc) used for the move. It is up to the
+ * compositor to visually indicate that the move is taking place, such as
+ * updating a pointer cursor, during the move. There is no guarantee
+ * that the device focus will return when the move is completed.
+ */
+static inline void
+xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_MOVE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Start a user-driven, interactive resize of the surface.
+ *
+ * This request must be used in response to some sort of user action
+ * like a button press, key press, or touch down event. The passed
+ * serial is used to determine the type of interactive resize (touch,
+ * pointer, etc).
+ *
+ * The server may ignore resize requests depending on the state of
+ * the surface (e.g. fullscreen or maximized).
+ *
+ * If triggered, the client will receive configure events with the
+ * "resize" state enum value and the expected sizes. See the "resize"
+ * enum value for more details about what is required. The client
+ * must also acknowledge configure events using "ack_configure". After
+ * the resize is completed, the client will receive another "configure"
+ * event without the resize state.
+ *
+ * If triggered, the surface also will lose the focus of the device
+ * (wl_pointer, wl_touch, etc) used for the resize. It is up to the
+ * compositor to visually indicate that the resize is taking place,
+ * such as updating a pointer cursor, during the resize. There is no
+ * guarantee that the device focus will return when the resize is
+ * completed.
+ *
+ * The edges parameter specifies how the surface should be resized, and
+ * is one of the values of the resize_edge enum. Values not matching
+ * a variant of the enum will cause a protocol error. The compositor
+ * may use this information to update the surface position for example
+ * when dragging the top left corner. The compositor may also use
+ * this information to adapt its behavior, e.g. choose an appropriate
+ * cursor image.
+ */
+static inline void
+xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, uint32_t edges)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_RESIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, seat, serial, edges);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Set a maximum size for the window.
+ *
+ * The client can specify a maximum size so that the compositor does
+ * not try to configure the window beyond this size.
+ *
+ * The width and height arguments are in window geometry coordinates.
+ * See xdg_surface.set_window_geometry.
+ *
+ * Values set in this way are double-buffered. They will get applied
+ * on the next commit.
+ *
+ * The compositor can use this information to allow or disallow
+ * different states like maximize or fullscreen and draw accurate
+ * animations.
+ *
+ * Similarly, a tiling window manager may use this information to
+ * place and resize client windows in a more effective way.
+ *
+ * The client should not rely on the compositor to obey the maximum
+ * size. The compositor may decide to ignore the values set by the
+ * client and request a larger size.
+ *
+ * If never set, or a value of zero in the request, means that the
+ * client has no expected maximum size in the given dimension.
+ * As a result, a client wishing to reset the maximum size
+ * to an unspecified state can use zero for width and height in the
+ * request.
+ *
+ * Requesting a maximum size to be smaller than the minimum size of
+ * a surface is illegal and will result in an invalid_size error.
+ *
+ * The width and height must be greater than or equal to zero. Using
+ * strictly negative values for width or height will result in a
+ * invalid_size error.
+ */
+static inline void
+xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_MAX_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Set a minimum size for the window.
+ *
+ * The client can specify a minimum size so that the compositor does
+ * not try to configure the window below this size.
+ *
+ * The width and height arguments are in window geometry coordinates.
+ * See xdg_surface.set_window_geometry.
+ *
+ * Values set in this way are double-buffered. They will get applied
+ * on the next commit.
+ *
+ * The compositor can use this information to allow or disallow
+ * different states like maximize or fullscreen and draw accurate
+ * animations.
+ *
+ * Similarly, a tiling window manager may use this information to
+ * place and resize client windows in a more effective way.
+ *
+ * The client should not rely on the compositor to obey the minimum
+ * size. The compositor may decide to ignore the values set by the
+ * client and request a smaller size.
+ *
+ * If never set, or a value of zero in the request, means that the
+ * client has no expected minimum size in the given dimension.
+ * As a result, a client wishing to reset the minimum size
+ * to an unspecified state can use zero for width and height in the
+ * request.
+ *
+ * Requesting a minimum size to be larger than the maximum size of
+ * a surface is illegal and will result in an invalid_size error.
+ *
+ * The width and height must be greater than or equal to zero. Using
+ * strictly negative values for width and height will result in a
+ * invalid_size error.
+ */
+static inline void
+xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_MIN_SIZE, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, width, height);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Maximize the surface.
+ *
+ * After requesting that the surface should be maximized, the compositor
+ * will respond by emitting a configure event. Whether this configure
+ * actually sets the window maximized is subject to compositor policies.
+ * The client must then update its content, drawing in the configured
+ * state. The client must also acknowledge the configure when committing
+ * the new content (see ack_configure).
+ *
+ * It is up to the compositor to decide how and where to maximize the
+ * surface, for example which output and what region of the screen should
+ * be used.
+ *
+ * If the surface was already maximized, the compositor will still emit
+ * a configure event with the "maximized" state.
+ *
+ * If the surface is in a fullscreen state, this request has no direct
+ * effect. It may alter the state the surface is returned to when
+ * unmaximized unless overridden by the compositor.
+ */
+static inline void
+xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Unmaximize the surface.
+ *
+ * After requesting that the surface should be unmaximized, the compositor
+ * will respond by emitting a configure event. Whether this actually
+ * un-maximizes the window is subject to compositor policies.
+ * If available and applicable, the compositor will include the window
+ * geometry dimensions the window had prior to being maximized in the
+ * configure event. The client must then update its content, drawing it in
+ * the configured state. The client must also acknowledge the configure
+ * when committing the new content (see ack_configure).
+ *
+ * It is up to the compositor to position the surface after it was
+ * unmaximized; usually the position the surface had before maximizing, if
+ * applicable.
+ *
+ * If the surface was already not maximized, the compositor will still
+ * emit a configure event without the "maximized" state.
+ *
+ * If the surface is in a fullscreen state, this request has no direct
+ * effect. It may alter the state the surface is returned to when
+ * unmaximized unless overridden by the compositor.
+ */
+static inline void
+xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_UNSET_MAXIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Make the surface fullscreen.
+ *
+ * After requesting that the surface should be fullscreened, the
+ * compositor will respond by emitting a configure event. Whether the
+ * client is actually put into a fullscreen state is subject to compositor
+ * policies. The client must also acknowledge the configure when
+ * committing the new content (see ack_configure).
+ *
+ * The output passed by the request indicates the client's preference as
+ * to which display it should be set fullscreen on. If this value is NULL,
+ * it's up to the compositor to choose which display will be used to map
+ * this surface.
+ *
+ * If the surface doesn't cover the whole output, the compositor will
+ * position the surface in the center of the output and compensate with
+ * with border fill covering the rest of the output. The content of the
+ * border fill is undefined, but should be assumed to be in some way that
+ * attempts to blend into the surrounding area (e.g. solid black).
+ *
+ * If the fullscreened surface is not opaque, the compositor must make
+ * sure that other screen content not part of the same surface tree (made
+ * up of subsurfaces, popups or similarly coupled surfaces) are not
+ * visible below the fullscreened surface.
+ */
+static inline void
+xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output *output)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0, output);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Make the surface no longer fullscreen.
+ *
+ * After requesting that the surface should be unfullscreened, the
+ * compositor will respond by emitting a configure event.
+ * Whether this actually removes the fullscreen state of the client is
+ * subject to compositor policies.
+ *
+ * Making a surface unfullscreen sets states for the surface based on the following:
+ * * the state(s) it may have had before becoming fullscreen
+ * * any state(s) decided by the compositor
+ * * any state(s) requested by the client while the surface was fullscreen
+ *
+ * The compositor may include the previous window geometry dimensions in
+ * the configure event, if applicable.
+ *
+ * The client must also acknowledge the configure when committing the new
+ * content (see ack_configure).
+ */
+static inline void
+xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_UNSET_FULLSCREEN, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0);
+}
+
+/**
+ * @ingroup iface_xdg_toplevel
+ *
+ * Request that the compositor minimize your surface. There is no
+ * way to know if the surface is currently minimized, nor is there
+ * any way to unset minimization on this surface.
+ *
+ * If you are looking to throttle redrawing when minimized, please
+ * instead use the wl_surface.frame event for this, as this will
+ * also work with live previews on windows in Alt-Tab, Expose or
+ * similar compositor features.
+ */
+static inline void
+xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_toplevel,
+ XDG_TOPLEVEL_SET_MINIMIZED, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_toplevel), 0);
+}
+
+#ifndef XDG_POPUP_ERROR_ENUM
+#define XDG_POPUP_ERROR_ENUM
+enum xdg_popup_error {
+ /**
+ * tried to grab after being mapped
+ */
+ XDG_POPUP_ERROR_INVALID_GRAB = 0,
+};
+#endif /* XDG_POPUP_ERROR_ENUM */
+
+/**
+ * @ingroup iface_xdg_popup
+ * @struct xdg_popup_listener
+ */
+struct xdg_popup_listener {
+ /**
+ * configure the popup surface
+ *
+ * This event asks the popup surface to configure itself given
+ * the configuration. The configured state should not be applied
+ * immediately. See xdg_surface.configure for details.
+ *
+ * The x and y arguments represent the position the popup was
+ * placed at given the xdg_positioner rule, relative to the upper
+ * left corner of the window geometry of the parent surface.
+ *
+ * For version 2 or older, the configure event for an xdg_popup is
+ * only ever sent once for the initial configuration. Starting with
+ * version 3, it may be sent again if the popup is setup with an
+ * xdg_positioner with set_reactive requested, or in response to
+ * xdg_popup.reposition requests.
+ * @param x x position relative to parent surface window geometry
+ * @param y y position relative to parent surface window geometry
+ * @param width window geometry width
+ * @param height window geometry height
+ */
+ void (*configure)(void *data,
+ struct xdg_popup *xdg_popup,
+ int32_t x,
+ int32_t y,
+ int32_t width,
+ int32_t height);
+ /**
+ * popup interaction is done
+ *
+ * The popup_done event is sent out when a popup is dismissed by
+ * the compositor. The client should destroy the xdg_popup object
+ * at this point.
+ */
+ void (*popup_done)(void *data,
+ struct xdg_popup *xdg_popup);
+ /**
+ * signal the completion of a repositioned request
+ *
+ * The repositioned event is sent as part of a popup
+ * configuration sequence, together with xdg_popup.configure and
+ * lastly xdg_surface.configure to notify the completion of a
+ * reposition request.
+ *
+ * The repositioned event is to notify about the completion of a
+ * xdg_popup.reposition request. The token argument is the token
+ * passed in the xdg_popup.reposition request.
+ *
+ * Immediately after this event is emitted, xdg_popup.configure and
+ * xdg_surface.configure will be sent with the updated size and
+ * position, as well as a new configure serial.
+ *
+ * The client should optionally update the content of the popup,
+ * but must acknowledge the new popup configuration for the new
+ * position to take effect. See xdg_surface.ack_configure for
+ * details.
+ * @param token reposition request token
+ * @since 3
+ */
+ void (*repositioned)(void *data,
+ struct xdg_popup *xdg_popup,
+ uint32_t token);
+};
+
+/**
+ * @ingroup iface_xdg_popup
+ */
+static inline int
+xdg_popup_add_listener(struct xdg_popup *xdg_popup,
+ const struct xdg_popup_listener *listener, void *data)
+{
+ return wl_proxy_add_listener((struct wl_proxy *) xdg_popup,
+ (void (**)(void)) listener, data);
+}
+
+#define XDG_POPUP_DESTROY 0
+#define XDG_POPUP_GRAB 1
+#define XDG_POPUP_REPOSITION 2
+
+/**
+ * @ingroup iface_xdg_popup
+ */
+#define XDG_POPUP_CONFIGURE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_popup
+ */
+#define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_popup
+ */
+#define XDG_POPUP_REPOSITIONED_SINCE_VERSION 3
+
+/**
+ * @ingroup iface_xdg_popup
+ */
+#define XDG_POPUP_DESTROY_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_popup
+ */
+#define XDG_POPUP_GRAB_SINCE_VERSION 1
+/**
+ * @ingroup iface_xdg_popup
+ */
+#define XDG_POPUP_REPOSITION_SINCE_VERSION 3
+
+/** @ingroup iface_xdg_popup */
+static inline void
+xdg_popup_set_user_data(struct xdg_popup *xdg_popup, void *user_data)
+{
+ wl_proxy_set_user_data((struct wl_proxy *) xdg_popup, user_data);
+}
+
+/** @ingroup iface_xdg_popup */
+static inline void *
+xdg_popup_get_user_data(struct xdg_popup *xdg_popup)
+{
+ return wl_proxy_get_user_data((struct wl_proxy *) xdg_popup);
+}
+
+static inline uint32_t
+xdg_popup_get_version(struct xdg_popup *xdg_popup)
+{
+ return wl_proxy_get_version((struct wl_proxy *) xdg_popup);
+}
+
+/**
+ * @ingroup iface_xdg_popup
+ *
+ * This destroys the popup. Explicitly destroying the xdg_popup
+ * object will also dismiss the popup, and unmap the surface.
+ *
+ * If this xdg_popup is not the "topmost" popup, a protocol error
+ * will be sent.
+ */
+static inline void
+xdg_popup_destroy(struct xdg_popup *xdg_popup)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup,
+ XDG_POPUP_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), WL_MARSHAL_FLAG_DESTROY);
+}
+
+/**
+ * @ingroup iface_xdg_popup
+ *
+ * This request makes the created popup take an explicit grab. An explicit
+ * grab will be dismissed when the user dismisses the popup, or when the
+ * client destroys the xdg_popup. This can be done by the user clicking
+ * outside the surface, using the keyboard, or even locking the screen
+ * through closing the lid or a timeout.
+ *
+ * If the compositor denies the grab, the popup will be immediately
+ * dismissed.
+ *
+ * This request must be used in response to some sort of user action like a
+ * button press, key press, or touch down event. The serial number of the
+ * event should be passed as 'serial'.
+ *
+ * The parent of a grabbing popup must either be an xdg_toplevel surface or
+ * another xdg_popup with an explicit grab. If the parent is another
+ * xdg_popup it means that the popups are nested, with this popup now being
+ * the topmost popup.
+ *
+ * Nested popups must be destroyed in the reverse order they were created
+ * in, e.g. the only popup you are allowed to destroy at all times is the
+ * topmost one.
+ *
+ * When compositors choose to dismiss a popup, they may dismiss every
+ * nested grabbing popup as well. When a compositor dismisses popups, it
+ * will follow the same dismissing order as required from the client.
+ *
+ * If the topmost grabbing popup is destroyed, the grab will be returned to
+ * the parent of the popup, if that parent previously had an explicit grab.
+ *
+ * If the parent is a grabbing popup which has already been dismissed, this
+ * popup will be immediately dismissed. If the parent is a popup that did
+ * not take an explicit grab, an error will be raised.
+ *
+ * During a popup grab, the client owning the grab will receive pointer
+ * and touch events for all their surfaces as normal (similar to an
+ * "owner-events" grab in X11 parlance), while the top most grabbing popup
+ * will always have keyboard focus.
+ */
+static inline void
+xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t serial)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup,
+ XDG_POPUP_GRAB, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, seat, serial);
+}
+
+/**
+ * @ingroup iface_xdg_popup
+ *
+ * Reposition an already-mapped popup. The popup will be placed given the
+ * details in the passed xdg_positioner object, and a
+ * xdg_popup.repositioned followed by xdg_popup.configure and
+ * xdg_surface.configure will be emitted in response. Any parameters set
+ * by the previous positioner will be discarded.
+ *
+ * The passed token will be sent in the corresponding
+ * xdg_popup.repositioned event. The new popup position will not take
+ * effect until the corresponding configure event is acknowledged by the
+ * client. See xdg_popup.repositioned for details. The token itself is
+ * opaque, and has no other special meaning.
+ *
+ * If multiple reposition requests are sent, the compositor may skip all
+ * but the last one.
+ *
+ * If the popup is repositioned in response to a configure event for its
+ * parent, the client should send an xdg_positioner.set_parent_configure
+ * and possibly an xdg_positioner.set_parent_size request to allow the
+ * compositor to properly constrain the popup.
+ *
+ * If the popup is repositioned together with a parent that is being
+ * resized, but not in response to a configure event, the client should
+ * send an xdg_positioner.set_parent_size request.
+ */
+static inline void
+xdg_popup_reposition(struct xdg_popup *xdg_popup, struct xdg_positioner *positioner, uint32_t token)
+{
+ wl_proxy_marshal_flags((struct wl_proxy *) xdg_popup,
+ XDG_POPUP_REPOSITION, NULL, wl_proxy_get_version((struct wl_proxy *) xdg_popup), 0, positioner, token);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/gio/giold/app/internal/wm/window.go b/gio/giold/app/internal/wm/window.go
new file mode 100644
index 0000000..82e3c38
--- /dev/null
+++ b/gio/giold/app/internal/wm/window.go
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// package wm implements platform specific windows
+// and GPU contexts.
+package wm
+
+import (
+ "errors"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/unit"
+)
+
+type Size struct {
+ Width unit.Value
+ Height unit.Value
+}
+
+type Options struct {
+ Size *Size
+ MinSize *Size
+ MaxSize *Size
+ Title *string
+ WindowMode *WindowMode
+}
+
+type WindowMode uint8
+
+const (
+ Windowed WindowMode = iota
+ Fullscreen
+)
+
+type FrameEvent struct {
+ system.FrameEvent
+
+ Sync bool
+}
+
+type Callbacks interface {
+ SetDriver(d Driver)
+ Event(e event.Event)
+}
+
+type Context interface {
+ API() gpu.API
+ Present() error
+ MakeCurrent() error
+ Release()
+ Lock()
+ Unlock()
+}
+
+// ErrDeviceLost is returned from Context.Present when
+// the underlying GPU device is gone and should be
+// recreated.
+var ErrDeviceLost = errors.New("GPU device lost")
+
+// Driver is the interface for the platform implementation
+// of a window.
+type Driver interface {
+ // SetAnimating sets the animation flag. When the window is animating,
+ // FrameEvents are delivered as fast as the display can handle them.
+ SetAnimating(anim bool)
+ // ShowTextInput updates the virtual keyboard state.
+ ShowTextInput(show bool)
+ NewContext() (Context, error)
+
+ // ReadClipboard requests the clipboard content.
+ ReadClipboard()
+ // WriteClipboard requests a clipboard write.
+ WriteClipboard(s string)
+
+ // Option processes option changes.
+ Option(opts *Options)
+
+ // SetCursor updates the current cursor to name.
+ SetCursor(name pointer.CursorName)
+
+ // Close the window.
+ Close()
+}
+
+type windowRendezvous struct {
+ in chan windowAndOptions
+ out chan windowAndOptions
+ errs chan error
+}
+
+type windowAndOptions struct {
+ window Callbacks
+ opts *Options
+}
+
+func newWindowRendezvous() *windowRendezvous {
+ wr := &windowRendezvous{
+ in: make(chan windowAndOptions),
+ out: make(chan windowAndOptions),
+ errs: make(chan error),
+ }
+ go func() {
+ var main windowAndOptions
+ var out chan windowAndOptions
+ for {
+ select {
+ case w := <-wr.in:
+ var err error
+ if main.window != nil {
+ err = errors.New("multiple windows are not supported")
+ }
+ wr.errs <- err
+ main = w
+ out = wr.out
+ case out <- main:
+ }
+ }
+ }()
+ return wr
+}
diff --git a/gio/giold/app/internal/xkb/xkb_unix.go b/gio/giold/app/internal/xkb/xkb_unix.go
new file mode 100644
index 0000000..be72a58
--- /dev/null
+++ b/gio/giold/app/internal/xkb/xkb_unix.go
@@ -0,0 +1,322 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build (linux && !android) || freebsd || openbsd
+// +build linux,!android freebsd openbsd
+
+// Package xkb implements a Go interface for the X Keyboard Extension library.
+package xkb
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "syscall"
+ "unicode"
+ "unicode/utf8"
+ "unsafe"
+
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+)
+
+/*
+#cgo linux pkg-config: xkbcommon
+#cgo freebsd openbsd CFLAGS: -I/usr/local/include
+#cgo freebsd openbsd LDFLAGS: -L/usr/local/lib -lxkbcommon
+
+#include
+#include
+#include
+*/
+import "C"
+
+type Context struct {
+ Ctx *C.struct_xkb_context
+ keyMap *C.struct_xkb_keymap
+ state *C.struct_xkb_state
+ compTable *C.struct_xkb_compose_table
+ compState *C.struct_xkb_compose_state
+ utf8Buf []byte
+}
+
+var (
+ _XKB_MOD_NAME_CTRL = []byte("Control\x00")
+ _XKB_MOD_NAME_SHIFT = []byte("Shift\x00")
+ _XKB_MOD_NAME_ALT = []byte("Mod1\x00")
+ _XKB_MOD_NAME_LOGO = []byte("Mod4\x00")
+)
+
+func (x *Context) Destroy() {
+ if x.compState != nil {
+ C.xkb_compose_state_unref(x.compState)
+ x.compState = nil
+ }
+ if x.compTable != nil {
+ C.xkb_compose_table_unref(x.compTable)
+ x.compTable = nil
+ }
+ x.DestroyKeymapState()
+ if x.Ctx != nil {
+ C.xkb_context_unref(x.Ctx)
+ x.Ctx = nil
+ }
+}
+
+func New() (*Context, error) {
+ ctx := &Context{
+ Ctx: C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS),
+ }
+ if ctx.Ctx == nil {
+ return nil, errors.New("newXKB: xkb_context_new failed")
+ }
+ locale := os.Getenv("LC_ALL")
+ if locale == "" {
+ locale = os.Getenv("LC_CTYPE")
+ }
+ if locale == "" {
+ locale = os.Getenv("LANG")
+ }
+ if locale == "" {
+ locale = "C"
+ }
+ cloc := C.CString(locale)
+ defer C.free(unsafe.Pointer(cloc))
+ ctx.compTable = C.xkb_compose_table_new_from_locale(ctx.Ctx, cloc,
+ C.XKB_COMPOSE_COMPILE_NO_FLAGS)
+ if ctx.compTable == nil {
+ ctx.Destroy()
+ return nil, errors.New("newXKB: xkb_compose_table_new_from_locale failed")
+ }
+ ctx.compState = C.xkb_compose_state_new(ctx.compTable,
+ C.XKB_COMPOSE_STATE_NO_FLAGS)
+ if ctx.compState == nil {
+ ctx.Destroy()
+ return nil, errors.New("newXKB: xkb_compose_state_new failed")
+ }
+ return ctx, nil
+}
+
+func (x *Context) DestroyKeymapState() {
+ if x.state != nil {
+ C.xkb_state_unref(x.state)
+ x.state = nil
+ }
+ if x.keyMap != nil {
+ C.xkb_keymap_unref(x.keyMap)
+ x.keyMap = nil
+ }
+}
+
+// SetKeymap sets the keymap and state. The context takes ownership of the
+// keymap and state and frees them in Destroy.
+func (x *Context) SetKeymap(xkbKeyMap, xkbState unsafe.Pointer) {
+ x.DestroyKeymapState()
+ x.keyMap = (*C.struct_xkb_keymap)(xkbKeyMap)
+ x.state = (*C.struct_xkb_state)(xkbState)
+}
+
+func (x *Context) LoadKeymap(format int, fd int, size int) error {
+ x.DestroyKeymapState()
+ mapData, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ,
+ syscall.MAP_SHARED)
+ if err != nil {
+ return fmt.Errorf("newXKB: mmap of keymap failed: %v", err)
+ }
+ defer syscall.Munmap(mapData)
+ keyMap := C.xkb_keymap_new_from_buffer(x.Ctx,
+ (*C.char)(unsafe.Pointer(&mapData[0])), C.size_t(size-1),
+ C.XKB_KEYMAP_FORMAT_TEXT_V1, C.XKB_KEYMAP_COMPILE_NO_FLAGS)
+ if keyMap == nil {
+ return errors.New("newXKB: xkb_keymap_new_from_buffer failed")
+ }
+ state := C.xkb_state_new(keyMap)
+ if state == nil {
+ C.xkb_keymap_unref(keyMap)
+ return errors.New("newXKB: xkb_state_new failed")
+ }
+ x.keyMap = keyMap
+ x.state = state
+ return nil
+}
+
+func (x *Context) Modifiers() key.Modifiers {
+ var mods key.Modifiers
+ if C.xkb_state_mod_name_is_active(x.state,
+ (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_CTRL[0])),
+ C.XKB_STATE_MODS_EFFECTIVE) == 1 {
+ mods |= key.ModCtrl
+ }
+ if C.xkb_state_mod_name_is_active(x.state,
+ (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_SHIFT[0])),
+ C.XKB_STATE_MODS_EFFECTIVE) == 1 {
+ mods |= key.ModShift
+ }
+ if C.xkb_state_mod_name_is_active(x.state,
+ (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_ALT[0])),
+ C.XKB_STATE_MODS_EFFECTIVE) == 1 {
+ mods |= key.ModAlt
+ }
+ if C.xkb_state_mod_name_is_active(x.state,
+ (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_LOGO[0])),
+ C.XKB_STATE_MODS_EFFECTIVE) == 1 {
+ mods |= key.ModSuper
+ }
+ return mods
+}
+
+func (x *Context) DispatchKey(keyCode uint32,
+ state key.State) (events []event.Event) {
+ if x.state == nil {
+ return
+ }
+ kc := C.xkb_keycode_t(keyCode)
+ if len(x.utf8Buf) == 0 {
+ x.utf8Buf = make([]byte, 1)
+ }
+ sym := C.xkb_state_key_get_one_sym(x.state, kc)
+ if name, ok := convertKeysym(sym); ok {
+ cmd := key.Event{
+ Name: name,
+ Modifiers: x.Modifiers(),
+ State: state,
+ }
+ // Ensure that a physical backtab key is translated to
+ // Shift-Tab.
+ if sym == C.XKB_KEY_ISO_Left_Tab {
+ cmd.Modifiers |= key.ModShift
+ }
+ events = append(events, cmd)
+ }
+ C.xkb_compose_state_feed(x.compState, sym)
+ var str []byte
+ switch C.xkb_compose_state_get_status(x.compState) {
+ case C.XKB_COMPOSE_CANCELLED, C.XKB_COMPOSE_COMPOSING:
+ return
+ case C.XKB_COMPOSE_COMPOSED:
+ size := C.xkb_compose_state_get_utf8(x.compState,
+ (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf)))
+ if int(size) >= len(x.utf8Buf) {
+ x.utf8Buf = make([]byte, size+1)
+ size = C.xkb_compose_state_get_utf8(x.compState,
+ (*C.char)(unsafe.Pointer(&x.utf8Buf[0])),
+ C.size_t(len(x.utf8Buf)))
+ }
+ C.xkb_compose_state_reset(x.compState)
+ str = x.utf8Buf[:size]
+ case C.XKB_COMPOSE_NOTHING:
+ mod := x.Modifiers()
+ if mod&(key.ModCtrl|key.ModAlt|key.ModSuper) == 0 {
+ str = x.charsForKeycode(kc)
+ }
+ }
+ // Report only printable runes.
+ var n int
+ for n < len(str) {
+ r, s := utf8.DecodeRune(str)
+ if unicode.IsPrint(r) {
+ n += s
+ } else {
+ copy(str[n:], str[n+s:])
+ str = str[:len(str)-s]
+ }
+ }
+ if state == key.Press && len(str) > 0 {
+ events = append(events, key.EditEvent{Text: string(str)})
+ }
+ return
+}
+
+func (x *Context) charsForKeycode(keyCode C.xkb_keycode_t) []byte {
+ size := C.xkb_state_key_get_utf8(x.state, keyCode,
+ (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf)))
+ if int(size) >= len(x.utf8Buf) {
+ x.utf8Buf = make([]byte, size+1)
+ size = C.xkb_state_key_get_utf8(x.state, keyCode,
+ (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf)))
+ }
+ return x.utf8Buf[:size]
+}
+
+func (x *Context) IsRepeatKey(keyCode uint32) bool {
+ kc := C.xkb_keycode_t(keyCode)
+ return C.xkb_keymap_key_repeats(x.keyMap, kc) == 1
+}
+
+func (x *Context) UpdateMask(depressed, latched, locked, depressedGroup, latchedGroup, lockedGroup uint32) {
+ if x.state == nil {
+ return
+ }
+ C.xkb_state_update_mask(x.state, C.xkb_mod_mask_t(depressed),
+ C.xkb_mod_mask_t(latched), C.xkb_mod_mask_t(locked),
+ C.xkb_layout_index_t(depressedGroup),
+ C.xkb_layout_index_t(latchedGroup), C.xkb_layout_index_t(lockedGroup))
+}
+
+func convertKeysym(s C.xkb_keysym_t) (string, bool) {
+ if 'a' <= s && s <= 'z' {
+ return string(rune(s - 'a' + 'A')), true
+ }
+ if ' ' < s && s <= '~' {
+ return string(rune(s)), true
+ }
+ var n string
+ switch s {
+ case C.XKB_KEY_Escape:
+ n = key.NameEscape
+ case C.XKB_KEY_Left:
+ n = key.NameLeftArrow
+ case C.XKB_KEY_Right:
+ n = key.NameRightArrow
+ case C.XKB_KEY_Return:
+ n = key.NameReturn
+ case C.XKB_KEY_KP_Enter:
+ n = key.NameEnter
+ case C.XKB_KEY_Up:
+ n = key.NameUpArrow
+ case C.XKB_KEY_Down:
+ n = key.NameDownArrow
+ case C.XKB_KEY_Home:
+ n = key.NameHome
+ case C.XKB_KEY_End:
+ n = key.NameEnd
+ case C.XKB_KEY_BackSpace:
+ n = key.NameDeleteBackward
+ case C.XKB_KEY_Delete:
+ n = key.NameDeleteForward
+ case C.XKB_KEY_Page_Up:
+ n = key.NamePageUp
+ case C.XKB_KEY_Page_Down:
+ n = key.NamePageDown
+ case C.XKB_KEY_F1:
+ n = "F1"
+ case C.XKB_KEY_F2:
+ n = "F2"
+ case C.XKB_KEY_F3:
+ n = "F3"
+ case C.XKB_KEY_F4:
+ n = "F4"
+ case C.XKB_KEY_F5:
+ n = "F5"
+ case C.XKB_KEY_F6:
+ n = "F6"
+ case C.XKB_KEY_F7:
+ n = "F7"
+ case C.XKB_KEY_F8:
+ n = "F8"
+ case C.XKB_KEY_F9:
+ n = "F9"
+ case C.XKB_KEY_F10:
+ n = "F10"
+ case C.XKB_KEY_F11:
+ n = "F11"
+ case C.XKB_KEY_F12:
+ n = "F12"
+ case C.XKB_KEY_Tab, C.XKB_KEY_KP_Tab, C.XKB_KEY_ISO_Left_Tab:
+ n = key.NameTab
+ case 0x20, C.XKB_KEY_KP_Space:
+ n = key.NameSpace
+ default:
+ return "", false
+ }
+ return n, true
+}
diff --git a/gio/giold/app/loop.go b/gio/giold/app/loop.go
new file mode 100644
index 0000000..6b2a57a
--- /dev/null
+++ b/gio/giold/app/loop.go
@@ -0,0 +1,162 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package app
+
+import (
+ "image"
+ "image/color"
+ "runtime"
+
+ "realy.lol/gio/app/internal/wm"
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/op"
+)
+
+type renderLoop struct {
+ summary string
+ drawing bool
+ err error
+
+ frames chan frame
+ results chan frameResult
+ refresh chan struct{}
+ refreshErr chan error
+ ack chan struct{}
+ stop chan struct{}
+ stopped chan struct{}
+}
+
+type frame struct {
+ viewport image.Point
+ ops *op.Ops
+}
+
+type frameResult struct {
+ profile string
+ err error
+}
+
+func newLoop(ctx wm.Context) (*renderLoop, error) {
+ l := &renderLoop{
+ frames: make(chan frame),
+ results: make(chan frameResult),
+ refresh: make(chan struct{}),
+ refreshErr: make(chan error),
+ // Ack is buffered so GPU commands can be issued after
+ // ack'ing the frame.
+ ack: make(chan struct{}, 1),
+ stop: make(chan struct{}),
+ stopped: make(chan struct{}),
+ }
+ if err := l.renderLoop(ctx); err != nil {
+ return nil, err
+ }
+ return l, nil
+}
+
+func (l *renderLoop) renderLoop(ctx wm.Context) error {
+ // GL Operations must happen on a single OS thread, so
+ // pass initialization result through a channel.
+ initErr := make(chan error)
+ go func() {
+ defer close(l.stopped)
+ runtime.LockOSThread()
+ // Don't UnlockOSThread to avoid reuse by the Go runtime.
+
+ if err := ctx.MakeCurrent(); err != nil {
+ initErr <- err
+ return
+ }
+ g, err := gpu.New(ctx.API())
+ if err != nil {
+ initErr <- err
+ return
+ }
+ defer g.Release()
+ initErr <- nil
+ loop:
+ for {
+ select {
+ case <-l.refresh:
+ l.refreshErr <- ctx.MakeCurrent()
+ case frame := <-l.frames:
+ ctx.Lock()
+ if runtime.GOOS == "js" {
+ // Use transparent black when Gio is embedded, to allow mixing of Gio and
+ // foreign content below.
+ g.Clear(color.NRGBA{A: 0x00, R: 0x00, G: 0x00, B: 0x00})
+ } else {
+ g.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
+ }
+ g.Collect(frame.viewport, frame.ops)
+ // Signal that we're done with the frame ops.
+ l.ack <- struct{}{}
+ var res frameResult
+ res.err = g.Frame()
+ if res.err == nil {
+ res.err = ctx.Present()
+ }
+ res.profile = g.Profile()
+ ctx.Unlock()
+ l.results <- res
+ case <-l.stop:
+ break loop
+ }
+ }
+ }()
+ return <-initErr
+}
+
+func (l *renderLoop) Release() {
+ // Flush error.
+ l.Flush()
+ close(l.stop)
+ <-l.stopped
+ l.stop = nil
+}
+
+func (l *renderLoop) Flush() error {
+ if l.drawing {
+ st := <-l.results
+ l.setErr(st.err)
+ if st.profile != "" {
+ l.summary = st.profile
+ }
+ l.drawing = false
+ }
+ return l.err
+}
+
+func (l *renderLoop) Summary() string {
+ return l.summary
+}
+
+func (l *renderLoop) Refresh() {
+ if l.err != nil {
+ return
+ }
+ // Make sure any pending frame is complete.
+ l.Flush()
+ l.refresh <- struct{}{}
+ l.setErr(<-l.refreshErr)
+}
+
+// Draw initiates a draw of a frame. It returns a channel
+// than signals when the frame is no longer being accessed.
+func (l *renderLoop) Draw(viewport image.Point,
+ frameOps *op.Ops) <-chan struct{} {
+ if l.err != nil {
+ l.ack <- struct{}{}
+ return l.ack
+ }
+ l.Flush()
+ l.frames <- frame{viewport, frameOps}
+ l.drawing = true
+ return l.ack
+}
+
+func (l *renderLoop) setErr(err error) {
+ if l.err == nil {
+ l.err = err
+ }
+}
diff --git a/gio/giold/app/permission/bluetooth/main.go b/gio/giold/app/permission/bluetooth/main.go
new file mode 100644
index 0000000..392bbbe
--- /dev/null
+++ b/gio/giold/app/permission/bluetooth/main.go
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package bluetooth implements permissions to access Bluetooth and Bluetooth
+Low Energy hardware, including the ability to discover and pair devices.
+
+Android
+
+The following entries will be added to AndroidManifest.xml:
+
+
+
+
+
+
+
+Note that ACCESS_FINE_LOCATION is required on Android before the Bluetooth
+device may be used.
+See https://developer.android.com/guide/topics/connectivity/bluetooth.
+
+ACCESS_FINE_LOCATION is a "dangerous" permission. See documentation for
+package realy.lol/gio/app/permission for more information.
+*/
+package bluetooth
diff --git a/gio/giold/app/permission/camera/main.go b/gio/giold/app/permission/camera/main.go
new file mode 100644
index 0000000..1e89a31
--- /dev/null
+++ b/gio/giold/app/permission/camera/main.go
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package camera implements permissions to access camera hardware.
+
+Android
+
+The following entries will be added to AndroidManifest.xml:
+
+
+
+
+CAMERA is a "dangerous" permission. See documentation for package
+realy.lol/gio/app/permission for more information.
+*/
+package camera
diff --git a/gio/giold/app/permission/doc.go b/gio/giold/app/permission/doc.go
new file mode 100644
index 0000000..878a5cb
--- /dev/null
+++ b/gio/giold/app/permission/doc.go
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package permission includes sub-packages that should be imported
+by a Gio program or by one of its dependencies to indicate that specific
+operating-system permissions are required. For example, if a Gio
+program requires access to a device's Bluetooth interface, it
+should import "realy.lol/gio/app/permission/bluetooth" as follows:
+
+ package main
+
+ import (
+ "realy.lol/gio/app"
+ _ "realy.lol/gio/app/permission/bluetooth"
+ )
+
+ func main() {
+ ...
+ }
+
+Since there are no exported identifiers in the app/permission/bluetooth
+package, the import uses the anonymous identifier (_) as the imported
+package name.
+
+As a special case, the gogio tool detects when a program directly or
+indirectly depends on the "net" package from the Go standard library as an
+indication that the program requires network access permissions. If a program
+requires network permissions but does not directly or indirectly import
+"net", it will be necessary to add the following code somewhere in the
+program's source code:
+
+ import (
+ ...
+ _ "net"
+ )
+
+Android -- Dangerous Permissions
+
+Certain permissions on Android are marked with a protection level of
+"dangerous". This means that, in addition to including the relevant
+Gio permission packages, your app will need to prompt the user
+specifically to request access. To access the Android Activity
+required for prompting, use app.ViewEvent (only available on Android).
+app.ViewEvent exposes the underlying Android View, on which the
+getContext method returns the Activity.
+
+For more information on dangerous permissions, see:
+https://developer.android.com/guide/topics/permissions/overview#dangerous_permissions
+*/
+package permission
diff --git a/gio/giold/app/permission/networkstate/main.go b/gio/giold/app/permission/networkstate/main.go
new file mode 100644
index 0000000..c594219
--- /dev/null
+++ b/gio/giold/app/permission/networkstate/main.go
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package networkstate implements permissions to access network connectivity information.
+
+Android
+
+The following entries will be added to AndroidManifest.xml:
+
+
+
+*/
+package networkstate
diff --git a/gio/giold/app/permission/storage/main.go b/gio/giold/app/permission/storage/main.go
new file mode 100644
index 0000000..623a624
--- /dev/null
+++ b/gio/giold/app/permission/storage/main.go
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package storage implements read and write storage permissions
+on mobile devices.
+
+Android
+
+The following entries will be added to AndroidManifest.xml:
+
+
+
+
+READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE are "dangerous" permissions.
+See documentation for package realy.lol/gio/app/permission for more information.
+*/
+package storage
diff --git a/gio/giold/app/sigpipe_darwin.go b/gio/giold/app/sigpipe_darwin.go
new file mode 100644
index 0000000..aca19b7
--- /dev/null
+++ b/gio/giold/app/sigpipe_darwin.go
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build !go1.14
+
+// Work around golang.org/issue/33384, fixed in CL 191785,
+// to be released in Go 1.14.
+
+package app
+
+import (
+ "os"
+ "os/signal"
+ "syscall"
+)
+
+func init() {
+ signal.Notify(make(chan os.Signal), syscall.SIGPIPE)
+}
diff --git a/gio/giold/app/window.go b/gio/giold/app/window.go
new file mode 100644
index 0000000..815e1e6
--- /dev/null
+++ b/gio/giold/app/window.go
@@ -0,0 +1,531 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package app
+
+import (
+ "errors"
+ "fmt"
+ "image"
+ "time"
+
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/profile"
+ "realy.lol/gio/io/router"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+
+ _ "realy.lol/gio/app/internal/log"
+ "realy.lol/gio/app/internal/wm"
+)
+
+// WindowOption configures a wm.
+type Option func(opts *wm.Options)
+
+// Window represents an operating system wm.
+type Window struct {
+ driver wm.Driver
+ ctx wm.Context
+ loop *renderLoop
+
+ // driverFuncs is a channel of functions to run when
+ // the Window has a valid driver.
+ driverFuncs chan func()
+
+ out chan event.Event
+ in chan event.Event
+ ack chan struct{}
+ invalidates chan struct{}
+ frames chan *op.Ops
+ frameAck chan struct{}
+ // dead is closed when the window is destroyed.
+ dead chan struct{}
+
+ stage system.Stage
+ animating bool
+ hasNextFrame bool
+ nextFrame time.Time
+ delayedDraw *time.Timer
+
+ queue queue
+ cursor pointer.CursorName
+
+ callbacks callbacks
+}
+
+type callbacks struct {
+ w *Window
+}
+
+// queue is an event.Queue implementation that distributes system events
+// to the input handlers declared in the most recent frame.
+type queue struct {
+ q router.Router
+}
+
+// driverEvent is sent when a new native driver
+// is available for the wm.
+type driverEvent struct {
+ driver wm.Driver
+}
+
+// Pre-allocate the ack event to avoid garbage.
+var ackEvent event.Event
+
+// NewWindow creates a new window for a set of window
+// options. The options are hints; the platform is free to
+// ignore or adjust them.
+//
+// If the current program is running on iOS and Android,
+// NewWindow returns the window previously created by the
+// platform.
+//
+// Calling NewWindow more than once is not supported on
+// iOS, Android, WebAssembly.
+func NewWindow(options ...Option) *Window {
+ opts := new(wm.Options)
+ // Default options.
+ Size(unit.Px(800), unit.Px(600))(opts)
+ Title("Gio")(opts)
+
+ for _, o := range options {
+ o(opts)
+ }
+
+ w := &Window{
+ in: make(chan event.Event),
+ out: make(chan event.Event),
+ ack: make(chan struct{}),
+ invalidates: make(chan struct{}, 1),
+ frames: make(chan *op.Ops),
+ frameAck: make(chan struct{}),
+ driverFuncs: make(chan func()),
+ dead: make(chan struct{}),
+ }
+ w.callbacks.w = w
+ go w.run(opts)
+ return w
+}
+
+// Events returns the channel where events are delivered.
+func (w *Window) Events() <-chan event.Event {
+ return w.out
+}
+
+// update updates the wm. Paint operations updates the
+// window contents, input operations declare input handlers,
+// and so on. The supplied operations list completely replaces
+// the window state from previous calls.
+func (w *Window) update(frame *op.Ops) {
+ w.frames <- frame
+ <-w.frameAck
+}
+
+func (w *Window) validateAndProcess(frameStart time.Time, size image.Point,
+ sync bool, frame *op.Ops) error {
+ for {
+ if w.loop != nil {
+ if err := w.loop.Flush(); err != nil {
+ w.destroyGPU()
+ if err == wm.ErrDeviceLost {
+ continue
+ }
+ return err
+ }
+ }
+ if w.loop == nil {
+ var err error
+ w.ctx, err = w.driver.NewContext()
+ if err != nil {
+ return err
+ }
+ w.loop, err = newLoop(w.ctx)
+ if err != nil {
+ w.ctx.Release()
+ return err
+ }
+ }
+ w.processFrame(frameStart, size, frame)
+ if sync {
+ if err := w.loop.Flush(); err != nil {
+ w.destroyGPU()
+ if err == wm.ErrDeviceLost {
+ continue
+ }
+ return err
+ }
+ }
+ return nil
+ }
+}
+
+func (w *Window) processFrame(frameStart time.Time, size image.Point,
+ frame *op.Ops) {
+ sync := w.loop.Draw(size, frame)
+ w.queue.q.Frame(frame)
+ switch w.queue.q.TextInputState() {
+ case router.TextInputOpen:
+ w.driver.ShowTextInput(true)
+ case router.TextInputClose:
+ w.driver.ShowTextInput(false)
+ }
+ if txt, ok := w.queue.q.WriteClipboard(); ok {
+ go w.WriteClipboard(txt)
+ }
+ if w.queue.q.ReadClipboard() {
+ go w.ReadClipboard()
+ }
+ if w.queue.q.Profiling() {
+ frameDur := time.Since(frameStart)
+ frameDur = frameDur.Truncate(100 * time.Microsecond)
+ q := 100 * time.Microsecond
+ timings := fmt.Sprintf("tot:%7s %s", frameDur.Round(q),
+ w.loop.Summary())
+ w.queue.q.Queue(profile.Event{Timings: timings})
+ }
+ if t, ok := w.queue.q.WakeupTime(); ok {
+ w.setNextFrame(t)
+ }
+ // Opportunistically check whether Invalidate has been called, to avoid
+ // stopping and starting animation mode.
+ select {
+ case <-w.invalidates:
+ w.setNextFrame(time.Time{})
+ default:
+ }
+ w.updateAnimation()
+ // Wait for the GPU goroutine to finish processing frame.
+ <-sync
+}
+
+// Invalidate the window such that a FrameEvent will be generated immediately.
+// If the window is inactive, the event is sent when the window becomes active.
+//
+// Note that Invalidate is intended for externally triggered updates, such as a
+// response from a network request. InvalidateOp is more efficient for animation
+// and similar internal updates.
+//
+// Invalidate is safe for concurrent use.
+func (w *Window) Invalidate() {
+ select {
+ case w.invalidates <- struct{}{}:
+ default:
+ }
+}
+
+// Option applies the options to the window.
+func (w *Window) Option(opts ...Option) {
+ go w.driverDo(func() {
+ o := new(wm.Options)
+ for _, opt := range opts {
+ opt(o)
+ }
+ w.driver.Option(o)
+ })
+}
+
+// ReadClipboard initiates a read of the clipboard in the form
+// of a clipboard.Event. Multiple reads may be coalesced
+// to a single event.
+func (w *Window) ReadClipboard() {
+ go w.driverDo(func() {
+ w.driver.ReadClipboard()
+ })
+}
+
+// WriteClipboard writes a string to the clipboard.
+func (w *Window) WriteClipboard(s string) {
+ go w.driverDo(func() {
+ w.driver.WriteClipboard(s)
+ })
+}
+
+// SetCursorName changes the current window cursor to name.
+func (w *Window) SetCursorName(name pointer.CursorName) {
+ go w.driverDo(func() {
+ w.driver.SetCursor(name)
+ })
+}
+
+// Close the wm. The window's event loop should exit when it receives
+// system.DestroyEvent.
+//
+// Currently, only macOS, Windows and X11 drivers implement this functionality,
+// all others are stubbed.
+func (w *Window) Close() {
+ go w.driverDo(func() {
+ w.driver.Close()
+ })
+}
+
+// driverDo waits for the window to have a valid driver attached and calls f.
+// It does nothing if the if the window was destroyed while waiting.
+func (w *Window) driverDo(f func()) {
+ select {
+ case w.driverFuncs <- f:
+ case <-w.dead:
+ }
+}
+
+func (w *Window) updateAnimation() {
+ animate := false
+ if w.delayedDraw != nil {
+ w.delayedDraw.Stop()
+ w.delayedDraw = nil
+ }
+ if w.stage >= system.StageRunning && w.hasNextFrame {
+ if dt := time.Until(w.nextFrame); dt <= 0 {
+ animate = true
+ } else {
+ w.delayedDraw = time.NewTimer(dt)
+ }
+ }
+ if animate != w.animating {
+ w.animating = animate
+ w.driver.SetAnimating(animate)
+ }
+}
+
+func (w *Window) setNextFrame(at time.Time) {
+ if !w.hasNextFrame || at.Before(w.nextFrame) {
+ w.hasNextFrame = true
+ w.nextFrame = at
+ }
+}
+
+func (c *callbacks) SetDriver(d wm.Driver) {
+ c.Event(driverEvent{d})
+}
+
+func (c *callbacks) Event(e event.Event) {
+ select {
+ case c.w.in <- e:
+ <-c.w.ack
+ case <-c.w.dead:
+ }
+}
+
+func (w *Window) waitAck() {
+ // Send a dummy event; when it gets through we
+ // know the application has processed the previous event.
+ w.out <- ackEvent
+}
+
+// Prematurely destroy the window and wait for the native window
+// destroy event.
+func (w *Window) destroy(err error) {
+ w.destroyGPU()
+ // Ack the current event.
+ w.ack <- struct{}{}
+ w.out <- system.DestroyEvent{Err: err}
+ close(w.dead)
+ for e := range w.in {
+ w.ack <- struct{}{}
+ if _, ok := e.(system.DestroyEvent); ok {
+ return
+ }
+ }
+}
+
+func (w *Window) destroyGPU() {
+ if w.loop != nil {
+ w.loop.Release()
+ w.loop = nil
+ }
+ if w.ctx != nil {
+ w.ctx.Release()
+ w.ctx = nil
+ }
+}
+
+// waitFrame waits for the client to either call FrameEvent.Frame
+// or to continue event handling. It returns whether the client
+// called Frame or not.
+func (w *Window) waitFrame() (*op.Ops, bool) {
+ select {
+ case frame := <-w.frames:
+ // The client called FrameEvent.Frame.
+ return frame, true
+ case w.out <- ackEvent:
+ // The client ignored FrameEvent and continued processing
+ // events.
+ return nil, false
+ }
+}
+
+func (w *Window) run(opts *wm.Options) {
+ defer close(w.in)
+ defer close(w.out)
+ if err := wm.NewWindow(&w.callbacks, opts); err != nil {
+ w.out <- system.DestroyEvent{Err: err}
+ return
+ }
+ for {
+ var driverFuncs chan func()
+ if w.driver != nil {
+ driverFuncs = w.driverFuncs
+ }
+ var timer <-chan time.Time
+ if w.delayedDraw != nil {
+ timer = w.delayedDraw.C
+ }
+ select {
+ case <-timer:
+ w.setNextFrame(time.Time{})
+ w.updateAnimation()
+ case <-w.invalidates:
+ w.setNextFrame(time.Time{})
+ w.updateAnimation()
+ case f := <-driverFuncs:
+ f()
+ case e := <-w.in:
+ switch e2 := e.(type) {
+ case system.StageEvent:
+ if w.loop != nil {
+ if e2.Stage < system.StageRunning {
+ w.destroyGPU()
+ } else {
+ w.loop.Refresh()
+ }
+ }
+ w.stage = e2.Stage
+ w.updateAnimation()
+ w.out <- e
+ w.waitAck()
+ case wm.FrameEvent:
+ if e2.Size == (image.Point{}) {
+ panic(errors.New("internal error: zero-sized Draw"))
+ }
+ if w.stage < system.StageRunning {
+ // No drawing if not visible.
+ break
+ }
+ frameStart := time.Now()
+ w.hasNextFrame = false
+ e2.Frame = w.update
+ e2.Queue = &w.queue
+ w.out <- e2.FrameEvent
+ if w.loop != nil {
+ if e2.Sync {
+ w.loop.Refresh()
+ }
+ }
+ frame, gotFrame := w.waitFrame()
+ err := w.validateAndProcess(frameStart, e2.Size, e2.Sync, frame)
+ if gotFrame {
+ // We're done with frame, let the client continue.
+ w.frameAck <- struct{}{}
+ }
+ if err != nil {
+ w.destroyGPU()
+ w.destroy(err)
+ return
+ }
+ w.updateCursor()
+ case *system.CommandEvent:
+ w.out <- e
+ w.waitAck()
+ case driverEvent:
+ w.driver = e2.driver
+ case system.DestroyEvent:
+ w.destroyGPU()
+ w.out <- e2
+ w.ack <- struct{}{}
+ return
+ case event.Event:
+ if w.queue.q.Queue(e2) {
+ w.setNextFrame(time.Time{})
+ w.updateAnimation()
+ }
+ w.updateCursor()
+ w.out <- e
+ }
+ w.ack <- struct{}{}
+ }
+ }
+}
+
+func (w *Window) updateCursor() {
+ if c := w.queue.q.Cursor(); c != w.cursor {
+ w.cursor = c
+ w.SetCursorName(c)
+ }
+}
+
+func (q *queue) Events(k event.Tag) []event.Event {
+ return q.q.Events(k)
+}
+
+const (
+ // Windowed is the normal window mode with OS specific window decorations.
+ Windowed = wm.Windowed
+ // Fullscreen is the full screen window mode.
+ Fullscreen = wm.Fullscreen
+)
+
+// WindowMode sets the window mode.
+//
+// Supported platforms are macOS, X11 and Windows.
+func WindowMode(mode wm.WindowMode) Option {
+ return func(opts *wm.Options) {
+ opts.WindowMode = &mode
+ }
+}
+
+// Title sets the title of the wm.
+func Title(t string) Option {
+ return func(opts *wm.Options) {
+ opts.Title = &t
+ }
+}
+
+// Size sets the size of the wm.
+func Size(w, h unit.Value) Option {
+ if w.V <= 0 {
+ panic("width must be larger than or equal to 0")
+ }
+ if h.V <= 0 {
+ panic("height must be larger than or equal to 0")
+ }
+ return func(opts *wm.Options) {
+ opts.Size = &wm.Size{
+ Width: w,
+ Height: h,
+ }
+ }
+}
+
+// MaxSize sets the maximum size of the wm.
+func MaxSize(w, h unit.Value) Option {
+ if w.V <= 0 {
+ panic("width must be larger than or equal to 0")
+ }
+ if h.V <= 0 {
+ panic("height must be larger than or equal to 0")
+ }
+ return func(opts *wm.Options) {
+ opts.MaxSize = &wm.Size{
+ Width: w,
+ Height: h,
+ }
+ }
+}
+
+// MinSize sets the minimum size of the wm.
+func MinSize(w, h unit.Value) Option {
+ if w.V <= 0 {
+ panic("width must be larger than or equal to 0")
+ }
+ if h.V <= 0 {
+ panic("height must be larger than or equal to 0")
+ }
+ return func(opts *wm.Options) {
+ opts.MinSize = &wm.Size{
+ Width: w,
+ Height: h,
+ }
+ }
+}
+
+func (driverEvent) ImplementsEvent() {}
diff --git a/gio/giold/cmd/go.local.sum b/gio/giold/cmd/go.local.sum
new file mode 100644
index 0000000..c197ac2
--- /dev/null
+++ b/gio/giold/cmd/go.local.sum
@@ -0,0 +1,53 @@
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o=
+github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
+github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg=
+github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
+github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194=
+github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
+github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
+github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
+github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
+github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
+github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210304124612-50617c2ba197 h1:7+SpRyhoo46QjKkYInQXpcfxx3TYFEYkn131lwGE9/0=
+golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/gio/giold/cmd/go.sum b/gio/giold/cmd/go.sum
new file mode 100644
index 0000000..c197ac2
--- /dev/null
+++ b/gio/giold/cmd/go.sum
@@ -0,0 +1,53 @@
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o=
+github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
+github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg=
+github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
+github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194=
+github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
+github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
+github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
+github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
+github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
+github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
+golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210304124612-50617c2ba197 h1:7+SpRyhoo46QjKkYInQXpcfxx3TYFEYkn131lwGE9/0=
+golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/gio/giold/cmd/gogio/android_test.go b/gio/giold/cmd/gogio/android_test.go
new file mode 100644
index 0000000..e73386f
--- /dev/null
+++ b/gio/giold/cmd/gogio/android_test.go
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main_test
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "image"
+ "image/png"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+)
+
+type AndroidTestDriver struct {
+ driverBase
+
+ sdkDir string
+ adbPath string
+}
+
+var rxAdbDevice = regexp.MustCompile(`(.*)\s+device$`)
+
+func (d *AndroidTestDriver) Start(path string) {
+ d.sdkDir = os.Getenv("ANDROID_SDK_ROOT")
+ if d.sdkDir == "" {
+ d.Skipf("Android SDK is required; set $ANDROID_SDK_ROOT")
+ }
+ d.adbPath = filepath.Join(d.sdkDir, "platform-tools", "adb")
+ if _, err := os.Stat(d.adbPath); os.IsNotExist(err) {
+ d.Skipf("adb not found")
+ }
+
+ devOut := bytes.TrimSpace(d.adb("devices"))
+ devices := rxAdbDevice.FindAllSubmatch(devOut, -1)
+ switch len(devices) {
+ case 0:
+ d.Skipf("no Android devices attached via adb; skipping")
+ case 1:
+ default:
+ d.Skipf("multiple Android devices attached via adb; skipping")
+ }
+
+ // If the device is attached but asleep, it's probably just charging.
+ // Don't use it; the screen needs to be on and unlocked for the test to
+ // work.
+ if !bytes.Contains(
+ d.adb("shell", "dumpsys", "power"),
+ []byte(" mWakefulness=Awake"),
+ ) {
+ d.Skipf("Android device isn't awake; skipping")
+ }
+
+ // First, build the app.
+ apk := filepath.Join(d.tempDir("gio-endtoend-android"), "e2e.apk")
+ d.gogio("-target=android", "-appid="+appid, "-o="+apk, path)
+
+ // Make sure the app isn't installed already, and try to uninstall it
+ // when we finish. Previous failed test runs might have left the app.
+ d.tryUninstall()
+ d.adb("install", apk)
+ d.Cleanup(d.tryUninstall)
+
+ // Force our e2e app to be fullscreen, so that the android system bar at
+ // the top doesn't mess with our screenshots.
+ // TODO(mvdan): is there a way to do this via gio, so that we don't need
+ // to set up a global Android setting via the shell?
+ d.adb("shell", "settings", "put", "global", "policy_control", "immersive.full="+appid)
+
+ // Make sure the app isn't already running.
+ d.adb("shell", "pm", "clear", appid)
+
+ // Start listening for log messages.
+ {
+ ctx, cancel := context.WithCancel(context.Background())
+ cmd := exec.CommandContext(ctx, d.adbPath,
+ "logcat",
+ "-s", // suppress other logs
+ "-T1", // don't show previous log messages
+ appid+":*", // show all logs from our gio app ID
+ )
+ output, err := cmd.StdoutPipe()
+ if err != nil {
+ d.Fatal(err)
+ }
+ cmd.Stderr = cmd.Stdout
+ d.output = output
+ if err := cmd.Start(); err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(cancel)
+ }
+
+ // Start the app.
+ d.adb("shell", "monkey", "-p", appid, "1")
+
+ // Wait for the gio app to render.
+ d.waitForFrame()
+}
+
+func (d *AndroidTestDriver) Screenshot() image.Image {
+ out := d.adb("shell", "screencap", "-p")
+ img, err := png.Decode(bytes.NewReader(out))
+ if err != nil {
+ d.Fatal(err)
+ }
+ return img
+}
+
+func (d *AndroidTestDriver) tryUninstall() {
+ cmd := exec.Command(d.adbPath, "shell", "pm", "uninstall", appid)
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ if bytes.Contains(out, []byte("Unknown package")) {
+ // The package is not installed. Don't log anything.
+ return
+ }
+ d.Logf("could not uninstall: %v\n%s", err, out)
+ }
+}
+
+func (d *AndroidTestDriver) adb(args ...interface{}) []byte {
+ strs := []string{}
+ for _, arg := range args {
+ strs = append(strs, fmt.Sprint(arg))
+ }
+ cmd := exec.Command(d.adbPath, strs...)
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ d.Errorf("%s", out)
+ d.Fatal(err)
+ }
+ return out
+}
+
+func (d *AndroidTestDriver) Click(x, y int) {
+ d.adb("shell", "input", "tap", x, y)
+
+ // Wait for the gio app to render after this click.
+ d.waitForFrame()
+}
diff --git a/gio/giold/cmd/gogio/androidbuild.go b/gio/giold/cmd/gogio/androidbuild.go
new file mode 100644
index 0000000..4a055b9
--- /dev/null
+++ b/gio/giold/cmd/gogio/androidbuild.go
@@ -0,0 +1,1032 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "archive/zip"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+ "text/template"
+
+ "golang.org/x/sync/errgroup"
+ "golang.org/x/tools/go/packages"
+)
+
+type androidTools struct {
+ buildtools string
+ androidjar string
+}
+
+// zip.Writer with a sticky error.
+type zipWriter struct {
+ err error
+ w *zip.Writer
+}
+
+// Writer that saves any errors.
+type errWriter struct {
+ w io.Writer
+ err *error
+}
+
+var exeSuffix string
+
+type manifestData struct {
+ AppID string
+ Version int
+ MinSDK int
+ TargetSDK int
+ Permissions []string
+ Features []string
+ IconSnip string
+ AppName string
+}
+
+const (
+ themes = `
+
+
+`
+ themesV21 = `
+
+
+`
+)
+
+func init() {
+ if runtime.GOOS == "windows" {
+ exeSuffix = ".exe"
+ }
+}
+
+func buildAndroid(tmpDir string, bi *buildInfo) error {
+ sdk := os.Getenv("ANDROID_SDK_ROOT")
+ if sdk == "" {
+ return errors.New("please set ANDROID_SDK_ROOT to the Android SDK path")
+ }
+ if _, err := os.Stat(sdk); err != nil {
+ return err
+ }
+ platform, err := latestPlatform(sdk)
+ if err != nil {
+ return err
+ }
+ buildtools, err := latestTools(sdk)
+ if err != nil {
+ return err
+ }
+
+ tools := &androidTools{
+ buildtools: buildtools,
+ androidjar: filepath.Join(platform, "android.jar"),
+ }
+ perms := []string{"default"}
+ const permPref = "realy.lol/gio/app/permission/"
+ cfg := &packages.Config{
+ Mode: packages.NeedName +
+ packages.NeedFiles +
+ packages.NeedImports +
+ packages.NeedDeps,
+ Env: append(
+ os.Environ(),
+ "GOOS=android",
+ "CGO_ENABLED=1",
+ ),
+ }
+ pkgs, err := packages.Load(cfg, bi.pkgPath)
+ if err != nil {
+ return err
+ }
+ var extraJars []string
+ visitedPkgs := make(map[string]bool)
+ var visitPkg func(*packages.Package) error
+ visitPkg = func(p *packages.Package) error {
+ if len(p.GoFiles) == 0 {
+ return nil
+ }
+ dir := filepath.Dir(p.GoFiles[0])
+ jars, err := filepath.Glob(filepath.Join(dir, "*.jar"))
+ if err != nil {
+ return err
+ }
+ extraJars = append(extraJars, jars...)
+ switch {
+ case p.PkgPath == "net":
+ perms = append(perms, "network")
+ case strings.HasPrefix(p.PkgPath, permPref):
+ perms = append(perms, p.PkgPath[len(permPref):])
+ }
+
+ for _, imp := range p.Imports {
+ if !visitedPkgs[imp.ID] {
+ visitPkg(imp)
+ visitedPkgs[imp.ID] = true
+ }
+ }
+ return nil
+ }
+ if err := visitPkg(pkgs[0]); err != nil {
+ return err
+ }
+
+ if err := compileAndroid(tmpDir, tools, bi); err != nil {
+ return err
+ }
+ switch *buildMode {
+ case "archive":
+ return archiveAndroid(tmpDir, bi, perms)
+ case "exe":
+ file := *destPath
+ if file == "" {
+ file = fmt.Sprintf("%s.apk", bi.name)
+ }
+
+ isBundle := false
+ switch filepath.Ext(file) {
+ case ".apk":
+ case ".aab":
+ isBundle = true
+ default:
+ return fmt.Errorf("the specified output %q does not end in '.apk' or '.aab'",
+ file)
+ }
+
+ if err := exeAndroid(tmpDir, tools, bi, extraJars, perms,
+ isBundle); err != nil {
+ return err
+ }
+ if isBundle {
+ return signAAB(tmpDir, file, tools, bi)
+ }
+ return signAPK(tmpDir, file, tools, bi)
+ default:
+ panic("unreachable")
+ }
+}
+
+func compileAndroid(tmpDir string, tools *androidTools,
+ bi *buildInfo) (err error) {
+ androidHome := os.Getenv("ANDROID_SDK_ROOT")
+ if androidHome == "" {
+ return errors.New("ANDROID_SDK_ROOT is not set. Please point it to the root of the Android SDK")
+ }
+ javac, err := findJavaC()
+ if err != nil {
+ return fmt.Errorf("could not find javac: %v", err)
+ }
+ ndkRoot, err := findNDK(androidHome)
+ if err != nil {
+ return err
+ }
+ minSDK := 16
+ if bi.minsdk > minSDK {
+ minSDK = bi.minsdk
+ }
+ tcRoot := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt",
+ archNDK())
+ var builds errgroup.Group
+ for _, a := range bi.archs {
+ arch := allArchs[a]
+ clang, err := latestCompiler(tcRoot, a, minSDK)
+ if err != nil {
+ return fmt.Errorf("%s. Please make sure you have NDK >= r19c installed. Use the command `sdkmanager ndk-bundle` to install it.",
+ err)
+ }
+ if runtime.GOOS == "windows" {
+ // Because of https://github.com/android-ndk/ndk/issues/920,
+ // we need NDK r19c, not just r19b. Check for the presence of
+ // clang++.cmd which is only available in r19c.
+ clangpp := clang + "++.cmd"
+ if _, err := os.Stat(clangpp); err != nil {
+ return fmt.Errorf("NDK version r19b detected, but >= r19c is required. Use the command `sdkmanager ndk-bundle` to install it")
+ }
+ }
+ archDir := filepath.Join(tmpDir, "jni", arch.jniArch)
+ if err := os.MkdirAll(archDir, 0755); err != nil {
+ return fmt.Errorf("failed to create %q: %v", archDir, err)
+ }
+ libFile := filepath.Join(archDir, "libgio.so")
+ cmd := exec.Command(
+ "go",
+ "build",
+ "-ldflags=-w -s "+bi.ldflags,
+ "-buildmode=c-shared",
+ "-tags", bi.tags,
+ "-o", libFile,
+ bi.pkgPath,
+ )
+ cmd.Env = append(
+ os.Environ(),
+ "GOOS=android",
+ "GOARCH="+a,
+ "GOARM=7", // Avoid softfloat.
+ "CGO_ENABLED=1",
+ "CC="+clang,
+ )
+ builds.Go(func() error {
+ _, err := runCmd(cmd)
+ return err
+ })
+ }
+ appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}",
+ "realy.lol/gio/app/internal/wm"))
+ if err != nil {
+ return err
+ }
+ javaFiles, err := filepath.Glob(filepath.Join(appDir, "*.java"))
+ if err != nil {
+ return err
+ }
+ if len(javaFiles) > 0 {
+ classes := filepath.Join(tmpDir, "classes")
+ if err := os.MkdirAll(classes, 0755); err != nil {
+ return err
+ }
+ javac := exec.Command(
+ javac,
+ "-target", "1.8",
+ "-source", "1.8",
+ "-sourcepath", appDir,
+ "-bootclasspath", tools.androidjar,
+ "-d", classes,
+ )
+ javac.Args = append(javac.Args, javaFiles...)
+ builds.Go(func() error {
+ _, err := runCmd(javac)
+ return err
+ })
+ }
+ return builds.Wait()
+}
+
+func archiveAndroid(tmpDir string, bi *buildInfo, perms []string) (err error) {
+ aarFile := *destPath
+ if aarFile == "" {
+ aarFile = fmt.Sprintf("%s.aar", bi.name)
+ }
+ if filepath.Ext(aarFile) != ".aar" {
+ return fmt.Errorf("the specified output %q does not end in '.aar'",
+ aarFile)
+ }
+ aar, err := os.Create(aarFile)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := aar.Close(); err == nil {
+ err = cerr
+ }
+ }()
+ aarw := newZipWriter(aar)
+ defer aarw.Close()
+ aarw.Create("R.txt")
+ themesXML := aarw.Create("res/values/themes.xml")
+ themesXML.Write([]byte(themes))
+ themesXML21 := aarw.Create("res/values-v21/themes.xml")
+ themesXML21.Write([]byte(themesV21))
+ permissions, features := getPermissions(perms)
+ // Disable input emulation on ChromeOS.
+ manifest := aarw.Create("AndroidManifest.xml")
+ manifestSrc := manifestData{
+ AppID: bi.appID,
+ MinSDK: bi.minsdk,
+ Permissions: permissions,
+ Features: features,
+ }
+ tmpl, err := template.New("manifest").Parse(
+ `
+
+{{range .Permissions}}
+{{end}}{{range .Features}}
+{{end}}
+`)
+ if err != nil {
+ panic(err)
+ }
+ err = tmpl.Execute(manifest, manifestSrc)
+ proguard := aarw.Create("proguard.txt")
+ proguard.Write([]byte(`-keep class org.gioui.** { *; }`))
+
+ for _, a := range bi.archs {
+ arch := allArchs[a]
+ libFile := filepath.Join("jni", arch.jniArch, "libgio.so")
+ aarw.Add(filepath.ToSlash(libFile), filepath.Join(tmpDir, libFile))
+ }
+ classes := filepath.Join(tmpDir, "classes")
+ if _, err := os.Stat(classes); err == nil {
+ jarFile := filepath.Join(tmpDir, "classes.jar")
+ if err := writeJar(jarFile, classes); err != nil {
+ return err
+ }
+ aarw.Add("classes.jar", jarFile)
+ }
+ return aarw.Close()
+}
+
+func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo,
+ extraJars, perms []string, isBundle bool) (err error) {
+ classes := filepath.Join(tmpDir, "classes")
+ var classFiles []string
+ err = filepath.Walk(classes,
+ func(path string, f os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if filepath.Ext(path) == ".class" {
+ classFiles = append(classFiles, path)
+ }
+ return nil
+ })
+ classFiles = append(classFiles, extraJars...)
+ dexDir := filepath.Join(tmpDir, "apk")
+ if err := os.MkdirAll(dexDir, 0755); err != nil {
+ return err
+ }
+ if len(classFiles) > 0 {
+ d8 := exec.Command(
+ filepath.Join(tools.buildtools, "d8"),
+ "--classpath", tools.androidjar,
+ "--output", dexDir,
+ )
+ d8.Args = append(d8.Args, classFiles...)
+ if _, err := runCmd(d8); err != nil {
+ return err
+ }
+ }
+
+ // Compile resources.
+ resDir := filepath.Join(tmpDir, "res")
+ valDir := filepath.Join(resDir, "values")
+ v21Dir := filepath.Join(resDir, "values-v21")
+ for _, dir := range []string{valDir, v21Dir} {
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return err
+ }
+ }
+ iconSnip := ""
+ if _, err := os.Stat(bi.iconPath); err == nil {
+ err := buildIcons(resDir, bi.iconPath, []iconVariant{
+ {path: filepath.Join("mipmap-hdpi", "ic_launcher.png"), size: 72},
+ {path: filepath.Join("mipmap-xhdpi", "ic_launcher.png"), size: 96},
+ {path: filepath.Join("mipmap-xxhdpi", "ic_launcher.png"),
+ size: 144},
+ {path: filepath.Join("mipmap-xxxhdpi", "ic_launcher.png"),
+ size: 192},
+ })
+ if err != nil {
+ return err
+ }
+ iconSnip = `android:icon="@mipmap/ic_launcher"`
+ }
+ err = ioutil.WriteFile(filepath.Join(valDir, "themes.xml"), []byte(themes),
+ 0660)
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(filepath.Join(v21Dir, "themes.xml"),
+ []byte(themesV21), 0660)
+ if err != nil {
+ return err
+ }
+ resZip := filepath.Join(tmpDir, "resources.zip")
+ aapt2 := filepath.Join(tools.buildtools, "aapt2")
+ _, err = runCmd(exec.Command(
+ aapt2,
+ "compile",
+ "-o", resZip,
+ "--dir", resDir))
+ if err != nil {
+ return err
+ }
+
+ // Link APK.
+ // Currently, new apps must have a target SDK version of at least 30.
+ // https://developer.android.com/distribute/best-practices/develop/target-sdk
+ targetSDK := 30
+ if bi.minsdk > targetSDK {
+ targetSDK = bi.minsdk
+ }
+ minSDK := 16
+ if bi.minsdk > minSDK {
+ minSDK = bi.minsdk
+ }
+ permissions, features := getPermissions(perms)
+ appName := strings.Title(bi.name)
+ manifestSrc := manifestData{
+ AppID: bi.appID,
+ Version: bi.version,
+ MinSDK: minSDK,
+ TargetSDK: targetSDK,
+ Permissions: permissions,
+ Features: features,
+ IconSnip: iconSnip,
+ AppName: appName,
+ }
+ tmpl, err := template.New("test").Parse(
+ `
+
+
+{{range .Permissions}}
+{{end}}{{range .Features}}
+{{end}}
+
+
+
+
+
+
+
+`)
+ var manifestBuffer bytes.Buffer
+ if err := tmpl.Execute(&manifestBuffer, manifestSrc); err != nil {
+ return err
+ }
+ manifest := filepath.Join(tmpDir, "AndroidManifest.xml")
+ if err := ioutil.WriteFile(manifest, manifestBuffer.Bytes(),
+ 0660); err != nil {
+ return err
+ }
+
+ linkAPK := filepath.Join(tmpDir, "link.apk")
+
+ args := []string{
+ "link",
+ "--manifest", manifest,
+ "-I", tools.androidjar,
+ "-o", linkAPK,
+ }
+ if isBundle {
+ args = append(args, "--proto-format")
+ }
+ args = append(args, resZip)
+
+ if _, err := runCmd(exec.Command(aapt2, args...)); err != nil {
+ return err
+ }
+
+ // The Go standard library archive/zip doesn't support appending to zip
+ // files. Copy files from `link.apk` (generated by aapt2) along with classes.dex and
+ // the Go libraries to a new `app.zip` file.
+
+ // Load link.apk as zip.
+ linkAPKZip, err := zip.OpenReader(linkAPK)
+ if err != nil {
+ return err
+ }
+ defer linkAPKZip.Close()
+
+ // Create new "APK".
+ unsignedAPK := filepath.Join(tmpDir, "app.zip")
+ unsignedAPKFile, err := os.Create(unsignedAPK)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := unsignedAPKFile.Close(); err == nil {
+ err = cerr
+ }
+ }()
+ unsignedAPKZip := zip.NewWriter(unsignedAPKFile)
+ defer unsignedAPKZip.Close()
+
+ // Copy files from linkAPK to unsignedAPK.
+ for _, f := range linkAPKZip.File {
+ header := zip.FileHeader{
+ Name: f.FileHeader.Name,
+ Method: f.FileHeader.Method,
+ }
+
+ if isBundle {
+ // AAB have pre-defined folders.
+ switch header.Name {
+ case "AndroidManifest.xml":
+ header.Name = "manifest/AndroidManifest.xml"
+ }
+ }
+
+ w, err := unsignedAPKZip.CreateHeader(&header)
+ if err != nil {
+ return err
+ }
+ r, err := f.Open()
+ if err != nil {
+ return err
+ }
+ if _, err := io.Copy(w, r); err != nil {
+ return err
+ }
+ }
+
+ // Append new files (that doesn't exists inside the link.apk).
+ appendToZip := func(path string, file string) error {
+ f, err := os.Open(file)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ w, err := unsignedAPKZip.CreateHeader(&zip.FileHeader{
+ Name: filepath.ToSlash(path),
+ Method: zip.Deflate,
+ })
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(w, f)
+ return err
+ }
+
+ // Append Go binaries (libgio.so).
+ for _, a := range bi.archs {
+ arch := allArchs[a]
+ libFile := filepath.Join(arch.jniArch, "libgio.so")
+ if err := appendToZip(filepath.Join("lib", libFile),
+ filepath.Join(tmpDir, "jni", libFile)); err != nil {
+ return err
+ }
+ }
+
+ // Append classes.dex.
+ classesFolder := "classes.dex"
+ if isBundle {
+ classesFolder = "dex/classes.dex"
+ }
+ if err := appendToZip(classesFolder,
+ filepath.Join(dexDir, "classes.dex")); err != nil {
+ return err
+ }
+
+ return unsignedAPKZip.Close()
+}
+
+func signAPK(tmpDir string, apkFile string, tools *androidTools,
+ bi *buildInfo) error {
+ if err := zipalign(tools, filepath.Join(tmpDir, "app.zip"),
+ apkFile); err != nil {
+ return err
+ }
+
+ if bi.key == "" {
+ if err := defaultAndroidKeystore(tmpDir, bi); err != nil {
+ return err
+ }
+ }
+
+ _, err := runCmd(exec.Command(
+ filepath.Join(tools.buildtools, "apksigner"),
+ "sign",
+ "--ks-pass", "pass:"+bi.password,
+ "--ks", bi.key,
+ apkFile,
+ ))
+
+ return err
+}
+
+func signAAB(tmpDir string, aabFile string, tools *androidTools,
+ bi *buildInfo) error {
+ allBundleTools, err := filepath.Glob(filepath.Join(tools.buildtools,
+ "bundletool*.jar"))
+ if err != nil {
+ return err
+ }
+
+ bundletool := ""
+ for _, v := range allBundleTools {
+ bundletool = v
+ break
+ }
+
+ if bundletool == "" {
+ return fmt.Errorf("bundletool was not found at %s. Download it from https://github.com/google/bundletool/releases and move to the respective folder",
+ tools.buildtools)
+ }
+
+ _, err = runCmd(exec.Command(
+ "java",
+ "-jar", bundletool,
+ "build-bundle",
+ "--modules="+filepath.Join(tmpDir, "app.zip"),
+ "--output="+filepath.Join(tmpDir, "app.aab"),
+ ))
+ if err != nil {
+ return err
+ }
+
+ if err := zipalign(tools, filepath.Join(tmpDir, "app.aab"),
+ aabFile); err != nil {
+ return err
+ }
+
+ if bi.key == "" {
+ if err := defaultAndroidKeystore(tmpDir, bi); err != nil {
+ return err
+ }
+ }
+
+ keytoolList, err := runCmd(exec.Command(
+ "keytool",
+ "-keystore", bi.key,
+ "-list",
+ "-keypass", bi.password,
+ "-v",
+ ))
+ if err != nil {
+ return err
+ }
+
+ var alias string
+ for _, t := range strings.Split(keytoolList, "\n") {
+ if i, _ := fmt.Sscanf(t, "Alias name: %s", &alias); i > 0 {
+ break
+ }
+ }
+
+ _, err = runCmd(exec.Command(
+ filepath.Join("jarsigner"),
+ "-sigalg", "SHA256withRSA",
+ "-digestalg", "SHA-256",
+ "-keystore", bi.key,
+ "-storepass", bi.password,
+ aabFile,
+ strings.TrimSpace(alias),
+ ))
+
+ return err
+}
+
+func zipalign(tools *androidTools, input, output string) error {
+ _, err := runCmd(exec.Command(
+ filepath.Join(tools.buildtools, "zipalign"),
+ "-f",
+ "4", // 32-bit alignment.
+ input,
+ output,
+ ))
+ return err
+}
+
+func defaultAndroidKeystore(tmpDir string, bi *buildInfo) error {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return err
+ }
+
+ // Use debug.keystore, if exists.
+ bi.key = filepath.Join(home, ".android", "debug.keystore")
+ bi.password = "android"
+ if _, err := os.Stat(bi.key); err == nil {
+ return nil
+ }
+
+ // Generate new key.
+ bi.key = filepath.Join(tmpDir, "sign.keystore")
+ keytool, err := findKeytool()
+ if err != nil {
+ return err
+ }
+ _, err = runCmd(exec.Command(
+ keytool,
+ "-genkey",
+ "-keystore", bi.key,
+ "-storepass", bi.password,
+ "-alias", "android",
+ "-keyalg", "RSA", "-keysize", "2048",
+ "-validity", "10000",
+ "-noprompt",
+ "-dname", "CN=android",
+ ))
+ return err
+}
+
+func findNDK(androidHome string) (string, error) {
+ ndks, err := filepath.Glob(filepath.Join(androidHome, "ndk", "*"))
+ if err != nil {
+ return "", err
+ }
+ if bestNDK, found := latestVersionPath(ndks); found {
+ return bestNDK, nil
+ }
+ // The old NDK path was $ANDROID_SDK_ROOT/ndk-bundle.
+ ndkBundle := filepath.Join(androidHome, "ndk-bundle")
+ if _, err := os.Stat(ndkBundle); err == nil {
+ return ndkBundle, nil
+ }
+ // Certain non-standard NDK isntallations set the $ANDROID_NDK_ROOT
+ // environment variable
+ if ndkBundle, ok := os.LookupEnv("ANDROID_NDK_ROOT"); ok {
+ if _, err := os.Stat(ndkBundle); err == nil {
+ return ndkBundle, nil
+ }
+ }
+
+ return "", fmt.Errorf("no NDK found in $ANDROID_SDK_ROOT (%s). Set $ANDROID_NDK_ROOT or use `sdkmanager ndk-bundle` to install the NDK",
+ androidHome)
+}
+
+func findKeytool() (string, error) {
+ keytool, err := exec.LookPath("keytool")
+ if err == nil {
+ return keytool, err
+ }
+ javaHome := os.Getenv("JAVA_HOME")
+ if javaHome == "" {
+ return "", err
+ }
+ keytool = filepath.Join(javaHome, "jre", "bin", "keytool"+exeSuffix)
+ if _, serr := os.Stat(keytool); serr == nil {
+ return keytool, nil
+ }
+ return "", err
+}
+
+func findJavaC() (string, error) {
+ javac, err := exec.LookPath("javac")
+ if err == nil {
+ return javac, err
+ }
+ javaHome := os.Getenv("JAVA_HOME")
+ if javaHome == "" {
+ return "", err
+ }
+ javac = filepath.Join(javaHome, "bin", "javac"+exeSuffix)
+ if _, serr := os.Stat(javac); serr == nil {
+ return javac, nil
+ }
+ return "", err
+}
+
+func writeJar(jarFile, dir string) (err error) {
+ jar, err := os.Create(jarFile)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := jar.Close(); err == nil {
+ err = cerr
+ }
+ }()
+ jarw := newZipWriter(jar)
+ const manifestHeader = `Manifest-Version: 1.0
+Created-By: 1.0 (Go)
+
+`
+ jarw.Create("META-INF/MANIFEST.MF").Write([]byte(manifestHeader))
+ err = filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if f.IsDir() {
+ return nil
+ }
+ if filepath.Ext(path) == ".class" {
+ rel := filepath.ToSlash(path[len(dir)+1:])
+ jarw.Add(rel, path)
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ return jarw.Close()
+}
+
+func archNDK() string {
+ var arch string
+ switch runtime.GOARCH {
+ case "386":
+ arch = "x86"
+ case "amd64":
+ arch = "x86_64"
+ default:
+ panic("unsupported GOARCH: " + runtime.GOARCH)
+ }
+ return runtime.GOOS + "-" + arch
+}
+
+func getPermissions(ps []string) ([]string, []string) {
+ var permissions, features []string
+ seenPermissions := make(map[string]bool)
+ seenFeatures := make(map[string]bool)
+ for _, perm := range ps {
+ for _, x := range AndroidPermissions[perm] {
+ if !seenPermissions[x] {
+ permissions = append(permissions, x)
+ seenPermissions[x] = true
+ }
+ }
+ for _, x := range AndroidFeatures[perm] {
+ if !seenFeatures[x] {
+ features = append(features, x)
+ seenFeatures[x] = true
+ }
+ }
+ }
+ return permissions, features
+}
+
+func latestPlatform(sdk string) (string, error) {
+ allPlats, err := filepath.Glob(filepath.Join(sdk, "platforms", "android-*"))
+ if err != nil {
+ return "", err
+ }
+ var bestVer int
+ var bestPlat string
+ for _, platform := range allPlats {
+ _, name := filepath.Split(platform)
+ // The glob above guarantees the "android-" prefix.
+ verStr := name[len("android-"):]
+ ver, err := strconv.Atoi(verStr)
+ if err != nil {
+ continue
+ }
+ if ver < bestVer {
+ continue
+ }
+ bestVer = ver
+ bestPlat = platform
+ }
+ if bestPlat == "" {
+ return "", fmt.Errorf("no platforms found in %q", sdk)
+ }
+ return bestPlat, nil
+}
+
+func latestCompiler(tcRoot, a string, minsdk int) (string, error) {
+ arch := allArchs[a]
+ allComps, err := filepath.Glob(filepath.Join(tcRoot, "bin",
+ arch.clangArch+"*-clang"))
+ if err != nil {
+ return "", err
+ }
+ var bestVer int
+ var firstVer int
+ var bestCompiler string
+ var firstCompiler string
+ for _, compiler := range allComps {
+ var ver int
+ pattern := filepath.Join(tcRoot, "bin", arch.clangArch) + "%d-clang"
+ if n, err := fmt.Sscanf(compiler, pattern, &ver); n < 1 || err != nil {
+ continue
+ }
+ if firstCompiler == "" || ver < firstVer {
+ firstVer = ver
+ firstCompiler = compiler
+ }
+ if ver < bestVer {
+ continue
+ }
+ if ver > minsdk {
+ continue
+ }
+ bestVer = ver
+ bestCompiler = compiler
+ }
+ if bestCompiler == "" {
+ bestCompiler = firstCompiler
+ }
+ if bestCompiler == "" {
+ return "", fmt.Errorf("no NDK compiler found for architecture %s in %s",
+ a, tcRoot)
+ }
+ return bestCompiler, nil
+}
+
+func latestTools(sdk string) (string, error) {
+ allTools, err := filepath.Glob(filepath.Join(sdk, "build-tools", "*"))
+ if err != nil {
+ return "", err
+ }
+ tools, found := latestVersionPath(allTools)
+ if !found {
+ return "", fmt.Errorf("no build-tools found in %q", sdk)
+ }
+ return tools, nil
+}
+
+// latestVersionFile finds the path with the highest version
+// among paths on the form
+//
+// /some/path/major.minor.patch
+func latestVersionPath(paths []string) (string, bool) {
+ var bestVer [3]int
+ var bestDir string
+loop:
+ for _, path := range paths {
+ name := filepath.Base(path)
+ s := strings.SplitN(name, ".", 3)
+ if len(s) != len(bestVer) {
+ continue
+ }
+ var version [3]int
+ for i, v := range s {
+ v, err := strconv.Atoi(v)
+ if err != nil {
+ continue loop
+ }
+ if v < bestVer[i] {
+ continue loop
+ }
+ if v > bestVer[i] {
+ break
+ }
+ version[i] = v
+ }
+ bestVer = version
+ bestDir = path
+ }
+ return bestDir, bestDir != ""
+}
+
+func newZipWriter(w io.Writer) *zipWriter {
+ return &zipWriter{
+ w: zip.NewWriter(w),
+ }
+}
+
+func (z *zipWriter) Close() error {
+ err := z.w.Close()
+ if z.err == nil {
+ z.err = err
+ }
+ return z.err
+}
+
+func (z *zipWriter) Create(name string) io.Writer {
+ if z.err != nil {
+ return ioutil.Discard
+ }
+ w, err := z.w.Create(name)
+ if err != nil {
+ z.err = err
+ return ioutil.Discard
+ }
+ return &errWriter{w: w, err: &z.err}
+}
+
+func (z *zipWriter) Store(name, file string) {
+ z.add(name, file, false)
+}
+
+func (z *zipWriter) Add(name, file string) {
+ z.add(name, file, true)
+}
+
+func (z *zipWriter) add(name, file string, compressed bool) {
+ if z.err != nil {
+ return
+ }
+ f, err := os.Open(file)
+ if err != nil {
+ z.err = err
+ return
+ }
+ defer f.Close()
+ fh := &zip.FileHeader{
+ Name: name,
+ }
+ if compressed {
+ fh.Method = zip.Deflate
+ }
+ w, err := z.w.CreateHeader(fh)
+ if err != nil {
+ z.err = err
+ return
+ }
+ if _, err := io.Copy(w, f); err != nil {
+ z.err = err
+ return
+ }
+}
+
+func (w *errWriter) Write(p []byte) (n int, err error) {
+ if err := *w.err; err != nil {
+ return 0, err
+ }
+ n, err = w.w.Write(p)
+ *w.err = err
+ return
+}
diff --git a/gio/giold/cmd/gogio/build_info.go b/gio/giold/cmd/gogio/build_info.go
new file mode 100644
index 0000000..ecda1f3
--- /dev/null
+++ b/gio/giold/cmd/gogio/build_info.go
@@ -0,0 +1,160 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+type buildInfo struct {
+ appID string
+ archs []string
+ ldflags string
+ minsdk int
+ name string
+ pkgDir string
+ pkgPath string
+ iconPath string
+ tags string
+ target string
+ version int
+ key string
+ password string
+}
+
+func newBuildInfo(pkgPath string) (*buildInfo, error) {
+ pkgMetadata, err := getPkgMetadata(pkgPath)
+ if err != nil {
+ return nil, err
+ }
+ appID := getAppID(pkgMetadata)
+ appIcon := filepath.Join(pkgMetadata.Dir, "appicon.png")
+ if *iconPath != "" {
+ appIcon = *iconPath
+ }
+ bi := &buildInfo{
+ appID: appID,
+ archs: getArchs(),
+ ldflags: getLdFlags(appID),
+ minsdk: *minsdk,
+ name: getPkgName(pkgMetadata),
+ pkgDir: pkgMetadata.Dir,
+ pkgPath: pkgPath,
+ iconPath: appIcon,
+ tags: *extraTags,
+ target: *target,
+ version: *version,
+ key: *signKey,
+ password: *signPass,
+ }
+ return bi, nil
+}
+
+func getArchs() []string {
+ if *archNames != "" {
+ return strings.Split(*archNames, ",")
+ }
+ switch *target {
+ case "js":
+ return []string{"wasm"}
+ case "ios", "tvos":
+ // Only 64-bit support.
+ return []string{"arm64", "amd64"}
+ case "android":
+ return []string{"arm", "arm64", "386", "amd64"}
+ case "windows":
+ goarch := os.Getenv("GOARCH")
+ if goarch == "" {
+ goarch = runtime.GOARCH
+ }
+ return []string{goarch}
+ default:
+ // TODO: Add flag tests.
+ panic("The target value has already been validated, this will never execute.")
+ }
+}
+
+func getLdFlags(appID string) string {
+ var ldflags []string
+ if extra := *extraLdflags; extra != "" {
+ ldflags = append(ldflags, strings.Split(extra, " ")...)
+ }
+ // Pass appID along, to be used for logging on platforms like Android.
+ ldflags = append(ldflags,
+ fmt.Sprintf("-X realy.lol/gio/app/internal/log.appID=%s", appID))
+ // Pass along all remaining arguments to the app.
+ if appArgs := flag.Args()[1:]; len(appArgs) > 0 {
+ ldflags = append(ldflags,
+ fmt.Sprintf("-X realy.lol/gio/app.extraArgs=%s",
+ strings.Join(appArgs, "|")))
+ }
+ if m := *linkMode; m != "" {
+ ldflags = append(ldflags, "-linkmode="+m)
+ }
+ return strings.Join(ldflags, " ")
+}
+
+type packageMetadata struct {
+ PkgPath string
+ Dir string
+}
+
+func getPkgMetadata(pkgPath string) (*packageMetadata, error) {
+ pkgImportPath, err := runCmd(exec.Command("go", "list", "-f",
+ "{{.ImportPath}}", pkgPath))
+ if err != nil {
+ return nil, err
+ }
+ pkgDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", pkgPath))
+ if err != nil {
+ return nil, err
+ }
+ return &packageMetadata{
+ PkgPath: pkgImportPath,
+ Dir: pkgDir,
+ }, nil
+}
+
+func getAppID(pkgMetadata *packageMetadata) string {
+ if *appID != "" {
+ return *appID
+ }
+ elems := strings.Split(pkgMetadata.PkgPath, "/")
+ domain := strings.Split(elems[0], ".")
+ name := ""
+ if len(elems) > 1 {
+ name = "." + elems[len(elems)-1]
+ }
+ if len(elems) < 2 && len(domain) < 2 {
+ name = "." + domain[0]
+ domain[0] = "localhost"
+ } else {
+ for i := 0; i < len(domain)/2; i++ {
+ opp := len(domain) - 1 - i
+ domain[i], domain[opp] = domain[opp], domain[i]
+ }
+ }
+
+ pkgDomain := strings.Join(domain, ".")
+ appid := []rune(pkgDomain + name)
+
+ // a Java-language-style package name may contain upper- and lower-case
+ // letters and underscores with individual parts separated by '.'.
+ // https://developer.android.com/guide/topics/manifest/manifest-element
+ for i, c := range appid {
+ if !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' ||
+ c == '_' || c == '.') {
+ appid[i] = '_'
+ }
+ }
+ return string(appid)
+}
+
+func getPkgName(pkgMetadata *packageMetadata) string {
+ return path.Base(pkgMetadata.PkgPath)
+}
diff --git a/gio/giold/cmd/gogio/build_info_test.go b/gio/giold/cmd/gogio/build_info_test.go
new file mode 100644
index 0000000..397e2a3
--- /dev/null
+++ b/gio/giold/cmd/gogio/build_info_test.go
@@ -0,0 +1,32 @@
+package main
+
+import "testing"
+
+type expval struct {
+ in, out string
+}
+
+func TestAppID(t *testing.T) {
+ t.Parallel()
+
+ tests := []expval{
+ {"example", "localhost.example"},
+ {"example.com", "com.example"},
+ {"www.example.com", "com.example.www"},
+ {"examplecom/app", "examplecom.app"},
+ {"example.com/app", "com.example.app"},
+ {"www.example.com/app", "com.example.www.app"},
+ {"www.en.example.com/app", "com.example.en.www.app"},
+ {"example.com/dir/app", "com.example.app"},
+ {"example.com/dir.ext/app", "com.example.app"},
+ {"example.com/dir/app.ext", "com.example.app.ext"},
+ {"example-com.net/dir/app", "net.example_com.app"},
+ }
+
+ for i, test := range tests {
+ got := getAppID(&packageMetadata{PkgPath: test.in})
+ if exp := test.out; got != exp {
+ t.Errorf("(%d): expected '%s', got '%s'", i, exp, got)
+ }
+ }
+}
diff --git a/gio/giold/cmd/gogio/doc.go b/gio/giold/cmd/gogio/doc.go
new file mode 100644
index 0000000..6b788fd
--- /dev/null
+++ b/gio/giold/cmd/gogio/doc.go
@@ -0,0 +1,10 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+The gogio tool builds and packages Gio programs for Android, iOS/tvOS
+and WebAssembly.
+
+Run gogio with no arguments for instructions, or see the examples at
+https://realy.lol/gio.
+*/
+package main
diff --git a/gio/giold/cmd/gogio/e2e_test.go b/gio/giold/cmd/gogio/e2e_test.go
new file mode 100644
index 0000000..893f580
--- /dev/null
+++ b/gio/giold/cmd/gogio/e2e_test.go
@@ -0,0 +1,334 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main_test
+
+import (
+ "bufio"
+ "errors"
+ "flag"
+ "fmt"
+ "image"
+ "image/color"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "strings"
+ "testing"
+ "time"
+)
+
+var raceEnabled = false
+
+var headless = flag.Bool("headless", true,
+ "run end-to-end tests in headless mode")
+
+const appid = "localhost.gogio.endtoend"
+
+// TestDriver is implemented by each of the platforms we can run end-to-end
+// tests on. None of its methods return any errors, as the errors are directly
+// reported to testing.T via methods like Fatal.
+type TestDriver interface {
+ initBase(t *testing.T, width, height int)
+
+ // Start opens the Gio app found at path. The driver should attempt to
+ // run the app with the base driver's width and height, and the
+ // platform's background should be white.
+ //
+ // When the function returns, the gio app must be ready to use on the
+ // platform, with its initial frame fully drawn.
+ Start(path string)
+
+ // Screenshot takes a screenshot of the Gio app on the platform.
+ Screenshot() image.Image
+
+ // Click performs a pointer click at the specified coordinates,
+ // including both press and release. It returns when the next frame is
+ // fully drawn.
+ Click(x, y int)
+}
+
+type driverBase struct {
+ *testing.T
+
+ width, height int
+
+ output io.Reader
+ frameNotifs chan bool
+}
+
+func (d *driverBase) initBase(t *testing.T, width, height int) {
+ d.T = t
+ d.width, d.height = width, height
+}
+
+func TestEndToEnd(t *testing.T) {
+ if testing.Short() {
+ t.Skipf("end-to-end tests tend to be slow")
+ }
+
+ t.Parallel()
+
+ const (
+ testdataWithGoImportPkgPath = "realy.lol/gio/cmd/gogio/testdata"
+ testdataWithRelativePkgPath = "testdata/testdata.go"
+ )
+ // Keep this list local, to not reuse TestDriver objects.
+ subtests := []struct {
+ name string
+ driver TestDriver
+ pkgPath string
+ }{
+ {"X11 using go import path", &X11TestDriver{},
+ testdataWithGoImportPkgPath},
+ {"X11", &X11TestDriver{}, testdataWithRelativePkgPath},
+ {"Wayland", &WaylandTestDriver{}, testdataWithRelativePkgPath},
+ {"JS", &JSTestDriver{}, testdataWithRelativePkgPath},
+ {"Android", &AndroidTestDriver{}, testdataWithRelativePkgPath},
+ {"Windows", &WineTestDriver{}, testdataWithRelativePkgPath},
+ }
+
+ for _, subtest := range subtests {
+ t.Run(subtest.name, func(t *testing.T) {
+ subtest := subtest // copy the changing loop variable
+ t.Parallel()
+ runEndToEndTest(t, subtest.driver, subtest.pkgPath)
+ })
+ }
+}
+
+func runEndToEndTest(t *testing.T, driver TestDriver, pkgPath string) {
+ size := image.Point{X: 800, Y: 600}
+ driver.initBase(t, size.X, size.Y)
+
+ t.Log("starting driver and gio app")
+ driver.Start(pkgPath)
+
+ beef := color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff}
+ white := color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
+ black := color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}
+ gray := color.NRGBA{R: 0xbb, G: 0xbb, B: 0xbb, A: 0xff}
+ red := color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
+
+ // These are the four colors at the beginning.
+ t.Log("taking initial screenshot")
+ withRetries(t, 4*time.Second, func() error {
+ img := driver.Screenshot()
+ size = img.Bounds().Size() // override the default size
+ return checkImageCorners(img, beef, white, black, gray)
+ })
+
+ // TODO(mvdan): implement this properly in the Wayland driver; swaymsg
+ // almost works to automate clicks, but the button presses end up in the
+ // wrong coordinates.
+ if _, ok := driver.(*WaylandTestDriver); ok {
+ return
+ }
+
+ // Click the first and last sections to turn them red.
+ t.Log("clicking twice and taking another screenshot")
+ driver.Click(1*(size.X/4), 1*(size.Y/4))
+ driver.Click(3*(size.X/4), 3*(size.Y/4))
+ withRetries(t, 4*time.Second, func() error {
+ img := driver.Screenshot()
+ return checkImageCorners(img, red, white, black, red)
+ })
+}
+
+// withRetries keeps retrying fn until it succeeds, or until the timeout is hit.
+// It uses a rudimentary kind of backoff, which starts with 100ms delays. As
+// such, timeout should generally be in the order of seconds.
+func withRetries(t *testing.T, timeout time.Duration, fn func() error) {
+ t.Helper()
+
+ timeoutTimer := time.NewTimer(timeout)
+ defer timeoutTimer.Stop()
+ backoff := 100 * time.Millisecond
+
+ tries := 0
+ var lastErr error
+ for {
+ if lastErr = fn(); lastErr == nil {
+ return
+ }
+ tries++
+ t.Logf("retrying after %s", backoff)
+
+ // Use a timer instead of a sleep, so that the timeout can stop
+ // the backoff early. Don't reuse this timer, since we're not in
+ // a hot loop, and we don't want tricky code.
+ backoffTimer := time.NewTimer(backoff)
+ defer backoffTimer.Stop()
+
+ select {
+ case <-timeoutTimer.C:
+ t.Errorf("last error: %v", lastErr)
+ t.Fatalf("hit timeout of %s after %d tries", timeout, tries)
+ case <-backoffTimer.C:
+ }
+
+ // Keep doubling it until a maximum. With the start at 100ms,
+ // we'll do: 100ms, 200ms, 400ms, 800ms, 1.6s, and 2s forever.
+ backoff *= 2
+ if max := 2 * time.Second; backoff > max {
+ backoff = max
+ }
+ }
+}
+
+type colorMismatch struct {
+ x, y int
+ wantRGB, gotRGB [3]uint32
+}
+
+func (m colorMismatch) String() string {
+ return fmt.Sprintf("%3d,%-3d got 0x%04x%04x%04x, want 0x%04x%04x%04x",
+ m.x, m.y,
+ m.gotRGB[0], m.gotRGB[1], m.gotRGB[2],
+ m.wantRGB[0], m.wantRGB[1], m.wantRGB[2],
+ )
+}
+
+func checkImageCorners(img image.Image,
+ topLeft, topRight, botLeft, botRight color.Color) error {
+ // The colors are split in four rectangular sections. Check the corners
+ // of each of the sections. We check the corners left to right, top to
+ // bottom, like when reading left-to-right text.
+
+ size := img.Bounds().Size()
+ var mismatches []colorMismatch
+
+ checkColor := func(x, y int, want color.Color) {
+ r, g, b, _ := want.RGBA()
+ got := img.At(x, y)
+ r_, g_, b_, _ := got.RGBA()
+ if r_ != r || g_ != g || b_ != b {
+ mismatches = append(mismatches, colorMismatch{
+ x: x,
+ y: y,
+ wantRGB: [3]uint32{r, g, b},
+ gotRGB: [3]uint32{r_, g_, b_},
+ })
+ }
+ }
+
+ {
+ minX, minY := 5, 5
+ maxX, maxY := (size.X/2)-5, (size.Y/2)-5
+ checkColor(minX, minY, topLeft)
+ checkColor(maxX, minY, topLeft)
+ checkColor(minX, maxY, topLeft)
+ checkColor(maxX, maxY, topLeft)
+ }
+ {
+ minX, minY := (size.X/2)+5, 5
+ maxX, maxY := size.X-5, (size.Y/2)-5
+ checkColor(minX, minY, topRight)
+ checkColor(maxX, minY, topRight)
+ checkColor(minX, maxY, topRight)
+ checkColor(maxX, maxY, topRight)
+ }
+ {
+ minX, minY := 5, (size.Y/2)+5
+ maxX, maxY := (size.X/2)-5, size.Y-5
+ checkColor(minX, minY, botLeft)
+ checkColor(maxX, minY, botLeft)
+ checkColor(minX, maxY, botLeft)
+ checkColor(maxX, maxY, botLeft)
+ }
+ {
+ minX, minY := (size.X/2)+5, (size.Y/2)+5
+ maxX, maxY := size.X-5, size.Y-5
+ checkColor(minX, minY, botRight)
+ checkColor(maxX, minY, botRight)
+ checkColor(minX, maxY, botRight)
+ checkColor(maxX, maxY, botRight)
+ }
+ if n := len(mismatches); n > 0 {
+ b := new(strings.Builder)
+ fmt.Fprintf(b, "encountered %d color mismatches:\n", n)
+ for _, m := range mismatches {
+ fmt.Fprintf(b, "%s\n", m)
+ }
+ return errors.New(b.String())
+ }
+ return nil
+}
+
+func (d *driverBase) waitForFrame() {
+ d.Helper()
+
+ if d.frameNotifs == nil {
+ // Start the goroutine that reads output lines and notifies of
+ // new frames via frameNotifs. The test doesn't wait for this
+ // goroutine to finish; it will naturally end when the output
+ // reader reaches an error like EOF.
+ d.frameNotifs = make(chan bool, 1)
+ if d.output == nil {
+ d.Fatal("need an output reader to be notified of frames")
+ }
+ go func() {
+ scanner := bufio.NewScanner(d.output)
+ for scanner.Scan() {
+ line := scanner.Text()
+ d.Log(line)
+ if strings.Contains(line, "gio frame ready") {
+ d.frameNotifs <- true
+ }
+ }
+ // Since we're only interested in the output while the
+ // app runs, and we don't know when it finishes here,
+ // ignore "already closed" pipe errors.
+ if err := scanner.Err(); err != nil && !errors.Is(err,
+ os.ErrClosed) {
+ d.Errorf("reading app output: %v", err)
+ }
+ }()
+ }
+
+ // Unfortunately, there isn't a way to select on a test failing, since
+ // testing.T doesn't have anything like a context or a "done" channel.
+ //
+ // We can't let selects block forever, since the default -test.timeout
+ // is ten minutes - far too long for tests that take seconds.
+ //
+ // For now, a static short timeout is better than nothing. 5s is plenty
+ // for our simple test app to render on any device.
+ select {
+ case <-d.frameNotifs:
+ case <-time.After(5 * time.Second):
+ d.Fatalf("timed out waiting for a frame to be ready")
+ }
+}
+
+func (d *driverBase) needPrograms(names ...string) {
+ d.Helper()
+ for _, name := range names {
+ if _, err := exec.LookPath(name); err != nil {
+ d.Skipf("%s needed to run", name)
+ }
+ }
+}
+
+func (d *driverBase) tempDir(name string) string {
+ d.Helper()
+ dir, err := ioutil.TempDir("", name)
+ if err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(func() { os.RemoveAll(dir) })
+ return dir
+}
+
+func (d *driverBase) gogio(args ...string) {
+ d.Helper()
+ prog, err := os.Executable()
+ if err != nil {
+ d.Fatal(err)
+ }
+ cmd := exec.Command(prog, args...)
+ cmd.Env = append(os.Environ(), "RUN_GOGIO=1")
+ if out, err := cmd.CombinedOutput(); err != nil {
+ d.Fatalf("gogio error: %s:\n%s", err, out)
+ }
+}
diff --git a/gio/giold/cmd/gogio/help.go b/gio/giold/cmd/gogio/help.go
new file mode 100644
index 0000000..c83d7a3
--- /dev/null
+++ b/gio/giold/cmd/gogio/help.go
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+const mainUsage = `The gogio command builds and packages Gio (realy.lol/gio) programs.
+
+Usage:
+
+ gogio -target [flags] [run arguments]
+
+The gogio tool builds and packages Gio programs for platforms where additional
+metadata or support files are required.
+
+The package argument specifies an import path or a single Go source file to
+package. Any run arguments are appended to os.Args at runtime.
+
+Compiled Java class files from jar files in the package directory are
+included in Android builds.
+
+The mandatory -target flag selects the target platform: ios or android for the
+mobile platforms, tvos for Apple's tvOS, js for WebAssembly/WebGL.
+
+The -arch flag specifies a comma separated list of GOARCHs to include. The
+default is all supported architectures.
+
+The -o flag specifies an output file or directory, depending on the target.
+
+The -buildmode flag selects the build mode. Two build modes are available, exe
+and archive. Buildmode exe outputs an .ipa file for iOS or tvOS, an .apk file
+for Android or a directory with the WebAssembly module and support files for
+a browser.
+
+The -ldflags and -tags flags pass extra linker flags and tags to the go tool.
+
+As a special case for iOS or tvOS, specifying a path that ends with ".app"
+will output an app directory suitable for a simulator.
+
+The other buildmode is archive, which will output an .aar library for Android
+or a .framework for iOS and tvOS.
+
+The -icon flag specifies a path to a PNG image to use as app icon on iOS and Android.
+If left unspecified, the appicon.png file from the main package is used
+(if it exists).
+
+The -appid flag specifies the package name for Android or the bundle id for
+iOS and tvOS. A bundle id must be provisioned through Xcode before the gogio
+tool can use it.
+
+The -version flag specifies the integer version code for Android and the last
+component of the 1.0.X version for iOS and tvOS.
+
+For Android builds the -minsdk flag specify the minimum SDK level. For example,
+use -minsdk 22 to target Android 5.1 (Lollipop) and later.
+
+For Windows builds the -minsdk flag specify the minimum OS version. For example,
+use -mindk 10 to target Windows 10 only, -minsdk 6 for Windows Vista and later.
+
+The -work flag prints the path to the working directory and suppress
+its deletion.
+
+The -x flag will print all the external commands executed by the gogio tool.
+
+The -signkey flag specifies the path of the keystore, used for signing Android apk/aab files.
+
+The -signpass flag specifies the password of the keystore, ignored if -signkey is not provided.
+`
diff --git a/gio/giold/cmd/gogio/iosbuild.go b/gio/giold/cmd/gogio/iosbuild.go
new file mode 100644
index 0000000..9674431
--- /dev/null
+++ b/gio/giold/cmd/gogio/iosbuild.go
@@ -0,0 +1,591 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "archive/zip"
+ "crypto/sha1"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "golang.org/x/sync/errgroup"
+)
+
+const minIOSVersion = "9.0"
+
+func buildIOS(tmpDir, target string, bi *buildInfo) error {
+ appName := bi.name
+ switch *buildMode {
+ case "archive":
+ framework := *destPath
+ if framework == "" {
+ framework = fmt.Sprintf("%s.framework", strings.Title(appName))
+ }
+ return archiveIOS(tmpDir, target, framework, bi)
+ case "exe":
+ out := *destPath
+ if out == "" {
+ out = appName + ".ipa"
+ }
+ forDevice := strings.HasSuffix(out, ".ipa")
+ // Filter out unsupported architectures.
+ for i := len(bi.archs) - 1; i >= 0; i-- {
+ switch bi.archs[i] {
+ case "arm", "arm64":
+ if forDevice {
+ continue
+ }
+ case "386", "amd64":
+ if !forDevice {
+ continue
+ }
+ }
+
+ bi.archs = append(bi.archs[:i], bi.archs[i+1:]...)
+ }
+ tmpFramework := filepath.Join(tmpDir, "Gio.framework")
+ if err := archiveIOS(tmpDir, target, tmpFramework, bi); err != nil {
+ return err
+ }
+ if !forDevice && !strings.HasSuffix(out, ".app") {
+ return fmt.Errorf("the specified output directory %q does not end in .app or .ipa",
+ out)
+ }
+ if !forDevice {
+ return exeIOS(tmpDir, target, out, bi)
+ }
+ payload := filepath.Join(tmpDir, "Payload")
+ appDir := filepath.Join(payload, appName+".app")
+ if err := os.MkdirAll(appDir, 0755); err != nil {
+ return err
+ }
+ if err := exeIOS(tmpDir, target, appDir, bi); err != nil {
+ return err
+ }
+ if err := signIOS(bi, tmpDir, appDir); err != nil {
+ return err
+ }
+ return zipDir(out, tmpDir, "Payload")
+ default:
+ panic("unreachable")
+ }
+}
+
+func signIOS(bi *buildInfo, tmpDir, app string) error {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return err
+ }
+ provPattern := filepath.Join(home, "Library", "MobileDevice",
+ "Provisioning Profiles", "*.mobileprovision")
+ provisions, err := filepath.Glob(provPattern)
+ if err != nil {
+ return err
+ }
+ provInfo := filepath.Join(tmpDir, "provision.plist")
+ var avail []string
+ for _, prov := range provisions {
+ // Decode the provision file to a plist.
+ _, err := runCmd(exec.Command("security", "cms", "-D", "-i", prov, "-o",
+ provInfo))
+ if err != nil {
+ return err
+ }
+ expUnix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c",
+ "Print:ExpirationDate", provInfo))
+ if err != nil {
+ return err
+ }
+ exp, err := time.Parse(time.UnixDate, expUnix)
+ if err != nil {
+ return fmt.Errorf("sign: failed to parse expiration date from %q: %v",
+ prov, err)
+ }
+ if exp.Before(time.Now()) {
+ continue
+ }
+ appIDPrefix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c",
+ "Print:ApplicationIdentifierPrefix:0", provInfo))
+ if err != nil {
+ return err
+ }
+ provAppID, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c",
+ "Print:Entitlements:application-identifier", provInfo))
+ if err != nil {
+ return err
+ }
+ expAppID := fmt.Sprintf("%s.%s", appIDPrefix, bi.appID)
+ avail = append(avail, provAppID)
+ if expAppID != provAppID {
+ continue
+ }
+ // Copy provisioning file.
+ embedded := filepath.Join(app, "embedded.mobileprovision")
+ if err := copyFile(embedded, prov); err != nil {
+ return err
+ }
+ certDER, err := runCmdRaw(exec.Command("/usr/libexec/PlistBuddy", "-c",
+ "Print:DeveloperCertificates:0", provInfo))
+ if err != nil {
+ return err
+ }
+ // Omit trailing newline.
+ certDER = certDER[:len(certDER)-1]
+ entitlements, err := runCmd(exec.Command("/usr/libexec/PlistBuddy",
+ "-x", "-c", "Print:Entitlements", provInfo))
+ if err != nil {
+ return err
+ }
+ entFile := filepath.Join(tmpDir, "entitlements.plist")
+ if err := ioutil.WriteFile(entFile, []byte(entitlements),
+ 0660); err != nil {
+ return err
+ }
+ identity := sha1.Sum(certDER)
+ idHex := hex.EncodeToString(identity[:])
+ _, err = runCmd(exec.Command("codesign", "-s", idHex, "-v",
+ "--entitlements", entFile, app))
+ return err
+ }
+ return fmt.Errorf("sign: no valid provisioning profile found for bundle id %q among %v",
+ bi.appID, avail)
+}
+
+func exeIOS(tmpDir, target, app string, bi *buildInfo) error {
+ if bi.appID == "" {
+ return errors.New("app id is empty; use -appid to set it")
+ }
+ if err := os.RemoveAll(app); err != nil {
+ return err
+ }
+ if err := os.Mkdir(app, 0755); err != nil {
+ return err
+ }
+ mainm := filepath.Join(tmpDir, "main.m")
+ const mainmSrc = `@import UIKit;
+@import Gio;
+
+@interface GioAppDelegate : UIResponder
+@property (strong, nonatomic) UIWindow *window;
+@end
+
+@implementation GioAppDelegate
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
+ GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil];
+ self.window.rootViewController = controller;
+ [self.window makeKeyAndVisible];
+ return YES;
+}
+@end
+
+int main(int argc, char * argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([GioAppDelegate class]));
+ }
+}`
+ if err := ioutil.WriteFile(mainm, []byte(mainmSrc), 0660); err != nil {
+ return err
+ }
+ appName := strings.Title(bi.name)
+ exe := filepath.Join(app, appName)
+ lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create")
+ var builds errgroup.Group
+ for _, a := range bi.archs {
+ clang, cflags, err := iosCompilerFor(target, a)
+ if err != nil {
+ return err
+ }
+ exeSlice := filepath.Join(tmpDir, "app-"+a)
+ lipo.Args = append(lipo.Args, exeSlice)
+ compile := exec.Command(clang, cflags...)
+ compile.Args = append(compile.Args,
+ "-Werror",
+ "-fmodules",
+ "-fobjc-arc",
+ "-x", "objective-c",
+ "-F", tmpDir,
+ "-o", exeSlice,
+ mainm,
+ )
+ builds.Go(func() error {
+ _, err := runCmd(compile)
+ return err
+ })
+ }
+ if err := builds.Wait(); err != nil {
+ return err
+ }
+ if _, err := runCmd(lipo); err != nil {
+ return err
+ }
+ infoPlist := buildInfoPlist(bi)
+ plistFile := filepath.Join(app, "Info.plist")
+ if err := ioutil.WriteFile(plistFile, []byte(infoPlist), 0660); err != nil {
+ return err
+ }
+ if _, err := os.Stat(bi.iconPath); err == nil {
+ assetPlist, err := iosIcons(bi, tmpDir, app, bi.iconPath)
+ if err != nil {
+ return err
+ }
+ // Merge assets plist with Info.plist
+ cmd := exec.Command(
+ "/usr/libexec/PlistBuddy",
+ "-c", "Merge "+assetPlist,
+ plistFile,
+ )
+ if _, err := runCmd(cmd); err != nil {
+ return err
+ }
+ }
+ if _, err := runCmd(exec.Command("plutil", "-convert", "binary1",
+ plistFile)); err != nil {
+ return err
+ }
+ return nil
+}
+
+// iosIcons builds an asset catalog and compile it with the Xcode command actool.
+// iosIcons returns the asset plist file to be merged into Info.plist.
+func iosIcons(bi *buildInfo, tmpDir, appDir, icon string) (string, error) {
+ assets := filepath.Join(tmpDir, "Assets.xcassets")
+ if err := os.Mkdir(assets, 0700); err != nil {
+ return "", err
+ }
+ appIcon := filepath.Join(assets, "AppIcon.appiconset")
+ err := buildIcons(appIcon, icon, []iconVariant{
+ {path: "ios_2x.png", size: 120},
+ {path: "ios_3x.png", size: 180},
+ // The App Store icon is not allowed to contain
+ // transparent pixels.
+ {path: "ios_store.png", size: 1024, fill: true},
+ })
+ if err != nil {
+ return "", err
+ }
+ contentJson := `{
+ "images" : [
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "ios_2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "ios_3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "ios_store.png",
+ "scale" : "1x"
+ }
+ ]
+}`
+ contentFile := filepath.Join(appIcon, "Contents.json")
+ if err := ioutil.WriteFile(contentFile, []byte(contentJson),
+ 0600); err != nil {
+ return "", err
+ }
+ assetPlist := filepath.Join(tmpDir, "assets.plist")
+ compile := exec.Command(
+ "actool",
+ "--compile", appDir,
+ "--platform", iosPlatformFor(bi.target),
+ "--minimum-deployment-target", minIOSVersion,
+ "--app-icon", "AppIcon",
+ "--output-partial-info-plist", assetPlist,
+ assets)
+ _, err = runCmd(compile)
+ return assetPlist, err
+}
+
+func buildInfoPlist(bi *buildInfo) string {
+ appName := strings.Title(bi.name)
+ platform := iosPlatformFor(bi.target)
+ var supportPlatform string
+ switch bi.target {
+ case "ios":
+ supportPlatform = "iPhoneOS"
+ case "tvos":
+ supportPlatform = "AppleTVOS"
+ }
+ return fmt.Sprintf(`
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ %s
+ CFBundleIdentifier
+ %s
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ %s
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0.%d
+ CFBundleVersion
+ %d
+ UILaunchStoryboardName
+ LaunchScreen
+ UIRequiredDeviceCapabilities
+ arm64
+ DTPlatformName
+ %s
+ DTPlatformVersion
+ 12.4
+ MinimumOSVersion
+ %s
+ UIDeviceFamily
+
+ 1
+
+ CFBundleSupportedPlatforms
+
+ %s
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ DTCompiler
+ com.apple.compilers.llvm.clang.1_0
+ DTPlatformBuild
+ 16G73
+ DTSDKBuild
+ 16G73
+ DTSDKName
+ %s12.4
+ DTXcode
+ 1030
+ DTXcodeBuild
+ 10G8
+
+`, appName, bi.appID, appName, bi.version, bi.version, platform,
+ minIOSVersion, supportPlatform, platform)
+}
+
+func iosPlatformFor(target string) string {
+ switch target {
+ case "ios":
+ return "iphoneos"
+ case "tvos":
+ return "appletvos"
+ default:
+ panic("invalid platform " + target)
+ }
+}
+
+func archiveIOS(tmpDir, target, frameworkRoot string, bi *buildInfo) error {
+ framework := filepath.Base(frameworkRoot)
+ const suf = ".framework"
+ if !strings.HasSuffix(framework, suf) {
+ return fmt.Errorf("the specified output %q does not end in '.framework'",
+ frameworkRoot)
+ }
+ framework = framework[:len(framework)-len(suf)]
+ if err := os.RemoveAll(frameworkRoot); err != nil {
+ return err
+ }
+ frameworkDir := filepath.Join(frameworkRoot, "Versions", "A")
+ for _, dir := range []string{"Headers", "Modules"} {
+ p := filepath.Join(frameworkDir, dir)
+ if err := os.MkdirAll(p, 0755); err != nil {
+ return err
+ }
+ }
+ symlinks := [][2]string{
+ {"Versions/Current/Headers", "Headers"},
+ {"Versions/Current/Modules", "Modules"},
+ {"Versions/Current/" + framework, framework},
+ {"A", filepath.Join("Versions", "Current")},
+ }
+ for _, l := range symlinks {
+ if err := os.Symlink(l[0], filepath.Join(frameworkRoot,
+ l[1])); err != nil && !os.IsExist(err) {
+ return err
+ }
+ }
+ exe := filepath.Join(frameworkDir, framework)
+ lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create")
+ var builds errgroup.Group
+ tags := bi.tags
+ goos := "ios"
+ supportsIOS, err := supportsGOOS("ios")
+ if err != nil {
+ return err
+ }
+ if !supportsIOS {
+ // Go 1.15 and earlier target iOS with GOOS=darwin, tags=ios.
+ goos = "darwin"
+ tags = "ios " + tags
+ }
+ for _, a := range bi.archs {
+ clang, cflags, err := iosCompilerFor(target, a)
+ if err != nil {
+ return err
+ }
+ lib := filepath.Join(tmpDir, "gio-"+a)
+ cmd := exec.Command(
+ "go",
+ "build",
+ "-ldflags=-s -w "+bi.ldflags,
+ "-buildmode=c-archive",
+ "-o", lib,
+ "-tags", tags,
+ bi.pkgPath,
+ )
+ lipo.Args = append(lipo.Args, lib)
+ cflagsLine := strings.Join(cflags, " ")
+ cmd.Env = append(
+ os.Environ(),
+ "GOOS="+goos,
+ "GOARCH="+a,
+ "CGO_ENABLED=1",
+ "CC="+clang,
+ "CGO_CFLAGS="+cflagsLine,
+ "CGO_LDFLAGS="+cflagsLine,
+ )
+ builds.Go(func() error {
+ _, err := runCmd(cmd)
+ return err
+ })
+ }
+ if err := builds.Wait(); err != nil {
+ return err
+ }
+ if _, err := runCmd(lipo); err != nil {
+ return err
+ }
+ appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}",
+ "realy.lol/gio/app/internal/wm"))
+ if err != nil {
+ return err
+ }
+ headerDst := filepath.Join(frameworkDir, "Headers", framework+".h")
+ headerSrc := filepath.Join(appDir, "framework_ios.h")
+ if err := copyFile(headerDst, headerSrc); err != nil {
+ return err
+ }
+ module := fmt.Sprintf(`framework module "%s" {
+ header "%[1]s.h"
+
+ export *
+}`, framework)
+ moduleFile := filepath.Join(frameworkDir, "Modules", "module.modulemap")
+ return ioutil.WriteFile(moduleFile, []byte(module), 0644)
+}
+
+func supportsGOOS(wantGoos string) (bool, error) {
+ geese, err := runCmd(exec.Command("go", "tool", "dist", "list"))
+ if err != nil {
+ return false, err
+ }
+ for _, pair := range strings.Split(geese, "\n") {
+ s := strings.SplitN(pair, "/", 2)
+ if len(s) != 2 {
+ return false, fmt.Errorf("go tool dist list: invalid GOOS/GOARCH pair: %s",
+ pair)
+ }
+ goos := s[0]
+ if goos == wantGoos {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func iosCompilerFor(target, arch string) (string, []string, error) {
+ var platformSDK string
+ var platformOS string
+ switch target {
+ case "ios":
+ platformOS = "ios"
+ platformSDK = "iphone"
+ case "tvos":
+ platformOS = "tvos"
+ platformSDK = "appletv"
+ }
+ switch arch {
+ case "arm", "arm64":
+ platformSDK += "os"
+ case "386", "amd64":
+ platformOS += "-simulator"
+ platformSDK += "simulator"
+ default:
+ return "", nil, fmt.Errorf("unsupported -arch: %s", arch)
+ }
+ sdkPath, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK,
+ "--show-sdk-path"))
+ if err != nil {
+ return "", nil, err
+ }
+ clang, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--find",
+ "clang"))
+ if err != nil {
+ return "", nil, err
+ }
+ cflags := []string{
+ "-fembed-bitcode",
+ "-arch", allArchs[arch].iosArch,
+ "-isysroot", sdkPath,
+ "-m" + platformOS + "-version-min=" + minIOSVersion,
+ }
+ return clang, cflags, nil
+}
+
+func zipDir(dst, base, dir string) (err error) {
+ f, err := os.Create(dst)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := f.Close(); err == nil {
+ err = cerr
+ }
+ }()
+ zipf := zip.NewWriter(f)
+ err = filepath.Walk(filepath.Join(base, dir),
+ func(path string, f os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if f.IsDir() {
+ return nil
+ }
+ rel := filepath.ToSlash(path[len(base)+1:])
+ entry, err := zipf.Create(rel)
+ if err != nil {
+ return err
+ }
+ src, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer src.Close()
+ _, err = io.Copy(entry, src)
+ return err
+ })
+ if err != nil {
+ return err
+ }
+ return zipf.Close()
+}
diff --git a/gio/giold/cmd/gogio/js_test.go b/gio/giold/cmd/gogio/js_test.go
new file mode 100644
index 0000000..2918737
--- /dev/null
+++ b/gio/giold/cmd/gogio/js_test.go
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main_test
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "image"
+ "image/png"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os/exec"
+
+ "github.com/chromedp/cdproto/runtime"
+ "github.com/chromedp/chromedp"
+
+)
+
+type JSTestDriver struct {
+ driverBase
+
+ // ctx is the chromedp context.
+ ctx context.Context
+}
+
+func (d *JSTestDriver) Start(path string) {
+ if raceEnabled {
+ d.Skipf("js/wasm doesn't support -race; skipping")
+ }
+
+ // First, build the app.
+ dir := d.tempDir("gio-endtoend-js")
+ d.gogio("-target=js", "-o="+dir, path)
+
+ // Second, start Chrome.
+ opts := append(chromedp.DefaultExecAllocatorOptions[:],
+ chromedp.Flag("headless", *headless),
+ )
+
+ actx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
+ d.Cleanup(cancel)
+
+ ctx, cancel := chromedp.NewContext(actx,
+ // Send all logf/errf calls to t.Logf
+ chromedp.WithLogf(d.Logf),
+ )
+ d.Cleanup(cancel)
+ d.ctx = ctx
+
+ if err := chromedp.Run(ctx); err != nil {
+ if errors.Is(err, exec.ErrNotFound) {
+ d.Skipf("test requires Chrome to be installed: %v", err)
+ return
+ }
+ d.Fatal(err)
+ }
+ pr, pw := io.Pipe()
+ d.Cleanup(func() { pw.Close() })
+ d.output = pr
+ chromedp.ListenTarget(ctx, func(ev interface{}) {
+ switch ev := ev.(type) {
+ case *runtime.EventConsoleAPICalled:
+ switch ev.Type {
+ case "log", "info", "warning", "error":
+ var b bytes.Buffer
+ b.WriteString("console.")
+ b.WriteString(string(ev.Type))
+ b.WriteString("(")
+ for i, arg := range ev.Args {
+ if i > 0 {
+ b.WriteString(", ")
+ }
+ b.Write(arg.Value)
+ }
+ b.WriteString(")\n")
+ pw.Write(b.Bytes())
+ }
+ }
+ })
+
+ // Third, serve the app folder, set the browser tab dimensions, and
+ // navigate to the folder.
+ ts := httptest.NewServer(http.FileServer(http.Dir(dir)))
+ d.Cleanup(ts.Close)
+
+ if err := chromedp.Run(ctx,
+ chromedp.EmulateViewport(int64(d.width), int64(d.height)),
+ chromedp.Navigate(ts.URL),
+ ); err != nil {
+ d.Fatal(err)
+ }
+
+ // Wait for the gio app to render.
+ d.waitForFrame()
+}
+
+func (d *JSTestDriver) Screenshot() image.Image {
+ var buf []byte
+ if err := chromedp.Run(d.ctx,
+ chromedp.CaptureScreenshot(&buf),
+ ); err != nil {
+ d.Fatal(err)
+ }
+ img, err := png.Decode(bytes.NewReader(buf))
+ if err != nil {
+ d.Fatal(err)
+ }
+ return img
+}
+
+func (d *JSTestDriver) Click(x, y int) {
+ if err := chromedp.Run(d.ctx,
+ chromedp.MouseClickXY(float64(x), float64(y)),
+ ); err != nil {
+ d.Fatal(err)
+ }
+
+ // Wait for the gio app to render after this click.
+ d.waitForFrame()
+}
diff --git a/gio/giold/cmd/gogio/jsbuild.go b/gio/giold/cmd/gogio/jsbuild.go
new file mode 100644
index 0000000..58bccc1
--- /dev/null
+++ b/gio/giold/cmd/gogio/jsbuild.go
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "golang.org/x/tools/go/packages"
+)
+
+func buildJS(bi *buildInfo) error {
+ out := *destPath
+ if out == "" {
+ out = bi.name
+ }
+ if err := os.MkdirAll(out, 0700); err != nil {
+ return err
+ }
+ cmd := exec.Command(
+ "go",
+ "build",
+ "-ldflags="+bi.ldflags,
+ "-tags="+bi.tags,
+ "-o", filepath.Join(out, "main.wasm"),
+ bi.pkgPath,
+ )
+ cmd.Env = append(
+ os.Environ(),
+ "GOOS=js",
+ "GOARCH=wasm",
+ )
+ _, err := runCmd(cmd)
+ if err != nil {
+ return err
+ }
+ if err := ioutil.WriteFile(filepath.Join(out, "index.html"), []byte(jsIndex), 0600); err != nil {
+ return err
+ }
+ goroot, err := runCmd(exec.Command("go", "env", "GOROOT"))
+ if err != nil {
+ return err
+ }
+ wasmJS := filepath.Join(goroot, "misc", "wasm", "wasm_exec.js")
+ if _, err := os.Stat(wasmJS); err != nil {
+ return fmt.Errorf("failed to find $GOROOT/misc/wasm/wasm_exec.js driver: %v", err)
+ }
+ pkgs, err := packages.Load(&packages.Config{
+ Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps,
+ Env: append(os.Environ(), "GOOS=js", "GOARCH=wasm"),
+ }, bi.pkgPath)
+ if err != nil {
+ return err
+ }
+ extraJS, err := findPackagesJS(pkgs[0], make(map[string]bool))
+ if err != nil {
+ return err
+ }
+
+ return mergeJSFiles(filepath.Join(out, "wasm.js"), append([]string{wasmJS}, extraJS...)...)
+}
+
+func findPackagesJS(p *packages.Package, visited map[string]bool) (extraJS []string, err error) {
+ if len(p.GoFiles) == 0 {
+ return nil, nil
+ }
+ js, err := filepath.Glob(filepath.Join(filepath.Dir(p.GoFiles[0]), "*_js.js"))
+ if err != nil {
+ return nil, err
+ }
+ extraJS = append(extraJS, js...)
+ for _, imp := range p.Imports {
+ if !visited[imp.ID] {
+ extra, err := findPackagesJS(imp, visited)
+ if err != nil {
+ return nil, err
+ }
+ extraJS = append(extraJS, extra...)
+ visited[imp.ID] = true
+ }
+ }
+ return extraJS, nil
+}
+
+// mergeJSFiles will merge all files into a single `wasm.js`. It will prepend the jsSetGo
+// and append the jsStartGo.
+func mergeJSFiles(dst string, files ...string) (err error) {
+ w, err := os.Create(dst)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := w.Close(); err != nil {
+ err = cerr
+ }
+ }()
+ _, err = io.Copy(w, strings.NewReader(jsSetGo))
+ if err != nil {
+ return err
+ }
+ for i := range files {
+ r, err := os.Open(files[i])
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(w, r)
+ r.Close()
+ if err != nil {
+ return err
+ }
+ }
+ _, err = io.Copy(w, strings.NewReader(jsStartGo))
+ return err
+}
+
+const (
+ jsIndex = `
+
+
+
+
+
+
+
+
+
+
+`
+ // jsSetGo sets the `window.go` variable.
+ jsSetGo = `(() => {
+ window.go = {argv: [], env: {}, importObject: {go: {}}};
+ const argv = new URLSearchParams(location.search).get("argv");
+ if (argv) {
+ window.go["argv"] = argv.split(" ");
+ }
+})();`
+ // jsStartGo initializes the main.wasm.
+ jsStartGo = `(() => {
+ defaultGo = new Go();
+ Object.assign(defaultGo["argv"], defaultGo["argv"].concat(go["argv"]));
+ Object.assign(defaultGo["env"], go["env"]);
+ for (let key in go["importObject"]) {
+ if (typeof defaultGo["importObject"][key] === "undefined") {
+ defaultGo["importObject"][key] = {};
+ }
+ Object.assign(defaultGo["importObject"][key], go["importObject"][key]);
+ }
+ window.go = defaultGo;
+ if (!WebAssembly.instantiateStreaming) { // polyfill
+ WebAssembly.instantiateStreaming = async (resp, importObject) => {
+ const source = await (await resp).arrayBuffer();
+ return await WebAssembly.instantiate(source, importObject);
+ };
+ }
+ WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
+ go.run(result.instance);
+ });
+})();`
+)
diff --git a/gio/giold/cmd/gogio/main.go b/gio/giold/cmd/gogio/main.go
new file mode 100644
index 0000000..da35401
--- /dev/null
+++ b/gio/giold/cmd/gogio/main.go
@@ -0,0 +1,225 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "image"
+ "image/color"
+ "image/png"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "golang.org/x/image/draw"
+ "golang.org/x/sync/errgroup"
+)
+
+var (
+ target = flag.String("target", "", "specify target (ios, tvos, android, js).\n")
+ archNames = flag.String("arch", "", "specify architecture(s) to include (arm, arm64, amd64).")
+ minsdk = flag.Int("minsdk", 0, "specify the minimum supported operating system level")
+ buildMode = flag.String("buildmode", "exe", "specify buildmode (archive, exe)")
+ destPath = flag.String("o", "", "output file or directory.\nFor -target ios or tvos, use the .app suffix to target simulators.")
+ appID = flag.String("appid", "", "app identifier (for -buildmode=exe)")
+ version = flag.Int("version", 1, "app version (for -buildmode=exe)")
+ printCommands = flag.Bool("x", false, "print the commands")
+ keepWorkdir = flag.Bool("work", false, "print the name of the temporary work directory and do not delete it when exiting.")
+ linkMode = flag.String("linkmode", "", "set the -linkmode flag of the go tool")
+ extraLdflags = flag.String("ldflags", "", "extra flags to the Go linker")
+ extraTags = flag.String("tags", "", "extra tags to the Go tool")
+ iconPath = flag.String("icon", "", "specify an icon for iOS and Android")
+ signKey = flag.String("signkey", "", "specify the path of the keystore to be used to sign Android apk files.")
+ signPass = flag.String("signpass", "", "specify the password to decrypt the signkey.")
+ noStrip = flag.Bool("nostrip", false, "leave debugging symbols in produced .so files")
+)
+
+func main() {
+ flag.Usage = func() {
+ fmt.Fprint(os.Stderr, mainUsage)
+ }
+ flag.Parse()
+ if err := flagValidate(); err != nil {
+ fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
+ os.Exit(1)
+ }
+ buildInfo, err := newBuildInfo(flag.Arg(0))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
+ os.Exit(1)
+ }
+ if err := build(buildInfo); err != nil {
+ fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
+ os.Exit(1)
+ }
+ os.Exit(0)
+}
+
+func flagValidate() error {
+ pkgPathArg := flag.Arg(0)
+ if pkgPathArg == "" {
+ return errors.New("specify a package")
+ }
+ if *target == "" {
+ return errors.New("please specify -target")
+ }
+ switch *target {
+ case "ios", "tvos", "android", "js", "windows":
+ default:
+ return fmt.Errorf("invalid -target %s", *target)
+ }
+ switch *buildMode {
+ case "archive", "exe":
+ default:
+ return fmt.Errorf("invalid -buildmode %s", *buildMode)
+ }
+ return nil
+}
+
+func build(bi *buildInfo) error {
+ tmpDir, err := ioutil.TempDir("", "gogio-")
+ if err != nil {
+ return err
+ }
+ if *keepWorkdir {
+ fmt.Fprintf(os.Stderr, "WORKDIR=%s\n", tmpDir)
+ } else {
+ defer os.RemoveAll(tmpDir)
+ }
+ switch *target {
+ case "js":
+ return buildJS(bi)
+ case "ios", "tvos":
+ return buildIOS(tmpDir, *target, bi)
+ case "android":
+ return buildAndroid(tmpDir, bi)
+ case "windows":
+ return buildWindows(tmpDir, bi)
+ default:
+ panic("unreachable")
+ }
+}
+
+func runCmdRaw(cmd *exec.Cmd) ([]byte, error) {
+ if *printCommands {
+ fmt.Printf("%s\n", strings.Join(cmd.Args, " "))
+ }
+ out, err := cmd.Output()
+ if err == nil {
+ return out, nil
+ }
+ if err, ok := err.(*exec.ExitError); ok {
+ return nil, fmt.Errorf("%s failed: %s%s", strings.Join(cmd.Args, " "), out, err.Stderr)
+ }
+ return nil, err
+}
+
+func runCmd(cmd *exec.Cmd) (string, error) {
+ out, err := runCmdRaw(cmd)
+ return string(bytes.TrimSpace(out)), err
+}
+
+func copyFile(dst, src string) (err error) {
+ r, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer r.Close()
+ w, err := os.Create(dst)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := w.Close(); err == nil {
+ err = cerr
+ }
+ }()
+ _, err = io.Copy(w, r)
+ return err
+}
+
+type arch struct {
+ iosArch string
+ jniArch string
+ clangArch string
+}
+
+var allArchs = map[string]arch{
+ "arm": {
+ iosArch: "armv7",
+ jniArch: "armeabi-v7a",
+ clangArch: "armv7a-linux-androideabi",
+ },
+ "arm64": {
+ iosArch: "arm64",
+ jniArch: "arm64-v8a",
+ clangArch: "aarch64-linux-android",
+ },
+ "386": {
+ iosArch: "i386",
+ jniArch: "x86",
+ clangArch: "i686-linux-android",
+ },
+ "amd64": {
+ iosArch: "x86_64",
+ jniArch: "x86_64",
+ clangArch: "x86_64-linux-android",
+ },
+}
+
+type iconVariant struct {
+ path string
+ size int
+ fill bool
+}
+
+func buildIcons(baseDir, icon string, variants []iconVariant) error {
+ f, err := os.Open(icon)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ img, _, err := image.Decode(f)
+ if err != nil {
+ return err
+ }
+ var resizes errgroup.Group
+ for _, v := range variants {
+ v := v
+ resizes.Go(func() (err error) {
+ path := filepath.Join(baseDir, v.path)
+ if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
+ return err
+ }
+ f, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if cerr := f.Close(); err == nil {
+ err = cerr
+ }
+ }()
+ return png.Encode(f, resizeIcon(v, img))
+ })
+ }
+ return resizes.Wait()
+}
+
+func resizeIcon(v iconVariant, img image.Image) *image.NRGBA {
+ scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: v.size, Y: v.size}})
+ op := draw.Src
+ if v.fill {
+ op = draw.Over
+ draw.Draw(scaled, scaled.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)
+ }
+ draw.CatmullRom.Scale(scaled, scaled.Bounds(), img, img.Bounds(), op, nil)
+
+ return scaled
+}
diff --git a/gio/giold/cmd/gogio/main_test.go b/gio/giold/cmd/gogio/main_test.go
new file mode 100644
index 0000000..98dcb27
--- /dev/null
+++ b/gio/giold/cmd/gogio/main_test.go
@@ -0,0 +1,17 @@
+package main
+
+import (
+ "os"
+ "testing"
+)
+
+func TestMain(m *testing.M) {
+ if os.Getenv("RUN_GOGIO") != "" {
+ // Allow the end-to-end tests to call the gogio tool without
+ // having to build it from scratch, nor having to refactor the
+ // main function to avoid using global variables.
+ main()
+ os.Exit(0) // main already exits, but just in case.
+ }
+ os.Exit(m.Run())
+}
diff --git a/gio/giold/cmd/gogio/permission.go b/gio/giold/cmd/gogio/permission.go
new file mode 100644
index 0000000..b22fcef
--- /dev/null
+++ b/gio/giold/cmd/gogio/permission.go
@@ -0,0 +1,33 @@
+package main
+
+var AndroidPermissions = map[string][]string{
+ "network": {
+ "android.permission.INTERNET",
+ },
+ "networkstate": {
+ "android.permission.ACCESS_NETWORK_STATE",
+ },
+ "bluetooth": {
+ "android.permission.BLUETOOTH",
+ "android.permission.BLUETOOTH_ADMIN",
+ "android.permission.ACCESS_FINE_LOCATION",
+ },
+ "camera": {
+ "android.permission.CAMERA",
+ },
+ "storage": {
+ "android.permission.READ_EXTERNAL_STORAGE",
+ "android.permission.WRITE_EXTERNAL_STORAGE",
+ },
+}
+
+var AndroidFeatures = map[string][]string{
+ "default": {`glEsVersion="0x00020000"`, `name="android.hardware.type.pc"`},
+ "bluetooth": {
+ `name="android.hardware.bluetooth"`,
+ `name="android.hardware.bluetooth_le"`,
+ },
+ "camera": {
+ `name="android.hardware.camera"`,
+ },
+}
diff --git a/gio/giold/cmd/gogio/race_test.go b/gio/giold/cmd/gogio/race_test.go
new file mode 100644
index 0000000..0749936
--- /dev/null
+++ b/gio/giold/cmd/gogio/race_test.go
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build race
+
+package main_test
+
+func init() { raceEnabled = true }
diff --git a/gio/giold/cmd/gogio/testdata/testdata.go b/gio/giold/cmd/gogio/testdata/testdata.go
new file mode 100644
index 0000000..b5c2493
--- /dev/null
+++ b/gio/giold/cmd/gogio/testdata/testdata.go
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// A simple app used for gogio's end-to-end tests.
+package main
+
+import (
+ "fmt"
+ "image"
+ "image/color"
+ "log"
+
+ "realy.lol/gio/app"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+)
+
+func main() {
+ go func() {
+ w := app.NewWindow()
+ if err := loop(w); err != nil {
+ log.Fatal(err)
+ }
+ }()
+ app.Main()
+}
+
+type notifyFrame int
+
+const (
+ notifyNone notifyFrame = iota
+ notifyInvalidate
+ notifyPrint
+)
+
+// notify keeps track of whether we want to print to stdout to notify the user
+// when a frame is ready. Initially we want to notify about the first frame.
+var notify = notifyInvalidate
+
+type (
+ C = layout.Context
+ D = layout.Dimensions
+)
+
+func loop(w *app.Window) error {
+ topLeft := quarterWidget{
+ color: color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff},
+ }
+ topRight := quarterWidget{
+ color: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff},
+ }
+ botLeft := quarterWidget{
+ color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff},
+ }
+ botRight := quarterWidget{
+ color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x80},
+ }
+
+ var ops op.Ops
+ for {
+ e := <-w.Events()
+ switch e := e.(type) {
+ case system.DestroyEvent:
+ return e.Err
+ case system.FrameEvent:
+ gtx := layout.NewContext(&ops, e)
+ // Clear background to white, even on embedded platforms such as webassembly.
+ paint.Fill(gtx.Ops, color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
+ layout.Flex{Axis: layout.Vertical}.Layout(gtx,
+ layout.Flexed(1, func(gtx C) D {
+ return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
+ // r1c1
+ layout.Flexed(1,
+ func(gtx C) D { return topLeft.Layout(gtx) }),
+ // r1c2
+ layout.Flexed(1,
+ func(gtx C) D { return topRight.Layout(gtx) }),
+ )
+ }),
+ layout.Flexed(1, func(gtx C) D {
+ return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
+ // r2c1
+ layout.Flexed(1,
+ func(gtx C) D { return botLeft.Layout(gtx) }),
+ // r2c2
+ layout.Flexed(1,
+ func(gtx C) D { return botRight.Layout(gtx) }),
+ )
+ }),
+ )
+
+ e.Frame(gtx.Ops)
+
+ switch notify {
+ case notifyInvalidate:
+ notify = notifyPrint
+ w.Invalidate()
+ case notifyPrint:
+ notify = notifyNone
+ fmt.Println("gio frame ready")
+ }
+ }
+ }
+}
+
+// quarterWidget paints a quarter of the screen with one color. When clicked, it
+// turns red, going back to its normal color when clicked again.
+type quarterWidget struct {
+ color color.NRGBA
+
+ clicked bool
+}
+
+var red = color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
+
+func (w *quarterWidget) Layout(gtx layout.Context) layout.Dimensions {
+ var color color.NRGBA
+ if w.clicked {
+ color = red
+ } else {
+ color = w.color
+ }
+
+ r := image.Rectangle{Max: gtx.Constraints.Max}
+ paint.FillShape(gtx.Ops, color, clip.Rect(r).Op())
+
+ pointer.Rect(image.Rectangle{
+ Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y),
+ }).Add(gtx.Ops)
+ pointer.InputOp{
+ Tag: w,
+ Types: pointer.Press,
+ }.Add(gtx.Ops)
+
+ for _, e := range gtx.Events(w) {
+ if e, ok := e.(pointer.Event); ok && e.Type == pointer.Press {
+ w.clicked = !w.clicked
+ // notify when we're done updating the frame.
+ notify = notifyInvalidate
+ }
+ }
+ return layout.Dimensions{Size: gtx.Constraints.Max}
+}
diff --git a/gio/giold/cmd/gogio/wayland_test.go b/gio/giold/cmd/gogio/wayland_test.go
new file mode 100644
index 0000000..df10410
--- /dev/null
+++ b/gio/giold/cmd/gogio/wayland_test.go
@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main_test
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "fmt"
+ "image"
+ "image/png"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "sync"
+ "text/template"
+ "time"
+)
+
+type WaylandTestDriver struct {
+ driverBase
+
+ runtimeDir string
+ socket string
+ display string
+}
+
+// No bars or anything fancy. Just a white background with our dimensions.
+var tmplSwayConfig = template.Must(template.New("").Parse(`
+output * bg #FFFFFF solid_color
+output * mode {{.Width}}x{{.Height}}
+default_border none
+`))
+
+var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`)
+
+func (d *WaylandTestDriver) Start(path string) {
+ // We want os.Environ, so that it can e.g. find $DISPLAY to run within
+ // X11. wlroots env vars are documented at:
+ // https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md
+ env := os.Environ()
+ if *headless {
+ env = append(env, "WLR_BACKENDS=headless")
+ }
+
+ d.needPrograms(
+ "sway", // to run a wayland compositor
+ "grim", // to take screenshots
+ "swaymsg", // to send input
+ )
+
+ // First, build the app.
+ dir := d.tempDir("gio-endtoend-wayland")
+ bin := filepath.Join(dir, "red")
+ flags := []string{"build", "-tags", "nox11", "-o=" + bin}
+ if raceEnabled {
+ flags = append(flags, "-race")
+ }
+ flags = append(flags, path)
+ cmd := exec.Command("go", flags...)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ d.Fatalf("could not build app: %s:\n%s", err, out)
+ }
+
+ conf := filepath.Join(dir, "config")
+ f, err := os.Create(conf)
+ if err != nil {
+ d.Fatal(err)
+ }
+ defer f.Close()
+ if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{
+ d.width, d.height,
+ }); err != nil {
+ d.Fatal(err)
+ }
+
+ d.socket = filepath.Join(dir, "socket")
+ env = append(env, "SWAYSOCK="+d.socket)
+ d.runtimeDir = dir
+ env = append(env, "XDG_RUNTIME_DIR="+d.runtimeDir)
+
+ var wg sync.WaitGroup
+ d.Cleanup(wg.Wait)
+
+ // First, start sway.
+ {
+ ctx, cancel := context.WithCancel(context.Background())
+ cmd := exec.CommandContext(ctx, "sway", "--config", conf, "--verbose")
+ cmd.Env = env
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ d.Fatal(err)
+ }
+ if err := cmd.Start(); err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(cancel)
+ d.Cleanup(func() {
+ // Give it a chance to exit gracefully, cleaning up
+ // after itself. After 10ms, the deferred cancel above
+ // will signal an os.Kill.
+ cmd.Process.Signal(os.Interrupt)
+ time.Sleep(10 * time.Millisecond)
+ })
+
+ // Wait for sway to be ready. We probably don't need a deadline
+ // here.
+ br := bufio.NewReader(stderr)
+ for {
+ line, err := br.ReadString('\n')
+ if err != nil {
+ d.Fatal(err)
+ }
+ if m := rxSwayReady.FindStringSubmatch(line); m != nil {
+ d.display = m[1]
+ break
+ }
+ }
+
+ wg.Add(1)
+ go func() {
+ if err := cmd.Wait(); err != nil && ctx.Err() == nil && !strings.Contains(err.Error(), "interrupt") {
+ // Don't print all stderr, since we use --verbose.
+ // TODO(mvdan): if it's useful, probably filter
+ // errors and show them.
+ d.Error(err)
+ }
+ wg.Done()
+ }()
+ }
+
+ // Then, start our program on the sway compositor above.
+ {
+ ctx, cancel := context.WithCancel(context.Background())
+ cmd := exec.CommandContext(ctx, bin)
+ cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
+ output, err := cmd.StdoutPipe()
+ if err != nil {
+ d.Fatal(err)
+ }
+ cmd.Stderr = cmd.Stdout
+ d.output = output
+ if err := cmd.Start(); err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(cancel)
+ wg.Add(1)
+ go func() {
+ if err := cmd.Wait(); err != nil && ctx.Err() == nil {
+ d.Error(err)
+ }
+ wg.Done()
+ }()
+ }
+
+ // Wait for the gio app to render.
+ d.waitForFrame()
+}
+
+func (d *WaylandTestDriver) Screenshot() image.Image {
+ cmd := exec.Command("grim", "/dev/stdout")
+ cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ d.Errorf("%s", out)
+ d.Fatal(err)
+ }
+ img, err := png.Decode(bytes.NewReader(out))
+ if err != nil {
+ d.Fatal(err)
+ }
+ return img
+}
+
+func (d *WaylandTestDriver) swaymsg(args ...interface{}) {
+ strs := []string{"--socket", d.socket}
+ for _, arg := range args {
+ strs = append(strs, fmt.Sprint(arg))
+ }
+ cmd := exec.Command("swaymsg", strs...)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ d.Errorf("%s", out)
+ d.Fatal(err)
+ }
+}
+
+func (d *WaylandTestDriver) Click(x, y int) {
+ d.swaymsg("seat", "-", "cursor", "set", x, y)
+ d.swaymsg("seat", "-", "cursor", "press", "button1")
+ d.swaymsg("seat", "-", "cursor", "release", "button1")
+
+ // Wait for the gio app to render after this click.
+ d.waitForFrame()
+}
diff --git a/gio/giold/cmd/gogio/windows_test.go b/gio/giold/cmd/gogio/windows_test.go
new file mode 100644
index 0000000..996b511
--- /dev/null
+++ b/gio/giold/cmd/gogio/windows_test.go
@@ -0,0 +1,152 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main_test
+
+import (
+ "context"
+ "image"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "sync"
+ "time"
+
+ "golang.org/x/image/draw"
+)
+
+// Wine is tightly coupled with X11 at the moment, and we can reuse the same
+// methods to automate screenshots and clicks. The main difference is how we
+// build and run the app.
+
+// The only quirk is that it seems impossible for the Wine window to take the
+// entirety of the X server's dimensions, even if we try to resize it to take
+// the entire display. It seems to want to leave some vertical space empty,
+// presumably for window decorations or the "start" bar on Windows. To work
+// around that, make the X server 50x50px bigger, and crop the screenshots back
+// to the original size.
+
+type WineTestDriver struct {
+ X11TestDriver
+}
+
+func (d *WineTestDriver) Start(path string) {
+ d.needPrograms("wine")
+
+ // First, build the app.
+ bin := filepath.Join(d.tempDir("gio-endtoend-windows"), "red.exe")
+ flags := []string{"build", "-o=" + bin}
+ if raceEnabled {
+ if runtime.GOOS != "windows" {
+ // cross-compilation disables CGo, which breaks -race.
+ d.Skipf("can't cross-compile -race for Windows; skipping")
+ }
+ flags = append(flags, "-race")
+ }
+ flags = append(flags, path)
+ cmd := exec.Command("go", flags...)
+ cmd.Env = os.Environ()
+ cmd.Env = append(cmd.Env, "GOOS=windows")
+ if out, err := cmd.CombinedOutput(); err != nil {
+ d.Fatalf("could not build app: %s:\n%s", err, out)
+ }
+
+ var wg sync.WaitGroup
+ d.Cleanup(wg.Wait)
+
+ // Add 50x50px to the display dimensions, as discussed earlier.
+ d.startServer(&wg, d.width+50, d.height+50)
+
+ // Then, start our program via Wine on the X server above.
+ {
+ cacheDir, err := os.UserCacheDir()
+ if err != nil {
+ d.Fatal(err)
+ }
+ // Use a wine directory separate from the default ~/.wine, so
+ // that the user's winecfg doesn't affect our test. This will
+ // default to ~/.cache/gio-e2e-wine. We use the user's cache,
+ // to reuse a previously set up wineprefix.
+ wineprefix := filepath.Join(cacheDir, "gio-e2e-wine")
+
+ // First, ensure that wineprefix is up to date with wineboot.
+ // Wait for this separately from the first frame, as setting up
+ // a new prefix might take 5s on its own.
+ env := []string{
+ "DISPLAY=" + d.display,
+ "WINEDEBUG=fixme-all", // hide "fixme" noise
+ "WINEPREFIX=" + wineprefix,
+
+ // Disable wine-gecko (Explorer) and wine-mono (.NET).
+ // Otherwise, if not installed, wineboot will get stuck
+ // with a prompt to install them on the virtual X
+ // display. Moreover, Gio doesn't need either, and wine
+ // is faster without them.
+ "WINEDLLOVERRIDES=mscoree,mshtml=",
+ }
+ {
+ start := time.Now()
+ cmd := exec.Command("wine", "wineboot", "-i")
+ cmd.Env = env
+ // Use a combined output pipe instead of CombinedOutput,
+ // so that we only wait for the child process to exit,
+ // and we don't need to wait for all of wine's
+ // grandchildren to exit and stop writing. This is
+ // relevant as wine leaves "wineserver" lingering for
+ // three seconds by default, to be reused later.
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ d.Fatal(err)
+ }
+ cmd.Stderr = cmd.Stdout
+ if err := cmd.Run(); err != nil {
+ io.Copy(os.Stderr, stdout)
+ d.Fatal(err)
+ }
+ d.Logf("set up WINEPREFIX in %s", time.Since(start))
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ cmd := exec.CommandContext(ctx, "wine", bin)
+ cmd.Env = env
+ output, err := cmd.StdoutPipe()
+ if err != nil {
+ d.Fatal(err)
+ }
+ cmd.Stderr = cmd.Stdout
+ d.output = output
+ if err := cmd.Start(); err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(cancel)
+ wg.Add(1)
+ go func() {
+ if err := cmd.Wait(); err != nil && ctx.Err() == nil {
+ d.Error(err)
+ }
+ wg.Done()
+ }()
+ }
+ // Wait for the gio app to render.
+ d.waitForFrame()
+
+ // xdotool seems to fail at actually moving the window if we use it
+ // immediately after Gio is ready. Why?
+ // We can't tell if the windowmove operation worked until we take a
+ // screenshot, because the getwindowgeometry op reports the 0x0
+ // coordinates even if the window wasn't moved properly.
+ // A sleep of ~20ms seems to be enough on an idle laptop. Use 20x that.
+ // TODO(mvdan): revisit this, when you have a spare three hours.
+ time.Sleep(400 * time.Millisecond)
+ id := d.xdotool("search", "--sync", "--onlyvisible", "--name", "Gio")
+ d.xdotool("windowmove", "--sync", id, 0, 0)
+}
+
+func (d *WineTestDriver) Screenshot() image.Image {
+ img := d.X11TestDriver.Screenshot()
+ // Crop the screenshot back to the original dimensions.
+ cropped := image.NewRGBA(image.Rect(0, 0, d.width, d.height))
+ draw.Draw(cropped, cropped.Bounds(), img, image.Point{}, draw.Src)
+ return cropped
+}
diff --git a/gio/giold/cmd/gogio/windowsbuild.go b/gio/giold/cmd/gogio/windowsbuild.go
new file mode 100644
index 0000000..1af8668
--- /dev/null
+++ b/gio/giold/cmd/gogio/windowsbuild.go
@@ -0,0 +1,412 @@
+package main
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "image/png"
+ "io"
+ "math"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "reflect"
+ "strconv"
+ "strings"
+ "text/template"
+
+ "github.com/akavel/rsrc/binutil"
+ "github.com/akavel/rsrc/coff"
+ "golang.org/x/text/encoding/unicode"
+)
+
+func buildWindows(tmpDir string, bi *buildInfo) error {
+ builder := &windowsBuilder{TempDir: tmpDir}
+ builder.DestDir = *destPath
+ if builder.DestDir == "" {
+ builder.DestDir = bi.pkgPath
+ }
+
+ name := bi.name
+ if *destPath != "" {
+ if filepath.Ext(*destPath) != ".exe" {
+ return fmt.Errorf("invalid output name %q, it must end with `.exe`", *destPath)
+ }
+ name = filepath.Base(*destPath)
+ }
+ name = strings.TrimSuffix(name, ".exe")
+ sdk := bi.minsdk
+ if sdk > 10 {
+ return fmt.Errorf("invalid minsdk (%d) it's higher than Windows 10", sdk)
+ }
+ version := strconv.Itoa(bi.version)
+ if bi.version > math.MaxUint16 {
+ return fmt.Errorf("version (%d) is larger than the maximum (%d)", bi.version, math.MaxUint16)
+ }
+
+ for _, arch := range bi.archs {
+ builder.Coff = coff.NewRSRC()
+ builder.Coff.Arch(arch)
+
+ if err := builder.embedIcon(bi.iconPath); err != nil {
+ return err
+ }
+
+ if err := builder.embedManifest(windowsManifest{
+ Version: "1.0.0." + version,
+ WindowsVersion: sdk,
+ Name: name,
+ }); err != nil {
+ return fmt.Errorf("can't create manifest: %v", err)
+ }
+
+ if err := builder.embedInfo(windowsResources{
+ Version: [2]uint32{uint32(1) << 16, uint32(bi.version)},
+ VersionHuman: "1.0.0." + version,
+ Name: name,
+ Language: 0x0400, // Process Default Language: https://docs.microsoft.com/en-us/previous-versions/ms957130(v=msdn.10)
+ }); err != nil {
+ return fmt.Errorf("can't create info: %v", err)
+ }
+
+ if err := builder.buildResource(bi, name, arch); err != nil {
+ return fmt.Errorf("can't build the resources: %v", err)
+ }
+
+ if err := builder.buildProgram(bi, name, arch); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+type (
+ windowsResources struct {
+ Version [2]uint32
+ VersionHuman string
+ Language uint16
+ Name string
+ }
+ windowsManifest struct {
+ Version string
+ WindowsVersion int
+ Name string
+ }
+ windowsBuilder struct {
+ TempDir string
+ DestDir string
+ Coff *coff.Coff
+ }
+)
+
+const (
+ // https://docs.microsoft.com/en-us/windows/win32/menurc/resource-types
+ windowsResourceIcon = 3
+ windowsResourceIconGroup = windowsResourceIcon + 11
+ windowsResourceManifest = 24
+ windowsResourceVersion = 16
+)
+
+type bufferCoff struct {
+ bytes.Buffer
+}
+
+func (b *bufferCoff) Size() int64 {
+ return int64(b.Len())
+}
+
+func (b *windowsBuilder) embedIcon(path string) (err error) {
+ iconFile, err := os.Open(path)
+ if err != nil {
+ return fmt.Errorf("can't read the icon located at %s: %v", path, err)
+ }
+ defer iconFile.Close()
+
+ iconImage, err := png.Decode(iconFile)
+ if err != nil {
+ return fmt.Errorf("can't decode the PNG file (%s): %v", path, err)
+ }
+
+ sizes := []int{16, 32, 48, 64, 128, 256}
+ var iconHeader bufferCoff
+
+ // GRPICONDIR structure.
+ if err := binary.Write(&iconHeader, binary.LittleEndian, [3]uint16{0, 1, uint16(len(sizes))}); err != nil {
+ return err
+ }
+
+ for _, size := range sizes {
+ var iconBuffer bufferCoff
+
+ if err := png.Encode(&iconBuffer, resizeIcon(iconVariant{size: size, fill: false}, iconImage)); err != nil {
+ return fmt.Errorf("can't encode image: %v", err)
+ }
+
+ b.Coff.AddResource(windowsResourceIcon, uint16(size), &iconBuffer)
+
+ if err := binary.Write(&iconHeader, binary.LittleEndian, struct {
+ Size [2]uint8
+ Color [2]uint8
+ Planes uint16
+ BitCount uint16
+ Length uint32
+ Id uint16
+ }{
+ Size: [2]uint8{uint8(size % 256), uint8(size % 256)}, // "0" means 256px.
+ Planes: 1,
+ BitCount: 32,
+ Length: uint32(iconBuffer.Len()),
+ Id: uint16(size),
+ }); err != nil {
+ return err
+ }
+ }
+
+ b.Coff.AddResource(windowsResourceIconGroup, 1, &iconHeader)
+
+ return nil
+}
+
+func (b *windowsBuilder) buildResource(buildInfo *buildInfo, name string, arch string) error {
+ out, err := os.Create(filepath.Join(buildInfo.pkgPath, name+"_windows_"+arch+".syso"))
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+ b.Coff.Freeze()
+
+ // See https://github.com/akavel/rsrc/internal/write.go#L13.
+ w := binutil.Writer{W: out}
+ binutil.Walk(b.Coff, func(v reflect.Value, path string) error {
+ if binutil.Plain(v.Kind()) {
+ w.WriteLE(v.Interface())
+ return nil
+ }
+ vv, ok := v.Interface().(binutil.SizedReader)
+ if ok {
+ w.WriteFromSized(vv)
+ return binutil.WALK_SKIP
+ }
+ return nil
+ })
+
+ if w.Err != nil {
+ return fmt.Errorf("error writing output file: %s", w.Err)
+ }
+
+ return nil
+}
+
+func (b *windowsBuilder) buildProgram(buildInfo *buildInfo, name string, arch string) error {
+ dest := b.DestDir
+ if len(buildInfo.archs) > 1 {
+ dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".exe")
+ }
+
+ cmd := exec.Command(
+ "go",
+ "build",
+ "-ldflags=-H=windowsgui "+buildInfo.ldflags,
+ "-tags="+buildInfo.tags,
+ "-o", dest,
+ buildInfo.pkgPath,
+ )
+ cmd.Env = append(
+ os.Environ(),
+ "GOOS=windows",
+ "GOARCH="+arch,
+ )
+ _, err := runCmd(cmd)
+ return err
+}
+
+func (b *windowsBuilder) embedManifest(v windowsManifest) error {
+ t, err := template.New("manifest").Parse(`
+
+
+ {{.Name}}
+
+
+ {{if (le .WindowsVersion 10)}}
+{{end}}
+ {{if (le .WindowsVersion 9)}}
+{{end}}
+ {{if (le .WindowsVersion 8)}}
+{{end}}
+ {{if (le .WindowsVersion 7)}}
+{{end}}
+ {{if (le .WindowsVersion 6)}}
+{{end}}
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+`)
+ if err != nil {
+ return err
+ }
+
+ var manifest bufferCoff
+ if err := t.Execute(&manifest, v); err != nil {
+ return err
+ }
+
+ b.Coff.AddResource(windowsResourceManifest, 1, &manifest)
+
+ return nil
+}
+
+func (b *windowsBuilder) embedInfo(v windowsResources) error {
+ page := uint16(1)
+
+ // https://docs.microsoft.com/pt-br/windows/win32/menurc/vs-versioninfo
+ t := newValue(valueBinary, "VS_VERSION_INFO", []io.WriterTo{
+ // https://docs.microsoft.com/pt-br/windows/win32/api/VerRsrc/ns-verrsrc-vs_fixedfileinfo
+ windowsInfoValueFixed{
+ Signature: 0xFEEF04BD,
+ StructVersion: 0x00010000,
+ FileVersion: v.Version,
+ ProductVersion: v.Version,
+ FileFlagMask: 0x3F,
+ FileFlags: 0,
+ FileOS: 0x40004,
+ FileType: 0x1,
+ FileSubType: 0,
+ },
+ // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringfileinfo
+ newValue(valueText, "StringFileInfo", []io.WriterTo{
+ // https://docs.microsoft.com/pt-br/windows/win32/menurc/stringtable
+ newValue(valueText, fmt.Sprintf("%04X%04X", v.Language, page), []io.WriterTo{
+ // https://docs.microsoft.com/pt-br/windows/win32/menurc/string-str
+ newValue(valueText, "ProductVersion", v.VersionHuman),
+ newValue(valueText, "FileVersion", v.VersionHuman),
+ newValue(valueText, "FileDescription", v.Name),
+ newValue(valueText, "ProductName", v.Name),
+ // TODO include more data: gogio must have some way to provide such information (like Company Name, Copyright...)
+ }),
+ }),
+ // https://docs.microsoft.com/pt-br/windows/win32/menurc/varfileinfo
+ newValue(valueBinary, "VarFileInfo", []io.WriterTo{
+ // https://docs.microsoft.com/pt-br/windows/win32/menurc/var-str
+ newValue(valueBinary, "Translation", uint32(page)<<16|uint32(v.Language)),
+ }),
+ })
+
+ // For some reason the ValueLength of the VS_VERSIONINFO must be the byte-length of `windowsInfoValueFixed`:
+ t.ValueLength = 52
+
+ var verrsrc bufferCoff
+ if _, err := t.WriteTo(&verrsrc); err != nil {
+ return err
+ }
+
+ b.Coff.AddResource(windowsResourceVersion, 1, &verrsrc)
+
+ return nil
+}
+
+type windowsInfoValueFixed struct {
+ Signature uint32
+ StructVersion uint32
+ FileVersion [2]uint32
+ ProductVersion [2]uint32
+ FileFlagMask uint32
+ FileFlags uint32
+ FileOS uint32
+ FileType uint32
+ FileSubType uint32
+ FileDate [2]uint32
+}
+
+func (v windowsInfoValueFixed) WriteTo(w io.Writer) (_ int64, err error) {
+ return 0, binary.Write(w, binary.LittleEndian, v)
+}
+
+type windowsInfoValue struct {
+ Length uint16
+ ValueLength uint16
+ Type uint16
+ Key []byte
+ Value []byte
+}
+
+func (v windowsInfoValue) WriteTo(w io.Writer) (_ int64, err error) {
+ // binary.Write doesn't support []byte inside struct.
+ if err = binary.Write(w, binary.LittleEndian, [3]uint16{v.Length, v.ValueLength, v.Type}); err != nil {
+ return 0, err
+ }
+ if _, err = w.Write(v.Key); err != nil {
+ return 0, err
+ }
+ if _, err = w.Write(v.Value); err != nil {
+ return 0, err
+ }
+ return 0, nil
+}
+
+const (
+ valueBinary uint16 = 0
+ valueText uint16 = 1
+)
+
+func newValue(valueType uint16, key string, input interface{}) windowsInfoValue {
+ v := windowsInfoValue{
+ Type: valueType,
+ Length: 6,
+ }
+
+ padding := func(in []byte) []byte {
+ if l := uint16(len(in)) + v.Length; l%4 != 0 {
+ return append(in, make([]byte, 4-l%4)...)
+ }
+ return in
+ }
+
+ v.Key = padding(utf16Encode(key))
+ v.Length += uint16(len(v.Key))
+
+ switch in := input.(type) {
+ case string:
+ v.Value = padding(utf16Encode(in))
+ v.ValueLength = uint16(len(v.Value) / 2)
+ case []io.WriterTo:
+ var buff bytes.Buffer
+ for k := range in {
+ if _, err := in[k].WriteTo(&buff); err != nil {
+ panic(err)
+ }
+ }
+ v.Value = buff.Bytes()
+ default:
+ var buff bytes.Buffer
+ if err := binary.Write(&buff, binary.LittleEndian, in); err != nil {
+ panic(err)
+ }
+ v.ValueLength = uint16(buff.Len())
+ v.Value = buff.Bytes()
+ }
+
+ v.Length += uint16(len(v.Value))
+
+ return v
+}
+
+// utf16Encode encodes the string to UTF16 with null-termination.
+func utf16Encode(s string) []byte {
+ b, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes([]byte(s))
+ if err != nil {
+ panic(err)
+ }
+ return append(b, 0x00, 0x00) // null-termination.
+}
diff --git a/gio/giold/cmd/gogio/x11_test.go b/gio/giold/cmd/gogio/x11_test.go
new file mode 100644
index 0000000..9bb3174
--- /dev/null
+++ b/gio/giold/cmd/gogio/x11_test.go
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main_test
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "image"
+ "image/png"
+ "io"
+ "math/rand"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sync"
+ "time"
+)
+
+type X11TestDriver struct {
+ driverBase
+
+ display string
+}
+
+func (d *X11TestDriver) Start(path string) {
+ // First, build the app.
+ bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red")
+ flags := []string{"build", "-tags", "nowayland", "-o=" + bin}
+ if raceEnabled {
+ flags = append(flags, "-race")
+ }
+ flags = append(flags, path)
+ cmd := exec.Command("go", flags...)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ d.Fatalf("could not build app: %s:\n%s", err, out)
+ }
+
+ var wg sync.WaitGroup
+ d.Cleanup(wg.Wait)
+
+ d.startServer(&wg, d.width, d.height)
+
+ // Then, start our program on the X server above.
+ {
+ ctx, cancel := context.WithCancel(context.Background())
+ cmd := exec.CommandContext(ctx, bin)
+ cmd.Env = []string{"DISPLAY=" + d.display}
+ output, err := cmd.StdoutPipe()
+ if err != nil {
+ d.Fatal(err)
+ }
+ cmd.Stderr = cmd.Stdout
+ d.output = output
+ if err := cmd.Start(); err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(cancel)
+ wg.Add(1)
+ go func() {
+ if err := cmd.Wait(); err != nil && ctx.Err() == nil {
+ d.Error(err)
+ }
+ wg.Done()
+ }()
+ }
+
+ // Wait for the gio app to render.
+ d.waitForFrame()
+}
+
+func (d *X11TestDriver) startServer(wg *sync.WaitGroup, width, height int) {
+ // Pick a random display number between 1 and 100,000. Most machines
+ // will only be using :0, so there's only a 0.001% chance of two
+ // concurrent test runs to run into a conflict.
+ rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
+ d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1)
+
+ var xprog string
+ xflags := []string{
+ "-wr", // we want a white background; the default is black
+ }
+ if *headless {
+ xprog = "Xvfb" // virtual X server
+ xflags = append(xflags, "-screen", "0", fmt.Sprintf("%dx%dx24", width, height))
+ } else {
+ xprog = "Xephyr" // nested X server as a window
+ xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height))
+ }
+ xflags = append(xflags, d.display)
+
+ d.needPrograms(
+ xprog, // to run the X server
+ "scrot", // to take screenshots
+ "xdotool", // to send input
+ )
+ ctx, cancel := context.WithCancel(context.Background())
+ cmd := exec.CommandContext(ctx, xprog, xflags...)
+ combined := &bytes.Buffer{}
+ cmd.Stdout = combined
+ cmd.Stderr = combined
+ if err := cmd.Start(); err != nil {
+ d.Fatal(err)
+ }
+ d.Cleanup(cancel)
+ d.Cleanup(func() {
+ // Give it a chance to exit gracefully, cleaning up
+ // after itself. After 10ms, the deferred cancel above
+ // will signal an os.Kill.
+ cmd.Process.Signal(os.Interrupt)
+ time.Sleep(10 * time.Millisecond)
+ })
+
+ // Wait for the X server to be ready. The socket path isn't
+ // terribly portable, but that's okay for now.
+ withRetries(d.T, time.Second, func() error {
+ socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:])
+ _, err := os.Stat(socket)
+ return err
+ })
+
+ wg.Add(1)
+ go func() {
+ if err := cmd.Wait(); err != nil && ctx.Err() == nil {
+ // Print all output and error.
+ io.Copy(os.Stdout, combined)
+ d.Error(err)
+ }
+ wg.Done()
+ }()
+}
+
+func (d *X11TestDriver) Screenshot() image.Image {
+ cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout")
+ cmd.Env = []string{"DISPLAY=" + d.display}
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ d.Errorf("%s", out)
+ d.Fatal(err)
+ }
+ img, err := png.Decode(bytes.NewReader(out))
+ if err != nil {
+ d.Fatal(err)
+ }
+ return img
+}
+
+func (d *X11TestDriver) xdotool(args ...interface{}) string {
+ d.Helper()
+ strs := make([]string, len(args))
+ for i, arg := range args {
+ strs[i] = fmt.Sprint(arg)
+ }
+ cmd := exec.Command("xdotool", strs...)
+ cmd.Env = []string{"DISPLAY=" + d.display}
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ d.Errorf("%s", out)
+ d.Fatal(err)
+ }
+ return string(bytes.TrimSpace(out))
+}
+
+func (d *X11TestDriver) Click(x, y int) {
+ d.xdotool("mousemove", "--sync", x, y)
+ d.xdotool("click", "1")
+
+ // Wait for the gio app to render after this click.
+ d.waitForFrame()
+}
diff --git a/gio/giold/f32/affine.go b/gio/giold/f32/affine.go
new file mode 100644
index 0000000..667f7e9
--- /dev/null
+++ b/gio/giold/f32/affine.go
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package f32
+
+import (
+ "fmt"
+ "math"
+)
+
+// Affine2D represents an affine 2D transformation. The zero value if Affine2D
+// represents the identity transform.
+type Affine2D struct {
+ // in order to make the zero value of Affine2D represent the identity
+ // transform we store it with the identity matrix subtracted, that is
+ // if the actual transformation matrix is:
+ // [sx, hx, ox]
+ // [hy, sy, oy]
+ // [ 0, 0, 1]
+ // we store a = sx-1 and e = sy-1
+ a, b, c float32
+ d, e, f float32
+}
+
+// NewAffine2D creates a new Affine2D transform from the matrix elements
+// in row major order. The rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1].
+func NewAffine2D(sx, hx, ox, hy, sy, oy float32) Affine2D {
+ return Affine2D{
+ a: sx - 1, b: hx, c: ox,
+ d: hy, e: sy - 1, f: oy,
+ }
+}
+
+// Offset the transformation.
+func (a Affine2D) Offset(offset Point) Affine2D {
+ return Affine2D{
+ a.a, a.b, a.c + offset.X,
+ a.d, a.e, a.f + offset.Y,
+ }
+}
+
+// Scale the transformation around the given origin.
+func (a Affine2D) Scale(origin, factor Point) Affine2D {
+ if origin == (Point{}) {
+ return a.scale(factor)
+ }
+ a = a.Offset(origin.Mul(-1))
+ a = a.scale(factor)
+ return a.Offset(origin)
+}
+
+// Rotate the transformation by the given angle (in radians) counter clockwise around the given origin.
+func (a Affine2D) Rotate(origin Point, radians float32) Affine2D {
+ if origin == (Point{}) {
+ return a.rotate(radians)
+ }
+ a = a.Offset(origin.Mul(-1))
+ a = a.rotate(radians)
+ return a.Offset(origin)
+}
+
+// Shear the transformation by the given angle (in radians) around the given origin.
+func (a Affine2D) Shear(origin Point, radiansX, radiansY float32) Affine2D {
+ if origin == (Point{}) {
+ return a.shear(radiansX, radiansY)
+ }
+ a = a.Offset(origin.Mul(-1))
+ a = a.shear(radiansX, radiansY)
+ return a.Offset(origin)
+}
+
+// Mul returns A*B.
+func (A Affine2D) Mul(B Affine2D) (r Affine2D) {
+ r.a = (A.a+1)*(B.a+1) + A.b*B.d - 1
+ r.b = (A.a+1)*B.b + A.b*(B.e+1)
+ r.c = (A.a+1)*B.c + A.b*B.f + A.c
+ r.d = A.d*(B.a+1) + (A.e+1)*B.d
+ r.e = A.d*B.b + (A.e+1)*(B.e+1) - 1
+ r.f = A.d*B.c + (A.e+1)*B.f + A.f
+ return r
+}
+
+// Invert the transformation. Note that if the matrix is close to singular
+// numerical errors may become large or infinity.
+func (a Affine2D) Invert() Affine2D {
+ if a.a == 0 && a.b == 0 && a.d == 0 && a.e == 0 {
+ return Affine2D{a: 0, b: 0, c: -a.c, d: 0, e: 0, f: -a.f}
+ }
+ a.a += 1
+ a.e += 1
+ det := a.a*a.e - a.b*a.d
+ a.a, a.e = a.e/det, a.a/det
+ a.b, a.d = -a.b/det, -a.d/det
+ temp := a.c
+ a.c = -a.a*a.c - a.b*a.f
+ a.f = -a.d*temp - a.e*a.f
+ a.a -= 1
+ a.e -= 1
+ return a
+}
+
+// Transform p by returning a*p.
+func (a Affine2D) Transform(p Point) Point {
+ return Point{
+ X: p.X*(a.a+1) + p.Y*a.b + a.c,
+ Y: p.X*a.d + p.Y*(a.e+1) + a.f,
+ }
+}
+
+// Elems returns the matrix elements of the transform in row-major order. The
+// rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1].
+func (a Affine2D) Elems() (sx, hx, ox, hy, sy, oy float32) {
+ return a.a + 1, a.b, a.c, a.d, a.e + 1, a.f
+}
+
+func (a Affine2D) scale(factor Point) Affine2D {
+ return Affine2D{
+ (a.a+1)*factor.X - 1, a.b * factor.X, a.c * factor.X,
+ a.d * factor.Y, (a.e+1)*factor.Y - 1, a.f * factor.Y,
+ }
+}
+
+func (a Affine2D) rotate(radians float32) Affine2D {
+ sin, cos := math.Sincos(float64(radians))
+ s, c := float32(sin), float32(cos)
+ return Affine2D{
+ (a.a+1)*c - a.d*s - 1, a.b*c - (a.e+1)*s, a.c*c - a.f*s,
+ (a.a+1)*s + a.d*c, a.b*s + (a.e+1)*c - 1, a.c*s + a.f*c,
+ }
+}
+
+func (a Affine2D) shear(radiansX, radiansY float32) Affine2D {
+ tx := float32(math.Tan(float64(radiansX)))
+ ty := float32(math.Tan(float64(radiansY)))
+ return Affine2D{
+ (a.a + 1) + a.d*tx - 1, a.b + (a.e+1)*tx, a.c + a.f*tx,
+ (a.a+1)*ty + a.d, a.b*ty + (a.e + 1) - 1, a.f*ty + a.f,
+ }
+}
+
+func (a Affine2D) String() string {
+ sx, hx, ox, hy, sy, oy := a.Elems()
+ return fmt.Sprintf("[[%f %f %f] [%f %f %f]]", sx, hx, ox, hy, sy, oy)
+}
diff --git a/gio/giold/f32/affine_test.go b/gio/giold/f32/affine_test.go
new file mode 100644
index 0000000..4077b8d
--- /dev/null
+++ b/gio/giold/f32/affine_test.go
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package f32
+
+import (
+ "math"
+ "testing"
+)
+
+func eq(p1, p2 Point) bool {
+ tol := 1e-5
+ dx, dy := p2.X-p1.X, p2.Y-p1.Y
+ return math.Abs(math.Sqrt(float64(dx*dx+dy*dy))) < tol
+}
+
+func eqaff(x, y Affine2D) bool {
+ tol := 1e-5
+ return math.Abs(float64(x.a-y.a)) < tol &&
+ math.Abs(float64(x.b-y.b)) < tol &&
+ math.Abs(float64(x.c-y.c)) < tol &&
+ math.Abs(float64(x.d-y.d)) < tol &&
+ math.Abs(float64(x.e-y.e)) < tol &&
+ math.Abs(float64(x.f-y.f)) < tol
+}
+
+func TestTransformOffset(t *testing.T) {
+ p := Point{X: 1, Y: 2}
+ o := Point{X: 2, Y: -3}
+
+ r := Affine2D{}.Offset(o).Transform(p)
+ if !eq(r, Pt(3, -1)) {
+ t.Errorf("offset transformation mismatch: have %v, want {3 -1}", r)
+ }
+ i := Affine2D{}.Offset(o).Invert().Transform(r)
+ if !eq(i, p) {
+ t.Errorf("offset transformation inverse mismatch: have %v, want %v", i, p)
+ }
+}
+
+func TestTransformScale(t *testing.T) {
+ p := Point{X: 1, Y: 2}
+ s := Point{X: -1, Y: 2}
+
+ r := Affine2D{}.Scale(Point{}, s).Transform(p)
+ if !eq(r, Pt(-1, 4)) {
+ t.Errorf("scale transformation mismatch: have %v, want {-1 4}", r)
+ }
+ i := Affine2D{}.Scale(Point{}, s).Invert().Transform(r)
+ if !eq(i, p) {
+ t.Errorf("scale transformation inverse mismatch: have %v, want %v", i, p)
+ }
+}
+
+func TestTransformRotate(t *testing.T) {
+ p := Point{X: 1, Y: 0}
+ a := float32(math.Pi / 2)
+
+ r := Affine2D{}.Rotate(Point{}, a).Transform(p)
+ if !eq(r, Pt(0, 1)) {
+ t.Errorf("rotate transformation mismatch: have %v, want {0 1}", r)
+ }
+ i := Affine2D{}.Rotate(Point{}, a).Invert().Transform(r)
+ if !eq(i, p) {
+ t.Errorf("rotate transformation inverse mismatch: have %v, want %v", i, p)
+ }
+}
+
+func TestTransformShear(t *testing.T) {
+ p := Point{X: 1, Y: 1}
+
+ r := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Transform(p)
+ if !eq(r, Pt(2, 1)) {
+ t.Errorf("shear transformation mismatch: have %v, want {2 1}", r)
+ }
+ i := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Invert().Transform(r)
+ if !eq(i, p) {
+ t.Errorf("shear transformation inverse mismatch: have %v, want %v", i, p)
+ }
+}
+
+func TestTransformMultiply(t *testing.T) {
+ p := Point{X: 1, Y: 2}
+ o := Point{X: 2, Y: -3}
+ s := Point{X: -1, Y: 2}
+ a := float32(-math.Pi / 2)
+
+ r := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Transform(p)
+ if !eq(r, Pt(1, 3)) {
+ t.Errorf("complex transformation mismatch: have %v, want {1 3}", r)
+ }
+ i := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Invert().Transform(r)
+ if !eq(i, p) {
+ t.Errorf("complex transformation inverse mismatch: have %v, want %v", i, p)
+ }
+}
+
+func TestPrimes(t *testing.T) {
+ xa := NewAffine2D(9, 11, 13, 17, 19, 23)
+ xb := NewAffine2D(29, 31, 37, 43, 47, 53)
+
+ pa := Point{X: 2, Y: 3}
+ pb := Point{X: 5, Y: 7}
+
+ for _, test := range []struct {
+ x Affine2D
+ p Point
+ exp Point
+ }{
+ {x: xa, p: pa, exp: Pt(64, 114)},
+ {x: xa, p: pb, exp: Pt(135, 241)},
+ {x: xb, p: pa, exp: Pt(188, 280)},
+ {x: xb, p: pb, exp: Pt(399, 597)},
+ } {
+ got := test.x.Transform(test.p)
+ if !eq(got, test.exp) {
+ t.Errorf("%v.Transform(%v): have %v, want %v", test.x, test.p, got, test.exp)
+ }
+ }
+
+ for _, test := range []struct {
+ x Affine2D
+ exp Affine2D
+ }{
+ {x: xa, exp: NewAffine2D(-1.1875, 0.6875, -0.375, 1.0625, -0.5625, -0.875)},
+ {x: xb, exp: NewAffine2D(1.5666667, -1.0333333, -3.2000008, -1.4333333, 1-0.03333336, 1.7999992)},
+ } {
+ got := test.x.Invert()
+ if !eqaff(got, test.exp) {
+ t.Errorf("%v.Invert(): have %v, want %v", test.x, got, test.exp)
+ }
+ }
+
+ got := xa.Mul(xb)
+ exp := NewAffine2D(734, 796, 929, 1310, 1420, 1659)
+ if !eqaff(got, exp) {
+ t.Errorf("%v.Mul(%v): have %v, want %v", xa, xb, got, exp)
+ }
+}
+
+func TestTransformScaleAround(t *testing.T) {
+ p := Pt(-1, -1)
+ target := Pt(-6, -13)
+ pt := Affine2D{}.Scale(Pt(4, 5), Pt(2, 3)).Transform(p)
+ if !eq(pt, target) {
+ t.Log(pt, "!=", target)
+ t.Error("Scale not as expected")
+ }
+}
+
+func TestTransformRotateAround(t *testing.T) {
+ p := Pt(-1, -1)
+ pt := Affine2D{}.Rotate(Pt(1, 1), -math.Pi/2).Transform(p)
+ target := Pt(-1, 3)
+ if !eq(pt, target) {
+ t.Log(pt, "!=", target)
+ t.Error("Rotate not as expected")
+ }
+}
+
+func TestMulOrder(t *testing.T) {
+ A := Affine2D{}.Offset(Pt(100, 100))
+ B := Affine2D{}.Scale(Point{}, Pt(2, 2))
+ _ = A
+ _ = B
+
+ T1 := Affine2D{}.Offset(Pt(100, 100)).Scale(Point{}, Pt(2, 2))
+ T2 := B.Mul(A)
+
+ if T1 != T2 {
+ t.Log(T1)
+ t.Log(T2)
+ t.Error("multiplication / transform order not as expected")
+ }
+}
+
+func BenchmarkTransformOffset(b *testing.B) {
+ p := Point{X: 1, Y: 2}
+ o := Point{X: 0.5, Y: 0.5}
+ aff := Affine2D{}.Offset(o)
+
+ for i := 0; i < b.N; i++ {
+ p = aff.Transform(p)
+ }
+ _ = p
+}
+
+func BenchmarkTransformScale(b *testing.B) {
+ p := Point{X: 1, Y: 2}
+ s := Point{X: 0.5, Y: 0.5}
+ aff := Affine2D{}.Scale(Point{}, s)
+ for i := 0; i < b.N; i++ {
+ p = aff.Transform(p)
+ }
+ _ = p
+}
+
+func BenchmarkTransformRotate(b *testing.B) {
+ p := Point{X: 1, Y: 2}
+ a := float32(math.Pi / 2)
+ aff := Affine2D{}.Rotate(Point{}, a)
+ for i := 0; i < b.N; i++ {
+ p = aff.Transform(p)
+ }
+ _ = p
+}
+
+func BenchmarkTransformTranslateMultiply(b *testing.B) {
+ a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3)
+ t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5})
+
+ for i := 0; i < b.N; i++ {
+ a = a.Mul(t)
+ }
+}
+
+func BenchmarkTransformScaleMultiply(b *testing.B) {
+ a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3)
+ t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Scale(Point{}, Point{X: 0.4, Y: -0.5})
+
+ for i := 0; i < b.N; i++ {
+ a = a.Mul(t)
+ }
+}
+
+func BenchmarkTransformMultiply(b *testing.B) {
+ a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3)
+ t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Rotate(Point{}, math.Pi/7)
+
+ for i := 0; i < b.N; i++ {
+ a = a.Mul(t)
+ }
+}
diff --git a/gio/giold/f32/f32.go b/gio/giold/f32/f32.go
new file mode 100644
index 0000000..69745ba
--- /dev/null
+++ b/gio/giold/f32/f32.go
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package f32 is a float32 implementation of package image's
+Point and Rectangle.
+
+The coordinate space has the origin in the top left
+corner with the axes extending right and down.
+*/
+package f32
+
+import "strconv"
+
+// A Point is a two dimensional point.
+type Point struct {
+ X, Y float32
+}
+
+// String return a string representation of p.
+func (p Point) String() string {
+ return "(" + strconv.FormatFloat(float64(p.X), 'f', -1, 32) +
+ "," + strconv.FormatFloat(float64(p.Y), 'f', -1, 32) + ")"
+}
+
+// A Rectangle contains the points (X, Y) where Min.X <= X < Max.X,
+// Min.Y <= Y < Max.Y.
+type Rectangle struct {
+ Min, Max Point
+}
+
+// String return a string representation of r.
+func (r Rectangle) String() string {
+ return r.Min.String() + "-" + r.Max.String()
+}
+
+// Rect is a shorthand for Rectangle{Point{x0, y0}, Point{x1, y1}}.
+// The returned Rectangle has x0 and y0 swapped if necessary so that
+// it's correctly formed.
+func Rect(x0, y0, x1, y1 float32) Rectangle {
+ if x0 > x1 {
+ x0, x1 = x1, x0
+ }
+ if y0 > y1 {
+ y0, y1 = y1, y0
+ }
+ return Rectangle{Point{x0, y0}, Point{x1, y1}}
+}
+
+// Pt is shorthand for Point{X: x, Y: y}.
+func Pt(x, y float32) Point {
+ return Point{X: x, Y: y}
+}
+
+// Add return the point p+p2.
+func (p Point) Add(p2 Point) Point {
+ return Point{X: p.X + p2.X, Y: p.Y + p2.Y}
+}
+
+// Sub returns the vector p-p2.
+func (p Point) Sub(p2 Point) Point {
+ return Point{X: p.X - p2.X, Y: p.Y - p2.Y}
+}
+
+// Mul returns p scaled by s.
+func (p Point) Mul(s float32) Point {
+ return Point{X: p.X * s, Y: p.Y * s}
+}
+
+// In reports whether p is in r.
+func (p Point) In(r Rectangle) bool {
+ return r.Min.X <= p.X && p.X < r.Max.X &&
+ r.Min.Y <= p.Y && p.Y < r.Max.Y
+}
+
+// Size returns r's width and height.
+func (r Rectangle) Size() Point {
+ return Point{X: r.Dx(), Y: r.Dy()}
+}
+
+// Dx returns r's width.
+func (r Rectangle) Dx() float32 {
+ return r.Max.X - r.Min.X
+}
+
+// Dy returns r's Height.
+func (r Rectangle) Dy() float32 {
+ return r.Max.Y - r.Min.Y
+}
+
+// Intersect returns the intersection of r and s.
+func (r Rectangle) Intersect(s Rectangle) Rectangle {
+ if r.Min.X < s.Min.X {
+ r.Min.X = s.Min.X
+ }
+ if r.Min.Y < s.Min.Y {
+ r.Min.Y = s.Min.Y
+ }
+ if r.Max.X > s.Max.X {
+ r.Max.X = s.Max.X
+ }
+ if r.Max.Y > s.Max.Y {
+ r.Max.Y = s.Max.Y
+ }
+ return r
+}
+
+// Union returns the union of r and s.
+func (r Rectangle) Union(s Rectangle) Rectangle {
+ if r.Min.X > s.Min.X {
+ r.Min.X = s.Min.X
+ }
+ if r.Min.Y > s.Min.Y {
+ r.Min.Y = s.Min.Y
+ }
+ if r.Max.X < s.Max.X {
+ r.Max.X = s.Max.X
+ }
+ if r.Max.Y < s.Max.Y {
+ r.Max.Y = s.Max.Y
+ }
+ return r
+}
+
+// Canon returns the canonical version of r, where Min is to
+// the upper left of Max.
+func (r Rectangle) Canon() Rectangle {
+ if r.Max.X < r.Min.X {
+ r.Min.X, r.Max.X = r.Max.X, r.Min.X
+ }
+ if r.Max.Y < r.Min.Y {
+ r.Min.Y, r.Max.Y = r.Max.Y, r.Min.Y
+ }
+ return r
+}
+
+// Empty reports whether r represents the empty area.
+func (r Rectangle) Empty() bool {
+ return r.Min.X >= r.Max.X || r.Min.Y >= r.Max.Y
+}
+
+// Add offsets r with the vector p.
+func (r Rectangle) Add(p Point) Rectangle {
+ return Rectangle{
+ Point{r.Min.X + p.X, r.Min.Y + p.Y},
+ Point{r.Max.X + p.X, r.Max.Y + p.Y},
+ }
+}
+
+// Sub offsets r with the vector -p.
+func (r Rectangle) Sub(p Point) Rectangle {
+ return Rectangle{
+ Point{r.Min.X - p.X, r.Min.Y - p.Y},
+ Point{r.Max.X - p.X, r.Max.Y - p.Y},
+ }
+}
diff --git a/gio/giold/font/gofont/gofont.go b/gio/giold/font/gofont/gofont.go
new file mode 100644
index 0000000..9dedcd5
--- /dev/null
+++ b/gio/giold/font/gofont/gofont.go
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package gofont exports the Go fonts as a text.Collection.
+//
+// See https://blog.golang.org/go-fonts for a description of the
+// fonts, and the golang.org/x/image/font/gofont packages for the
+// font data.
+package gofont
+
+import (
+ "fmt"
+ "sync"
+
+ "golang.org/x/image/font/gofont/gobold"
+ "golang.org/x/image/font/gofont/gobolditalic"
+ "golang.org/x/image/font/gofont/goitalic"
+ "golang.org/x/image/font/gofont/gomedium"
+ "golang.org/x/image/font/gofont/gomediumitalic"
+ "golang.org/x/image/font/gofont/gomono"
+ "golang.org/x/image/font/gofont/gomonobold"
+ "golang.org/x/image/font/gofont/gomonobolditalic"
+ "golang.org/x/image/font/gofont/gomonoitalic"
+ "golang.org/x/image/font/gofont/goregular"
+ "golang.org/x/image/font/gofont/gosmallcaps"
+ "golang.org/x/image/font/gofont/gosmallcapsitalic"
+
+ "realy.lol/gio/font/opentype"
+ "realy.lol/gio/text"
+)
+
+var (
+ once sync.Once
+ collection []text.FontFace
+)
+
+func Collection() []text.FontFace {
+ once.Do(func() {
+ register(text.Font{}, goregular.TTF)
+ register(text.Font{Style: text.Italic}, goitalic.TTF)
+ register(text.Font{Weight: text.Bold}, gobold.TTF)
+ register(text.Font{Style: text.Italic, Weight: text.Bold},
+ gobolditalic.TTF)
+ register(text.Font{Weight: text.Medium}, gomedium.TTF)
+ register(text.Font{Weight: text.Medium, Style: text.Italic},
+ gomediumitalic.TTF)
+ register(text.Font{Variant: "Mono"}, gomono.TTF)
+ register(text.Font{Variant: "Mono", Weight: text.Bold}, gomonobold.TTF)
+ register(text.Font{Variant: "Mono", Weight: text.Bold,
+ Style: text.Italic}, gomonobolditalic.TTF)
+ register(text.Font{Variant: "Mono", Style: text.Italic},
+ gomonoitalic.TTF)
+ register(text.Font{Variant: "Smallcaps"}, gosmallcaps.TTF)
+ register(text.Font{Variant: "Smallcaps", Style: text.Italic},
+ gosmallcapsitalic.TTF)
+ // Ensure that any outside appends will not reuse the backing store.
+ n := len(collection)
+ collection = collection[:n:n]
+ })
+ return collection
+}
+
+func register(fnt text.Font, ttf []byte) {
+ face, err := opentype.Parse(ttf)
+ if err != nil {
+ panic(fmt.Errorf("failed to parse font: %v", err))
+ }
+ fnt.Typeface = "Go"
+ collection = append(collection, text.FontFace{Font: fnt, Face: face})
+}
diff --git a/gio/giold/font/opentype/opentype.go b/gio/giold/font/opentype/opentype.go
new file mode 100644
index 0000000..dd74e73
--- /dev/null
+++ b/gio/giold/font/opentype/opentype.go
@@ -0,0 +1,421 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package opentype implements text layout and shaping for OpenType
+// files.
+package opentype
+
+import (
+ "bytes"
+ "io"
+ "unicode"
+ "unicode/utf8"
+
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/sfnt"
+ "golang.org/x/image/math/fixed"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/text"
+)
+
+// Font implements text.Face. Its methods are safe to use
+// concurrently.
+type Font struct {
+ font *sfnt.Font
+}
+
+// Collection is a collection of one or more fonts. When used as a text.Face,
+// each rune will be assigned a glyph from the first font in the collection
+// that supports it.
+type Collection struct {
+ fonts []*opentype
+}
+
+type opentype struct {
+ Font *sfnt.Font
+ Hinting font.Hinting
+}
+
+// a glyph represents a rune and its advance according to a Font.
+// TODO: remove this type and work on io.Readers directly.
+type glyph struct {
+ Rune rune
+ Advance fixed.Int26_6
+}
+
+// NewFont parses an SFNT font, such as TTF or OTF data, from a []byte
+// data source.
+func Parse(src []byte) (*Font, error) {
+ fnt, err := sfnt.Parse(src)
+ if err != nil {
+ return nil, err
+ }
+ return &Font{font: fnt}, nil
+}
+
+// ParseCollection parses an SFNT font collection, such as TTC or OTC data,
+// from a []byte data source.
+//
+// If passed data for a single font, a TTF or OTF instead of a TTC or OTC,
+// it will return a collection containing 1 font.
+func ParseCollection(src []byte) (*Collection, error) {
+ c, err := sfnt.ParseCollection(src)
+ if err != nil {
+ return nil, err
+ }
+ return newCollectionFrom(c)
+}
+
+// ParseCollectionReaderAt parses an SFNT collection, such as TTC or OTC data,
+// from an io.ReaderAt data source.
+//
+// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it
+// will return a collection containing 1 font.
+func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) {
+ c, err := sfnt.ParseCollectionReaderAt(src)
+ if err != nil {
+ return nil, err
+ }
+ return newCollectionFrom(c)
+}
+
+func newCollectionFrom(coll *sfnt.Collection) (*Collection, error) {
+ fonts := make([]*opentype, coll.NumFonts())
+ for i := range fonts {
+ fnt, err := coll.Font(i)
+ if err != nil {
+ return nil, err
+ }
+ fonts[i] = &opentype{
+ Font: fnt,
+ Hinting: font.HintingFull,
+ }
+ }
+ return &Collection{fonts: fonts}, nil
+}
+
+// NumFonts returns the number of fonts in the collection.
+func (c *Collection) NumFonts() int {
+ return len(c.fonts)
+}
+
+// Font returns the i'th font in the collection.
+func (c *Collection) Font(i int) (*Font, error) {
+ if i < 0 || len(c.fonts) <= i {
+ return nil, sfnt.ErrNotFound
+ }
+ return &Font{font: c.fonts[i].Font}, nil
+}
+
+func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int,
+ txt io.Reader) ([]text.Line, error) {
+ glyphs, err := readGlyphs(txt)
+ if err != nil {
+ return nil, err
+ }
+ fonts := []*opentype{{Font: f.font, Hinting: font.HintingFull}}
+ var buf sfnt.Buffer
+ return layoutText(&buf, ppem, maxWidth, fonts, glyphs)
+}
+
+func (f *Font) Shape(ppem fixed.Int26_6, str text.Layout) op.CallOp {
+ var buf sfnt.Buffer
+ return textPath(&buf, ppem,
+ []*opentype{{Font: f.font, Hinting: font.HintingFull}}, str)
+}
+
+func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics {
+ o := &opentype{Font: f.font, Hinting: font.HintingFull}
+ var buf sfnt.Buffer
+ return o.Metrics(&buf, ppem)
+}
+
+func (c *Collection) Layout(ppem fixed.Int26_6, maxWidth int,
+ txt io.Reader) ([]text.Line, error) {
+ glyphs, err := readGlyphs(txt)
+ if err != nil {
+ return nil, err
+ }
+ var buf sfnt.Buffer
+ return layoutText(&buf, ppem, maxWidth, c.fonts, glyphs)
+}
+
+func (c *Collection) Shape(ppem fixed.Int26_6, str text.Layout) op.CallOp {
+ var buf sfnt.Buffer
+ return textPath(&buf, ppem, c.fonts, str)
+}
+
+func fontForGlyph(buf *sfnt.Buffer, fonts []*opentype, r rune) *opentype {
+ if len(fonts) < 1 {
+ return nil
+ }
+ for _, f := range fonts {
+ if f.HasGlyph(buf, r) {
+ return f
+ }
+ }
+ return fonts[0] // Use replacement character from the first font if necessary
+}
+
+func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int,
+ fonts []*opentype, glyphs []glyph) ([]text.Line, error) {
+ var lines []text.Line
+ var nextLine text.Line
+ updateBounds := func(f *opentype) {
+ m := f.Metrics(sbuf, ppem)
+ if m.Ascent > nextLine.Ascent {
+ nextLine.Ascent = m.Ascent
+ }
+ // m.Height is equal to m.Ascent + m.Descent + linegap.
+ // Compute the descent including the linegap.
+ descent := m.Height - m.Ascent
+ if descent > nextLine.Descent {
+ nextLine.Descent = descent
+ }
+ b := f.Bounds(sbuf, ppem)
+ nextLine.Bounds = nextLine.Bounds.Union(b)
+ }
+ maxDotX := fixed.I(maxWidth)
+ type state struct {
+ r rune
+ f *opentype
+ adv fixed.Int26_6
+ x fixed.Int26_6
+ idx int
+ len int
+ valid bool
+ }
+ var prev, word state
+ endLine := func() {
+ if prev.f == nil && len(fonts) > 0 {
+ prev.f = fonts[0]
+ }
+ updateBounds(prev.f)
+ nextLine.Layout = toLayout(glyphs[:prev.idx:prev.idx])
+ nextLine.Width = prev.x + prev.adv
+ nextLine.Bounds.Max.X += prev.x
+ lines = append(lines, nextLine)
+ glyphs = glyphs[prev.idx:]
+ nextLine = text.Line{}
+ prev = state{}
+ word = state{}
+ }
+ for prev.idx < len(glyphs) {
+ g := &glyphs[prev.idx]
+ next := state{
+ r: g.Rune,
+ f: fontForGlyph(sbuf, fonts, g.Rune),
+ idx: prev.idx + 1,
+ len: prev.len + utf8.RuneLen(g.Rune),
+ x: prev.x + prev.adv,
+ }
+ if next.f != nil {
+ if next.f != prev.f {
+ updateBounds(next.f)
+ }
+ next.adv, next.valid = next.f.GlyphAdvance(sbuf, ppem, g.Rune)
+ }
+ if g.Rune == '\n' {
+ // The newline is zero width; use the previous
+ // character for line measurements.
+ prev.idx = next.idx
+ prev.len = next.len
+ endLine()
+ continue
+ }
+ var k fixed.Int26_6
+ if prev.valid && next.f != nil {
+ k = next.f.Kern(sbuf, ppem, prev.r, next.r)
+ }
+ // Break the line if we're out of space.
+ if prev.idx > 0 && next.x+next.adv+k > maxDotX {
+ // If the line contains no word breaks, break off the last rune.
+ if word.idx == 0 {
+ word = prev
+ }
+ next.x -= word.x + word.adv
+ next.idx -= word.idx
+ next.len -= word.len
+ prev = word
+ endLine()
+ } else if k != 0 {
+ glyphs[prev.idx-1].Advance += k
+ next.x += k
+ }
+ g.Advance = next.adv
+ if unicode.IsSpace(g.Rune) {
+ word = next
+ }
+ prev = next
+ }
+ endLine()
+ return lines, nil
+}
+
+// toLayout converts a slice of glyphs to a text.Layout.
+func toLayout(glyphs []glyph) text.Layout {
+ var buf bytes.Buffer
+ advs := make([]fixed.Int26_6, len(glyphs))
+ for i, g := range glyphs {
+ buf.WriteRune(g.Rune)
+ advs[i] = glyphs[i].Advance
+ }
+ return text.Layout{Text: buf.String(), Advances: advs}
+}
+
+func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype,
+ str text.Layout) op.CallOp {
+ var lastPos f32.Point
+ var builder clip.Path
+ ops := new(op.Ops)
+ m := op.Record(ops)
+ var x fixed.Int26_6
+ builder.Begin(ops)
+ rune := 0
+ for _, r := range str.Text {
+ if !unicode.IsSpace(r) {
+ f := fontForGlyph(buf, fonts, r)
+ if f == nil {
+ continue
+ }
+ segs, ok := f.LoadGlyph(buf, ppem, r)
+ if !ok {
+ continue
+ }
+ // Move to glyph position.
+ pos := f32.Point{
+ X: float32(x) / 64,
+ }
+ builder.Move(pos.Sub(lastPos))
+ lastPos = pos
+ var lastArg f32.Point
+ // Convert sfnt.Segments to relative segments.
+ for _, fseg := range segs {
+ nargs := 1
+ switch fseg.Op {
+ case sfnt.SegmentOpQuadTo:
+ nargs = 2
+ case sfnt.SegmentOpCubeTo:
+ nargs = 3
+ }
+ var args [3]f32.Point
+ for i := 0; i < nargs; i++ {
+ a := f32.Point{
+ X: float32(fseg.Args[i].X) / 64,
+ Y: float32(fseg.Args[i].Y) / 64,
+ }
+ args[i] = a.Sub(lastArg)
+ if i == nargs-1 {
+ lastArg = a
+ }
+ }
+ switch fseg.Op {
+ case sfnt.SegmentOpMoveTo:
+ builder.Move(args[0])
+ case sfnt.SegmentOpLineTo:
+ builder.Line(args[0])
+ case sfnt.SegmentOpQuadTo:
+ builder.Quad(args[0], args[1])
+ case sfnt.SegmentOpCubeTo:
+ builder.Cube(args[0], args[1], args[2])
+ default:
+ panic("unsupported segment op")
+ }
+ }
+ lastPos = lastPos.Add(lastArg)
+ }
+ x += str.Advances[rune]
+ rune++
+ }
+ clip.Outline{
+ Path: builder.End(),
+ }.Op().Add(ops)
+ return m.Stop()
+}
+
+func readGlyphs(r io.Reader) ([]glyph, error) {
+ var glyphs []glyph
+ buf := make([]byte, 0, 1024)
+ for {
+ n, err := r.Read(buf[len(buf):cap(buf)])
+ buf = buf[:len(buf)+n]
+ lim := len(buf)
+ // Read full runes if possible.
+ if err != io.EOF {
+ lim -= utf8.UTFMax - 1
+ }
+ i := 0
+ for i < lim {
+ c, s := utf8.DecodeRune(buf[i:])
+ i += s
+ glyphs = append(glyphs, glyph{Rune: c})
+ }
+ n = copy(buf, buf[i:])
+ buf = buf[:n]
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return nil, err
+ }
+ }
+ return glyphs, nil
+}
+
+func (f *opentype) HasGlyph(buf *sfnt.Buffer, r rune) bool {
+ g, err := f.Font.GlyphIndex(buf, r)
+ return g != 0 && err == nil
+}
+
+func (f *opentype) GlyphAdvance(buf *sfnt.Buffer, ppem fixed.Int26_6,
+ r rune) (advance fixed.Int26_6, ok bool) {
+ g, err := f.Font.GlyphIndex(buf, r)
+ if err != nil {
+ return 0, false
+ }
+ adv, err := f.Font.GlyphAdvance(buf, g, ppem, f.Hinting)
+ return adv, err == nil
+}
+
+func (f *opentype) Kern(buf *sfnt.Buffer, ppem fixed.Int26_6,
+ r0, r1 rune) fixed.Int26_6 {
+ g0, err := f.Font.GlyphIndex(buf, r0)
+ if err != nil {
+ return 0
+ }
+ g1, err := f.Font.GlyphIndex(buf, r1)
+ if err != nil {
+ return 0
+ }
+ adv, err := f.Font.Kern(buf, g0, g1, ppem, f.Hinting)
+ if err != nil {
+ return 0
+ }
+ return adv
+}
+
+func (f *opentype) Metrics(buf *sfnt.Buffer, ppem fixed.Int26_6) font.Metrics {
+ m, _ := f.Font.Metrics(buf, ppem, f.Hinting)
+ return m
+}
+
+func (f *opentype) Bounds(buf *sfnt.Buffer,
+ ppem fixed.Int26_6) fixed.Rectangle26_6 {
+ r, _ := f.Font.Bounds(buf, ppem, f.Hinting)
+ return r
+}
+
+func (f *opentype) LoadGlyph(buf *sfnt.Buffer, ppem fixed.Int26_6,
+ r rune) ([]sfnt.Segment, bool) {
+ g, err := f.Font.GlyphIndex(buf, r)
+ if err != nil {
+ return nil, false
+ }
+ segs, err := f.Font.LoadGlyph(buf, g, ppem, nil)
+ if err != nil {
+ return nil, false
+ }
+ return segs, true
+}
diff --git a/gio/giold/font/opentype/opentype_test.go b/gio/giold/font/opentype/opentype_test.go
new file mode 100644
index 0000000..d72708e
--- /dev/null
+++ b/gio/giold/font/opentype/opentype_test.go
@@ -0,0 +1,222 @@
+package opentype
+
+import (
+ "bytes"
+ "compress/gzip"
+ "encoding/binary"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+ "testing"
+
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/gofont/goregular"
+ "golang.org/x/image/font/sfnt"
+ "golang.org/x/image/math/fixed"
+
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/op"
+ "realy.lol/gio/text"
+)
+
+func TestCollectionAsFace(t *testing.T) {
+ // Load two fonts with disjoint glyphs. Font 1 supports only '1', and font 2 supports only '2'.
+ // The fonts have different glyphs for the replacement character (".notdef").
+ font1, ttf1, err := decompressFontFile("testdata/only1.ttf.gz")
+ if err != nil {
+ t.Fatalf("failed to load test font 1: %v", err)
+ }
+ font2, ttf2, err := decompressFontFile("testdata/only2.ttf.gz")
+ if err != nil {
+ t.Fatalf("failed to load test font 2: %v", err)
+ }
+
+ otc := mergeFonts(ttf1, ttf2)
+ coll, err := ParseCollection(otc)
+ if err != nil {
+ t.Fatalf("failed to load merged test font: %v", err)
+ }
+
+ shapeValid1, err := shapeRune(font1, '1')
+ if err != nil {
+ t.Fatalf("failed shaping valid glyph with font 1: %v", err)
+ }
+ shapeInvalid1, err := shapeRune(font1, '3')
+ if err != nil {
+ t.Fatalf("failed shaping invalid glyph with font 1: %v", err)
+ }
+ shapeValid2, err := shapeRune(font2, '2')
+ if err != nil {
+ t.Fatalf("failed shaping valid glyph with font 2: %v", err)
+ }
+ shapeInvalid2, err := shapeRune(font2,
+ '3') // Same invalid glyph as before to test replacement glyph difference
+ if err != nil {
+ t.Fatalf("failed shaping invalid glyph with font 2: %v", err)
+ }
+ shapeCollValid1, err := shapeRune(coll, '1')
+ if err != nil {
+ t.Fatalf("failed shaping valid glyph for font 1 with font collection: %v",
+ err)
+ }
+ shapeCollValid2, err := shapeRune(coll, '2')
+ if err != nil {
+ t.Fatalf("failed shaping valid glyph for font 2 with font collection: %v",
+ err)
+ }
+ shapeCollInvalid, err := shapeRune(coll,
+ '4') // Different invalid glyph to confirm use of the replacement glyph
+ if err != nil {
+ t.Fatalf("failed shaping invalid glyph with font collection: %v", err)
+ }
+
+ // All shapes from the original fonts should be distinct because the glyphs are distinct, including the replacement
+ // glyphs.
+ distinctShapes := []op.CallOp{shapeValid1, shapeInvalid1, shapeValid2,
+ shapeInvalid2}
+ for i := 0; i < len(distinctShapes); i++ {
+ for j := i + 1; j < len(distinctShapes); j++ {
+ if areShapesEqual(distinctShapes[i], distinctShapes[j]) {
+ t.Errorf("font shapes %d and %d are not distinct", i, j)
+ }
+ }
+ }
+
+ // Font collections should render glyphs from the first supported font. Replacement glyphs should come from the
+ // first font in all cases.
+ if !areShapesEqual(shapeCollValid1, shapeValid1) {
+ t.Error("font collection did not render the valid glyph using font 1")
+ }
+ if !areShapesEqual(shapeCollValid2, shapeValid2) {
+ t.Error("font collection did not render the valid glyph using font 2")
+ }
+ if !areShapesEqual(shapeCollInvalid, shapeInvalid1) {
+ t.Error("font collection did not render the invalid glyph using the replacement from font 1")
+ }
+}
+
+func TestEmptyString(t *testing.T) {
+ face, err := Parse(goregular.TTF)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ppem := fixed.I(200)
+
+ lines, err := face.Layout(ppem, 2000, strings.NewReader(""))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(lines) == 0 {
+ t.Fatalf("Layout returned no lines for empty string; expected 1")
+ }
+ l := lines[0]
+ exp, err := face.font.Bounds(new(sfnt.Buffer), ppem, font.HintingFull)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got := l.Bounds; got != exp {
+ t.Errorf("got bounds %+v for empty string; expected %+v", got, exp)
+ }
+}
+
+func decompressFontFile(name string) (*Font, []byte, error) {
+ f, err := os.Open(name)
+ if err != nil {
+ return nil, nil, fmt.Errorf("could not open file for reading: %s: %v",
+ name, err)
+ }
+ defer f.Close()
+ gz, err := gzip.NewReader(f)
+ if err != nil {
+ return nil, nil, fmt.Errorf("font file contains invalid gzip data: %v",
+ err)
+ }
+ src, err := ioutil.ReadAll(gz)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to decompress font file: %v", err)
+ }
+ fnt, err := Parse(src)
+ if err != nil {
+ return nil, nil, fmt.Errorf("file did not contain a valid font: %v",
+ err)
+ }
+ return fnt, src, nil
+}
+
+// mergeFonts produces a trivial OpenType Collection (OTC) file for two source fonts.
+// It makes many assumptions and is not meant for general use.
+// For file format details, see https://docs.microsoft.com/en-us/typography/opentype/spec/otff
+// For a robust tool to generate these files, see https://pypi.org/project/afdko/
+func mergeFonts(ttf1, ttf2 []byte) []byte {
+ // Locations to place the two embedded fonts. All of the offsets to the fonts' internal tables will need to be
+ // shifted from the start of the file by the appropriate amount, and then everything will work as expected.
+ offset1 := uint32(20) // Length of OpenType collection headers
+ offset2 := offset1 + uint32(len(ttf1))
+
+ var buf bytes.Buffer
+ _, _ = buf.Write([]byte("ttcf\x00\x01\x00\x00\x00\x00\x00\x02"))
+ _ = binary.Write(&buf, binary.BigEndian, offset1)
+ _ = binary.Write(&buf, binary.BigEndian, offset2)
+
+ // Inline function to copy a font into the collection verbatim, except for adding an offset to all of the font's
+ // table positions.
+ copyOffsetTTF := func(ttf []byte, offset uint32) {
+ _, _ = buf.Write(ttf[:12])
+ numTables := binary.BigEndian.Uint16(ttf[4:6])
+ for i := uint16(0); i < numTables; i++ {
+ p := 12 + 16*i
+ _, _ = buf.Write(ttf[p : p+8])
+ tblLoc := binary.BigEndian.Uint32(ttf[p+8:p+12]) + offset
+ _ = binary.Write(&buf, binary.BigEndian, tblLoc)
+ _, _ = buf.Write(ttf[p+12 : p+16])
+ }
+ _, _ = buf.Write(ttf[12+16*numTables:])
+ }
+ copyOffsetTTF(ttf1, offset1)
+ copyOffsetTTF(ttf2, offset2)
+
+ return buf.Bytes()
+}
+
+// shapeRune uses a given Face to shape exactly one rune at a fixed size, then returns the resulting shape data.
+func shapeRune(f text.Face, r rune) (op.CallOp, error) {
+ ppem := fixed.I(200)
+ lines, err := f.Layout(ppem, 2000, strings.NewReader(string(r)))
+ if err != nil {
+ return op.CallOp{}, err
+ }
+ if len(lines) != 1 {
+ return op.CallOp{}, fmt.Errorf("unexpected rendering for \"U+%08X\": got %d lines (expected: 1)",
+ r, len(lines))
+ }
+ return f.Shape(ppem, lines[0].Layout), nil
+}
+
+// areShapesEqual returns true iff both given text shapes are produced with identical operations.
+func areShapesEqual(shape1, shape2 op.CallOp) bool {
+ var ops1, ops2 op.Ops
+ shape1.Add(&ops1)
+ shape2.Add(&ops2)
+ var r1, r2 ops.Reader
+ r1.Reset(&ops1)
+ r2.Reset(&ops2)
+ for {
+ encOp1, ok1 := r1.Decode()
+ encOp2, ok2 := r2.Decode()
+ if ok1 != ok2 {
+ return false
+ }
+ if !ok1 {
+ break
+ }
+ if len(encOp1.Refs) > 0 || len(encOp2.Refs) > 0 {
+ panic("unexpected ops with refs in font shaping test")
+ }
+ if !bytes.Equal(encOp1.Data, encOp2.Data) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/gio/giold/font/opentype/testdata/only1.ttf.gz b/gio/giold/font/opentype/testdata/only1.ttf.gz
new file mode 100644
index 0000000..544159d
Binary files /dev/null and b/gio/giold/font/opentype/testdata/only1.ttf.gz differ
diff --git a/gio/giold/font/opentype/testdata/only2.ttf.gz b/gio/giold/font/opentype/testdata/only2.ttf.gz
new file mode 100644
index 0000000..87a3e68
Binary files /dev/null and b/gio/giold/font/opentype/testdata/only2.ttf.gz differ
diff --git a/gio/giold/gesture/gesture.go b/gio/giold/gesture/gesture.go
new file mode 100644
index 0000000..bc0324a
--- /dev/null
+++ b/gio/giold/gesture/gesture.go
@@ -0,0 +1,437 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package gesture implements common pointer gestures.
+
+Gestures accept low level pointer Events from an event
+Queue and detect higher level actions such as clicks
+and scrolling.
+*/
+package gesture
+
+import (
+ "image"
+ "math"
+ "runtime"
+ "time"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+
+ "realy.lol/gio/internal/fling"
+)
+
+// The duration is somewhat arbitrary.
+const doubleClickDuration = 300 * time.Millisecond
+
+// Click detects click gestures in the form
+// of ClickEvents.
+type Click struct {
+ // clickedAt is the timestamp at which
+ // the last click occurred.
+ clickedAt time.Duration
+ // clicks is incremented if successive clicks
+ // are performed within a fixed duration.
+ clicks int
+ // pressed tracks whether the pointer is pressed.
+ pressed bool
+ // entered tracks whether the pointer is inside the gesture.
+ entered bool
+ // pid is the pointer.ID.
+ pid pointer.ID
+ Button pointer.Buttons
+}
+
+type ClickState uint8
+
+// ClickEvent represent a click action, either a
+// TypePress for the beginning of a click or a
+// TypeClick for a completed click.
+type ClickEvent struct {
+ Type ClickType
+ Position f32.Point
+ Source pointer.Source
+ Modifiers key.Modifiers
+ // NumClicks records successive clicks occurring
+ // within a short duration of each other.
+ NumClicks int
+ Button pointer.Buttons
+}
+
+type ClickType uint8
+
+// Drag detects drag gestures in the form of pointer.Drag events.
+type Drag struct {
+ dragging bool
+ pid pointer.ID
+ start f32.Point
+ grab bool
+}
+
+// Scroll detects scroll gestures and reduces them to
+// scroll distances. Scroll recognizes mouse wheel
+// movements as well as drag and fling touch gestures.
+type Scroll struct {
+ dragging bool
+ axis Axis
+ estimator fling.Extrapolation
+ flinger fling.Animation
+ pid pointer.ID
+ grab bool
+ last int
+ // Leftover scroll.
+ scroll float32
+}
+
+type ScrollState uint8
+
+type Axis uint8
+
+const (
+ Horizontal Axis = iota
+ Vertical
+ Both
+)
+
+const (
+ // TypePress is reported for the first pointer
+ // press.
+ TypePress ClickType = iota
+ // TypeClick is reported when a click action
+ // is complete.
+ TypeClick
+ // TypeCancel is reported when the gesture is
+ // cancelled.
+ TypeCancel
+)
+
+const (
+ // StateIdle is the default scroll state.
+ StateIdle ScrollState = iota
+ // StateDrag is reported during drag gestures.
+ StateDragging
+ // StateFlinging is reported when a fling is
+ // in progress.
+ StateFlinging
+)
+
+var touchSlop = unit.Dp(3)
+
+// Add the handler to the operation list to receive click events.
+func (c *Click) Add(ops *op.Ops) {
+ op := pointer.InputOp{
+ Tag: c,
+ Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
+ }
+ op.Add(ops)
+}
+
+// Hovered returns whether a pointer is inside the area.
+func (c *Click) Hovered() bool {
+ return c.entered
+}
+
+// Pressed returns whether a pointer is pressing.
+func (c *Click) Pressed() bool {
+ return c.pressed
+}
+
+// Events returns the next click event, if any.
+func (c *Click) Events(q event.Queue) []ClickEvent {
+ var events []ClickEvent
+ for _, evt := range q.Events(c) {
+ // I.S(evt)
+ e, ok := evt.(pointer.Event)
+ if !ok {
+ continue
+ }
+ switch e.Type {
+ case pointer.Release:
+ if !c.pressed || c.pid != e.PointerID {
+ break
+ }
+ c.pressed = false
+ if c.entered {
+ if e.Time-c.clickedAt < doubleClickDuration ||
+ (c.clicks == 2 && e.Time-c.clickedAt < doubleClickDuration*2) {
+ c.clicks++
+ } else {
+ c.clicks = 1
+ }
+ c.clickedAt = e.Time
+ events = append(events, ClickEvent{
+ Type: TypeClick, Position: e.Position, Source: e.Source,
+ Modifiers: e.Modifiers,
+ Button: e.Buttons, NumClicks: c.clicks,
+ })
+ } else {
+ events = append(events, ClickEvent{Type: TypeCancel})
+ }
+ case pointer.Cancel:
+ wasPressed := c.pressed
+ c.pressed = false
+ c.entered = false
+ if wasPressed {
+ events = append(events, ClickEvent{Type: TypeCancel})
+ }
+ case pointer.Press:
+ if c.pressed {
+ break
+ }
+ // if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary {
+ // break
+ // }
+ if !c.entered {
+ c.pid = e.PointerID
+ }
+ if c.pid != e.PointerID {
+ break
+ }
+ c.pressed = true
+ events = append(events, ClickEvent{
+ Type: TypePress, Position: e.Position, Source: e.Source,
+ Modifiers: e.Modifiers, Button: e.Buttons,
+ })
+ case pointer.Leave:
+ if !c.pressed {
+ c.pid = e.PointerID
+ }
+ if c.pid == e.PointerID {
+ c.entered = false
+ }
+ case pointer.Enter:
+ if !c.pressed {
+ c.pid = e.PointerID
+ }
+ if c.pid == e.PointerID {
+ c.entered = true
+ }
+ }
+ }
+ return events
+}
+
+func (ClickEvent) ImplementsEvent() {}
+
+// Add the handler to the operation list to receive scroll events.
+func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) {
+ oph := pointer.InputOp{
+ Tag: s,
+ Grab: s.grab,
+ Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
+ ScrollBounds: bounds,
+ }
+ oph.Add(ops)
+ if s.flinger.Active() {
+ op.InvalidateOp{}.Add(ops)
+ }
+}
+
+// Stop any remaining fling movement.
+func (s *Scroll) Stop() {
+ s.flinger = fling.Animation{}
+}
+
+// Scroll detects the scrolling distance from the available events and
+// ongoing fling gestures.
+func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time,
+ axis Axis) int {
+ if s.axis != axis {
+ s.axis = axis
+ return 0
+ }
+ total := 0
+ for _, evt := range q.Events(s) {
+ e, ok := evt.(pointer.Event)
+ if !ok {
+ continue
+ }
+ switch e.Type {
+ case pointer.Press:
+ if s.dragging {
+ break
+ }
+ // Only scroll on touch drags, or on Android where mice
+ // drags also scroll by convention.
+ if e.Source != pointer.Touch && runtime.GOOS != "android" {
+ break
+ }
+ s.Stop()
+ s.estimator = fling.Extrapolation{}
+ v := s.val(e.Position)
+ s.last = int(math.Round(float64(v)))
+ s.estimator.Sample(e.Time, v)
+ s.dragging = true
+ s.pid = e.PointerID
+ case pointer.Release:
+ if s.pid != e.PointerID {
+ break
+ }
+ fling := s.estimator.Estimate()
+ if slop, d := float32(cfg.Px(touchSlop)), fling.Distance; d < -slop || d > slop {
+ s.flinger.Start(cfg, t, fling.Velocity)
+ }
+ fallthrough
+ case pointer.Cancel:
+ s.dragging = false
+ s.grab = false
+ case pointer.Scroll:
+ switch s.axis {
+ case Horizontal:
+ s.scroll += e.Scroll.X
+ case Vertical:
+ s.scroll += e.Scroll.Y
+ }
+ iscroll := int(s.scroll)
+ s.scroll -= float32(iscroll)
+ total += iscroll
+ case pointer.Drag:
+ if !s.dragging || s.pid != e.PointerID {
+ continue
+ }
+ val := s.val(e.Position)
+ s.estimator.Sample(e.Time, val)
+ v := int(math.Round(float64(val)))
+ dist := s.last - v
+ if e.Priority < pointer.Grabbed {
+ slop := cfg.Px(touchSlop)
+ if dist := dist; dist >= slop || -slop >= dist {
+ s.grab = true
+ }
+ } else {
+ s.last = v
+ total += dist
+ }
+ }
+ }
+ total += s.flinger.Tick(t)
+ return total
+}
+
+func (s *Scroll) val(p f32.Point) float32 {
+ if s.axis == Horizontal {
+ return p.X
+ } else {
+ return p.Y
+ }
+}
+
+// State reports the scroll state.
+func (s *Scroll) State() ScrollState {
+ switch {
+ case s.flinger.Active():
+ return StateFlinging
+ case s.dragging:
+ return StateDragging
+ default:
+ return StateIdle
+ }
+}
+
+// Add the handler to the operation list to receive drag events.
+func (d *Drag) Add(ops *op.Ops) {
+ op := pointer.InputOp{
+ Tag: d,
+ Grab: d.grab,
+ Types: pointer.Press | pointer.Drag | pointer.Release,
+ }
+ op.Add(ops)
+}
+
+// Events returns the next drag events, if any.
+func (d *Drag) Events(cfg unit.Metric, q event.Queue,
+ axis Axis) []pointer.Event {
+ var events []pointer.Event
+ for _, e := range q.Events(d) {
+ e, ok := e.(pointer.Event)
+ if !ok {
+ continue
+ }
+
+ switch e.Type {
+ case pointer.Press:
+ if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) {
+ continue
+ }
+ if d.dragging {
+ continue
+ }
+ d.dragging = true
+ d.pid = e.PointerID
+ d.start = e.Position
+ case pointer.Drag:
+ if !d.dragging || e.PointerID != d.pid {
+ continue
+ }
+ switch axis {
+ case Horizontal:
+ e.Position.Y = d.start.Y
+ case Vertical:
+ e.Position.X = d.start.X
+ case Both:
+ // Do nothing
+ }
+ if e.Priority < pointer.Grabbed {
+ diff := e.Position.Sub(d.start)
+ slop := cfg.Px(touchSlop)
+ if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
+ d.grab = true
+ }
+ }
+ case pointer.Release, pointer.Cancel:
+ if !d.dragging || e.PointerID != d.pid {
+ continue
+ }
+ d.dragging = false
+ d.grab = false
+ }
+
+ events = append(events, e)
+ }
+
+ return events
+}
+
+// Dragging reports whether it's currently in use.
+func (d *Drag) Dragging() bool { return d.dragging }
+
+func (a Axis) String() string {
+ switch a {
+ case Horizontal:
+ return "Horizontal"
+ case Vertical:
+ return "Vertical"
+ default:
+ panic("invalid Axis")
+ }
+}
+
+func (ct ClickType) String() string {
+ switch ct {
+ case TypePress:
+ return "TypePress"
+ case TypeClick:
+ return "TypeClick"
+ case TypeCancel:
+ return "TypeCancel"
+ default:
+ panic("invalid ClickType")
+ }
+}
+
+func (s ScrollState) String() string {
+ switch s {
+ case StateIdle:
+ return "StateIdle"
+ case StateDragging:
+ return "StateDragging"
+ case StateFlinging:
+ return "StateFlinging"
+ default:
+ panic("unreachable")
+ }
+}
diff --git a/gio/giold/gesture/gesture_test.go b/gio/giold/gesture/gesture_test.go
new file mode 100644
index 0000000..d2f69ea
--- /dev/null
+++ b/gio/giold/gesture/gesture_test.go
@@ -0,0 +1,88 @@
+package gesture
+
+import (
+ "testing"
+ "time"
+
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/router"
+ "realy.lol/gio/op"
+)
+
+func TestMouseClicks(t *testing.T) {
+ for _, tc := range []struct {
+ label string
+ events []event.Event
+ clicks []int // number of combined clicks per click (single, double...)
+ }{
+ {
+ label: "single click",
+ events: mouseClickEvents(200 * time.Millisecond),
+ clicks: []int{1},
+ },
+ {
+ label: "double click",
+ events: mouseClickEvents(
+ 100*time.Millisecond,
+ 100*time.Millisecond+doubleClickDuration-1),
+ clicks: []int{1, 2},
+ },
+ {
+ label: "two single clicks",
+ events: mouseClickEvents(
+ 100*time.Millisecond,
+ 100*time.Millisecond+doubleClickDuration+1),
+ clicks: []int{1, 1},
+ },
+ } {
+ t.Run(tc.label, func(t *testing.T) {
+ var click Click
+ var ops op.Ops
+ click.Add(&ops)
+
+ var r router.Router
+ r.Frame(&ops)
+ r.Queue(tc.events...)
+
+ events := click.Events(&r)
+ clicks := filterMouseClicks(events)
+ if got, want := len(clicks), len(tc.clicks); got != want {
+ t.Fatalf("got %d mouse clicks, expected %d", got, want)
+ }
+
+ for i, click := range clicks {
+ if got, want := click.NumClicks, tc.clicks[i]; got != want {
+ t.Errorf("got %d combined mouse clicks, expected %d", got,
+ want)
+ }
+ }
+ })
+ }
+}
+
+func mouseClickEvents(times ...time.Duration) []event.Event {
+ press := pointer.Event{
+ Type: pointer.Press,
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ }
+ events := make([]event.Event, 0, 2*len(times))
+ for _, t := range times {
+ release := press
+ release.Type = pointer.Release
+ release.Time = t
+ events = append(events, press, release)
+ }
+ return events
+}
+
+func filterMouseClicks(events []ClickEvent) []ClickEvent {
+ var clicks []ClickEvent
+ for _, ev := range events {
+ if ev.Type == TypeClick {
+ clicks = append(clicks, ev)
+ }
+ }
+ return clicks
+}
diff --git a/gio/giold/gesture/log.go b/gio/giold/gesture/log.go
new file mode 100644
index 0000000..9e79319
--- /dev/null
+++ b/gio/giold/gesture/log.go
@@ -0,0 +1,9 @@
+package gesture
+
+// import (
+// "github.com/p9c/log"
+//
+// "github.com/p9c/gel/version"
+// )
+//
+// var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase))
diff --git a/gio/giold/gpu/api.go b/gio/giold/gpu/api.go
new file mode 100644
index 0000000..1a87684
--- /dev/null
+++ b/gio/giold/gpu/api.go
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+import "realy.lol/gio/gpu/internal/driver"
+
+// An API carries the necessary GPU API specific resources to create a Device.
+// There is an API type for each supported GPU API such as OpenGL and Direct3D.
+type API = driver.API
+
+// OpenGL denotes the OpenGL or OpenGL ES API.
+type OpenGL = driver.OpenGL
+
+// Direct3D11 denotes the Direct3D API.
+type Direct3D11 = driver.Direct3D11
diff --git a/gio/giold/gpu/caches.go b/gio/giold/gpu/caches.go
new file mode 100644
index 0000000..3dd93cf
--- /dev/null
+++ b/gio/giold/gpu/caches.go
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+import (
+ "fmt"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/ops"
+)
+
+type resourceCache struct {
+ res map[interface{}]resource
+ newRes map[interface{}]resource
+}
+
+// opCache is like a resourceCache but using concrete types and a
+// freelist instead of two maps to avoid runtime.mapaccess2 calls
+// since benchmarking showed them as a bottleneck.
+type opCache struct {
+ // store the index + 1 in cache this key is stored in
+ index map[ops.Key]int
+ // list of indexes in cache that are free and can be used
+ freelist []int
+ cache []opCacheValue
+}
+
+type opCacheValue struct {
+ data pathData
+ // computePath is the encoded path for compute.
+ computePath encoder
+
+ bounds f32.Rectangle
+ // the fields below are handled by opCache
+ key ops.Key
+ keep bool
+}
+
+func newResourceCache() *resourceCache {
+ return &resourceCache{
+ res: make(map[interface{}]resource),
+ newRes: make(map[interface{}]resource),
+ }
+}
+
+func (r *resourceCache) get(key interface{}) (resource, bool) {
+ v, exists := r.res[key]
+ if exists {
+ r.newRes[key] = v
+ }
+ return v, exists
+}
+
+func (r *resourceCache) put(key interface{}, val resource) {
+ if _, exists := r.newRes[key]; exists {
+ panic(fmt.Errorf("key exists, %p", key))
+ }
+ r.res[key] = val
+ r.newRes[key] = val
+}
+
+func (r *resourceCache) frame() {
+ for k, v := range r.res {
+ if _, exists := r.newRes[k]; !exists {
+ delete(r.res, k)
+ v.release()
+ }
+ }
+ for k, v := range r.newRes {
+ delete(r.newRes, k)
+ r.res[k] = v
+ }
+}
+
+func (r *resourceCache) release() {
+ for _, v := range r.newRes {
+ v.release()
+ }
+ r.newRes = nil
+ r.res = nil
+}
+
+func newOpCache() *opCache {
+ return &opCache{
+ index: make(map[ops.Key]int),
+ freelist: make([]int, 0),
+ cache: make([]opCacheValue, 0),
+ }
+}
+
+func (r *opCache) get(key ops.Key) (o opCacheValue, exist bool) {
+ v := r.index[key]
+ if v == 0 {
+ return
+ }
+ r.cache[v-1].keep = true
+ return r.cache[v-1], true
+}
+
+func (r *opCache) put(key ops.Key, val opCacheValue) {
+ v := r.index[key]
+ val.keep = true
+ val.key = key
+ if v == 0 {
+ // not in cache
+ i := len(r.cache)
+ if len(r.freelist) > 0 {
+ i = r.freelist[len(r.freelist)-1]
+ r.freelist = r.freelist[:len(r.freelist)-1]
+ r.cache[i] = val
+ } else {
+ r.cache = append(r.cache, val)
+ }
+ r.index[key] = i + 1
+ } else {
+ r.cache[v-1] = val
+ }
+}
+
+func (r *opCache) frame() {
+ r.freelist = r.freelist[:0]
+ for i, v := range r.cache {
+ r.cache[i].keep = false
+ if v.keep {
+ continue
+ }
+ if v.data.data != nil {
+ v.data.release()
+ r.cache[i].data.data = nil
+ }
+ delete(r.index, v.key)
+ r.freelist = append(r.freelist, i)
+ }
+}
+
+func (r *opCache) release() {
+ for i := range r.cache {
+ r.cache[i].keep = false
+ }
+ r.frame()
+ r.index = nil
+ r.freelist = nil
+ r.cache = nil
+}
diff --git a/gio/giold/gpu/clip.go b/gio/giold/gpu/clip.go
new file mode 100644
index 0000000..7e24449
--- /dev/null
+++ b/gio/giold/gpu/clip.go
@@ -0,0 +1,98 @@
+package gpu
+
+import (
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/stroke"
+)
+
+type quadSplitter struct {
+ bounds f32.Rectangle
+ contour uint32
+ d *drawOps
+}
+
+func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) {
+ // NW.
+ encodeVertex(data, meta, -1, 1, from, ctrl, to)
+ // NE.
+ encodeVertex(data[vertStride:], meta, 1, 1, from, ctrl, to)
+ // SW.
+ encodeVertex(data[vertStride*2:], meta, -1, -1, from, ctrl, to)
+ // SE.
+ encodeVertex(data[vertStride*3:], meta, 1, -1, from, ctrl, to)
+}
+
+func encodeVertex(data []byte, meta uint32, cornerx, cornery int16,
+ from, ctrl, to f32.Point) {
+ var corner float32
+ if cornerx == 1 {
+ corner += .5
+ }
+ if cornery == 1 {
+ corner += .25
+ }
+ v := vertex{
+ Corner: corner,
+ FromX: from.X,
+ FromY: from.Y,
+ CtrlX: ctrl.X,
+ CtrlY: ctrl.Y,
+ ToX: to.X,
+ ToY: to.Y,
+ }
+ v.encode(data, meta)
+}
+
+func (qs *quadSplitter) encodeQuadTo(from, ctrl, to f32.Point) {
+ data := qs.d.writeVertCache(vertStride * 4)
+ encodeQuadTo(data, qs.contour, from, ctrl, to)
+}
+
+func (qs *quadSplitter) splitAndEncode(quad stroke.QuadSegment) {
+ cbnd := f32.Rectangle{
+ Min: quad.From,
+ Max: quad.To,
+ }.Canon()
+ from, ctrl, to := quad.From, quad.Ctrl, quad.To
+
+ // If the curve contain areas where a vertical line
+ // intersects it twice, split the curve in two x monotone
+ // lower and upper curves. The stencil fragment program
+ // expects only one intersection per curve.
+
+ // Find the t where the derivative in x is 0.
+ v0 := ctrl.Sub(from)
+ v1 := to.Sub(ctrl)
+ d := v0.X - v1.X
+ // t = v0 / d. Split if t is in ]0;1[.
+ if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X {
+ t := v0.X / d
+ ctrl0 := from.Mul(1 - t).Add(ctrl.Mul(t))
+ ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t))
+ mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t))
+ qs.encodeQuadTo(from, ctrl0, mid)
+ qs.encodeQuadTo(mid, ctrl1, to)
+ if mid.X > cbnd.Max.X {
+ cbnd.Max.X = mid.X
+ }
+ if mid.X < cbnd.Min.X {
+ cbnd.Min.X = mid.X
+ }
+ } else {
+ qs.encodeQuadTo(from, ctrl, to)
+ }
+ // Find the y extremum, if any.
+ d = v0.Y - v1.Y
+ if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y {
+ t := v0.Y / d
+ y := (1-t)*(1-t)*from.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y
+ if y > cbnd.Max.Y {
+ cbnd.Max.Y = y
+ }
+ if y < cbnd.Min.Y {
+ cbnd.Min.Y = y
+ }
+ }
+
+ qs.bounds = qs.bounds.Union(cbnd)
+}
diff --git a/gio/giold/gpu/compute.go b/gio/giold/gpu/compute.go
new file mode 100644
index 0000000..e7c7fd6
--- /dev/null
+++ b/gio/giold/gpu/compute.go
@@ -0,0 +1,1093 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "image"
+ "image/color"
+ "math/bits"
+ "time"
+ "unsafe"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/internal/byteslice"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/internal/scene"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+)
+
+type compute struct {
+ ctx driver.Device
+ enc encoder
+
+ drawOps drawOps
+ texOps []textureOp
+ cache *resourceCache
+ maxTextureDim int
+
+ programs struct {
+ elements driver.Program
+ tileAlloc driver.Program
+ pathCoarse driver.Program
+ backdrop driver.Program
+ binning driver.Program
+ coarse driver.Program
+ kernel4 driver.Program
+ }
+ buffers struct {
+ config driver.Buffer
+ scene sizedBuffer
+ state sizedBuffer
+ memory sizedBuffer
+ }
+ output struct {
+ size image.Point
+ // image is the output texture. Note that it is in RGBA format,
+ // but contains data in sRGB. See blitOutput for more detail.
+ image driver.Texture
+ blitProg driver.Program
+ }
+ // images contains ImageOp images packed into a texture atlas.
+ images struct {
+ packer packer
+ // positions maps imageOpData.handles to positions inside tex.
+ positions map[interface{}]image.Point
+ tex driver.Texture
+ }
+ // materials contains the pre-processed materials (transformed images for
+ // now, gradients etc. later) packed in a texture atlas. The atlas is used
+ // as source in kernel4.
+ materials struct {
+ // offsets maps texture ops to the offsets to put in their FillImage commands.
+ offsets map[textureKey]image.Point
+
+ prog driver.Program
+ layout driver.InputLayout
+
+ packer packer
+
+ tex driver.Texture
+ fbo driver.Framebuffer
+ quads []materialVertex
+
+ bufSize int
+ buffer driver.Buffer
+ }
+ timers struct {
+ profile string
+ t *timers
+ elements *timer
+ tileAlloc *timer
+ pathCoarse *timer
+ backdropBinning *timer
+ coarse *timer
+ kernel4 *timer
+ }
+
+ // The following fields hold scratch space to avoid garbage.
+ zeroSlice []byte
+ memHeader *memoryHeader
+ conf *config
+}
+
+// materialVertex describes a vertex of a quad used to render a transformed
+// material.
+type materialVertex struct {
+ posX, posY float32
+ u, v float32
+}
+
+// textureKey identifies textureOp.
+type textureKey struct {
+ handle interface{}
+ transform f32.Affine2D
+}
+
+// textureOp represents an imageOp that requires texture space.
+type textureOp struct {
+ // sceneIdx is the index in the scene that contains the fill image command
+ // that corresponds to the operation.
+ sceneIdx int
+ key textureKey
+ img imageOpData
+
+ // pos is the position of the untransformed image in the images texture.
+ pos image.Point
+}
+
+type encoder struct {
+ scene []scene.Command
+ npath int
+ npathseg int
+ ntrans int
+}
+
+type encodeState struct {
+ trans f32.Affine2D
+ clip f32.Rectangle
+}
+
+type sizedBuffer struct {
+ size int
+ buffer driver.Buffer
+}
+
+// config matches Config in setup.h
+type config struct {
+ n_elements uint32 // paths
+ n_pathseg uint32
+ width_in_tiles uint32
+ height_in_tiles uint32
+ tile_alloc memAlloc
+ bin_alloc memAlloc
+ ptcl_alloc memAlloc
+ pathseg_alloc memAlloc
+ anno_alloc memAlloc
+ trans_alloc memAlloc
+}
+
+// memAlloc matches Alloc in mem.h
+type memAlloc struct {
+ offset uint32
+ // size uint32
+}
+
+// memoryHeader matches the header of Memory in mem.h.
+type memoryHeader struct {
+ mem_offset uint32
+ mem_error uint32
+}
+
+// GPU structure sizes and constants.
+const (
+ tileWidthPx = 32
+ tileHeightPx = 32
+ ptclInitialAlloc = 1024
+ kernel4OutputUnit = 2
+ kernel4AtlasUnit = 3
+
+ pathSize = 12
+ binSize = 8
+ pathsegSize = 52
+ annoSize = 32
+ transSize = 24
+ stateSize = 60
+ stateStride = 4 + 2*stateSize
+)
+
+// mem.h constants.
+const (
+ memNoError = 0 // NO_ERROR
+ memMallocFailed = 1 // ERR_MALLOC_FAILED
+)
+
+func newCompute(ctx driver.Device) (*compute, error) {
+ maxDim := ctx.Caps().MaxTextureSize
+ // Large atlas textures cause artifacts due to precision loss in
+ // shaders.
+ if cap := 8192; maxDim > cap {
+ maxDim = cap
+ }
+ g := &compute{
+ ctx: ctx,
+ cache: newResourceCache(),
+ maxTextureDim: maxDim,
+ conf: new(config),
+ memHeader: new(memoryHeader),
+ }
+
+ blitProg, err := ctx.NewProgram(shader_copy_vert, shader_copy_frag)
+ if err != nil {
+ g.Release()
+ return nil, err
+ }
+ g.output.blitProg = blitProg
+
+ materialProg, err := ctx.NewProgram(shader_material_vert,
+ shader_material_frag)
+ if err != nil {
+ g.Release()
+ return nil, err
+ }
+ g.materials.prog = materialProg
+ progLayout, err := ctx.NewInputLayout(shader_material_vert,
+ []driver.InputDesc{
+ {Type: driver.DataTypeFloat, Size: 2, Offset: 0},
+ {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2},
+ })
+ if err != nil {
+ g.Release()
+ return nil, err
+ }
+ g.materials.layout = progLayout
+
+ g.drawOps.pathCache = newOpCache()
+ g.drawOps.compute = true
+
+ buf, err := ctx.NewBuffer(driver.BufferBindingShaderStorage,
+ int(unsafe.Sizeof(config{})))
+ if err != nil {
+ g.Release()
+ return nil, err
+ }
+ g.buffers.config = buf
+
+ shaders := []struct {
+ prog *driver.Program
+ src driver.ShaderSources
+ }{
+ {&g.programs.elements, shader_elements_comp},
+ {&g.programs.tileAlloc, shader_tile_alloc_comp},
+ {&g.programs.pathCoarse, shader_path_coarse_comp},
+ {&g.programs.backdrop, shader_backdrop_comp},
+ {&g.programs.binning, shader_binning_comp},
+ {&g.programs.coarse, shader_coarse_comp},
+ {&g.programs.kernel4, shader_kernel4_comp},
+ }
+ for _, shader := range shaders {
+ p, err := ctx.NewComputeProgram(shader.src)
+ if err != nil {
+ g.Release()
+ return nil, err
+ }
+ *shader.prog = p
+ }
+ return g, nil
+}
+
+func (g *compute) Collect(viewport image.Point, ops *op.Ops) {
+ g.drawOps.reset(g.cache, viewport)
+ g.drawOps.collect(g.ctx, g.cache, ops, viewport)
+ for _, img := range g.drawOps.allImageOps {
+ expandPathOp(img.path, img.clip)
+ }
+ if g.drawOps.profile && g.timers.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
+ t := &g.timers
+ t.t = newTimers(g.ctx)
+ t.elements = g.timers.t.newTimer()
+ t.tileAlloc = g.timers.t.newTimer()
+ t.pathCoarse = g.timers.t.newTimer()
+ t.backdropBinning = g.timers.t.newTimer()
+ t.coarse = g.timers.t.newTimer()
+ t.kernel4 = g.timers.t.newTimer()
+ }
+}
+
+func (g *compute) Clear(col color.NRGBA) {
+ g.drawOps.clear = true
+ g.drawOps.clearColor = f32color.LinearFromSRGB(col)
+}
+
+func (g *compute) Frame() error {
+ viewport := g.drawOps.viewport
+ tileDims := image.Point{
+ X: (viewport.X + tileWidthPx - 1) / tileWidthPx,
+ Y: (viewport.Y + tileHeightPx - 1) / tileHeightPx,
+ }
+
+ defFBO := g.ctx.BeginFrame()
+ defer g.ctx.EndFrame()
+
+ if err := g.encode(viewport); err != nil {
+ return err
+ }
+ if err := g.uploadImages(); err != nil {
+ return err
+ }
+ if err := g.renderMaterials(); err != nil {
+ return err
+ }
+ if err := g.render(tileDims); err != nil {
+ return err
+ }
+ g.ctx.BindFramebuffer(defFBO)
+ g.blitOutput(viewport)
+ g.cache.frame()
+ g.drawOps.pathCache.frame()
+ t := &g.timers
+ if g.drawOps.profile && t.t.ready() {
+ et, tat, pct, bbt := t.elements.Elapsed, t.tileAlloc.Elapsed, t.pathCoarse.Elapsed, t.backdropBinning.Elapsed
+ ct, k4t := t.coarse.Elapsed, t.kernel4.Elapsed
+ ft := et + tat + pct + bbt + ct + k4t
+ q := 100 * time.Microsecond
+ ft = ft.Round(q)
+ et, tat, pct, bbt = et.Round(q), tat.Round(q), pct.Round(q), bbt.Round(q)
+ ct, k4t = ct.Round(q), k4t.Round(q)
+ t.profile = fmt.Sprintf("ft:%7s et:%7s tat:%7s pct:%7s bbt:%7s ct:%7s k4t:%7s",
+ ft, et, tat, pct, bbt, ct, k4t)
+ }
+ g.drawOps.clear = false
+ return nil
+}
+
+func (g *compute) Profile() string {
+ return g.timers.profile
+}
+
+// blitOutput copies the compute render output to the output FBO. We need to
+// copy because compute shaders can only write to textures, not FBOs. Compute
+// shader can only write to RGBA textures, but since we actually render in sRGB
+// format we can't use glBlitFramebuffer, because it does sRGB conversion.
+func (g *compute) blitOutput(viewport image.Point) {
+ if !g.drawOps.clear {
+ g.ctx.BlendFunc(driver.BlendFactorOne,
+ driver.BlendFactorOneMinusSrcAlpha)
+ g.ctx.SetBlend(true)
+ defer g.ctx.SetBlend(false)
+ }
+ g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
+ g.ctx.BindTexture(0, g.output.image)
+ g.ctx.BindProgram(g.output.blitProg)
+ g.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4)
+}
+
+func (g *compute) encode(viewport image.Point) error {
+ g.texOps = g.texOps[:0]
+ g.enc.reset()
+
+ // Flip Y-axis.
+ flipY := f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(1, -1)).Offset(f32.Pt(0,
+ float32(viewport.Y)))
+ g.enc.transform(flipY)
+ if g.drawOps.clear {
+ g.enc.rect(f32.Rectangle{Max: layout.FPt(viewport)})
+ g.enc.fillColor(f32color.NRGBAToRGBA(g.drawOps.clearColor.SRGB()))
+ }
+ return g.encodeOps(flipY, viewport, g.drawOps.allImageOps)
+}
+
+func (g *compute) renderMaterials() error {
+ m := &g.materials
+ m.quads = m.quads[:0]
+ resize := false
+ reclaimed := false
+restart:
+ for {
+ for _, op := range g.texOps {
+ if off, exists := m.offsets[op.key]; exists {
+ g.enc.setFillImageOffset(op.sceneIdx, off)
+ continue
+ }
+ quad, bounds := g.materialQuad(op.key.transform, op.img, op.pos)
+
+ // A material is clipped to avoid drawing outside its bounds inside the atlas. However,
+ // imprecision in the clipping may cause a single pixel overflow. Be safe.
+ size := bounds.Size().Add(image.Pt(1, 1))
+ place, fits := m.packer.tryAdd(size)
+ if !fits {
+ m.offsets = nil
+ m.quads = m.quads[:0]
+ m.packer.clear()
+ if !reclaimed {
+ // Some images may no longer be in use, try again
+ // after clearing existing maps.
+ reclaimed = true
+ } else {
+ m.packer.maxDim += 256
+ resize = true
+ if m.packer.maxDim > g.maxTextureDim {
+ return errors.New("compute: no space left in material atlas")
+ }
+ }
+ m.packer.newPage()
+ continue restart
+ }
+ // Position quad to match place.
+ offset := place.Pos.Sub(bounds.Min)
+ offsetf := layout.FPt(offset)
+ for i := range quad {
+ quad[i].posX += offsetf.X
+ quad[i].posY += offsetf.Y
+ }
+ // Draw quad as two triangles.
+ m.quads = append(m.quads, quad[0], quad[1], quad[3], quad[3],
+ quad[1], quad[2])
+ if m.offsets == nil {
+ m.offsets = make(map[textureKey]image.Point)
+ }
+ m.offsets[op.key] = offset
+ g.enc.setFillImageOffset(op.sceneIdx, offset)
+ }
+ break
+ }
+ if len(m.quads) == 0 {
+ return nil
+ }
+ texSize := m.packer.maxDim
+ if resize {
+ if m.fbo != nil {
+ m.fbo.Release()
+ m.fbo = nil
+ }
+ if m.tex != nil {
+ m.tex.Release()
+ m.tex = nil
+ }
+ handle, err := g.ctx.NewTexture(driver.TextureFormatRGBA8, texSize,
+ texSize,
+ driver.FilterNearest, driver.FilterNearest,
+ driver.BufferBindingShaderStorage|driver.BufferBindingFramebuffer)
+ if err != nil {
+ return fmt.Errorf("compute: failed to create material atlas: %v",
+ err)
+ }
+ m.tex = handle
+ fbo, err := g.ctx.NewFramebuffer(handle, 0)
+ if err != nil {
+ return fmt.Errorf("compute: failed to create material framebuffer: %v",
+ err)
+ }
+ m.fbo = fbo
+ }
+ // TODO: move to shaders.
+ // Transform to clip space: [-1, -1] - [1, 1].
+ clip := f32.Affine2D{}.Scale(f32.Pt(0, 0),
+ f32.Pt(2/float32(texSize), 2/float32(texSize))).Offset(f32.Pt(-1, -1))
+ for i, v := range m.quads {
+ p := clip.Transform(f32.Pt(v.posX, v.posY))
+ m.quads[i].posX = p.X
+ m.quads[i].posY = p.Y
+ }
+ vertexData := byteslice.Slice(m.quads)
+ if len(vertexData) > m.bufSize {
+ if m.buffer != nil {
+ m.buffer.Release()
+ m.buffer = nil
+ }
+ n := pow2Ceil(len(vertexData))
+ buf, err := g.ctx.NewBuffer(driver.BufferBindingVertices, n)
+ if err != nil {
+ return err
+ }
+ m.bufSize = n
+ m.buffer = buf
+ }
+ m.buffer.Upload(vertexData)
+ g.ctx.BindTexture(0, g.images.tex)
+ g.ctx.BindFramebuffer(m.fbo)
+ g.ctx.Viewport(0, 0, texSize, texSize)
+ if reclaimed {
+ g.ctx.Clear(0, 0, 0, 0)
+ }
+ g.ctx.BindProgram(m.prog)
+ g.ctx.BindVertexBuffer(m.buffer, int(unsafe.Sizeof(m.quads[0])), 0)
+ g.ctx.BindInputLayout(m.layout)
+ g.ctx.DrawArrays(driver.DrawModeTriangles, 0, len(m.quads))
+ return nil
+}
+
+func (g *compute) uploadImages() error {
+ // padding is the number of pixels added to the right and below
+ // images, to avoid atlas filtering artifacts.
+ const padding = 1
+
+ a := &g.images
+ var uploads map[interface{}]*image.RGBA
+ resize := false
+ reclaimed := false
+restart:
+ for {
+ for i, op := range g.texOps {
+ if pos, exists := a.positions[op.img.handle]; exists {
+ g.texOps[i].pos = pos
+ continue
+ }
+ size := op.img.src.Bounds().Size().Add(image.Pt(padding, padding))
+ place, fits := a.packer.tryAdd(size)
+ if !fits {
+ a.positions = nil
+ uploads = nil
+ a.packer.clear()
+ if !reclaimed {
+ // Some images may no longer be in use, try again
+ // after clearing existing maps.
+ reclaimed = true
+ } else {
+ a.packer.maxDim += 256
+ resize = true
+ if a.packer.maxDim > g.maxTextureDim {
+ return errors.New("compute: no space left in image atlas")
+ }
+ }
+ a.packer.newPage()
+ continue restart
+ }
+ if a.positions == nil {
+ a.positions = make(map[interface{}]image.Point)
+ }
+ a.positions[op.img.handle] = place.Pos
+ g.texOps[i].pos = place.Pos
+ if uploads == nil {
+ uploads = make(map[interface{}]*image.RGBA)
+ }
+ uploads[op.img.handle] = op.img.src
+ }
+ break
+ }
+ if len(uploads) == 0 {
+ return nil
+ }
+ if resize {
+ if a.tex != nil {
+ a.tex.Release()
+ a.tex = nil
+ }
+ sz := a.packer.maxDim
+ handle, err := g.ctx.NewTexture(driver.TextureFormatSRGB, sz, sz,
+ driver.FilterLinear, driver.FilterLinear,
+ driver.BufferBindingTexture)
+ if err != nil {
+ return fmt.Errorf("compute: failed to create image atlas: %v", err)
+ }
+ a.tex = handle
+ }
+ for h, img := range uploads {
+ pos, ok := a.positions[h]
+ if !ok {
+ panic("compute: internal error: image not placed")
+ }
+ size := img.Bounds().Size()
+ driver.UploadImage(a.tex, pos, img)
+ rightPadding := image.Pt(padding, size.Y)
+ a.tex.Upload(image.Pt(pos.X+size.X, pos.Y), rightPadding,
+ g.zeros(rightPadding.X*rightPadding.Y*4))
+ bottomPadding := image.Pt(size.X, padding)
+ a.tex.Upload(image.Pt(pos.X, pos.Y+size.Y), bottomPadding,
+ g.zeros(bottomPadding.X*bottomPadding.Y*4))
+ }
+ return nil
+}
+
+func pow2Ceil(v int) int {
+ exp := bits.Len(uint(v))
+ if bits.OnesCount(uint(v)) == 1 {
+ exp--
+ }
+ return 1 << exp
+}
+
+// materialQuad constructs a quad that represents the transformed image. It returns the quad
+// and its bounds.
+func (g *compute) materialQuad(M f32.Affine2D, img imageOpData,
+ uvPos image.Point) ([4]materialVertex, image.Rectangle) {
+ imgSize := layout.FPt(img.src.Bounds().Size())
+ sx, hx, ox, hy, sy, oy := M.Elems()
+ transOff := f32.Pt(ox, oy)
+ // The 4 corners of the image rectangle transformed by M, excluding its offset, are:
+ //
+ // q0: M * (0, 0) q3: M * (w, 0)
+ // q1: M * (0, h) q2: M * (w, h)
+ //
+ // Note that q0 = M*0 = 0, q2 = q1 + q3.
+ q0 := f32.Pt(0, 0)
+ q1 := f32.Pt(hx*imgSize.Y, sy*imgSize.Y)
+ q3 := f32.Pt(sx*imgSize.X, hy*imgSize.X)
+ q2 := q1.Add(q3)
+ q0 = q0.Add(transOff)
+ q1 = q1.Add(transOff)
+ q2 = q2.Add(transOff)
+ q3 = q3.Add(transOff)
+
+ boundsf := f32.Rectangle{
+ Min: min(min(q0, q1), min(q2, q3)),
+ Max: max(max(q0, q1), max(q2, q3)),
+ }
+
+ bounds := boundRectF(boundsf)
+ uvPosf := layout.FPt(uvPos)
+ atlasScale := 1 / float32(g.images.packer.maxDim)
+ uvBounds := f32.Rectangle{
+ Min: uvPosf.Mul(atlasScale),
+ Max: uvPosf.Add(imgSize).Mul(atlasScale),
+ }
+ quad := [4]materialVertex{
+ {posX: q0.X, posY: q0.Y, u: uvBounds.Min.X, v: uvBounds.Min.Y},
+ {posX: q1.X, posY: q1.Y, u: uvBounds.Min.X, v: uvBounds.Max.Y},
+ {posX: q2.X, posY: q2.Y, u: uvBounds.Max.X, v: uvBounds.Max.Y},
+ {posX: q3.X, posY: q3.Y, u: uvBounds.Max.X, v: uvBounds.Min.Y},
+ }
+ return quad, bounds
+}
+
+func max(p1, p2 f32.Point) f32.Point {
+ p := p1
+ if p2.X > p.X {
+ p.X = p2.X
+ }
+ if p2.Y > p.Y {
+ p.Y = p2.Y
+ }
+ return p
+}
+
+func min(p1, p2 f32.Point) f32.Point {
+ p := p1
+ if p2.X < p.X {
+ p.X = p2.X
+ }
+ if p2.Y < p.Y {
+ p.Y = p2.Y
+ }
+ return p
+}
+
+func (g *compute) encodeOps(trans f32.Affine2D, viewport image.Point,
+ ops []imageOp) error {
+ for _, op := range ops {
+ bounds := layout.FRect(op.clip)
+ // clip is the union of all drawing affected by the clipping
+ // operation. TODO: tighten.
+ clip := f32.Rect(0, 0, float32(viewport.X), float32(viewport.Y))
+ nclips := g.encodeClipStack(clip, bounds, op.path, false)
+ m := op.material
+ switch m.material {
+ case materialTexture:
+ t := trans.Mul(m.trans)
+ g.texOps = append(g.texOps, textureOp{
+ sceneIdx: len(g.enc.scene),
+ img: m.data,
+ key: textureKey{
+ transform: t,
+ handle: m.data.handle,
+ },
+ })
+ // Add fill command, its offset is resolved and filled in renderMaterials.
+ g.enc.fillImage(0)
+ case materialColor:
+ g.enc.fillColor(f32color.NRGBAToRGBA(op.material.color.SRGB()))
+ case materialLinearGradient:
+ // TODO: implement.
+ g.enc.fillColor(f32color.NRGBAToRGBA(op.material.color1.SRGB()))
+ default:
+ panic("not implemented")
+ }
+ if op.path != nil && op.path.path {
+ g.enc.fillMode(scene.FillModeNonzero)
+ g.enc.transform(op.path.trans.Invert())
+ }
+ // Pop the clip stack.
+ for i := 0; i < nclips; i++ {
+ g.enc.endClip(clip)
+ }
+ }
+ return nil
+}
+
+// encodeClips encodes a stack of clip paths and return the stack depth.
+func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp,
+ begin bool) int {
+ nclips := 0
+ if p != nil && p.parent != nil {
+ nclips += g.encodeClipStack(clip, bounds, p.parent, true)
+ nclips += 1
+ }
+ isStroke := p.stroke.Width > 0
+ if p != nil && p.path {
+ if isStroke {
+ g.enc.fillMode(scene.FillModeStroke)
+ g.enc.lineWidth(p.stroke.Width)
+ }
+ pathData, _ := g.drawOps.pathCache.get(p.pathKey)
+ g.enc.transform(p.trans)
+ g.enc.append(pathData.computePath)
+ } else {
+ g.enc.rect(bounds)
+ }
+ if begin {
+ g.enc.beginClip(clip)
+ if isStroke {
+ g.enc.fillMode(scene.FillModeNonzero)
+ }
+ if p != nil && p.path {
+ g.enc.transform(p.trans.Invert())
+ }
+ }
+ return nclips
+}
+
+func encodePath(verts []byte) encoder {
+ var enc encoder
+ for len(verts) >= scene.CommandSize+4 {
+ cmd := ops.DecodeCommand(verts[4:])
+ enc.scene = append(enc.scene, cmd)
+ enc.npathseg++
+ verts = verts[scene.CommandSize+4:]
+ }
+ return enc
+}
+
+func (g *compute) render(tileDims image.Point) error {
+ const (
+ // wgSize is the largest and most common workgroup size.
+ wgSize = 128
+ // PARTITION_SIZE from elements.comp
+ partitionSize = 32 * 4
+ )
+ widthInBins := (tileDims.X + 15) / 16
+ heightInBins := (tileDims.Y + 7) / 8
+ if widthInBins*heightInBins > wgSize {
+ return fmt.Errorf("gpu: output too large (%dx%d)",
+ tileDims.X*tileWidthPx, tileDims.Y*tileHeightPx)
+ }
+
+ // Pad scene with zeroes to avoid reading garbage in elements.comp.
+ scenePadding := partitionSize - len(g.enc.scene)%partitionSize
+ g.enc.scene = append(g.enc.scene, make([]scene.Command, scenePadding)...)
+
+ realloced := false
+ scene := byteslice.Slice(g.enc.scene)
+ if s := len(scene); s > g.buffers.scene.size {
+ realloced = true
+ paddedCap := s * 11 / 10
+ if err := g.buffers.scene.ensureCapacity(g.ctx, paddedCap); err != nil {
+ return err
+ }
+ }
+ g.buffers.scene.buffer.Upload(scene)
+
+ w, h := tileDims.X*tileWidthPx, tileDims.Y*tileHeightPx
+ if g.output.size.X != w || g.output.size.Y != h {
+ if err := g.resizeOutput(image.Pt(w, h)); err != nil {
+ return err
+ }
+ }
+ g.ctx.BindImageTexture(kernel4OutputUnit, g.output.image,
+ driver.AccessWrite, driver.TextureFormatRGBA8)
+ if t := g.materials.tex; t != nil {
+ g.ctx.BindImageTexture(kernel4AtlasUnit, t, driver.AccessRead,
+ driver.TextureFormatRGBA8)
+ }
+
+ // alloc is the number of allocated bytes for static buffers.
+ var alloc uint32
+ round := func(v, quantum int) int {
+ return (v + quantum - 1) &^ (quantum - 1)
+ }
+ malloc := func(size int) memAlloc {
+ size = round(size, 4)
+ offset := alloc
+ alloc += uint32(size)
+ return memAlloc{offset /*, uint32(size)*/}
+ }
+
+ *g.conf = config{
+ n_elements: uint32(g.enc.npath),
+ n_pathseg: uint32(g.enc.npathseg),
+ width_in_tiles: uint32(tileDims.X),
+ height_in_tiles: uint32(tileDims.Y),
+ tile_alloc: malloc(g.enc.npath * pathSize),
+ bin_alloc: malloc(round(g.enc.npath, wgSize) * binSize),
+ ptcl_alloc: malloc(tileDims.X * tileDims.Y * ptclInitialAlloc),
+ pathseg_alloc: malloc(g.enc.npathseg * pathsegSize),
+ anno_alloc: malloc(g.enc.npath * annoSize),
+ trans_alloc: malloc(g.enc.ntrans * transSize),
+ }
+
+ numPartitions := (g.enc.numElements() + 127) / 128
+ // clearSize is the atomic partition counter plus flag and 2 states per partition.
+ clearSize := 4 + numPartitions*stateStride
+ if clearSize > g.buffers.state.size {
+ realloced = true
+ paddedCap := clearSize * 11 / 10
+ if err := g.buffers.state.ensureCapacity(g.ctx, paddedCap); err != nil {
+ return err
+ }
+ }
+
+ g.buffers.config.Upload(byteslice.Struct(g.conf))
+
+ minSize := int(unsafe.Sizeof(memoryHeader{})) + int(alloc)
+ if minSize > g.buffers.memory.size {
+ realloced = true
+ // Add space for dynamic GPU allocations.
+ const sizeBump = 4 * 1024 * 1024
+ minSize += sizeBump
+ if err := g.buffers.memory.ensureCapacity(g.ctx, minSize); err != nil {
+ return err
+ }
+ }
+ for {
+ *g.memHeader = memoryHeader{
+ mem_offset: alloc,
+ }
+ g.buffers.memory.buffer.Upload(byteslice.Struct(g.memHeader))
+ g.buffers.state.buffer.Upload(g.zeros(clearSize))
+
+ if realloced {
+ realloced = false
+ g.bindBuffers()
+ }
+ t := &g.timers
+ g.ctx.MemoryBarrier()
+ t.elements.begin()
+ g.ctx.BindProgram(g.programs.elements)
+ g.ctx.DispatchCompute(numPartitions, 1, 1)
+ g.ctx.MemoryBarrier()
+ t.elements.end()
+ t.tileAlloc.begin()
+ g.ctx.BindProgram(g.programs.tileAlloc)
+ g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1)
+ g.ctx.MemoryBarrier()
+ t.tileAlloc.end()
+ t.pathCoarse.begin()
+ g.ctx.BindProgram(g.programs.pathCoarse)
+ g.ctx.DispatchCompute((g.enc.npathseg+31)/32, 1, 1)
+ g.ctx.MemoryBarrier()
+ t.pathCoarse.end()
+ t.backdropBinning.begin()
+ g.ctx.BindProgram(g.programs.backdrop)
+ g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1)
+ // No barrier needed between backdrop and binning.
+ g.ctx.BindProgram(g.programs.binning)
+ g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1)
+ g.ctx.MemoryBarrier()
+ t.backdropBinning.end()
+ t.coarse.begin()
+ g.ctx.BindProgram(g.programs.coarse)
+ g.ctx.DispatchCompute(widthInBins, heightInBins, 1)
+ g.ctx.MemoryBarrier()
+ t.coarse.end()
+ t.kernel4.begin()
+ g.ctx.BindProgram(g.programs.kernel4)
+ g.ctx.DispatchCompute(tileDims.X, tileDims.Y, 1)
+ g.ctx.MemoryBarrier()
+ t.kernel4.end()
+
+ if err := g.buffers.memory.buffer.Download(byteslice.Struct(g.memHeader)); err != nil {
+ if err == driver.ErrContentLost {
+ continue
+ }
+ return err
+ }
+ switch errCode := g.memHeader.mem_error; errCode {
+ case memNoError:
+ return nil
+ case memMallocFailed:
+ // Resize memory and try again.
+ realloced = true
+ sz := g.buffers.memory.size * 15 / 10
+ if err := g.buffers.memory.ensureCapacity(g.ctx, sz); err != nil {
+ return err
+ }
+ continue
+ default:
+ return fmt.Errorf("compute: shader program failed with error %d",
+ errCode)
+ }
+ }
+}
+
+// zeros returns a byte slice with size bytes of zeros.
+func (g *compute) zeros(size int) []byte {
+ if cap(g.zeroSlice) < size {
+ g.zeroSlice = append(g.zeroSlice, make([]byte, size)...)
+ }
+ return g.zeroSlice[:size]
+}
+
+func (g *compute) resizeOutput(size image.Point) error {
+ if g.output.image != nil {
+ g.output.image.Release()
+ g.output.image = nil
+ }
+ img, err := g.ctx.NewTexture(driver.TextureFormatRGBA8, size.X, size.Y,
+ driver.FilterNearest,
+ driver.FilterNearest,
+ driver.BufferBindingShaderStorage|driver.BufferBindingTexture)
+ if err != nil {
+ return err
+ }
+ g.output.image = img
+ g.output.size = size
+ return nil
+}
+
+func (g *compute) Release() {
+ if g.drawOps.pathCache != nil {
+ g.drawOps.pathCache.release()
+ }
+ if g.cache != nil {
+ g.cache.release()
+ }
+ progs := []driver.Program{
+ g.programs.elements,
+ g.programs.tileAlloc,
+ g.programs.pathCoarse,
+ g.programs.backdrop,
+ g.programs.binning,
+ g.programs.coarse,
+ g.programs.kernel4,
+ }
+ if p := g.output.blitProg; p != nil {
+ p.Release()
+ }
+ for _, p := range progs {
+ if p != nil {
+ p.Release()
+ }
+ }
+ g.buffers.scene.release()
+ g.buffers.state.release()
+ g.buffers.memory.release()
+ if b := g.buffers.config; b != nil {
+ b.Release()
+ }
+ if g.output.image != nil {
+ g.output.image.Release()
+ }
+ if g.images.tex != nil {
+ g.images.tex.Release()
+ }
+ if g.materials.layout != nil {
+ g.materials.layout.Release()
+ }
+ if g.materials.prog != nil {
+ g.materials.prog.Release()
+ }
+ if g.materials.fbo != nil {
+ g.materials.fbo.Release()
+ }
+ if g.materials.tex != nil {
+ g.materials.tex.Release()
+ }
+ if g.materials.buffer != nil {
+ g.materials.buffer.Release()
+ }
+ if g.timers.t != nil {
+ g.timers.t.release()
+ }
+
+ *g = compute{}
+}
+
+func (g *compute) bindBuffers() {
+ bindStorageBuffers(g.programs.elements, g.buffers.memory.buffer,
+ g.buffers.config, g.buffers.scene.buffer, g.buffers.state.buffer)
+ bindStorageBuffers(g.programs.tileAlloc, g.buffers.memory.buffer,
+ g.buffers.config)
+ bindStorageBuffers(g.programs.pathCoarse, g.buffers.memory.buffer,
+ g.buffers.config)
+ bindStorageBuffers(g.programs.backdrop, g.buffers.memory.buffer,
+ g.buffers.config)
+ bindStorageBuffers(g.programs.binning, g.buffers.memory.buffer,
+ g.buffers.config)
+ bindStorageBuffers(g.programs.coarse, g.buffers.memory.buffer,
+ g.buffers.config)
+ bindStorageBuffers(g.programs.kernel4, g.buffers.memory.buffer,
+ g.buffers.config)
+}
+
+func (b *sizedBuffer) release() {
+ if b.buffer == nil {
+ return
+ }
+ b.buffer.Release()
+ *b = sizedBuffer{}
+}
+
+func (b *sizedBuffer) ensureCapacity(ctx driver.Device, size int) error {
+ if b.size >= size {
+ return nil
+ }
+ if b.buffer != nil {
+ b.release()
+ }
+ buf, err := ctx.NewBuffer(driver.BufferBindingShaderStorage, size)
+ if err != nil {
+ return err
+ }
+ b.buffer = buf
+ b.size = size
+ return nil
+}
+
+func bindStorageBuffers(prog driver.Program, buffers ...driver.Buffer) {
+ for i, buf := range buffers {
+ prog.SetStorageBuffer(i, buf)
+ }
+}
+
+var bo = binary.LittleEndian
+
+func (e *encoder) reset() {
+ e.scene = e.scene[:0]
+ e.npath = 0
+ e.npathseg = 0
+ e.ntrans = 0
+}
+
+func (e *encoder) numElements() int {
+ return len(e.scene)
+}
+
+func (e *encoder) append(e2 encoder) {
+ e.scene = append(e.scene, e2.scene...)
+ e.npath += e2.npath
+ e.npathseg += e2.npathseg
+ e.ntrans += e2.ntrans
+}
+
+func (e *encoder) transform(m f32.Affine2D) {
+ e.scene = append(e.scene, scene.Transform(m))
+ e.ntrans++
+}
+
+func (e *encoder) lineWidth(width float32) {
+ e.scene = append(e.scene, scene.SetLineWidth(width))
+}
+
+func (e *encoder) fillMode(mode scene.FillMode) {
+ e.scene = append(e.scene, scene.SetFillMode(mode))
+}
+
+func (e *encoder) beginClip(bbox f32.Rectangle) {
+ e.scene = append(e.scene, scene.BeginClip(bbox))
+ e.npath++
+}
+
+func (e *encoder) endClip(bbox f32.Rectangle) {
+ e.scene = append(e.scene, scene.EndClip(bbox))
+ e.npath++
+}
+
+func (e *encoder) rect(r f32.Rectangle) {
+ // Rectangle corners, clock-wise.
+ c0, c1, c2, c3 := r.Min, f32.Pt(r.Min.X, r.Max.Y), r.Max, f32.Pt(r.Max.X,
+ r.Min.Y)
+ e.line(c0, c1)
+ e.line(c1, c2)
+ e.line(c2, c3)
+ e.line(c3, c0)
+}
+
+func (e *encoder) fillColor(col color.RGBA) {
+ e.scene = append(e.scene, scene.FillColor(col))
+ e.npath++
+}
+
+func (e *encoder) setFillImageOffset(index int, offset image.Point) {
+ x := int16(offset.X)
+ y := int16(offset.Y)
+ e.scene[index][2] = uint32(uint16(x)) | uint32(uint16(y))<<16
+}
+
+func (e *encoder) fillImage(index int) {
+ e.scene = append(e.scene, scene.FillImage(index))
+ e.npath++
+}
+
+func (e *encoder) line(start, end f32.Point) {
+ e.scene = append(e.scene, scene.Line(start, end))
+ e.npathseg++
+}
+
+func (e *encoder) quad(start, ctrl, end f32.Point) {
+ e.scene = append(e.scene, scene.Quad(start, ctrl, end))
+ e.npathseg++
+}
diff --git a/gio/giold/gpu/gen.go b/gio/giold/gpu/gen.go
new file mode 100644
index 0000000..238f002
--- /dev/null
+++ b/gio/giold/gpu/gen.go
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+//go:generate go run ./internal/convertshaders -package gpu
diff --git a/gio/giold/gpu/gpu.go b/gio/giold/gpu/gpu.go
new file mode 100644
index 0000000..7ff12e5
--- /dev/null
+++ b/gio/giold/gpu/gpu.go
@@ -0,0 +1,1505 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package gpu implements the rendering of Gio drawing operations. It
+is used by package app and package app/headless and is otherwise not
+useful except for integrating with external window implementations.
+*/
+package gpu
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "image"
+ "image/color"
+ "math"
+ "os"
+ "reflect"
+ "time"
+ "unsafe"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/internal/byteslice"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/internal/scene"
+ "realy.lol/gio/internal/stroke"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+
+ // Register backends.
+ _ "realy.lol/gio/gpu/internal/d3d11"
+ _ "realy.lol/gio/gpu/internal/opengl"
+)
+
+type GPU interface {
+ // Release non-Go resources. The GPU is no longer valid after Release.
+ Release()
+ // Clear sets the clear color for the next Frame.
+ Clear(color color.NRGBA)
+ // Collect the graphics operations from frame, given the viewport.
+ Collect(viewport image.Point, frame *op.Ops)
+ // Frame clears the color buffer and draws the collected operations.
+ Frame() error
+ // Profile returns the last available profiling information. Profiling
+ // information is requested when Collect sees a ProfileOp, and the result
+ // is available through Profile at some later time.
+ Profile() string
+}
+
+type gpu struct {
+ cache *resourceCache
+
+ profile string
+ timers *timers
+ frameStart time.Time
+ zopsTimer, stencilTimer, coverTimer, cleanupTimer *timer
+ drawOps drawOps
+ ctx driver.Device
+ renderer *renderer
+}
+
+type renderer struct {
+ ctx driver.Device
+ blitter *blitter
+ pather *pather
+ packer packer
+ intersections packer
+}
+
+type drawOps struct {
+ profile bool
+ reader ops.Reader
+ states []drawState
+ cache *resourceCache
+ vertCache []byte
+ viewport image.Point
+ clear bool
+ clearColor f32color.RGBA
+ // allImageOps is the combined list of imageOps and
+ // zimageOps, in drawing order.
+ allImageOps []imageOp
+ imageOps []imageOp
+ // zimageOps are the rectangle clipped opaque images
+ // that can use fast front-to-back rendering with z-test
+ // and no blending.
+ zimageOps []imageOp
+ pathOps []*pathOp
+ pathOpCache []pathOp
+ qs quadSplitter
+ pathCache *opCache
+ // hack for the compute renderer to access
+ // converted path data.
+ compute bool
+}
+
+type drawState struct {
+ clip f32.Rectangle
+ t f32.Affine2D
+ cpath *pathOp
+ rect bool
+
+ matType materialType
+ // Current paint.ImageOp
+ image imageOpData
+ // Current paint.ColorOp, if any.
+ color color.NRGBA
+
+ // Current paint.LinearGradientOp.
+ stop1 f32.Point
+ stop2 f32.Point
+ color1 color.NRGBA
+ color2 color.NRGBA
+}
+
+type pathOp struct {
+ off f32.Point
+ // clip is the union of all
+ // later clip rectangles.
+ clip image.Rectangle
+ bounds f32.Rectangle
+ pathKey ops.Key
+ path bool
+ pathVerts []byte
+ parent *pathOp
+ place placement
+
+ // For compute
+ trans f32.Affine2D
+ stroke clip.StrokeStyle
+}
+
+type imageOp struct {
+ z float32
+ path *pathOp
+ clip image.Rectangle
+ material material
+ clipType clipType
+ place placement
+}
+
+func decodeStrokeOp(data []byte) clip.StrokeStyle {
+ _ = data[4]
+ if opconst.OpType(data[0]) != opconst.TypeStroke {
+ panic("invalid op")
+ }
+ bo := binary.LittleEndian
+ return clip.StrokeStyle{
+ Width: math.Float32frombits(bo.Uint32(data[1:])),
+ }
+}
+
+type quadsOp struct {
+ key ops.Key
+ aux []byte
+}
+
+type material struct {
+ material materialType
+ opaque bool
+ // For materialTypeColor.
+ color f32color.RGBA
+ // For materialTypeLinearGradient.
+ color1 f32color.RGBA
+ color2 f32color.RGBA
+ // For materialTypeTexture.
+ data imageOpData
+ uvTrans f32.Affine2D
+
+ // For the compute backend.
+ trans f32.Affine2D
+}
+
+// clipOp is the shadow of clip.Op.
+type clipOp struct {
+ // TODO: Use image.Rectangle?
+ bounds f32.Rectangle
+ outline bool
+}
+
+// imageOpData is the shadow of paint.ImageOp.
+type imageOpData struct {
+ src *image.RGBA
+ handle interface{}
+}
+
+type linearGradientOpData struct {
+ stop1 f32.Point
+ color1 color.NRGBA
+ stop2 f32.Point
+ color2 color.NRGBA
+}
+
+func (op *clipOp) decode(data []byte) {
+ if opconst.OpType(data[0]) != opconst.TypeClip {
+ panic("invalid op")
+ }
+ bo := binary.LittleEndian
+ r := image.Rectangle{
+ Min: image.Point{
+ X: int(int32(bo.Uint32(data[1:]))),
+ Y: int(int32(bo.Uint32(data[5:]))),
+ },
+ Max: image.Point{
+ X: int(int32(bo.Uint32(data[9:]))),
+ Y: int(int32(bo.Uint32(data[13:]))),
+ },
+ }
+ *op = clipOp{
+ bounds: layout.FRect(r),
+ outline: data[17] == 1,
+ }
+}
+
+func decodeImageOp(data []byte, refs []interface{}) imageOpData {
+ if opconst.OpType(data[0]) != opconst.TypeImage {
+ panic("invalid op")
+ }
+ handle := refs[1]
+ if handle == nil {
+ return imageOpData{}
+ }
+ return imageOpData{
+ src: refs[0].(*image.RGBA),
+ handle: handle,
+ }
+}
+
+func decodeColorOp(data []byte) color.NRGBA {
+ if opconst.OpType(data[0]) != opconst.TypeColor {
+ panic("invalid op")
+ }
+ return color.NRGBA{
+ R: data[1],
+ G: data[2],
+ B: data[3],
+ A: data[4],
+ }
+}
+
+func decodeLinearGradientOp(data []byte) linearGradientOpData {
+ if opconst.OpType(data[0]) != opconst.TypeLinearGradient {
+ panic("invalid op")
+ }
+ bo := binary.LittleEndian
+ return linearGradientOpData{
+ stop1: f32.Point{
+ X: math.Float32frombits(bo.Uint32(data[1:])),
+ Y: math.Float32frombits(bo.Uint32(data[5:])),
+ },
+ stop2: f32.Point{
+ X: math.Float32frombits(bo.Uint32(data[9:])),
+ Y: math.Float32frombits(bo.Uint32(data[13:])),
+ },
+ color1: color.NRGBA{
+ R: data[17+0],
+ G: data[17+1],
+ B: data[17+2],
+ A: data[17+3],
+ },
+ color2: color.NRGBA{
+ R: data[21+0],
+ G: data[21+1],
+ B: data[21+2],
+ A: data[21+3],
+ },
+ }
+}
+
+type clipType uint8
+
+type resource interface {
+ release()
+}
+
+type texture struct {
+ src *image.RGBA
+ tex driver.Texture
+}
+
+type blitter struct {
+ ctx driver.Device
+ viewport image.Point
+ prog [3]*program
+ layout driver.InputLayout
+ colUniforms *blitColUniforms
+ texUniforms *blitTexUniforms
+ linearGradientUniforms *blitLinearGradientUniforms
+ quadVerts driver.Buffer
+}
+
+type blitColUniforms struct {
+ vert struct {
+ blitUniforms
+ _ [12]byte // Padding to a multiple of 16.
+ }
+ frag struct {
+ colorUniforms
+ }
+}
+
+type blitTexUniforms struct {
+ vert struct {
+ blitUniforms
+ _ [12]byte // Padding to a multiple of 16.
+ }
+}
+
+type blitLinearGradientUniforms struct {
+ vert struct {
+ blitUniforms
+ _ [12]byte // Padding to a multiple of 16.
+ }
+ frag struct {
+ gradientUniforms
+ }
+}
+
+type uniformBuffer struct {
+ buf driver.Buffer
+ ptr []byte
+}
+
+type program struct {
+ prog driver.Program
+ vertUniforms *uniformBuffer
+ fragUniforms *uniformBuffer
+}
+
+type blitUniforms struct {
+ transform [4]float32
+ uvTransformR1 [4]float32
+ uvTransformR2 [4]float32
+ z float32
+}
+
+type colorUniforms struct {
+ color f32color.RGBA
+}
+
+type gradientUniforms struct {
+ color1 f32color.RGBA
+ color2 f32color.RGBA
+}
+
+type materialType uint8
+
+const (
+ clipTypeNone clipType = iota
+ clipTypePath
+ clipTypeIntersection
+)
+
+const (
+ materialColor materialType = iota
+ materialLinearGradient
+ materialTexture
+)
+
+func New(api API) (GPU, error) {
+ d, err := driver.NewDevice(api)
+ if err != nil {
+ return nil, err
+ }
+ forceCompute := os.Getenv("GIORENDERER") == "forcecompute"
+ feats := d.Caps().Features
+ switch {
+ case !forceCompute && feats.Has(driver.FeatureFloatRenderTargets):
+ return newGPU(d)
+ case feats.Has(driver.FeatureCompute):
+ return newCompute(d)
+ default:
+ return nil, errors.New("gpu: no support for float render targets nor compute")
+ }
+}
+
+func newGPU(ctx driver.Device) (*gpu, error) {
+ g := &gpu{
+ cache: newResourceCache(),
+ }
+ g.drawOps.pathCache = newOpCache()
+ if err := g.init(ctx); err != nil {
+ return nil, err
+ }
+ return g, nil
+}
+
+func (g *gpu) init(ctx driver.Device) error {
+ g.ctx = ctx
+ g.renderer = newRenderer(ctx)
+ return nil
+}
+
+func (g *gpu) Clear(col color.NRGBA) {
+ g.drawOps.clear = true
+ g.drawOps.clearColor = f32color.LinearFromSRGB(col)
+}
+
+func (g *gpu) Release() {
+ g.renderer.release()
+ g.drawOps.pathCache.release()
+ g.cache.release()
+ if g.timers != nil {
+ g.timers.release()
+ }
+ g.ctx.Release()
+}
+
+func (g *gpu) Collect(viewport image.Point, frameOps *op.Ops) {
+ g.renderer.blitter.viewport = viewport
+ g.renderer.pather.viewport = viewport
+ g.drawOps.reset(g.cache, viewport)
+ g.drawOps.collect(g.ctx, g.cache, frameOps, viewport)
+ g.frameStart = time.Now()
+ if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
+ g.timers = newTimers(g.ctx)
+ g.zopsTimer = g.timers.newTimer()
+ g.stencilTimer = g.timers.newTimer()
+ g.coverTimer = g.timers.newTimer()
+ g.cleanupTimer = g.timers.newTimer()
+ }
+}
+
+func (g *gpu) Frame() error {
+ defFBO := g.ctx.BeginFrame()
+ defer g.ctx.EndFrame()
+ viewport := g.renderer.blitter.viewport
+ for _, img := range g.drawOps.imageOps {
+ expandPathOp(img.path, img.clip)
+ }
+ if g.drawOps.profile {
+ g.zopsTimer.begin()
+ }
+ g.ctx.BindFramebuffer(defFBO)
+ g.ctx.DepthFunc(driver.DepthFuncGreater)
+ // Note that Clear must be before ClearDepth if nothing else is rendered
+ // (len(zimageOps) == 0). If not, the Fairphone 2 will corrupt the depth buffer.
+ if g.drawOps.clear {
+ g.drawOps.clear = false
+ g.ctx.Clear(g.drawOps.clearColor.Float32())
+ }
+ g.ctx.ClearDepth(0.0)
+ g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
+ g.renderer.drawZOps(g.cache, g.drawOps.zimageOps)
+ g.zopsTimer.end()
+ g.stencilTimer.begin()
+ g.ctx.SetBlend(true)
+ g.renderer.packStencils(&g.drawOps.pathOps)
+ g.renderer.stencilClips(g.drawOps.pathCache, g.drawOps.pathOps)
+ g.renderer.packIntersections(g.drawOps.imageOps)
+ g.renderer.intersect(g.drawOps.imageOps)
+ g.stencilTimer.end()
+ g.coverTimer.begin()
+ g.ctx.BindFramebuffer(defFBO)
+ g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
+ g.renderer.drawOps(g.cache, g.drawOps.imageOps)
+ g.ctx.SetBlend(false)
+ g.renderer.pather.stenciler.invalidateFBO()
+ g.coverTimer.end()
+ g.ctx.BindFramebuffer(defFBO)
+ g.cleanupTimer.begin()
+ g.cache.frame()
+ g.drawOps.pathCache.frame()
+ g.cleanupTimer.end()
+ if g.drawOps.profile && g.timers.ready() {
+ zt, st, covt, cleant := g.zopsTimer.Elapsed, g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
+ ft := zt + st + covt + cleant
+ q := 100 * time.Microsecond
+ zt, st, covt = zt.Round(q), st.Round(q), covt.Round(q)
+ frameDur := time.Since(g.frameStart).Round(q)
+ ft = ft.Round(q)
+ g.profile = fmt.Sprintf("draw:%7s gpu:%7s zt:%7s st:%7s cov:%7s",
+ frameDur, ft, zt, st, covt)
+ }
+ return nil
+}
+
+func (g *gpu) Profile() string {
+ return g.profile
+}
+
+func (r *renderer) texHandle(cache *resourceCache,
+ data imageOpData) driver.Texture {
+ var tex *texture
+ t, exists := cache.get(data.handle)
+ if !exists {
+ t = &texture{
+ src: data.src,
+ }
+ cache.put(data.handle, t)
+ }
+ tex = t.(*texture)
+ if tex.tex != nil {
+ return tex.tex
+ }
+ handle, err := r.ctx.NewTexture(driver.TextureFormatSRGB,
+ data.src.Bounds().Dx(), data.src.Bounds().Dy(), driver.FilterLinear,
+ driver.FilterLinear, driver.BufferBindingTexture)
+ if err != nil {
+ panic(err)
+ }
+ driver.UploadImage(handle, image.Pt(0, 0), data.src)
+ tex.tex = handle
+ return tex.tex
+}
+
+func (t *texture) release() {
+ if t.tex != nil {
+ t.tex.Release()
+ }
+}
+
+func newRenderer(ctx driver.Device) *renderer {
+ r := &renderer{
+ ctx: ctx,
+ blitter: newBlitter(ctx),
+ pather: newPather(ctx),
+ }
+
+ maxDim := ctx.Caps().MaxTextureSize
+ // Large atlas textures cause artifacts due to precision loss in
+ // shaders.
+ if cap := 8192; maxDim > cap {
+ maxDim = cap
+ }
+
+ r.packer.maxDim = maxDim
+ r.intersections.maxDim = maxDim
+ return r
+}
+
+func (r *renderer) release() {
+ r.pather.release()
+ r.blitter.release()
+}
+
+func newBlitter(ctx driver.Device) *blitter {
+ quadVerts, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices,
+ byteslice.Slice([]float32{
+ -1, +1, 0, 0,
+ +1, +1, 1, 0,
+ -1, -1, 0, 1,
+ +1, -1, 1, 1,
+ }),
+ )
+ if err != nil {
+ panic(err)
+ }
+ b := &blitter{
+ ctx: ctx,
+ quadVerts: quadVerts,
+ }
+ b.colUniforms = new(blitColUniforms)
+ b.texUniforms = new(blitTexUniforms)
+ b.linearGradientUniforms = new(blitLinearGradientUniforms)
+ prog, layout, err := createColorPrograms(ctx, shader_blit_vert,
+ shader_blit_frag,
+ [3]interface{}{&b.colUniforms.vert, &b.linearGradientUniforms.vert,
+ &b.texUniforms.vert},
+ [3]interface{}{&b.colUniforms.frag, &b.linearGradientUniforms.frag,
+ nil},
+ )
+ if err != nil {
+ panic(err)
+ }
+ b.prog = prog
+ b.layout = layout
+ return b
+}
+
+func (b *blitter) release() {
+ b.quadVerts.Release()
+ for _, p := range b.prog {
+ p.Release()
+ }
+ b.layout.Release()
+}
+
+func createColorPrograms(b driver.Device, vsSrc driver.ShaderSources,
+ fsSrc [3]driver.ShaderSources,
+ vertUniforms, fragUniforms [3]interface{}) ([3]*program, driver.InputLayout,
+ error) {
+ var progs [3]*program
+ {
+ prog, err := b.NewProgram(vsSrc, fsSrc[materialTexture])
+ if err != nil {
+ return progs, nil, err
+ }
+ var vertBuffer, fragBuffer *uniformBuffer
+ if u := vertUniforms[materialTexture]; u != nil {
+ vertBuffer = newUniformBuffer(b, u)
+ prog.SetVertexUniforms(vertBuffer.buf)
+ }
+ if u := fragUniforms[materialTexture]; u != nil {
+ fragBuffer = newUniformBuffer(b, u)
+ prog.SetFragmentUniforms(fragBuffer.buf)
+ }
+ progs[materialTexture] = newProgram(prog, vertBuffer, fragBuffer)
+ }
+ {
+ var vertBuffer, fragBuffer *uniformBuffer
+ prog, err := b.NewProgram(vsSrc, fsSrc[materialColor])
+ if err != nil {
+ progs[materialTexture].Release()
+ return progs, nil, err
+ }
+ if u := vertUniforms[materialColor]; u != nil {
+ vertBuffer = newUniformBuffer(b, u)
+ prog.SetVertexUniforms(vertBuffer.buf)
+ }
+ if u := fragUniforms[materialColor]; u != nil {
+ fragBuffer = newUniformBuffer(b, u)
+ prog.SetFragmentUniforms(fragBuffer.buf)
+ }
+ progs[materialColor] = newProgram(prog, vertBuffer, fragBuffer)
+ }
+ {
+ var vertBuffer, fragBuffer *uniformBuffer
+ prog, err := b.NewProgram(vsSrc, fsSrc[materialLinearGradient])
+ if err != nil {
+ progs[materialTexture].Release()
+ progs[materialColor].Release()
+ return progs, nil, err
+ }
+ if u := vertUniforms[materialLinearGradient]; u != nil {
+ vertBuffer = newUniformBuffer(b, u)
+ prog.SetVertexUniforms(vertBuffer.buf)
+ }
+ if u := fragUniforms[materialLinearGradient]; u != nil {
+ fragBuffer = newUniformBuffer(b, u)
+ prog.SetFragmentUniforms(fragBuffer.buf)
+ }
+ progs[materialLinearGradient] = newProgram(prog, vertBuffer, fragBuffer)
+ }
+ layout, err := b.NewInputLayout(vsSrc, []driver.InputDesc{
+ {Type: driver.DataTypeFloat, Size: 2, Offset: 0},
+ {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2},
+ })
+ if err != nil {
+ progs[materialTexture].Release()
+ progs[materialColor].Release()
+ progs[materialLinearGradient].Release()
+ return progs, nil, err
+ }
+ return progs, layout, nil
+}
+
+func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) {
+ if len(r.packer.sizes) == 0 {
+ return
+ }
+ fbo := -1
+ r.pather.begin(r.packer.sizes)
+ for _, p := range ops {
+ if fbo != p.place.Idx {
+ fbo = p.place.Idx
+ f := r.pather.stenciler.cover(fbo)
+ r.ctx.BindFramebuffer(f.fbo)
+ r.ctx.Clear(0.0, 0.0, 0.0, 0.0)
+ }
+ v, _ := pathCache.get(p.pathKey)
+ r.pather.stencilPath(p.clip, p.off, p.place.Pos, v.data)
+ }
+}
+
+func (r *renderer) intersect(ops []imageOp) {
+ if len(r.intersections.sizes) == 0 {
+ return
+ }
+ fbo := -1
+ r.pather.stenciler.beginIntersect(r.intersections.sizes)
+ r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0)
+ r.ctx.BindInputLayout(r.pather.stenciler.iprog.layout)
+ for _, img := range ops {
+ if img.clipType != clipTypeIntersection {
+ continue
+ }
+ if fbo != img.place.Idx {
+ fbo = img.place.Idx
+ f := r.pather.stenciler.intersections.fbos[fbo]
+ r.ctx.BindFramebuffer(f.fbo)
+ r.ctx.Clear(1.0, 0.0, 0.0, 0.0)
+ }
+ r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(),
+ img.clip.Dy())
+ r.intersectPath(img.path, img.clip)
+ }
+}
+
+func (r *renderer) intersectPath(p *pathOp, clip image.Rectangle) {
+ if p.parent != nil {
+ r.intersectPath(p.parent, clip)
+ }
+ if !p.path {
+ return
+ }
+ uv := image.Rectangle{
+ Min: p.place.Pos,
+ Max: p.place.Pos.Add(p.clip.Size()),
+ }
+ o := clip.Min.Sub(p.clip.Min)
+ sub := image.Rectangle{
+ Min: o,
+ Max: o.Add(clip.Size()),
+ }
+ fbo := r.pather.stenciler.cover(p.place.Idx)
+ r.ctx.BindTexture(0, fbo.tex)
+ coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size)
+ subScale, subOff := texSpaceTransform(layout.FRect(sub), p.clip.Size())
+ r.pather.stenciler.iprog.uniforms.vert.uvTransform = [4]float32{coverScale.X,
+ coverScale.Y, coverOff.X, coverOff.Y}
+ r.pather.stenciler.iprog.uniforms.vert.subUVTransform = [4]float32{subScale.X,
+ subScale.Y, subOff.X, subOff.Y}
+ r.pather.stenciler.iprog.prog.UploadUniforms()
+ r.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4)
+}
+
+func (r *renderer) packIntersections(ops []imageOp) {
+ r.intersections.clear()
+ for i, img := range ops {
+ var npaths int
+ var onePath *pathOp
+ for p := img.path; p != nil; p = p.parent {
+ if p.path {
+ onePath = p
+ npaths++
+ }
+ }
+ switch npaths {
+ case 0:
+ case 1:
+ place := onePath.place
+ place.Pos = place.Pos.Sub(onePath.clip.Min).Add(img.clip.Min)
+ ops[i].place = place
+ ops[i].clipType = clipTypePath
+ default:
+ sz := image.Point{X: img.clip.Dx(), Y: img.clip.Dy()}
+ place, ok := r.intersections.add(sz)
+ if !ok {
+ panic("internal error: if the intersection fit, the intersection should fit as well")
+ }
+ ops[i].clipType = clipTypeIntersection
+ ops[i].place = place
+ }
+ }
+}
+
+func (r *renderer) packStencils(pops *[]*pathOp) {
+ r.packer.clear()
+ ops := *pops
+ // Allocate atlas space for cover textures.
+ var i int
+ for i < len(ops) {
+ p := ops[i]
+ if p.clip.Empty() {
+ ops[i] = ops[len(ops)-1]
+ ops = ops[:len(ops)-1]
+ continue
+ }
+ sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()}
+ place, ok := r.packer.add(sz)
+ if !ok {
+ // The clip area is at most the entire screen. Hopefully no
+ // screen is larger than GL_MAX_TEXTURE_SIZE.
+ panic(fmt.Errorf("clip area %v is larger than maximum texture size %dx%d",
+ p.clip, r.packer.maxDim, r.packer.maxDim))
+ }
+ p.place = place
+ i++
+ }
+ *pops = ops
+}
+
+// intersects intersects clip and b where b is offset by off.
+// ceilRect returns a bounding image.Rectangle for a f32.Rectangle.
+func boundRectF(r f32.Rectangle) image.Rectangle {
+ return image.Rectangle{
+ Min: image.Point{
+ X: int(floor(r.Min.X)),
+ Y: int(floor(r.Min.Y)),
+ },
+ Max: image.Point{
+ X: int(ceil(r.Max.X)),
+ Y: int(ceil(r.Max.Y)),
+ },
+ }
+}
+
+func ceil(v float32) int {
+ return int(math.Ceil(float64(v)))
+}
+
+func floor(v float32) int {
+ return int(math.Floor(float64(v)))
+}
+
+func (d *drawOps) reset(cache *resourceCache, viewport image.Point) {
+ d.profile = false
+ d.cache = cache
+ d.viewport = viewport
+ d.imageOps = d.imageOps[:0]
+ d.allImageOps = d.allImageOps[:0]
+ d.zimageOps = d.zimageOps[:0]
+ d.pathOps = d.pathOps[:0]
+ d.pathOpCache = d.pathOpCache[:0]
+ d.vertCache = d.vertCache[:0]
+}
+
+func (d *drawOps) collect(ctx driver.Device, cache *resourceCache, root *op.Ops,
+ viewport image.Point) {
+ clip := f32.Rectangle{
+ Max: f32.Point{X: float32(viewport.X), Y: float32(viewport.Y)},
+ }
+ d.reader.Reset(root)
+ state := drawState{
+ clip: clip,
+ rect: true,
+ color: color.NRGBA{A: 0xff},
+ }
+ d.collectOps(&d.reader, state)
+ for _, p := range d.pathOps {
+ if v, exists := d.pathCache.get(p.pathKey); !exists || v.data.data == nil {
+ data := buildPath(ctx, p.pathVerts)
+ var computePath encoder
+ if d.compute {
+ computePath = encodePath(p.pathVerts)
+ }
+ d.pathCache.put(p.pathKey, opCacheValue{
+ data: data,
+ bounds: p.bounds,
+ computePath: computePath,
+ })
+ }
+ p.pathVerts = nil
+ }
+}
+
+func (d *drawOps) newPathOp() *pathOp {
+ d.pathOpCache = append(d.pathOpCache, pathOp{})
+ return &d.pathOpCache[len(d.pathOpCache)-1]
+}
+
+func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key,
+ bounds f32.Rectangle, off f32.Point, tr f32.Affine2D,
+ stroke clip.StrokeStyle) {
+ npath := d.newPathOp()
+ *npath = pathOp{
+ parent: state.cpath,
+ bounds: bounds,
+ off: off,
+ trans: tr,
+ stroke: stroke,
+ }
+ state.cpath = npath
+ if len(aux) > 0 {
+ state.rect = false
+ state.cpath.pathKey = auxKey
+ state.cpath.path = true
+ state.cpath.pathVerts = aux
+ d.pathOps = append(d.pathOps, state.cpath)
+ }
+}
+
+// split a transform into two parts, one which is pur offset and the
+// other representing the scaling, shearing and rotation part
+func splitTransform(t f32.Affine2D) (srs f32.Affine2D, offset f32.Point) {
+ sx, hx, ox, hy, sy, oy := t.Elems()
+ offset = f32.Point{X: ox, Y: oy}
+ srs = f32.NewAffine2D(sx, hx, 0, hy, sy, 0)
+ return
+}
+
+func (d *drawOps) save(id int, state drawState) {
+ if extra := id - len(d.states) + 1; extra > 0 {
+ d.states = append(d.states, make([]drawState, extra)...)
+ }
+ d.states[id] = state
+}
+
+func (d *drawOps) collectOps(r *ops.Reader, state drawState) {
+ var (
+ quads quadsOp
+ str clip.StrokeStyle
+ z int
+ )
+ d.save(opconst.InitialStateID, state)
+loop:
+ for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
+ switch opconst.OpType(encOp.Data[0]) {
+ case opconst.TypeProfile:
+ d.profile = true
+ case opconst.TypeTransform:
+ dop := ops.DecodeTransform(encOp.Data)
+ state.t = state.t.Mul(dop)
+
+ case opconst.TypeStroke:
+ str = decodeStrokeOp(encOp.Data)
+
+ case opconst.TypePath:
+ encOp, ok = r.Decode()
+ if !ok {
+ break loop
+ }
+ quads.aux = encOp.Data[opconst.TypeAuxLen:]
+ quads.key = encOp.Key
+
+ case opconst.TypeClip:
+ var op clipOp
+ op.decode(encOp.Data)
+ bounds := op.bounds
+ trans, off := splitTransform(state.t)
+ if len(quads.aux) > 0 {
+ // There is a clipping path, build the gpu data and update the
+ // cache key such that it will be equal only if the transform is the
+ // same also. Use cached data if we have it.
+ quads.key = quads.key.SetTransform(trans)
+ if v, ok := d.pathCache.get(quads.key); ok {
+ // Since the GPU data exists in the cache aux will not be used.
+ // Why is this not used for the offset shapes?
+ op.bounds = v.bounds
+ } else {
+ pathData, bounds := d.buildVerts(
+ quads.aux, trans, op.outline, str,
+ )
+ op.bounds = bounds
+ if !d.compute {
+ quads.aux = pathData
+ }
+ // add it to the cache, without GPU data, so the transform can be
+ // reused.
+ d.pathCache.put(quads.key, opCacheValue{bounds: op.bounds})
+ }
+ } else {
+ quads.aux, op.bounds, _ = d.boundsForTransformedRect(bounds,
+ trans)
+ quads.key = encOp.Key
+ quads.key.SetTransform(trans)
+ }
+ state.clip = state.clip.Intersect(op.bounds.Add(off))
+ d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, state.t,
+ str)
+ quads = quadsOp{}
+ str = clip.StrokeStyle{}
+
+ case opconst.TypeColor:
+ state.matType = materialColor
+ state.color = decodeColorOp(encOp.Data)
+ case opconst.TypeLinearGradient:
+ state.matType = materialLinearGradient
+ op := decodeLinearGradientOp(encOp.Data)
+ state.stop1 = op.stop1
+ state.stop2 = op.stop2
+ state.color1 = op.color1
+ state.color2 = op.color2
+ case opconst.TypeImage:
+ state.matType = materialTexture
+ state.image = decodeImageOp(encOp.Data, encOp.Refs)
+ case opconst.TypePaint:
+ // Transform (if needed) the painting rectangle and if so generate a clip path,
+ // for those cases also compute a partialTrans that maps texture coordinates between
+ // the new bounding rectangle and the transformed original paint rectangle.
+ trans, off := splitTransform(state.t)
+ // Fill the clip area, unless the material is a (bounded) image.
+ // TODO: Find a tighter bound.
+ inf := float32(1e6)
+ dst := f32.Rect(-inf, -inf, inf, inf)
+ if state.matType == materialTexture {
+ dst = layout.FRect(state.image.src.Rect)
+ }
+ clipData, bnd, partialTrans := d.boundsForTransformedRect(dst,
+ trans)
+ cl := state.clip.Intersect(bnd.Add(off))
+ if cl.Empty() {
+ continue
+ }
+
+ wasrect := state.rect
+ if clipData != nil {
+ // The paint operation is sheared or rotated, add a clip path representing
+ // this transformed rectangle.
+ encOp.Key.SetTransform(trans)
+ d.addClipPath(&state, clipData, encOp.Key, bnd, off, state.t,
+ clip.StrokeStyle{})
+ }
+
+ bounds := boundRectF(cl)
+ mat := state.materialFor(bnd, off, partialTrans, bounds, state.t)
+
+ if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && state.rect && mat.opaque && (mat.material == materialColor) {
+ // The image is a uniform opaque color and takes up the whole screen.
+ // Scrap images up to and including this image and set clear color.
+ d.allImageOps = d.allImageOps[:0]
+ d.zimageOps = d.zimageOps[:0]
+ d.imageOps = d.imageOps[:0]
+ z = 0
+ d.clearColor = mat.color.Opaque()
+ d.clear = true
+ continue
+ }
+ z++
+ if z != int(uint16(z)) {
+ // TODO(eliasnaur) realy.lol/gio/issue/127.
+ panic("more than 65k paint objects not supported")
+ }
+ // Assume 16-bit depth buffer.
+ const zdepth = 1 << 16
+ // Convert z to window-space, assuming depth range [0;1].
+ zf := float32(z)*2/zdepth - 1.0
+ img := imageOp{
+ z: zf,
+ path: state.cpath,
+ clip: bounds,
+ material: mat,
+ }
+
+ d.allImageOps = append(d.allImageOps, img)
+ if state.rect && img.material.opaque {
+ d.zimageOps = append(d.zimageOps, img)
+ } else {
+ d.imageOps = append(d.imageOps, img)
+ }
+ if clipData != nil {
+ // we added a clip path that should not remain
+ state.cpath = state.cpath.parent
+ state.rect = wasrect
+ }
+ case opconst.TypeSave:
+ id := ops.DecodeSave(encOp.Data)
+ d.save(id, state)
+ case opconst.TypeLoad:
+ id, mask := ops.DecodeLoad(encOp.Data)
+ s := d.states[id]
+ if mask&opconst.TransformState != 0 {
+ state.t = s.t
+ }
+ if mask&^opconst.TransformState != 0 {
+ state = s
+ }
+ }
+ }
+}
+
+func expandPathOp(p *pathOp, clip image.Rectangle) {
+ for p != nil {
+ pclip := p.clip
+ if !pclip.Empty() {
+ clip = clip.Union(pclip)
+ }
+ p.clip = clip
+ p = p.parent
+ }
+}
+
+func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point,
+ partTrans f32.Affine2D, clip image.Rectangle, trans f32.Affine2D) material {
+ var m material
+ switch d.matType {
+ case materialColor:
+ m.material = materialColor
+ m.color = f32color.LinearFromSRGB(d.color)
+ m.opaque = m.color.A == 1.0
+ case materialLinearGradient:
+ m.material = materialLinearGradient
+
+ m.color1 = f32color.LinearFromSRGB(d.color1)
+ m.color2 = f32color.LinearFromSRGB(d.color2)
+ m.opaque = m.color1.A == 1.0 && m.color2.A == 1.0
+
+ m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1,
+ d.stop2))
+ case materialTexture:
+ m.material = materialTexture
+ dr := boundRectF(rect.Add(off))
+ sz := d.image.src.Bounds().Size()
+ sr := f32.Rectangle{
+ Max: f32.Point{
+ X: float32(sz.X),
+ Y: float32(sz.Y),
+ },
+ }
+ dx := float32(dr.Dx())
+ sdx := sr.Dx()
+ sr.Min.X += float32(clip.Min.X-dr.Min.X) * sdx / dx
+ sr.Max.X -= float32(dr.Max.X-clip.Max.X) * sdx / dx
+ dy := float32(dr.Dy())
+ sdy := sr.Dy()
+ sr.Min.Y += float32(clip.Min.Y-dr.Min.Y) * sdy / dy
+ sr.Max.Y -= float32(dr.Max.Y-clip.Max.Y) * sdy / dy
+ uvScale, uvOffset := texSpaceTransform(sr, sz)
+ m.uvTrans = partTrans.Mul(f32.Affine2D{}.Scale(f32.Point{},
+ uvScale).Offset(uvOffset))
+ m.trans = trans
+ m.data = d.image
+ }
+ return m
+}
+
+func (r *renderer) drawZOps(cache *resourceCache, ops []imageOp) {
+ r.ctx.SetDepthTest(true)
+ r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0)
+ r.ctx.BindInputLayout(r.blitter.layout)
+ // Render front to back.
+ for i := len(ops) - 1; i >= 0; i-- {
+ img := ops[i]
+ m := img.material
+ switch m.material {
+ case materialTexture:
+ r.ctx.BindTexture(0, r.texHandle(cache, m.data))
+ }
+ drc := img.clip
+ scale, off := clipSpaceTransform(drc, r.blitter.viewport)
+ r.blitter.blit(img.z, m.material, m.color, m.color1, m.color2, scale,
+ off, m.uvTrans)
+ }
+ r.ctx.SetDepthTest(false)
+}
+
+func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
+ r.ctx.SetDepthTest(true)
+ r.ctx.DepthMask(false)
+ r.ctx.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOneMinusSrcAlpha)
+ r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0)
+ r.ctx.BindInputLayout(r.pather.coverer.layout)
+ var coverTex driver.Texture
+ for _, img := range ops {
+ m := img.material
+ switch m.material {
+ case materialTexture:
+ r.ctx.BindTexture(0, r.texHandle(cache, m.data))
+ }
+ drc := img.clip
+
+ scale, off := clipSpaceTransform(drc, r.blitter.viewport)
+ var fbo stencilFBO
+ switch img.clipType {
+ case clipTypeNone:
+ r.blitter.blit(img.z, m.material, m.color, m.color1, m.color2,
+ scale, off, m.uvTrans)
+ continue
+ case clipTypePath:
+ fbo = r.pather.stenciler.cover(img.place.Idx)
+ case clipTypeIntersection:
+ fbo = r.pather.stenciler.intersections.fbos[img.place.Idx]
+ }
+ if coverTex != fbo.tex {
+ coverTex = fbo.tex
+ r.ctx.BindTexture(1, coverTex)
+ }
+ uv := image.Rectangle{
+ Min: img.place.Pos,
+ Max: img.place.Pos.Add(drc.Size()),
+ }
+ coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size)
+ r.pather.cover(img.z, m.material, m.color, m.color1, m.color2, scale,
+ off, m.uvTrans, coverScale, coverOff)
+ }
+ r.ctx.DepthMask(true)
+ r.ctx.SetDepthTest(false)
+}
+
+func (b *blitter) blit(z float32, mat materialType, col f32color.RGBA,
+ col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) {
+ p := b.prog[mat]
+ b.ctx.BindProgram(p.prog)
+ var uniforms *blitUniforms
+ switch mat {
+ case materialColor:
+ b.colUniforms.frag.color = col
+ uniforms = &b.colUniforms.vert.blitUniforms
+ case materialTexture:
+ t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
+ b.texUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3,
+ 0}
+ b.texUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6,
+ 0}
+ uniforms = &b.texUniforms.vert.blitUniforms
+ case materialLinearGradient:
+ b.linearGradientUniforms.frag.color1 = col1
+ b.linearGradientUniforms.frag.color2 = col2
+
+ t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
+ b.linearGradientUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1,
+ t2, t3, 0}
+ b.linearGradientUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4,
+ t5, t6, 0}
+ uniforms = &b.linearGradientUniforms.vert.blitUniforms
+ }
+ uniforms.z = z
+ uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
+ p.UploadUniforms()
+ b.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4)
+}
+
+// newUniformBuffer creates a new GPU uniform buffer backed by the
+// structure uniformBlock points to.
+func newUniformBuffer(b driver.Device,
+ uniformBlock interface{}) *uniformBuffer {
+ ref := reflect.ValueOf(uniformBlock)
+ // Determine the size of the uniforms structure, *uniforms.
+ size := ref.Elem().Type().Size()
+ // Map the uniforms structure as a byte slice.
+ ptr := (*[1 << 30]byte)(unsafe.Pointer(ref.Pointer()))[:size:size]
+ ubuf, err := b.NewBuffer(driver.BufferBindingUniforms, len(ptr))
+ if err != nil {
+ panic(err)
+ }
+ return &uniformBuffer{buf: ubuf, ptr: ptr}
+}
+
+func (u *uniformBuffer) Upload() {
+ u.buf.Upload(u.ptr)
+}
+
+func (u *uniformBuffer) Release() {
+ u.buf.Release()
+ u.buf = nil
+}
+
+func newProgram(prog driver.Program,
+ vertUniforms, fragUniforms *uniformBuffer) *program {
+ if vertUniforms != nil {
+ prog.SetVertexUniforms(vertUniforms.buf)
+ }
+ if fragUniforms != nil {
+ prog.SetFragmentUniforms(fragUniforms.buf)
+ }
+ return &program{prog: prog, vertUniforms: vertUniforms,
+ fragUniforms: fragUniforms}
+}
+
+func (p *program) UploadUniforms() {
+ if p.vertUniforms != nil {
+ p.vertUniforms.Upload()
+ }
+ if p.fragUniforms != nil {
+ p.fragUniforms.Upload()
+ }
+}
+
+func (p *program) Release() {
+ p.prog.Release()
+ p.prog = nil
+ if p.vertUniforms != nil {
+ p.vertUniforms.Release()
+ p.vertUniforms = nil
+ }
+ if p.fragUniforms != nil {
+ p.fragUniforms.Release()
+ p.fragUniforms = nil
+ }
+}
+
+// texSpaceTransform return the scale and offset that transforms the given subimage
+// into quad texture coordinates.
+func texSpaceTransform(r f32.Rectangle, bounds image.Point) (f32.Point,
+ f32.Point) {
+ size := f32.Point{X: float32(bounds.X), Y: float32(bounds.Y)}
+ scale := f32.Point{X: r.Dx() / size.X, Y: r.Dy() / size.Y}
+ offset := f32.Point{X: r.Min.X / size.X, Y: r.Min.Y / size.Y}
+ return scale, offset
+}
+
+// gradientSpaceTransform transforms stop1 and stop2 to [(0,0), (1,1)].
+func gradientSpaceTransform(clip image.Rectangle, off f32.Point,
+ stop1, stop2 f32.Point) f32.Affine2D {
+ d := stop2.Sub(stop1)
+ l := float32(math.Sqrt(float64(d.X*d.X + d.Y*d.Y)))
+ a := float32(math.Atan2(float64(-d.Y), float64(d.X)))
+
+ // TODO: optimize
+ zp := f32.Point{}
+ return f32.Affine2D{}.
+ Scale(zp, layout.FPt(clip.Size())). // scale to pixel space
+ Offset(zp.Sub(off).Add(layout.FPt(clip.Min))). // offset to clip space
+ Offset(zp.Sub(stop1)). // offset to first stop point
+ Rotate(zp, a). // rotate to align gradient
+ Scale(zp, f32.Pt(1/l, 1/l)) // scale gradient to right size
+}
+
+// clipSpaceTransform returns the scale and offset that transforms the given
+// rectangle from a viewport into OpenGL clip space.
+func clipSpaceTransform(r image.Rectangle, viewport image.Point) (f32.Point,
+ f32.Point) {
+ // First, transform UI coordinates to OpenGL coordinates:
+ //
+ // [(-1, +1) (+1, +1)]
+ // [(-1, -1) (+1, -1)]
+ //
+ x, y := float32(r.Min.X), float32(r.Min.Y)
+ w, h := float32(r.Dx()), float32(r.Dy())
+ vx, vy := 2/float32(viewport.X), 2/float32(viewport.Y)
+ x = x*vx - 1
+ y = 1 - y*vy
+ w *= vx
+ h *= vy
+
+ // Then, compute the transformation from the fullscreen quad to
+ // the rectangle at (x, y) and dimensions (w, h).
+ scale := f32.Point{X: w * .5, Y: h * .5}
+ offset := f32.Point{X: x + w*.5, Y: y - h*.5}
+
+ return scale, offset
+}
+
+// Fill in maximal Y coordinates of the NW and NE corners.
+func fillMaxY(verts []byte) {
+ contour := 0
+ bo := binary.LittleEndian
+ for len(verts) > 0 {
+ maxy := float32(math.Inf(-1))
+ i := 0
+ for ; i+vertStride*4 <= len(verts); i += vertStride * 4 {
+ vert := verts[i : i+vertStride]
+ // MaxY contains the integer contour index.
+ pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).MaxY)):]))
+ if contour != pathContour {
+ contour = pathContour
+ break
+ }
+ fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).FromY)):]))
+ ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).CtrlY)):]))
+ toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).ToY)):]))
+ if fromy > maxy {
+ maxy = fromy
+ }
+ if ctrly > maxy {
+ maxy = ctrly
+ }
+ if toy > maxy {
+ maxy = toy
+ }
+ }
+ fillContourMaxY(maxy, verts[:i])
+ verts = verts[i:]
+ }
+}
+
+func fillContourMaxY(maxy float32, verts []byte) {
+ bo := binary.LittleEndian
+ for i := 0; i < len(verts); i += vertStride {
+ off := int(unsafe.Offsetof(((*vertex)(nil)).MaxY))
+ bo.PutUint32(verts[i+off:], math.Float32bits(maxy))
+ }
+}
+
+func (d *drawOps) writeVertCache(n int) []byte {
+ d.vertCache = append(d.vertCache, make([]byte, n)...)
+ return d.vertCache[len(d.vertCache)-n:]
+}
+
+// transform, split paths as needed, calculate maxY, bounds and create GPU vertices.
+func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool,
+ str clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) {
+ inf := float32(math.Inf(+1))
+ d.qs.bounds = f32.Rectangle{
+ Min: f32.Point{X: inf, Y: inf},
+ Max: f32.Point{X: -inf, Y: -inf},
+ }
+ d.qs.d = d
+ startLength := len(d.vertCache)
+
+ switch {
+ case str.Width > 0:
+ // Stroke path.
+ ss := stroke.StrokeStyle{
+ Width: str.Width,
+ Miter: str.Miter,
+ Cap: stroke.StrokeCap(str.Cap),
+ Join: stroke.StrokeJoin(str.Join),
+ }
+ quads := stroke.StrokePathCommands(ss, stroke.DashOp{}, pathData)
+ for _, quad := range quads {
+ d.qs.contour = quad.Contour
+ quad.Quad = quad.Quad.Transform(tr)
+
+ d.qs.splitAndEncode(quad.Quad)
+ }
+
+ case outline:
+ decodeToOutlineQuads(&d.qs, tr, pathData)
+ }
+
+ fillMaxY(d.vertCache[startLength:])
+ return d.vertCache[startLength:], d.qs.bounds
+}
+
+// decodeOutlineQuads decodes scene commands, splits them into quadratic bƩziers
+// as needed and feeds them to the supplied splitter.
+func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
+ for len(pathData) >= scene.CommandSize+4 {
+ qs.contour = bo.Uint32(pathData)
+ cmd := ops.DecodeCommand(pathData[4:])
+ switch cmd.Op() {
+ case scene.OpLine:
+ var q stroke.QuadSegment
+ q.From, q.To = scene.DecodeLine(cmd)
+ q.Ctrl = q.From.Add(q.To).Mul(.5)
+ q = q.Transform(tr)
+ qs.splitAndEncode(q)
+ case scene.OpQuad:
+ var q stroke.QuadSegment
+ q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
+ q = q.Transform(tr)
+ qs.splitAndEncode(q)
+ case scene.OpCubic:
+ for _, q := range stroke.SplitCubic(scene.DecodeCubic(cmd)) {
+ q = q.Transform(tr)
+ qs.splitAndEncode(q)
+ }
+ default:
+ panic("unsupported scene command")
+ }
+ pathData = pathData[scene.CommandSize+4:]
+ }
+}
+
+// create GPU vertices for transformed r, find the bounds and establish texture transform.
+func (d *drawOps) boundsForTransformedRect(r f32.Rectangle,
+ tr f32.Affine2D) (aux []byte, bnd f32.Rectangle, ptr f32.Affine2D) {
+ if isPureOffset(tr) {
+ // fast-path to allow blitting of pure rectangles
+ _, _, ox, _, _, oy := tr.Elems()
+ off := f32.Pt(ox, oy)
+ bnd.Min = r.Min.Add(off)
+ bnd.Max = r.Max.Add(off)
+ return
+ }
+
+ // transform all corners, find new bounds
+ corners := [4]f32.Point{
+ tr.Transform(r.Min), tr.Transform(f32.Pt(r.Max.X, r.Min.Y)),
+ tr.Transform(r.Max), tr.Transform(f32.Pt(r.Min.X, r.Max.Y)),
+ }
+ bnd.Min = f32.Pt(math.MaxFloat32, math.MaxFloat32)
+ bnd.Max = f32.Pt(-math.MaxFloat32, -math.MaxFloat32)
+ for _, c := range corners {
+ if c.X < bnd.Min.X {
+ bnd.Min.X = c.X
+ }
+ if c.Y < bnd.Min.Y {
+ bnd.Min.Y = c.Y
+ }
+ if c.X > bnd.Max.X {
+ bnd.Max.X = c.X
+ }
+ if c.Y > bnd.Max.Y {
+ bnd.Max.Y = c.Y
+ }
+ }
+
+ // build the GPU vertices
+ l := len(d.vertCache)
+ if !d.compute {
+ d.vertCache = append(d.vertCache, make([]byte, vertStride*4*4)...)
+ aux = d.vertCache[l:]
+ encodeQuadTo(aux, 0, corners[0], corners[0].Add(corners[1]).Mul(0.5),
+ corners[1])
+ encodeQuadTo(aux[vertStride*4:], 0, corners[1],
+ corners[1].Add(corners[2]).Mul(0.5), corners[2])
+ encodeQuadTo(aux[vertStride*4*2:], 0, corners[2],
+ corners[2].Add(corners[3]).Mul(0.5), corners[3])
+ encodeQuadTo(aux[vertStride*4*3:], 0, corners[3],
+ corners[3].Add(corners[0]).Mul(0.5), corners[0])
+ fillMaxY(aux)
+ } else {
+ d.vertCache = append(d.vertCache,
+ make([]byte, (scene.CommandSize+4)*4)...)
+ aux = d.vertCache[l:]
+ buf := aux
+ bo := binary.LittleEndian
+ bo.PutUint32(buf, 0) // Contour
+ ops.EncodeCommand(buf[4:], scene.Line(r.Min, f32.Pt(r.Max.X, r.Min.Y)))
+ buf = buf[4+scene.CommandSize:]
+ bo.PutUint32(buf, 0)
+ ops.EncodeCommand(buf[4:], scene.Line(f32.Pt(r.Max.X, r.Min.Y), r.Max))
+ buf = buf[4+scene.CommandSize:]
+ bo.PutUint32(buf, 0)
+ ops.EncodeCommand(buf[4:], scene.Line(r.Max, f32.Pt(r.Min.X, r.Max.Y)))
+ buf = buf[4+scene.CommandSize:]
+ bo.PutUint32(buf, 0)
+ ops.EncodeCommand(buf[4:], scene.Line(f32.Pt(r.Min.X, r.Max.Y), r.Min))
+ }
+
+ // establish the transform mapping from bounds rectangle to transformed corners
+ var P1, P2, P3 f32.Point
+ P1.X = (corners[1].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
+ P1.Y = (corners[1].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
+ P2.X = (corners[2].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
+ P2.Y = (corners[2].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
+ P3.X = (corners[3].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
+ P3.Y = (corners[3].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
+ sx, sy := P2.X-P3.X, P2.Y-P3.Y
+ ptr = f32.NewAffine2D(sx, P2.X-P1.X, P1.X-sx, sy, P2.Y-P1.Y,
+ P1.Y-sy).Invert()
+
+ return
+}
+
+func isPureOffset(t f32.Affine2D) bool {
+ a, b, _, d, e, _ := t.Elems()
+ return a == 1 && b == 0 && d == 0 && e == 1
+}
diff --git a/gio/giold/gpu/headless/driver_test.go b/gio/giold/gpu/headless/driver_test.go
new file mode 100644
index 0000000..07c0b06
--- /dev/null
+++ b/gio/giold/gpu/headless/driver_test.go
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package headless
+
+import (
+ "bytes"
+ "flag"
+ "image"
+ "image/color"
+ "image/png"
+ "io/ioutil"
+ "runtime"
+ "testing"
+
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/internal/byteslice"
+ "realy.lol/gio/internal/f32color"
+)
+
+var dumpImages = flag.Bool("saveimages", false, "save test images")
+
+var clearCol = color.NRGBA{A: 0xff, R: 0xde, G: 0xad, B: 0xbe}
+var clearColExpect = f32color.NRGBAToRGBA(clearCol)
+
+func TestFramebufferClear(t *testing.T) {
+ b := newDriver(t)
+ sz := image.Point{X: 800, Y: 600}
+ fbo := setupFBO(t, b, sz)
+ img := screenshot(t, b, fbo, sz)
+ if got := img.RGBAAt(0, 0); got != clearColExpect {
+ t.Errorf("got color %v, expected %v", got, clearColExpect)
+ }
+}
+
+func TestSimpleShader(t *testing.T) {
+ b := newDriver(t)
+ sz := image.Point{X: 800, Y: 600}
+ fbo := setupFBO(t, b, sz)
+ p, err := b.NewProgram(shader_simple_vert, shader_simple_frag)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer p.Release()
+ b.BindProgram(p)
+ b.DrawArrays(driver.DrawModeTriangles, 0, 3)
+ img := screenshot(t, b, fbo, sz)
+ if got := img.RGBAAt(0, 0); got != clearColExpect {
+ t.Errorf("got color %v, expected %v", got, clearColExpect)
+ }
+ // Just off the center to catch inverted triangles.
+ cx, cy := 300, 400
+ shaderCol := f32color.RGBA{R: .25, G: .55, B: .75, A: 1.0}
+ if got, exp := img.RGBAAt(cx,
+ cy), shaderCol.SRGB(); got != f32color.NRGBAToRGBA(exp) {
+ t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(exp))
+ }
+}
+
+func TestInputShader(t *testing.T) {
+ b := newDriver(t)
+ sz := image.Point{X: 800, Y: 600}
+ fbo := setupFBO(t, b, sz)
+ p, err := b.NewProgram(shader_input_vert, shader_simple_frag)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer p.Release()
+ b.BindProgram(p)
+ buf, err := b.NewImmutableBuffer(driver.BufferBindingVertices,
+ byteslice.Slice([]float32{
+ 0, .5, .5, 1,
+ -.5, -.5, .5, 1,
+ .5, -.5, .5, 1,
+ }),
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer buf.Release()
+ b.BindVertexBuffer(buf, 4*4, 0)
+ layout, err := b.NewInputLayout(shader_input_vert, []driver.InputDesc{
+ {
+ Type: driver.DataTypeFloat,
+ Size: 4,
+ Offset: 0,
+ },
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer layout.Release()
+ b.BindInputLayout(layout)
+ b.DrawArrays(driver.DrawModeTriangles, 0, 3)
+ img := screenshot(t, b, fbo, sz)
+ if got := img.RGBAAt(0, 0); got != clearColExpect {
+ t.Errorf("got color %v, expected %v", got, clearColExpect)
+ }
+ cx, cy := 300, 400
+ shaderCol := f32color.RGBA{R: .25, G: .55, B: .75, A: 1.0}
+ if got, exp := img.RGBAAt(cx,
+ cy), shaderCol.SRGB(); got != f32color.NRGBAToRGBA(exp) {
+ t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(exp))
+ }
+}
+
+func TestFramebuffers(t *testing.T) {
+ b := newDriver(t)
+ sz := image.Point{X: 800, Y: 600}
+ fbo1 := newFBO(t, b, sz)
+ fbo2 := newFBO(t, b, sz)
+ var (
+ col1 = color.NRGBA{R: 0xac, G: 0xbd, B: 0xef, A: 0xde}
+ col2 = color.NRGBA{R: 0xfe, G: 0xba, B: 0xbe, A: 0xca}
+ )
+ fcol1, fcol2 := f32color.LinearFromSRGB(col1), f32color.LinearFromSRGB(col2)
+ b.BindFramebuffer(fbo1)
+ b.Clear(fcol1.Float32())
+ b.BindFramebuffer(fbo2)
+ b.Clear(fcol2.Float32())
+ img := screenshot(t, b, fbo1, sz)
+ if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col1) {
+ t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col1))
+ }
+ img = screenshot(t, b, fbo2, sz)
+ if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col2) {
+ t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col2))
+ }
+}
+
+func setupFBO(t *testing.T, b driver.Device,
+ size image.Point) driver.Framebuffer {
+ fbo := newFBO(t, b, size)
+ b.BindFramebuffer(fbo)
+ // ClearColor accepts linear RGBA colors, while 8-bit colors
+ // are in the sRGB color space.
+ col := f32color.LinearFromSRGB(clearCol)
+ b.Clear(col.Float32())
+ b.ClearDepth(0.0)
+ b.Viewport(0, 0, size.X, size.Y)
+ return fbo
+}
+
+func newFBO(t *testing.T, b driver.Device,
+ size image.Point) driver.Framebuffer {
+ fboTex, err := b.NewTexture(
+ driver.TextureFormatSRGB,
+ size.X, size.Y,
+ driver.FilterNearest, driver.FilterNearest,
+ driver.BufferBindingFramebuffer,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ fboTex.Release()
+ })
+ const depthBits = 16
+ fbo, err := b.NewFramebuffer(fboTex, depthBits)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ fbo.Release()
+ })
+ return fbo
+}
+
+func newDriver(t *testing.T) driver.Device {
+ ctx, err := newContext()
+ if err != nil {
+ t.Skipf("no context available: %v", err)
+ }
+ runtime.LockOSThread()
+ if err := ctx.MakeCurrent(); err != nil {
+ t.Fatal(err)
+ }
+ b, err := driver.NewDevice(ctx.API())
+ if err != nil {
+ t.Fatal(err)
+ }
+ b.BeginFrame()
+ t.Cleanup(func() {
+ b.EndFrame()
+ ctx.ReleaseCurrent()
+ runtime.UnlockOSThread()
+ ctx.Release()
+ })
+ return b
+}
+
+func screenshot(t *testing.T, d driver.Device, fbo driver.Framebuffer,
+ size image.Point) *image.RGBA {
+ img, err := driver.DownloadImage(d, fbo, image.Rectangle{Max: size})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if *dumpImages {
+ if err := saveImage(t.Name()+".png", img); err != nil {
+ t.Error(err)
+ }
+ }
+ return img
+}
+
+func saveImage(file string, img image.Image) error {
+ var buf bytes.Buffer
+ if err := png.Encode(&buf, img); err != nil {
+ return err
+ }
+ return ioutil.WriteFile(file, buf.Bytes(), 0666)
+}
diff --git a/gio/giold/gpu/headless/gen.go b/gio/giold/gpu/headless/gen.go
new file mode 100644
index 0000000..b9e1fed
--- /dev/null
+++ b/gio/giold/gpu/headless/gen.go
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package headless
+
+//go:generate go run ../internal/convertshaders -package headless
diff --git a/gio/giold/gpu/headless/headless.go b/gio/giold/gpu/headless/headless.go
new file mode 100644
index 0000000..0f2e172
--- /dev/null
+++ b/gio/giold/gpu/headless/headless.go
@@ -0,0 +1,148 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package headless implements headless windows for rendering
+// an operation list to an image.
+package headless
+
+import (
+ "image"
+ "image/color"
+ "runtime"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/op"
+)
+
+// Window is a headless window.
+type Window struct {
+ size image.Point
+ ctx context
+ dev driver.Device
+ gpu gpu.GPU
+ fboTex driver.Texture
+ fbo driver.Framebuffer
+}
+
+type context interface {
+ API() gpu.API
+ MakeCurrent() error
+ ReleaseCurrent()
+ Release()
+}
+
+// NewWindow creates a new headless window.
+func NewWindow(width, height int) (*Window, error) {
+ ctx, err := newContext()
+ if err != nil {
+ return nil, err
+ }
+ w := &Window{
+ size: image.Point{X: width, Y: height},
+ ctx: ctx,
+ }
+ err = contextDo(ctx, func() error {
+ api := ctx.API()
+ dev, err := driver.NewDevice(api)
+ if err != nil {
+ return err
+ }
+ dev.Viewport(0, 0, width, height)
+ fboTex, err := dev.NewTexture(
+ driver.TextureFormatSRGB,
+ width, height,
+ driver.FilterNearest, driver.FilterNearest,
+ driver.BufferBindingFramebuffer,
+ )
+ if err != nil {
+ return nil
+ }
+ const depthBits = 16
+ fbo, err := dev.NewFramebuffer(fboTex, depthBits)
+ if err != nil {
+ fboTex.Release()
+ return err
+ }
+ gp, err := gpu.New(api)
+ if err != nil {
+ fbo.Release()
+ fboTex.Release()
+ return err
+ }
+ w.fboTex = fboTex
+ w.fbo = fbo
+ w.gpu = gp
+ w.dev = dev
+ return err
+ })
+ if err != nil {
+ ctx.Release()
+ return nil, err
+ }
+ return w, nil
+}
+
+// Release resources associated with the window.
+func (w *Window) Release() {
+ contextDo(w.ctx, func() error {
+ if w.fbo != nil {
+ w.fbo.Release()
+ w.fbo = nil
+ }
+ if w.fboTex != nil {
+ w.fboTex.Release()
+ w.fboTex = nil
+ }
+ if w.gpu != nil {
+ w.gpu.Release()
+ w.gpu = nil
+ }
+ return nil
+ })
+ if w.ctx != nil {
+ w.ctx.Release()
+ w.ctx = nil
+ }
+}
+
+// Frame replace the window content and state with the
+// operation list.
+func (w *Window) Frame(frame *op.Ops) error {
+ return contextDo(w.ctx, func() error {
+ w.dev.BindFramebuffer(w.fbo)
+ w.gpu.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
+ w.gpu.Collect(w.size, frame)
+ return w.gpu.Frame()
+ })
+}
+
+// Screenshot returns an image with the content of the window.
+func (w *Window) Screenshot() (*image.RGBA, error) {
+ var img *image.RGBA
+ err := contextDo(w.ctx, func() error {
+ var err error
+ img, err = driver.DownloadImage(w.dev, w.fbo,
+ image.Rectangle{Max: w.size})
+ return err
+ })
+ if err != nil {
+ return nil, err
+ }
+ return img, nil
+}
+
+func contextDo(ctx context, f func() error) error {
+ errCh := make(chan error)
+ go func() {
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+ if err := ctx.MakeCurrent(); err != nil {
+ errCh <- err
+ return
+ }
+ err := f()
+ ctx.ReleaseCurrent()
+ errCh <- err
+ }()
+ return <-errCh
+}
diff --git a/gio/giold/gpu/headless/headless_darwin.go b/gio/giold/gpu/headless/headless_darwin.go
new file mode 100644
index 0000000..75a233a
--- /dev/null
+++ b/gio/giold/gpu/headless/headless_darwin.go
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package headless
+
+import (
+ "realy.lol/gio/gpu"
+ _ "realy.lol/gio/internal/cocoainit"
+)
+
+/*
+#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c
+
+#include
+
+__attribute__ ((visibility ("hidden"))) CFTypeRef gio_headless_newContext(void);
+__attribute__ ((visibility ("hidden"))) void gio_headless_releaseContext(CFTypeRef ctxRef);
+__attribute__ ((visibility ("hidden"))) void gio_headless_clearCurrentContext(CFTypeRef ctxRef);
+__attribute__ ((visibility ("hidden"))) void gio_headless_makeCurrentContext(CFTypeRef ctxRef);
+__attribute__ ((visibility ("hidden"))) void gio_headless_prepareContext(CFTypeRef ctxRef);
+*/
+import "C"
+
+type nsContext struct {
+ ctx C.CFTypeRef
+ prepared bool
+}
+
+func newGLContext() (context, error) {
+ ctx := C.gio_headless_newContext()
+ return &nsContext{ctx: ctx}, nil
+}
+
+func (c *nsContext) API() gpu.API {
+ return gpu.OpenGL{}
+}
+
+func (c *nsContext) MakeCurrent() error {
+ C.gio_headless_makeCurrentContext(c.ctx)
+ if !c.prepared {
+ C.gio_headless_prepareContext(c.ctx)
+ c.prepared = true
+ }
+ return nil
+}
+
+func (c *nsContext) ReleaseCurrent() {
+ C.gio_headless_clearCurrentContext(c.ctx)
+}
+
+func (d *nsContext) Release() {
+ if d.ctx != 0 {
+ C.gio_headless_releaseContext(d.ctx)
+ d.ctx = 0
+ }
+}
diff --git a/gio/giold/gpu/headless/headless_egl.go b/gio/giold/gpu/headless/headless_egl.go
new file mode 100644
index 0000000..7d8c1e4
--- /dev/null
+++ b/gio/giold/gpu/headless/headless_egl.go
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build linux || freebsd || windows || openbsd
+// +build linux freebsd windows openbsd
+
+package headless
+
+import (
+ "realy.lol/gio/internal/egl"
+)
+
+func newGLContext() (context, error) {
+ return egl.NewContext(egl.EGL_DEFAULT_DISPLAY)
+}
diff --git a/gio/giold/gpu/headless/headless_gl.go b/gio/giold/gpu/headless/headless_gl.go
new file mode 100644
index 0000000..c00083e
--- /dev/null
+++ b/gio/giold/gpu/headless/headless_gl.go
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build !windows
+
+package headless
+
+func newContext() (context, error) {
+ return newGLContext()
+}
diff --git a/gio/giold/gpu/headless/headless_ios.m b/gio/giold/gpu/headless/headless_ios.m
new file mode 100644
index 0000000..fd72d25
--- /dev/null
+++ b/gio/giold/gpu/headless/headless_ios.m
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build darwin,ios
+
+@import OpenGLES;
+
+#include
+#include "_cgo_export.h"
+
+void gio_headless_releaseContext(CFTypeRef ctxRef) {
+ CFBridgingRelease(ctxRef);
+}
+
+CFTypeRef gio_headless_newContext(void) {
+ EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
+ if (ctx == nil) {
+ return nil;
+ }
+ return CFBridgingRetain(ctx);
+}
+
+void gio_headless_clearCurrentContext(CFTypeRef ctxRef) {
+ [EAGLContext setCurrentContext:nil];
+}
+
+void gio_headless_makeCurrentContext(CFTypeRef ctxRef) {
+ EAGLContext *ctx = (__bridge EAGLContext *)ctxRef;
+ [EAGLContext setCurrentContext:ctx];
+}
+
+void gio_headless_prepareContext(CFTypeRef ctxRef) {
+}
diff --git a/gio/giold/gpu/headless/headless_js.go b/gio/giold/gpu/headless/headless_js.go
new file mode 100644
index 0000000..f79963e
--- /dev/null
+++ b/gio/giold/gpu/headless/headless_js.go
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package headless
+
+import (
+ "errors"
+ "syscall/js"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/internal/gl"
+)
+
+type jsContext struct {
+ ctx js.Value
+}
+
+func newGLContext() (context, error) {
+ doc := js.Global().Get("document")
+ cnv := doc.Call("createElement", "canvas")
+ ctx := cnv.Call("getContext", "webgl2")
+ if ctx.IsNull() {
+ ctx = cnv.Call("getContext", "webgl")
+ }
+ if ctx.IsNull() {
+ return nil, errors.New("headless: webgl is not supported")
+ }
+ c := &jsContext{
+ ctx: ctx,
+ }
+ return c, nil
+}
+
+func (c *jsContext) API() gpu.API {
+ return gpu.OpenGL{Context: gl.Context(c.ctx)}
+}
+
+func (c *jsContext) Release() {
+}
+
+func (c *jsContext) ReleaseCurrent() {
+}
+
+func (c *jsContext) MakeCurrent() error {
+ return nil
+}
diff --git a/gio/giold/gpu/headless/headless_macos.m b/gio/giold/gpu/headless/headless_macos.m
new file mode 100644
index 0000000..46deb37
--- /dev/null
+++ b/gio/giold/gpu/headless/headless_macos.m
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build darwin,!ios
+
+@import AppKit;
+@import OpenGL;
+@import OpenGL.GL;
+@import OpenGL.GL3;
+
+#include
+#include "_cgo_export.h"
+
+void gio_headless_releaseContext(CFTypeRef ctxRef) {
+ CFBridgingRelease(ctxRef);
+}
+
+CFTypeRef gio_headless_newContext(void) {
+ NSOpenGLPixelFormatAttribute attr[] = {
+ NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
+ NSOpenGLPFAColorSize, 24,
+ NSOpenGLPFAAccelerated,
+ // Opt-in to automatic GPU switching. CGL-only property.
+ kCGLPFASupportsAutomaticGraphicsSwitching,
+ NSOpenGLPFAAllowOfflineRenderers,
+ 0
+ };
+ NSOpenGLPixelFormat *pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
+ if (pixFormat == nil) {
+ return NULL;
+ }
+ NSOpenGLContext *ctx = [[NSOpenGLContext alloc] initWithFormat:pixFormat shareContext:nil];
+ return CFBridgingRetain(ctx);
+}
+
+void gio_headless_clearCurrentContext(CFTypeRef ctxRef) {
+ NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef;
+ CGLUnlockContext([ctx CGLContextObj]);
+ [NSOpenGLContext clearCurrentContext];
+}
+
+void gio_headless_makeCurrentContext(CFTypeRef ctxRef) {
+ NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef;
+ [ctx makeCurrentContext];
+ CGLLockContext([ctx CGLContextObj]);
+}
+
+void gio_headless_prepareContext(CFTypeRef ctxRef) {
+ // Bind a default VBA to emulate OpenGL ES 2.
+ GLuint defVBA;
+ glGenVertexArrays(1, &defVBA);
+ glBindVertexArray(defVBA);
+ glEnable(GL_FRAMEBUFFER_SRGB);
+}
diff --git a/gio/giold/gpu/headless/headless_test.go b/gio/giold/gpu/headless/headless_test.go
new file mode 100644
index 0000000..3ceec3f
--- /dev/null
+++ b/gio/giold/gpu/headless/headless_test.go
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package headless
+
+import (
+ "image"
+ "image/color"
+ "testing"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+)
+
+func TestHeadless(t *testing.T) {
+ w, release := newTestWindow(t)
+ defer release()
+
+ sz := w.size
+ col := color.NRGBA{A: 0xff, R: 0xca, G: 0xfe}
+ var ops op.Ops
+ paint.ColorOp{Color: col}.Add(&ops)
+ // Paint only part of the screen to avoid the glClear optimization.
+ paint.FillShape(&ops, col,
+ clip.Rect(image.Rect(0, 0, sz.X-100, sz.Y-100)).Op())
+ if err := w.Frame(&ops); err != nil {
+ t.Fatal(err)
+ }
+
+ img, err := w.Screenshot()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if isz := img.Bounds().Size(); isz != sz {
+ t.Errorf("got %v screenshot, expected %v", isz, sz)
+ }
+ if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col) {
+ t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col))
+ }
+}
+
+func TestClipping(t *testing.T) {
+ w, release := newTestWindow(t)
+ defer release()
+
+ col := color.NRGBA{A: 0xff, R: 0xca, G: 0xfe}
+ col2 := color.NRGBA{A: 0xff, R: 0x00, G: 0xfe}
+ var ops op.Ops
+ paint.ColorOp{Color: col}.Add(&ops)
+ clip.RRect{
+ Rect: f32.Rectangle{
+ Min: f32.Point{X: 50, Y: 50},
+ Max: f32.Point{X: 250, Y: 250},
+ },
+ SE: 75,
+ }.Add(&ops)
+ paint.PaintOp{}.Add(&ops)
+ paint.ColorOp{Color: col2}.Add(&ops)
+ clip.RRect{
+ Rect: f32.Rectangle{
+ Min: f32.Point{X: 100, Y: 100},
+ Max: f32.Point{X: 350, Y: 350},
+ },
+ NW: 75,
+ }.Add(&ops)
+ paint.PaintOp{}.Add(&ops)
+ if err := w.Frame(&ops); err != nil {
+ t.Fatal(err)
+ }
+
+ img, err := w.Screenshot()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if *dumpImages {
+ if err := saveImage("clip.png", img); err != nil {
+ t.Fatal(err)
+ }
+ }
+ bg := color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}
+ tests := []struct {
+ x, y int
+ color color.NRGBA
+ }{
+ {120, 120, col},
+ {130, 130, col2},
+ {210, 210, col2},
+ {230, 230, bg},
+ }
+ for _, test := range tests {
+ if got := img.RGBAAt(test.x,
+ test.y); got != f32color.NRGBAToRGBA(test.color) {
+ t.Errorf("(%d,%d): got color %v, expected %v", test.x, test.y, got,
+ f32color.NRGBAToRGBA(test.color))
+ }
+ }
+}
+
+func TestDepth(t *testing.T) {
+ w, release := newTestWindow(t)
+ defer release()
+ var ops op.Ops
+
+ blue := color.NRGBA{B: 0xFF, A: 0xFF}
+ paint.FillShape(&ops, blue, clip.Rect(image.Rect(0, 0, 50, 100)).Op())
+ red := color.NRGBA{R: 0xFF, A: 0xFF}
+ paint.FillShape(&ops, red, clip.Rect(image.Rect(0, 0, 100, 50)).Op())
+ if err := w.Frame(&ops); err != nil {
+ t.Fatal(err)
+ }
+
+ img, err := w.Screenshot()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if *dumpImages {
+ if err := saveImage("depth.png", img); err != nil {
+ t.Fatal(err)
+ }
+ }
+ tests := []struct {
+ x, y int
+ color color.NRGBA
+ }{
+ {25, 25, red},
+ {75, 25, red},
+ {25, 75, blue},
+ }
+ for _, test := range tests {
+ if got := img.RGBAAt(test.x,
+ test.y); got != f32color.NRGBAToRGBA(test.color) {
+ t.Errorf("(%d,%d): got color %v, expected %v", test.x, test.y, got,
+ f32color.NRGBAToRGBA(test.color))
+ }
+ }
+}
+
+func newTestWindow(t *testing.T) (*Window, func()) {
+ t.Helper()
+ sz := image.Point{X: 800, Y: 600}
+ w, err := NewWindow(sz.X, sz.Y)
+ if err != nil {
+ t.Skipf("headless windows not supported: %v", err)
+ }
+ return w, func() {
+ w.Release()
+ }
+}
diff --git a/gio/giold/gpu/headless/headless_windows.go b/gio/giold/gpu/headless/headless_windows.go
new file mode 100644
index 0000000..bd42d12
--- /dev/null
+++ b/gio/giold/gpu/headless/headless_windows.go
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package headless
+
+import (
+ "unsafe"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/internal/d3d11"
+)
+
+type d3d11Context struct {
+ dev *d3d11.Device
+}
+
+func newContext() (context, error) {
+ dev, ctx, _, err := d3d11.CreateDevice(
+ d3d11.DRIVER_TYPE_HARDWARE,
+ 0,
+ )
+ if err != nil {
+ return nil, err
+ }
+ // Don't need it.
+ d3d11.IUnknownRelease(unsafe.Pointer(ctx), ctx.Vtbl.Release)
+ return &d3d11Context{dev: dev}, nil
+}
+
+func (c *d3d11Context) API() gpu.API {
+ return gpu.Direct3D11{Device: unsafe.Pointer(c.dev)}
+}
+
+func (c *d3d11Context) MakeCurrent() error {
+ return nil
+}
+
+func (c *d3d11Context) ReleaseCurrent() {
+}
+
+func (c *d3d11Context) Release() {
+ d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release)
+ c.dev = nil
+}
diff --git a/gio/giold/gpu/headless/shaders.go b/gio/giold/gpu/headless/shaders.go
new file mode 100644
index 0000000..95e05b2
--- /dev/null
+++ b/gio/giold/gpu/headless/shaders.go
@@ -0,0 +1,233 @@
+// Code generated by build.go. DO NOT EDIT.
+
+package headless
+
+import "realy.lol/gio/gpu/internal/driver"
+
+var (
+ shader_input_vert = driver.ShaderSources{
+ Name: "input.vert",
+ Inputs: []driver.InputLocation{{Name: "position", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 4}},
+ GLSL100ES: `#version 100
+
+attribute vec4 position;
+
+void main()
+{
+ gl_Position = position;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+layout(location = 0) in vec4 position;
+
+void main()
+{
+ gl_Position = position;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+in vec4 position;
+
+void main()
+{
+ gl_Position = position;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+in vec4 position;
+
+void main()
+{
+ gl_Position = position;
+}
+
+`,
+ HLSL: "DXBC\x1eĀ»\x11\xd3iX7\xd4F\xb9\xa4\xf4R\xf9J\x01\x00\x00\x00\x10\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\x9c\x00\x00\x00\xe0\x00\x00\x00\\\x01\x00\x00\xa8\x01\x00\x00\xdc\x01\x00\x00Aon9\\\x00\x00\x00\\\x00\x00\x00\x00\x02\xfe\xff4\x00\x00\x00(\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x01\x00$\x00\x00\x00\x00\x00\x00\x02\xfe\xff\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x04\x00\x00\x04\x00\x00\x03\xc0\x00\x00\xff\x90\x00\x00\xe4\xa0\x00\x00\xe4\x90\x01\x00\x00\x02\x00\x00\f\xc0\x00\x00\xe4\x90\xff\xff\x00\x00SHDR<\x00\x00\x00@\x00\x01\x00\x0f\x00\x00\x00_\x00\x00\x03\xf2\x10\x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x006\x00\x00\x05\xf2 \x10\x00\x00\x00\x00\x00F\x1e\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x0f\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00",
+ }
+ shader_simple_frag = driver.ShaderSources{
+ Name: "simple.frag",
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+void main()
+{
+ gl_FragData[0] = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0);
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+layout(location = 0) out vec4 fragColor;
+
+void main()
+{
+ fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0);
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+out vec4 fragColor;
+
+void main()
+{
+ fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0);
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+out vec4 fragColor;
+
+void main()
+{
+ fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0);
+}
+
+`,
+ HLSL: "DXBC\xf5F\xdef$)\xa8\xbbV\xeas\xb5ks\x12r\x01\x00\x00\x00\xdc\x01\x00\x00\x06\x00\x00\x008\x00\x00\x00\x90\x00\x00\x00\xd0\x00\x00\x00L\x01\x00\x00\x98\x01\x00\x00\xa8\x01\x00\x00Aon9P\x00\x00\x00P\x00\x00\x00\x00\x02\xff\xff,\x00\x00\x00$\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x80>\xcd\xcc\f?\x00\x00@?\x00\x00\x80?\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\xa0\xff\xff\x00\x00SHDR8\x00\x00\x00@\x00\x00\x00\x0e\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x006\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80>\xcd\xcc\f?\x00\x00@?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\b\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ }
+ shader_simple_vert = driver.ShaderSources{
+ Name: "simple.vert",
+ GLSL100ES: `#version 100
+
+void main()
+{
+ float x;
+ float y;
+ if (gl_VertexID == 0)
+ {
+ x = 0.0;
+ y = 0.5;
+ }
+ else
+ {
+ if (gl_VertexID == 1)
+ {
+ x = 0.5;
+ y = -0.5;
+ }
+ else
+ {
+ x = -0.5;
+ y = -0.5;
+ }
+ }
+ gl_Position = vec4(x, y, 0.5, 1.0);
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+void main()
+{
+ float x;
+ float y;
+ if (gl_VertexID == 0)
+ {
+ x = 0.0;
+ y = 0.5;
+ }
+ else
+ {
+ if (gl_VertexID == 1)
+ {
+ x = 0.5;
+ y = -0.5;
+ }
+ else
+ {
+ x = -0.5;
+ y = -0.5;
+ }
+ }
+ gl_Position = vec4(x, y, 0.5, 1.0);
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+void main()
+{
+ float x;
+ float y;
+ if (gl_VertexID == 0)
+ {
+ x = 0.0;
+ y = 0.5;
+ }
+ else
+ {
+ if (gl_VertexID == 1)
+ {
+ x = 0.5;
+ y = -0.5;
+ }
+ else
+ {
+ x = -0.5;
+ y = -0.5;
+ }
+ }
+ gl_Position = vec4(x, y, 0.5, 1.0);
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+void main()
+{
+ float x;
+ float y;
+ if (gl_VertexID == 0)
+ {
+ x = 0.0;
+ y = 0.5;
+ }
+ else
+ {
+ if (gl_VertexID == 1)
+ {
+ x = 0.5;
+ y = -0.5;
+ }
+ else
+ {
+ x = -0.5;
+ y = -0.5;
+ }
+ }
+ gl_Position = vec4(x, y, 0.5, 1.0);
+}
+
+`,
+ HLSL: "DXBC\xc8 \\\"\xec\xe9\xb2)@\xdf|Z(\xea\f\xb8\x01\x00\x00\x00H\x02\x00\x00\x05\x00\x00\x004\x00\x00\x00\x80\x00\x00\x00\xb4\x00\x00\x00\xe8\x00\x00\x00\xcc\x01\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00SV_VertexID\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00SHDR\xdc\x00\x00\x00@\x00\x01\x007\x00\x00\x00`\x00\x00\x04\x12\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00 \x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x01\x00\x00\x007\x00\x00\x0f2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x007\x00\x00\f2 \x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ }
+)
diff --git a/gio/giold/gpu/headless/shaders/input.vert b/gio/giold/gpu/headless/shaders/input.vert
new file mode 100644
index 0000000..ed9a4bd
--- /dev/null
+++ b/gio/giold/gpu/headless/shaders/input.vert
@@ -0,0 +1,11 @@
+#version 310 es
+
+// SPDX-License-Identifier: Unlicense OR MIT
+
+precision highp float;
+
+layout(location=0) in vec4 position;
+
+void main() {
+ gl_Position = position;
+}
diff --git a/gio/giold/gpu/headless/shaders/simple.frag b/gio/giold/gpu/headless/shaders/simple.frag
new file mode 100644
index 0000000..4614f33
--- /dev/null
+++ b/gio/giold/gpu/headless/shaders/simple.frag
@@ -0,0 +1,11 @@
+#version 310 es
+
+// SPDX-License-Identifier: Unlicense OR MIT
+
+precision mediump float;
+
+layout(location = 0) out vec4 fragColor;
+
+void main() {
+ fragColor = vec4(.25, .55, .75, 1.0);
+}
diff --git a/gio/giold/gpu/headless/shaders/simple.vert b/gio/giold/gpu/headless/shaders/simple.vert
new file mode 100644
index 0000000..a226816
--- /dev/null
+++ b/gio/giold/gpu/headless/shaders/simple.vert
@@ -0,0 +1,20 @@
+#version 310 es
+
+// SPDX-License-Identifier: Unlicense OR MIT
+
+precision highp float;
+
+void main() {
+ float x, y;
+ if (gl_VertexIndex == 0) {
+ x = 0.0;
+ y = .5;
+ } else if (gl_VertexIndex == 1) {
+ x = .5;
+ y = -.5;
+ } else {
+ x = -.5;
+ y = -.5;
+ }
+ gl_Position = vec4(x, y, 0.5, 1.0);
+}
diff --git a/gio/giold/gpu/internal/convertshaders/glslvalidate.go b/gio/giold/gpu/internal/convertshaders/glslvalidate.go
new file mode 100644
index 0000000..0d02a29
--- /dev/null
+++ b/gio/giold/gpu/internal/convertshaders/glslvalidate.go
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os/exec"
+ "path/filepath"
+)
+
+// GLSLValidator is OpenGL reference compiler.
+type GLSLValidator struct {
+ Bin string
+ WorkDir WorkDir
+}
+
+func NewGLSLValidator() *GLSLValidator { return &GLSLValidator{Bin: "glslangValidator"} }
+
+// Convert converts a glsl shader to spirv.
+func (glsl *GLSLValidator) Convert(path, variant string, hlsl bool, input []byte) ([]byte, error) {
+ base := glsl.WorkDir.Path(filepath.Base(path), variant)
+ pathout := base + ".out"
+
+ cmd := exec.Command(glsl.Bin,
+ "--stdin",
+ "-I"+filepath.Dir(path),
+ "-V", // OpenGL ES 3.1.
+ "-w", // Suppress warnings.
+ "-S", filepath.Ext(path)[1:],
+ "-o", pathout,
+ )
+ if hlsl {
+ cmd.Args = append(cmd.Args, "-DHLSL")
+ }
+ cmd.Stdin = bytes.NewBuffer(input)
+
+ out, err := cmd.Output()
+ if err != nil {
+ return nil, fmt.Errorf("%s\nfailed to run %v: %w", out, cmd.Args, err)
+ }
+
+ compiled, err := ioutil.ReadFile(pathout)
+ if err != nil {
+ return nil, fmt.Errorf("unable to read output %q: %w", pathout, err)
+ }
+
+ return compiled, nil
+}
diff --git a/gio/giold/gpu/internal/convertshaders/hlsl.go b/gio/giold/gpu/internal/convertshaders/hlsl.go
new file mode 100644
index 0000000..a007925
--- /dev/null
+++ b/gio/giold/gpu/internal/convertshaders/hlsl.go
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+// FXC is hlsl compiler that targets ShaderModel 5.x and lower.
+type FXC struct {
+ Bin string
+ WorkDir WorkDir
+}
+
+func NewFXC() *FXC { return &FXC{Bin: "fxc.exe"} }
+
+// Compile compiles the input shader.
+func (fxc *FXC) Compile(path, variant string, input []byte, entryPoint string, profileVersion string) (string, error) {
+ base := fxc.WorkDir.Path(filepath.Base(path), variant, profileVersion)
+ pathin := base + ".in"
+ pathout := base + ".out"
+ result := pathout
+
+ if err := fxc.WorkDir.WriteFile(pathin, input); err != nil {
+ return "", fmt.Errorf("unable to write shader to disk: %w", err)
+ }
+
+ cmd := exec.Command(fxc.Bin)
+ if runtime.GOOS != "windows" {
+ cmd = exec.Command("wine", fxc.Bin)
+ if err := winepath(&pathin, &pathout); err != nil {
+ return "", err
+ }
+ }
+
+ var profile string
+ switch filepath.Ext(path) {
+ case ".frag":
+ profile = "ps_" + profileVersion
+ case ".vert":
+ profile = "vs_" + profileVersion
+ case ".comp":
+ profile = "cs_" + profileVersion
+ default:
+ return "", fmt.Errorf("unrecognized shader type %s", path)
+ }
+
+ cmd.Args = append(cmd.Args,
+ "/Fo", pathout,
+ "/T", profile,
+ "/E", entryPoint,
+ pathin,
+ )
+
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ info := ""
+ if runtime.GOOS != "windows" {
+ info = "If the fxc tool cannot be found, set WINEPATH to the Windows path for the Windows SDK.\n"
+ }
+ return "", fmt.Errorf("%s\n%sfailed to run %v: %w", output, info, cmd.Args, err)
+ }
+
+ compiled, err := ioutil.ReadFile(result)
+ if err != nil {
+ return "", fmt.Errorf("unable to read output %q: %w", pathout, err)
+ }
+
+ return string(compiled), nil
+}
+
+// DXC is hlsl compiler that targets ShaderModel 6.0 and newer.
+type DXC struct {
+ Bin string
+ WorkDir WorkDir
+}
+
+func NewDXC() *DXC { return &DXC{Bin: "dxc"} }
+
+// Compile compiles the input shader.
+func (dxc *DXC) Compile(path, variant string, input []byte, entryPoint string, profile string) (string, error) {
+ base := dxc.WorkDir.Path(filepath.Base(path), variant, profile)
+ pathin := base + ".in"
+ pathout := base + ".out"
+ result := pathout
+
+ if err := dxc.WorkDir.WriteFile(pathin, input); err != nil {
+ return "", fmt.Errorf("unable to write shader to disk: %w", err)
+ }
+
+ cmd := exec.Command(dxc.Bin)
+
+ cmd.Args = append(cmd.Args,
+ "-Fo", pathout,
+ "-T", profile,
+ "-E", entryPoint,
+ "-Qstrip_reflect",
+ pathin,
+ )
+
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", fmt.Errorf("%s\nfailed to run %v: %w", output, cmd.Args, err)
+ }
+
+ compiled, err := ioutil.ReadFile(result)
+ if err != nil {
+ return "", fmt.Errorf("unable to read output %q: %w", pathout, err)
+ }
+
+ return string(compiled), nil
+}
+
+// winepath uses the winepath tool to convert a paths to Windows format.
+// The returned path can be used as arguments for Windows command line tools.
+func winepath(paths ...*string) error {
+ winepath := exec.Command("winepath", "--windows")
+ for _, path := range paths {
+ winepath.Args = append(winepath.Args, *path)
+ }
+ // Use a pipe instead of Output, because winepath may have left wineserver
+ // running for several seconds as a grandchild.
+ out, err := winepath.StdoutPipe()
+ if err != nil {
+ return fmt.Errorf("unable to start winepath: %w", err)
+ }
+ if err := winepath.Start(); err != nil {
+ return fmt.Errorf("unable to start winepath: %w", err)
+ }
+ var buf bytes.Buffer
+ if _, err := io.Copy(&buf, out); err != nil {
+ return fmt.Errorf("unable to run winepath: %w", err)
+ }
+ winPaths := strings.Split(strings.TrimSpace(buf.String()), "\n")
+ for i, path := range paths {
+ *path = winPaths[i]
+ }
+ return nil
+}
diff --git a/gio/giold/gpu/internal/convertshaders/main.go b/gio/giold/gpu/internal/convertshaders/main.go
new file mode 100644
index 0000000..a0589dc
--- /dev/null
+++ b/gio/giold/gpu/internal/convertshaders/main.go
@@ -0,0 +1,436 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "text/template"
+
+ "realy.lol/gio/gpu/internal/driver"
+)
+
+func main() {
+ packageName := flag.String("package", "", "specify Go package name")
+ workdir := flag.String("work", "",
+ "temporary working directory (default TEMP)")
+ shadersDir := flag.String("dir", "shaders", "shaders directory")
+ directCompute := flag.Bool("directcompute", false,
+ "enable compiling DirectCompute shaders")
+
+ flag.Parse()
+
+ var work WorkDir
+ cleanup := func() {}
+ if *workdir == "" {
+ tempdir, err := ioutil.TempDir("", "shader-convert")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to create tempdir: %v\n", err)
+ os.Exit(1)
+ }
+ cleanup = func() { os.RemoveAll(tempdir) }
+ defer cleanup()
+
+ work = WorkDir(tempdir)
+ } else {
+ if abs, err := filepath.Abs(*workdir); err == nil {
+ *workdir = abs
+ }
+ work = WorkDir(*workdir)
+ }
+
+ var out bytes.Buffer
+ conv := NewConverter(work, *packageName, *shadersDir, *directCompute)
+ if err := conv.Run(&out); err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ cleanup()
+ os.Exit(1)
+ }
+
+ if err := ioutil.WriteFile("shaders.go", out.Bytes(), 0644); err != nil {
+ fmt.Fprintf(os.Stderr, "failed to create shaders: %v\n", err)
+ cleanup()
+ os.Exit(1)
+ }
+
+ cmd := exec.Command("gofmt", "-s", "-w", "shaders.go")
+ cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
+ if err := cmd.Run(); err != nil {
+ fmt.Fprintf(os.Stderr, "formatting shaders.go failed: %v\n", err)
+ cleanup()
+ os.Exit(1)
+ }
+}
+
+type Converter struct {
+ workDir WorkDir
+ shadersDir string
+ directCompute bool
+
+ packageName string
+
+ glslvalidator *GLSLValidator
+ spirv *SPIRVCross
+ fxc *FXC
+}
+
+func NewConverter(workDir WorkDir, packageName, shadersDir string,
+ directCompute bool) *Converter {
+ if abs, err := filepath.Abs(shadersDir); err == nil {
+ shadersDir = abs
+ }
+
+ conv := &Converter{}
+ conv.workDir = workDir
+ conv.shadersDir = shadersDir
+ conv.directCompute = directCompute
+
+ conv.packageName = packageName
+
+ conv.glslvalidator = NewGLSLValidator()
+ conv.spirv = NewSPIRVCross()
+ conv.fxc = NewFXC()
+
+ verifyBinaryPath(&conv.glslvalidator.Bin)
+ verifyBinaryPath(&conv.spirv.Bin)
+ // We cannot check fxc since it may depend on wine.
+
+ conv.glslvalidator.WorkDir = workDir.Dir("glslvalidator")
+ conv.fxc.WorkDir = workDir.Dir("fxc")
+ conv.spirv.WorkDir = workDir.Dir("spirv")
+
+ return conv
+}
+
+func verifyBinaryPath(bin *string) {
+ new, err := exec.LookPath(*bin)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "unable to find %q: %v\n", *bin, err)
+ } else {
+ *bin = new
+ }
+}
+
+func (conv *Converter) Run(out io.Writer) error {
+ shaders, err := filepath.Glob(filepath.Join(conv.shadersDir, "*"))
+ if len(shaders) == 0 || err != nil {
+ return fmt.Errorf("failed to list shaders in %q: %w", conv.shadersDir,
+ err)
+ }
+
+ sort.Strings(shaders)
+
+ var workers Workers
+
+ type ShaderResult struct {
+ Path string
+ Shaders []driver.ShaderSources
+ Error error
+ }
+ shaderResults := make([]ShaderResult, len(shaders))
+
+ for i, shaderPath := range shaders {
+ i, shaderPath := i, shaderPath
+
+ switch filepath.Ext(shaderPath) {
+ case ".vert", ".frag":
+ workers.Go(func() {
+ shaders, err := conv.Shader(shaderPath)
+ shaderResults[i] = ShaderResult{
+ Path: shaderPath,
+ Shaders: shaders,
+ Error: err,
+ }
+ })
+ case ".comp":
+ workers.Go(func() {
+ shaders, err := conv.ComputeShader(shaderPath)
+ shaderResults[i] = ShaderResult{
+ Path: shaderPath,
+ Shaders: shaders,
+ Error: err,
+ }
+ })
+ default:
+ continue
+ }
+ }
+
+ workers.Wait()
+
+ var allErrors string
+ for _, r := range shaderResults {
+ if r.Error != nil {
+ if len(allErrors) > 0 {
+ allErrors += "\n\n"
+ }
+ allErrors += "--- " + r.Path + " --- \n\n" + r.Error.Error() + "\n"
+ }
+ }
+ if len(allErrors) > 0 {
+ return errors.New(allErrors)
+ }
+
+ fmt.Fprintf(out, "// Code generated by build.go. DO NOT EDIT.\n\n")
+ fmt.Fprintf(out, "package %s\n\n", conv.packageName)
+ fmt.Fprintf(out, "import %q\n\n", "realy.lol/gio/gpu/internal/driver")
+
+ fmt.Fprintf(out, "var (\n")
+
+ for _, r := range shaderResults {
+ if len(r.Shaders) == 0 {
+ continue
+ }
+
+ name := filepath.Base(r.Path)
+ name = strings.ReplaceAll(name, ".", "_")
+ fmt.Fprintf(out, "\tshader_%s = ", name)
+
+ multiVariant := len(r.Shaders) > 1
+ if multiVariant {
+ fmt.Fprintf(out, "[...]driver.ShaderSources{\n")
+ }
+
+ for _, src := range r.Shaders {
+ fmt.Fprintf(out, "driver.ShaderSources{\n")
+ fmt.Fprintf(out, "Name: %#v,\n", src.Name)
+ if len(src.Inputs) > 0 {
+ fmt.Fprintf(out, "Inputs: %#v,\n", src.Inputs)
+ }
+ if u := src.Uniforms; len(u.Blocks) > 0 {
+ fmt.Fprintf(out, "Uniforms: driver.UniformsReflection{\n")
+ fmt.Fprintf(out, "Blocks: %#v,\n", u.Blocks)
+ fmt.Fprintf(out, "Locations: %#v,\n", u.Locations)
+ fmt.Fprintf(out, "Size: %d,\n", u.Size)
+ fmt.Fprintf(out, "},\n")
+ }
+ if len(src.Textures) > 0 {
+ fmt.Fprintf(out, "Textures: %#v,\n", src.Textures)
+ }
+ if len(src.GLSL100ES) > 0 {
+ fmt.Fprintf(out, "GLSL100ES: `%s`,\n", src.GLSL100ES)
+ }
+ if len(src.GLSL300ES) > 0 {
+ fmt.Fprintf(out, "GLSL300ES: `%s`,\n", src.GLSL300ES)
+ }
+ if len(src.GLSL310ES) > 0 {
+ fmt.Fprintf(out, "GLSL310ES: `%s`,\n", src.GLSL310ES)
+ }
+ if len(src.GLSL130) > 0 {
+ fmt.Fprintf(out, "GLSL130: `%s`,\n", src.GLSL130)
+ }
+ if len(src.GLSL150) > 0 {
+ fmt.Fprintf(out, "GLSL150: `%s`,\n", src.GLSL150)
+ }
+ if len(src.HLSL) > 0 {
+ fmt.Fprintf(out, "HLSL: %q,\n", src.HLSL)
+ }
+ fmt.Fprintf(out, "}")
+ if multiVariant {
+ fmt.Fprintf(out, ",")
+ }
+ fmt.Fprintf(out, "\n")
+ }
+ if multiVariant {
+ fmt.Fprintf(out, "}\n")
+ }
+ }
+ fmt.Fprintf(out, ")\n")
+
+ return nil
+}
+
+func (conv *Converter) Shader(shaderPath string) ([]driver.ShaderSources,
+ error) {
+ type Variant struct {
+ FetchColorExpr string
+ Header string
+ }
+ variantArgs := [...]Variant{
+ {
+ FetchColorExpr: `_color.color`,
+ Header: `layout(binding=0) uniform Color { vec4 color; } _color;`,
+ },
+ {
+ FetchColorExpr: `mix(_gradient.color1, _gradient.color2, clamp(vUV.x, 0.0, 1.0))`,
+ Header: `layout(binding=0) uniform Gradient { vec4 color1; vec4 color2; } _gradient;`,
+ },
+ {
+ FetchColorExpr: `texture(tex, vUV)`,
+ Header: `layout(binding=0) uniform sampler2D tex;`,
+ },
+ }
+
+ shaderTemplate, err := template.ParseFiles(shaderPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse template %q: %w", shaderPath,
+ err)
+ }
+
+ var variants []driver.ShaderSources
+ for i, variantArg := range variantArgs {
+ variantName := strconv.Itoa(i)
+ var buf bytes.Buffer
+ err := shaderTemplate.Execute(&buf, variantArg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute template %q with %#v: %w",
+ shaderPath, variantArg, err)
+ }
+
+ var sources driver.ShaderSources
+ sources.Name = filepath.Base(shaderPath)
+
+ // Ignore error; some shaders are not meant to run in GLSL 1.00.
+ sources.GLSL100ES, _, _ = conv.ShaderVariant(shaderPath, variantName,
+ buf.Bytes(), "es", "100")
+
+ var metadata Metadata
+ sources.GLSL300ES, metadata, err = conv.ShaderVariant(shaderPath,
+ variantName, buf.Bytes(), "es", "300")
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert GLSL300ES:\n%w", err)
+ }
+
+ sources.GLSL130, _, err = conv.ShaderVariant(shaderPath, variantName,
+ buf.Bytes(), "glsl", "130")
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert GLSL130:\n%w", err)
+ }
+
+ hlsl, _, err := conv.ShaderVariant(shaderPath, variantName, buf.Bytes(),
+ "hlsl", "40")
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert HLSL:\n%w", err)
+ }
+ sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName,
+ []byte(hlsl), "main", "4_0_level_9_1")
+ if err != nil {
+ // Attempt shader model 4.0. Only the gpu/headless
+ // test shaders use features not supported by level
+ // 9.1.
+ sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName,
+ []byte(hlsl), "main", "4_0")
+ if err != nil {
+ return nil, fmt.Errorf("failed to compile HLSL: %w", err)
+ }
+ }
+
+ sources.GLSL150, _, err = conv.ShaderVariant(shaderPath, variantName,
+ buf.Bytes(), "glsl", "150")
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert GLSL150:\n%w", err)
+ }
+
+ sources.Uniforms = metadata.Uniforms
+ sources.Inputs = metadata.Inputs
+ sources.Textures = metadata.Textures
+
+ variants = append(variants, sources)
+ }
+
+ // If the shader don't use the variant arguments, output only a single version.
+ if variants[0].GLSL100ES == variants[1].GLSL100ES {
+ variants = variants[:1]
+ }
+
+ return variants, nil
+}
+
+func (conv *Converter) ShaderVariant(shaderPath, variant string, src []byte,
+ lang, profile string) (string, Metadata, error) {
+ spirv, err := conv.glslvalidator.Convert(shaderPath, variant,
+ lang == "hlsl", src)
+ if err != nil {
+ return "", Metadata{}, fmt.Errorf("failed to generate SPIR-V for %q: %w",
+ shaderPath, err)
+ }
+
+ dst, err := conv.spirv.Convert(shaderPath, variant, spirv, lang, profile)
+ if err != nil {
+ return "", Metadata{}, fmt.Errorf("failed to convert shader %q: %w",
+ shaderPath, err)
+ }
+
+ meta, err := conv.spirv.Metadata(shaderPath, variant, spirv)
+ if err != nil {
+ return "", Metadata{}, fmt.Errorf("failed to extract metadata for shader %q: %w",
+ shaderPath, err)
+ }
+
+ return dst, meta, nil
+}
+
+func (conv *Converter) ComputeShader(shaderPath string) ([]driver.ShaderSources,
+ error) {
+ shader, err := ioutil.ReadFile(shaderPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load shader %q: %w", shaderPath, err)
+ }
+
+ spirv, err := conv.glslvalidator.Convert(shaderPath, "", false, shader)
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert compute shader %q: %w",
+ shaderPath, err)
+ }
+
+ var sources driver.ShaderSources
+ sources.Name = filepath.Base(shaderPath)
+
+ sources.GLSL310ES, err = conv.spirv.Convert(shaderPath, "", spirv, "es",
+ "310")
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert es compute shader %q: %w",
+ shaderPath, err)
+ }
+ sources.GLSL310ES = unixLineEnding(sources.GLSL310ES)
+
+ hlslSource, err := conv.spirv.Convert(shaderPath, "", spirv, "hlsl", "50")
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert hlsl compute shader %q: %w",
+ shaderPath, err)
+ }
+
+ dxil, err := conv.fxc.Compile(shaderPath, "0", []byte(hlslSource), "main",
+ "5_0")
+ if err != nil {
+ return nil, fmt.Errorf("failed to compile hlsl compute shader %q: %w",
+ shaderPath, err)
+ }
+ if conv.directCompute {
+ sources.HLSL = dxil
+ }
+
+ return []driver.ShaderSources{sources}, nil
+}
+
+// Workers implements wait group with synchronous logging.
+type Workers struct {
+ running sync.WaitGroup
+}
+
+func (lg *Workers) Go(fn func()) {
+ lg.running.Add(1)
+ go func() {
+ defer lg.running.Done()
+ fn()
+ }()
+}
+
+func (lg *Workers) Wait() {
+ lg.running.Wait()
+}
+
+func unixLineEnding(s string) string {
+ return strings.ReplaceAll(s, "\r\n", "\n")
+}
diff --git a/gio/giold/gpu/internal/convertshaders/spirvcross.go b/gio/giold/gpu/internal/convertshaders/spirvcross.go
new file mode 100644
index 0000000..4252469
--- /dev/null
+++ b/gio/giold/gpu/internal/convertshaders/spirvcross.go
@@ -0,0 +1,218 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "os/exec"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "realy.lol/gio/gpu/internal/driver"
+)
+
+// Metadata contains reflection data about a shader.
+type Metadata struct {
+ Uniforms driver.UniformsReflection
+ Inputs []driver.InputLocation
+ Textures []driver.TextureBinding
+}
+
+// SPIRVCross cross-compiles spirv shaders to es, hlsl and others.
+type SPIRVCross struct {
+ Bin string
+ WorkDir WorkDir
+}
+
+func NewSPIRVCross() *SPIRVCross { return &SPIRVCross{Bin: "spirv-cross"} }
+
+// Convert converts compute shader from spirv format to a target format.
+func (spirv *SPIRVCross) Convert(path, variant string, shader []byte,
+ target, version string) (string, error) {
+ base := spirv.WorkDir.Path(filepath.Base(path), variant)
+
+ if err := spirv.WorkDir.WriteFile(base, shader); err != nil {
+ return "", fmt.Errorf("unable to write shader to disk: %w", err)
+ }
+
+ var cmd *exec.Cmd
+ switch target {
+ case "glsl":
+ cmd = exec.Command(spirv.Bin,
+ "--no-es",
+ "--version", version,
+ )
+ case "es":
+ cmd = exec.Command(spirv.Bin,
+ "--es",
+ "--version", version,
+ )
+ case "hlsl":
+ cmd = exec.Command(spirv.Bin,
+ "--hlsl",
+ "--shader-model", version,
+ )
+ default:
+ return "", fmt.Errorf("unknown target %q", target)
+ }
+ cmd.Args = append(cmd.Args, "--no-420pack-extension", base)
+
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", fmt.Errorf("%s\nfailed to run %v: %w", out, cmd.Args, err)
+ }
+ s := string(out)
+ if target != "hlsl" {
+ // Strip Windows \r in line endings.
+ s = unixLineEnding(s)
+ }
+
+ return s, nil
+}
+
+// Metadata extracts metadata for a SPIR-V shader.
+func (spirv *SPIRVCross) Metadata(path, variant string,
+ shader []byte) (Metadata, error) {
+ base := spirv.WorkDir.Path(filepath.Base(path), variant)
+
+ if err := spirv.WorkDir.WriteFile(base, shader); err != nil {
+ return Metadata{}, fmt.Errorf("unable to write shader to disk: %w", err)
+ }
+
+ cmd := exec.Command(spirv.Bin,
+ base,
+ "--reflect",
+ )
+
+ out, err := cmd.Output()
+ if err != nil {
+ return Metadata{}, fmt.Errorf("failed to run %v: %w", cmd.Args, err)
+ }
+
+ meta, err := parseMetadata(out)
+ if err != nil {
+ return Metadata{}, fmt.Errorf("%s\nfailed to parse metadata: %w", out,
+ err)
+ }
+
+ return meta, nil
+}
+
+func parseMetadata(data []byte) (Metadata, error) {
+ var reflect struct {
+ Types map[string]struct {
+ Name string `json:"name"`
+ Members []struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Offset int `json:"offset"`
+ } `json:"members"`
+ } `json:"types"`
+ Inputs []struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Location int `json:"location"`
+ } `json:"inputs"`
+ Textures []struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Set int `json:"set"`
+ Binding int `json:"binding"`
+ } `json:"textures"`
+ UBOs []struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ BlockSize int `json:"block_size"`
+ Set int `json:"set"`
+ Binding int `json:"binding"`
+ } `json:"ubos"`
+ }
+ if err := json.Unmarshal(data, &reflect); err != nil {
+ return Metadata{}, fmt.Errorf("failed to parse reflection data: %w",
+ err)
+ }
+
+ var m Metadata
+
+ for _, input := range reflect.Inputs {
+ dataType, dataSize, err := parseDataType(input.Type)
+ if err != nil {
+ return Metadata{}, fmt.Errorf("parseReflection: %v", err)
+ }
+ m.Inputs = append(m.Inputs, driver.InputLocation{
+ Name: input.Name,
+ Location: input.Location,
+ Semantic: "TEXCOORD",
+ SemanticIndex: input.Location,
+ Type: dataType,
+ Size: dataSize,
+ })
+ }
+
+ sort.Slice(m.Inputs, func(i, j int) bool {
+ return m.Inputs[i].Location < m.Inputs[j].Location
+ })
+
+ blockOffset := 0
+ for _, block := range reflect.UBOs {
+ m.Uniforms.Blocks = append(m.Uniforms.Blocks, driver.UniformBlock{
+ Name: block.Name,
+ Binding: block.Binding,
+ })
+ t := reflect.Types[block.Type]
+ // By convention uniform block variables are named by prepending an underscore
+ // and converting to lowercase.
+ blockVar := "_" + strings.ToLower(block.Name)
+ for _, member := range t.Members {
+ dataType, size, err := parseDataType(member.Type)
+ if err != nil {
+ return Metadata{}, fmt.Errorf("failed to parse reflection data: %v",
+ err)
+ }
+ m.Uniforms.Locations = append(m.Uniforms.Locations,
+ driver.UniformLocation{
+ Name: fmt.Sprintf("%s.%s", blockVar, member.Name),
+ Type: dataType,
+ Size: size,
+ Offset: blockOffset + member.Offset,
+ })
+ }
+ blockOffset += block.BlockSize
+ }
+ m.Uniforms.Size = blockOffset
+
+ for _, texture := range reflect.Textures {
+ m.Textures = append(m.Textures, driver.TextureBinding{
+ Name: texture.Name,
+ Binding: texture.Binding,
+ })
+ }
+
+ // return m, fmt.Errorf("not yet!: %+v", reflect)
+ return m, nil
+}
+
+func parseDataType(t string) (driver.DataType, int, error) {
+ switch t {
+ case "float":
+ return driver.DataTypeFloat, 1, nil
+ case "vec2":
+ return driver.DataTypeFloat, 2, nil
+ case "vec3":
+ return driver.DataTypeFloat, 3, nil
+ case "vec4":
+ return driver.DataTypeFloat, 4, nil
+ case "int":
+ return driver.DataTypeInt, 1, nil
+ case "int2":
+ return driver.DataTypeInt, 2, nil
+ case "int3":
+ return driver.DataTypeInt, 3, nil
+ case "int4":
+ return driver.DataTypeInt, 4, nil
+ default:
+ return 0, 0, fmt.Errorf("unsupported input data type: %s", t)
+ }
+}
diff --git a/gio/giold/gpu/internal/convertshaders/workdir.go b/gio/giold/gpu/internal/convertshaders/workdir.go
new file mode 100644
index 0000000..4c1c092
--- /dev/null
+++ b/gio/giold/gpu/internal/convertshaders/workdir.go
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+type WorkDir string
+
+func (wd WorkDir) Dir(path string) WorkDir {
+ dirname := filepath.Join(string(wd), path)
+ if err := os.Mkdir(dirname, 0755); err != nil {
+ if !os.IsExist(err) {
+ fmt.Fprintf(os.Stderr, "failed to create %q: %v\n", dirname, err)
+ }
+ }
+ return WorkDir(dirname)
+}
+
+func (wd WorkDir) Path(path ...string) (fullpath string) {
+ return filepath.Join(string(wd), strings.Join(path, "."))
+}
+
+func (wd WorkDir) WriteFile(path string, data []byte) error {
+ err := ioutil.WriteFile(path, data, 0644)
+ if err != nil {
+ return fmt.Errorf("unable to create %v: %w", path, err)
+ }
+ return nil
+}
diff --git a/gio/giold/gpu/internal/d3d11/d3d11.go b/gio/giold/gpu/internal/d3d11/d3d11.go
new file mode 100644
index 0000000..3ddf7c3
--- /dev/null
+++ b/gio/giold/gpu/internal/d3d11/d3d11.go
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// This file exists so this package builds on non-Windows platforms.
+
+package d3d11
diff --git a/gio/giold/gpu/internal/d3d11/d3d11_windows.go b/gio/giold/gpu/internal/d3d11/d3d11_windows.go
new file mode 100644
index 0000000..217ea98
--- /dev/null
+++ b/gio/giold/gpu/internal/d3d11/d3d11_windows.go
@@ -0,0 +1,787 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package d3d11
+
+import (
+ "errors"
+ "fmt"
+ "image"
+ "math"
+ "reflect"
+ "unsafe"
+
+ "golang.org/x/sys/windows"
+
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/internal/d3d11"
+)
+
+type Backend struct {
+ dev *d3d11.Device
+ ctx *d3d11.DeviceContext
+
+ // Temporary storage to avoid garbage.
+ clearColor [4]float32
+ viewport d3d11.VIEWPORT
+ depthState depthState
+ blendState blendState
+
+ // Current program.
+ prog *Program
+
+ caps driver.Caps
+
+ // fbo is the currently bound fbo.
+ fbo *Framebuffer
+
+ floatFormat uint32
+
+ // cached state objects.
+ depthStates map[depthState]*d3d11.DepthStencilState
+ blendStates map[blendState]*d3d11.BlendState
+}
+
+type blendState struct {
+ enable bool
+ sfactor driver.BlendFactor
+ dfactor driver.BlendFactor
+}
+
+type depthState struct {
+ enable bool
+ mask bool
+ fn driver.DepthFunc
+}
+
+type Texture struct {
+ backend *Backend
+ format uint32
+ bindings driver.BufferBinding
+ tex *d3d11.Texture2D
+ sampler *d3d11.SamplerState
+ resView *d3d11.ShaderResourceView
+ width int
+ height int
+}
+
+type Program struct {
+ backend *Backend
+
+ vert struct {
+ shader *d3d11.VertexShader
+ uniforms *Buffer
+ }
+ frag struct {
+ shader *d3d11.PixelShader
+ uniforms *Buffer
+ }
+}
+
+type Framebuffer struct {
+ dev *d3d11.Device
+ ctx *d3d11.DeviceContext
+ format uint32
+ resource *d3d11.Resource
+ renderTarget *d3d11.RenderTargetView
+ depthView *d3d11.DepthStencilView
+ foreign bool
+}
+
+type Buffer struct {
+ backend *Backend
+ bind uint32
+ buf *d3d11.Buffer
+ immutable bool
+}
+
+type InputLayout struct {
+ layout *d3d11.InputLayout
+}
+
+func init() {
+ driver.NewDirect3D11Device = newDirect3D11Device
+}
+
+func detectFloatFormat(dev *d3d11.Device) (uint32, bool) {
+ formats := []uint32{
+ d3d11.DXGI_FORMAT_R16_FLOAT,
+ d3d11.DXGI_FORMAT_R32_FLOAT,
+ d3d11.DXGI_FORMAT_R16G16_FLOAT,
+ d3d11.DXGI_FORMAT_R32G32_FLOAT,
+ // These last two are really wasteful, but c'est la vie.
+ d3d11.DXGI_FORMAT_R16G16B16A16_FLOAT,
+ d3d11.DXGI_FORMAT_R32G32B32A32_FLOAT,
+ }
+ for _, format := range formats {
+ need := uint32(d3d11.FORMAT_SUPPORT_TEXTURE2D | d3d11.FORMAT_SUPPORT_RENDER_TARGET)
+ if support, _ := dev.CheckFormatSupport(format); support&need == need {
+ return format, true
+ }
+ }
+ return 0, false
+}
+
+func newDirect3D11Device(api driver.Direct3D11) (driver.Device, error) {
+ dev := (*d3d11.Device)(api.Device)
+ b := &Backend{
+ dev: dev,
+ ctx: dev.GetImmediateContext(),
+ caps: driver.Caps{
+ MaxTextureSize: 2048, // 9.1 maximum
+ },
+ depthStates: make(map[depthState]*d3d11.DepthStencilState),
+ blendStates: make(map[blendState]*d3d11.BlendState),
+ }
+ featLvl := dev.GetFeatureLevel()
+ if featLvl < d3d11.FEATURE_LEVEL_9_1 {
+ d3d11.IUnknownRelease(unsafe.Pointer(dev), dev.Vtbl.Release)
+ d3d11.IUnknownRelease(unsafe.Pointer(b.ctx), b.ctx.Vtbl.Release)
+ return nil, fmt.Errorf("d3d11: feature level too low: %d", featLvl)
+ }
+ switch {
+ case featLvl >= d3d11.FEATURE_LEVEL_11_0:
+ b.caps.MaxTextureSize = 16384
+ case featLvl >= d3d11.FEATURE_LEVEL_9_3:
+ b.caps.MaxTextureSize = 4096
+ }
+ if fmt, ok := detectFloatFormat(dev); ok {
+ b.floatFormat = fmt
+ b.caps.Features |= driver.FeatureFloatRenderTargets
+ }
+ // Enable depth mask to match OpenGL.
+ b.depthState.mask = true
+ // Disable backface culling to match OpenGL.
+ state, err := dev.CreateRasterizerState(&d3d11.RASTERIZER_DESC{
+ CullMode: d3d11.CULL_NONE,
+ FillMode: d3d11.FILL_SOLID,
+ DepthClipEnable: 1,
+ })
+ if err != nil {
+ return nil, err
+ }
+ defer d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release)
+ b.ctx.RSSetState(state)
+ return b, nil
+}
+
+func (b *Backend) BeginFrame() driver.Framebuffer {
+ renderTarget, depthView := b.ctx.OMGetRenderTargets()
+ // Assume someone else is holding on to the render targets.
+ if renderTarget != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(renderTarget),
+ renderTarget.Vtbl.Release)
+ }
+ if depthView != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(depthView), depthView.Vtbl.Release)
+ }
+ return &Framebuffer{ctx: b.ctx, dev: b.dev, renderTarget: renderTarget,
+ depthView: depthView, foreign: true}
+}
+
+func (b *Backend) EndFrame() {
+}
+
+func (b *Backend) Caps() driver.Caps {
+ return b.caps
+}
+
+func (b *Backend) NewTimer() driver.Timer {
+ panic("timers not supported")
+}
+
+func (b *Backend) IsTimeContinuous() bool {
+ panic("timers not supported")
+}
+
+func (b *Backend) Release() {
+ for _, state := range b.depthStates {
+ d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release)
+ }
+ for _, state := range b.blendStates {
+ d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release)
+ }
+ d3d11.IUnknownRelease(unsafe.Pointer(b.ctx), b.ctx.Vtbl.Release)
+ *b = Backend{}
+}
+
+func (b *Backend) NewTexture(format driver.TextureFormat, width, height int,
+ minFilter, magFilter driver.TextureFilter,
+ bindings driver.BufferBinding) (driver.Texture, error) {
+ var d3dfmt uint32
+ switch format {
+ case driver.TextureFormatFloat:
+ d3dfmt = b.floatFormat
+ case driver.TextureFormatSRGB:
+ d3dfmt = d3d11.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB
+ default:
+ return nil, fmt.Errorf("unsupported texture format %d", format)
+ }
+ tex, err := b.dev.CreateTexture2D(&d3d11.TEXTURE2D_DESC{
+ Width: uint32(width),
+ Height: uint32(height),
+ MipLevels: 1,
+ ArraySize: 1,
+ Format: d3dfmt,
+ SampleDesc: d3d11.DXGI_SAMPLE_DESC{
+ Count: 1,
+ Quality: 0,
+ },
+ BindFlags: convBufferBinding(bindings),
+ })
+ if err != nil {
+ return nil, err
+ }
+ var (
+ sampler *d3d11.SamplerState
+ resView *d3d11.ShaderResourceView
+ )
+ if bindings&driver.BufferBindingTexture != 0 {
+ var filter uint32
+ switch {
+ case minFilter == driver.FilterNearest && magFilter == driver.FilterNearest:
+ filter = d3d11.FILTER_MIN_MAG_MIP_POINT
+ case minFilter == driver.FilterLinear && magFilter == driver.FilterLinear:
+ filter = d3d11.FILTER_MIN_MAG_LINEAR_MIP_POINT
+ default:
+ d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release)
+ return nil, fmt.Errorf("unsupported texture filter combination %d, %d",
+ minFilter, magFilter)
+ }
+ var err error
+ sampler, err = b.dev.CreateSamplerState(&d3d11.SAMPLER_DESC{
+ Filter: filter,
+ AddressU: d3d11.TEXTURE_ADDRESS_CLAMP,
+ AddressV: d3d11.TEXTURE_ADDRESS_CLAMP,
+ AddressW: d3d11.TEXTURE_ADDRESS_CLAMP,
+ MaxAnisotropy: 1,
+ MinLOD: -math.MaxFloat32,
+ MaxLOD: math.MaxFloat32,
+ })
+ if err != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release)
+ return nil, err
+ }
+ resView, err = b.dev.CreateShaderResourceViewTEX2D(
+ (*d3d11.Resource)(unsafe.Pointer(tex)),
+ &d3d11.SHADER_RESOURCE_VIEW_DESC_TEX2D{
+ SHADER_RESOURCE_VIEW_DESC: d3d11.SHADER_RESOURCE_VIEW_DESC{
+ Format: d3dfmt,
+ ViewDimension: d3d11.SRV_DIMENSION_TEXTURE2D,
+ },
+ Texture2D: d3d11.TEX2D_SRV{
+ MostDetailedMip: 0,
+ MipLevels: ^uint32(0),
+ },
+ },
+ )
+ if err != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release)
+ d3d11.IUnknownRelease(unsafe.Pointer(sampler), sampler.Vtbl.Release)
+ return nil, err
+ }
+ }
+ return &Texture{backend: b, format: d3dfmt, tex: tex, sampler: sampler,
+ resView: resView, bindings: bindings, width: width, height: height}, nil
+}
+
+func (b *Backend) NewFramebuffer(tex driver.Texture,
+ depthBits int) (driver.Framebuffer, error) {
+ d3dtex := tex.(*Texture)
+ if d3dtex.bindings&driver.BufferBindingFramebuffer == 0 {
+ return nil, errors.New("the texture was created without BufferBindingFramebuffer binding")
+ }
+ resource := (*d3d11.Resource)(unsafe.Pointer(d3dtex.tex))
+ renderTarget, err := b.dev.CreateRenderTargetView(resource)
+ if err != nil {
+ return nil, err
+ }
+ fbo := &Framebuffer{ctx: b.ctx, dev: b.dev, format: d3dtex.format,
+ resource: resource, renderTarget: renderTarget}
+ if depthBits > 0 {
+ depthView, err := d3d11.CreateDepthView(b.dev, d3dtex.width,
+ d3dtex.height, depthBits)
+ if err != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(renderTarget),
+ renderTarget.Vtbl.Release)
+ return nil, err
+ }
+ fbo.depthView = depthView
+ }
+ return fbo, nil
+}
+
+func (b *Backend) NewInputLayout(vertexShader driver.ShaderSources,
+ layout []driver.InputDesc) (driver.InputLayout, error) {
+ if len(vertexShader.Inputs) != len(layout) {
+ return nil, fmt.Errorf("NewInputLayout: got %d inputs, expected %d",
+ len(layout), len(vertexShader.Inputs))
+ }
+ descs := make([]d3d11.INPUT_ELEMENT_DESC, len(layout))
+ for i, l := range layout {
+ inp := vertexShader.Inputs[i]
+ cname, err := windows.BytePtrFromString(inp.Semantic)
+ if err != nil {
+ return nil, err
+ }
+ var format uint32
+ switch l.Type {
+ case driver.DataTypeFloat:
+ switch l.Size {
+ case 1:
+ format = d3d11.DXGI_FORMAT_R32_FLOAT
+ case 2:
+ format = d3d11.DXGI_FORMAT_R32G32_FLOAT
+ case 3:
+ format = d3d11.DXGI_FORMAT_R32G32B32_FLOAT
+ case 4:
+ format = d3d11.DXGI_FORMAT_R32G32B32A32_FLOAT
+ default:
+ panic("unsupported data size")
+ }
+ case driver.DataTypeShort:
+ switch l.Size {
+ case 1:
+ format = d3d11.DXGI_FORMAT_R16_SINT
+ case 2:
+ format = d3d11.DXGI_FORMAT_R16G16_SINT
+ default:
+ panic("unsupported data size")
+ }
+ default:
+ panic("unsupported data type")
+ }
+ descs[i] = d3d11.INPUT_ELEMENT_DESC{
+ SemanticName: cname,
+ SemanticIndex: uint32(inp.SemanticIndex),
+ Format: format,
+ AlignedByteOffset: uint32(l.Offset),
+ }
+ }
+ l, err := b.dev.CreateInputLayout(descs, []byte(vertexShader.HLSL))
+ if err != nil {
+ return nil, err
+ }
+ return &InputLayout{layout: l}, nil
+}
+
+func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer,
+ error) {
+ if typ&driver.BufferBindingUniforms != 0 {
+ if typ != driver.BufferBindingUniforms {
+ return nil, errors.New("uniform buffers cannot have other bindings")
+ }
+ if size%16 != 0 {
+ return nil, fmt.Errorf("constant buffer size is %d, expected a multiple of 16",
+ size)
+ }
+ }
+ bind := convBufferBinding(typ)
+ buf, err := b.dev.CreateBuffer(&d3d11.BUFFER_DESC{
+ ByteWidth: uint32(size),
+ BindFlags: bind,
+ }, nil)
+ if err != nil {
+ return nil, err
+ }
+ return &Buffer{backend: b, buf: buf, bind: bind}, nil
+}
+
+func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding,
+ data []byte) (driver.Buffer, error) {
+ if typ&driver.BufferBindingUniforms != 0 {
+ if typ != driver.BufferBindingUniforms {
+ return nil, errors.New("uniform buffers cannot have other bindings")
+ }
+ if len(data)%16 != 0 {
+ return nil, fmt.Errorf("constant buffer size is %d, expected a multiple of 16",
+ len(data))
+ }
+ }
+ bind := convBufferBinding(typ)
+ buf, err := b.dev.CreateBuffer(&d3d11.BUFFER_DESC{
+ ByteWidth: uint32(len(data)),
+ Usage: d3d11.USAGE_IMMUTABLE,
+ BindFlags: bind,
+ }, data)
+ if err != nil {
+ return nil, err
+ }
+ return &Buffer{backend: b, buf: buf, bind: bind, immutable: true}, nil
+}
+
+func (b *Backend) NewComputeProgram(shader driver.ShaderSources) (driver.Program,
+ error) {
+ panic("not implemented")
+}
+
+func (b *Backend) NewProgram(vertexShader, fragmentShader driver.ShaderSources) (driver.Program,
+ error) {
+ vs, err := b.dev.CreateVertexShader([]byte(vertexShader.HLSL))
+ if err != nil {
+ return nil, err
+ }
+ ps, err := b.dev.CreatePixelShader([]byte(fragmentShader.HLSL))
+ if err != nil {
+ return nil, err
+ }
+ p := &Program{backend: b}
+ p.vert.shader = vs
+ p.frag.shader = ps
+ return p, nil
+}
+
+func (b *Backend) Clear(colr, colg, colb, cola float32) {
+ b.clearColor = [4]float32{colr, colg, colb, cola}
+ b.ctx.ClearRenderTargetView(b.fbo.renderTarget, &b.clearColor)
+}
+
+func (b *Backend) ClearDepth(depth float32) {
+ if b.fbo.depthView != nil {
+ b.ctx.ClearDepthStencilView(b.fbo.depthView,
+ d3d11.CLEAR_DEPTH|d3d11.CLEAR_STENCIL, depth, 0)
+ }
+}
+
+func (b *Backend) Viewport(x, y, width, height int) {
+ b.viewport = d3d11.VIEWPORT{
+ TopLeftX: float32(x),
+ TopLeftY: float32(y),
+ Width: float32(width),
+ Height: float32(height),
+ MinDepth: 0.0,
+ MaxDepth: 1.0,
+ }
+ b.ctx.RSSetViewports(&b.viewport)
+}
+
+func (b *Backend) DrawArrays(mode driver.DrawMode, off, count int) {
+ b.prepareDraw(mode)
+ b.ctx.Draw(uint32(count), uint32(off))
+}
+
+func (b *Backend) DrawElements(mode driver.DrawMode, off, count int) {
+ b.prepareDraw(mode)
+ b.ctx.DrawIndexed(uint32(count), uint32(off), 0)
+}
+
+func (b *Backend) prepareDraw(mode driver.DrawMode) {
+ if p := b.prog; p != nil {
+ b.ctx.VSSetShader(p.vert.shader)
+ b.ctx.PSSetShader(p.frag.shader)
+ if buf := p.vert.uniforms; buf != nil {
+ b.ctx.VSSetConstantBuffers(buf.buf)
+ }
+ if buf := p.frag.uniforms; buf != nil {
+ b.ctx.PSSetConstantBuffers(buf.buf)
+ }
+ }
+ var topology uint32
+ switch mode {
+ case driver.DrawModeTriangles:
+ topology = d3d11.PRIMITIVE_TOPOLOGY_TRIANGLELIST
+ case driver.DrawModeTriangleStrip:
+ topology = d3d11.PRIMITIVE_TOPOLOGY_TRIANGLESTRIP
+ default:
+ panic("unsupported draw mode")
+ }
+ b.ctx.IASetPrimitiveTopology(topology)
+
+ depthState, ok := b.depthStates[b.depthState]
+ if !ok {
+ var desc d3d11.DEPTH_STENCIL_DESC
+ if b.depthState.enable {
+ desc.DepthEnable = 1
+ }
+ if b.depthState.mask {
+ desc.DepthWriteMask = d3d11.DEPTH_WRITE_MASK_ALL
+ }
+ switch b.depthState.fn {
+ case driver.DepthFuncGreater:
+ desc.DepthFunc = d3d11.COMPARISON_GREATER
+ case driver.DepthFuncGreaterEqual:
+ desc.DepthFunc = d3d11.COMPARISON_GREATER_EQUAL
+ default:
+ panic("unsupported depth func")
+ }
+ var err error
+ depthState, err = b.dev.CreateDepthStencilState(&desc)
+ if err != nil {
+ panic(err)
+ }
+ b.depthStates[b.depthState] = depthState
+ }
+ b.ctx.OMSetDepthStencilState(depthState, 0)
+
+ blendState, ok := b.blendStates[b.blendState]
+ if !ok {
+ var desc d3d11.BLEND_DESC
+ t0 := &desc.RenderTarget[0]
+ t0.RenderTargetWriteMask = d3d11.COLOR_WRITE_ENABLE_ALL
+ t0.BlendOp = d3d11.BLEND_OP_ADD
+ t0.BlendOpAlpha = d3d11.BLEND_OP_ADD
+ if b.blendState.enable {
+ t0.BlendEnable = 1
+ }
+ scol, salpha := toBlendFactor(b.blendState.sfactor)
+ dcol, dalpha := toBlendFactor(b.blendState.dfactor)
+ t0.SrcBlend = scol
+ t0.SrcBlendAlpha = salpha
+ t0.DestBlend = dcol
+ t0.DestBlendAlpha = dalpha
+ var err error
+ blendState, err = b.dev.CreateBlendState(&desc)
+ if err != nil {
+ panic(err)
+ }
+ b.blendStates[b.blendState] = blendState
+ }
+ b.ctx.OMSetBlendState(blendState, nil, 0xffffffff)
+}
+
+func (b *Backend) DepthFunc(f driver.DepthFunc) {
+ b.depthState.fn = f
+}
+
+func (b *Backend) SetBlend(enable bool) {
+ b.blendState.enable = enable
+}
+
+func (b *Backend) SetDepthTest(enable bool) {
+ b.depthState.enable = enable
+}
+
+func (b *Backend) DepthMask(mask bool) {
+ b.depthState.mask = mask
+}
+
+func (b *Backend) BlendFunc(sfactor, dfactor driver.BlendFactor) {
+ b.blendState.sfactor = sfactor
+ b.blendState.dfactor = dfactor
+}
+
+func (b *Backend) BindImageTexture(unit int, tex driver.Texture,
+ access driver.AccessBits, f driver.TextureFormat) {
+ panic("not implemented")
+}
+
+func (b *Backend) MemoryBarrier() {
+ panic("not implemented")
+}
+
+func (b *Backend) DispatchCompute(x, y, z int) {
+ panic("not implemented")
+}
+
+func (t *Texture) Upload(offset, size image.Point, pixels []byte) {
+ stride := size.X * 4
+ dst := &d3d11.BOX{
+ Left: uint32(offset.X),
+ Top: uint32(offset.Y),
+ Right: uint32(offset.X + size.X),
+ Bottom: uint32(offset.Y + size.Y),
+ Front: 0,
+ Back: 1,
+ }
+ res := (*d3d11.Resource)(unsafe.Pointer(t.tex))
+ t.backend.ctx.UpdateSubresource(res, dst, uint32(stride),
+ uint32(len(pixels)), pixels)
+}
+
+func (t *Texture) Release() {
+ d3d11.IUnknownRelease(unsafe.Pointer(t.tex), t.tex.Vtbl.Release)
+ t.tex = nil
+ if t.sampler != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(t.sampler), t.sampler.Vtbl.Release)
+ t.sampler = nil
+ }
+ if t.resView != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(t.resView), t.resView.Vtbl.Release)
+ t.resView = nil
+ }
+}
+
+func (b *Backend) BindTexture(unit int, tex driver.Texture) {
+ t := tex.(*Texture)
+ b.ctx.PSSetSamplers(uint32(unit), t.sampler)
+ b.ctx.PSSetShaderResources(uint32(unit), t.resView)
+}
+
+func (b *Backend) BindProgram(prog driver.Program) {
+ b.prog = prog.(*Program)
+}
+
+func (p *Program) Release() {
+ d3d11.IUnknownRelease(unsafe.Pointer(p.vert.shader),
+ p.vert.shader.Vtbl.Release)
+ d3d11.IUnknownRelease(unsafe.Pointer(p.frag.shader),
+ p.frag.shader.Vtbl.Release)
+ p.vert.shader = nil
+ p.frag.shader = nil
+}
+
+func (p *Program) SetStorageBuffer(binding int, buffer driver.Buffer) {
+ panic("not implemented")
+}
+
+func (p *Program) SetVertexUniforms(buf driver.Buffer) {
+ p.vert.uniforms = buf.(*Buffer)
+}
+
+func (p *Program) SetFragmentUniforms(buf driver.Buffer) {
+ p.frag.uniforms = buf.(*Buffer)
+}
+
+func (b *Backend) BindVertexBuffer(buf driver.Buffer, stride, offset int) {
+ b.ctx.IASetVertexBuffers(buf.(*Buffer).buf, uint32(stride), uint32(offset))
+}
+
+func (b *Backend) BindIndexBuffer(buf driver.Buffer) {
+ b.ctx.IASetIndexBuffer(buf.(*Buffer).buf, d3d11.DXGI_FORMAT_R16_UINT, 0)
+}
+
+func (b *Buffer) Download(data []byte) error {
+ panic("not implemented")
+}
+
+func (b *Buffer) Upload(data []byte) {
+ b.backend.ctx.UpdateSubresource((*d3d11.Resource)(unsafe.Pointer(b.buf)),
+ nil, 0, 0, data)
+}
+
+func (b *Buffer) Release() {
+ d3d11.IUnknownRelease(unsafe.Pointer(b.buf), b.buf.Vtbl.Release)
+ b.buf = nil
+}
+
+func (f *Framebuffer) ReadPixels(src image.Rectangle, pixels []byte) error {
+ if f.resource == nil {
+ return errors.New("framebuffer does not support ReadPixels")
+ }
+ w, h := src.Dx(), src.Dy()
+ tex, err := f.dev.CreateTexture2D(&d3d11.TEXTURE2D_DESC{
+ Width: uint32(w),
+ Height: uint32(h),
+ MipLevels: 1,
+ ArraySize: 1,
+ Format: f.format,
+ SampleDesc: d3d11.DXGI_SAMPLE_DESC{
+ Count: 1,
+ Quality: 0,
+ },
+ Usage: d3d11.USAGE_STAGING,
+ CPUAccessFlags: d3d11.CPU_ACCESS_READ,
+ })
+ if err != nil {
+ return fmt.Errorf("ReadPixels: %v", err)
+ }
+ defer d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release)
+ res := (*d3d11.Resource)(unsafe.Pointer(tex))
+ f.ctx.CopySubresourceRegion(
+ res,
+ 0, // Destination subresource.
+ 0, 0, 0, // Destination coordinates (x, y, z).
+ f.resource,
+ 0, // Source subresource.
+ &d3d11.BOX{
+ Left: uint32(src.Min.X),
+ Top: uint32(src.Min.Y),
+ Right: uint32(src.Max.X),
+ Bottom: uint32(src.Max.Y),
+ Front: 0,
+ Back: 1,
+ },
+ )
+ resMap, err := f.ctx.Map(res, 0, d3d11.MAP_READ, 0)
+ if err != nil {
+ return fmt.Errorf("ReadPixels: %v", err)
+ }
+ defer f.ctx.Unmap(res, 0)
+ srcPitch := w * 4
+ dstPitch := int(resMap.RowPitch)
+ mapSize := dstPitch * h
+ data := sliceOf(resMap.PData, mapSize)
+ width := w * 4
+ for r := 0; r < h; r++ {
+ pixels := pixels[r*srcPitch:]
+ copy(pixels[:width], data[r*dstPitch:])
+ }
+ return nil
+}
+
+func (b *Backend) BindFramebuffer(fbo driver.Framebuffer) {
+ b.fbo = fbo.(*Framebuffer)
+ b.ctx.OMSetRenderTargets(b.fbo.renderTarget, b.fbo.depthView)
+}
+
+func (f *Framebuffer) Invalidate() {
+}
+
+func (f *Framebuffer) Release() {
+ if f.foreign {
+ panic("framebuffer not created by NewFramebuffer")
+ }
+ if f.renderTarget != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(f.renderTarget),
+ f.renderTarget.Vtbl.Release)
+ f.renderTarget = nil
+ }
+ if f.depthView != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(f.depthView),
+ f.depthView.Vtbl.Release)
+ f.depthView = nil
+ }
+}
+
+func (b *Backend) BindInputLayout(layout driver.InputLayout) {
+ b.ctx.IASetInputLayout(layout.(*InputLayout).layout)
+}
+
+func (l *InputLayout) Release() {
+ d3d11.IUnknownRelease(unsafe.Pointer(l.layout), l.layout.Vtbl.Release)
+ l.layout = nil
+}
+
+func convBufferBinding(typ driver.BufferBinding) uint32 {
+ var bindings uint32
+ if typ&driver.BufferBindingVertices != 0 {
+ bindings |= d3d11.BIND_VERTEX_BUFFER
+ }
+ if typ&driver.BufferBindingIndices != 0 {
+ bindings |= d3d11.BIND_INDEX_BUFFER
+ }
+ if typ&driver.BufferBindingUniforms != 0 {
+ bindings |= d3d11.BIND_CONSTANT_BUFFER
+ }
+ if typ&driver.BufferBindingTexture != 0 {
+ bindings |= d3d11.BIND_SHADER_RESOURCE
+ }
+ if typ&driver.BufferBindingFramebuffer != 0 {
+ bindings |= d3d11.BIND_RENDER_TARGET
+ }
+ return bindings
+}
+
+func toBlendFactor(f driver.BlendFactor) (uint32, uint32) {
+ switch f {
+ case driver.BlendFactorOne:
+ return d3d11.BLEND_ONE, d3d11.BLEND_ONE
+ case driver.BlendFactorOneMinusSrcAlpha:
+ return d3d11.BLEND_INV_SRC_ALPHA, d3d11.BLEND_INV_SRC_ALPHA
+ case driver.BlendFactorZero:
+ return d3d11.BLEND_ZERO, d3d11.BLEND_ZERO
+ case driver.BlendFactorDstColor:
+ return d3d11.BLEND_DEST_COLOR, d3d11.BLEND_DEST_ALPHA
+ default:
+ panic("unsupported blend source factor")
+ }
+}
+
+// sliceOf returns a slice from a (native) pointer.
+func sliceOf(ptr uintptr, cap int) []byte {
+ var data []byte
+ h := (*reflect.SliceHeader)(unsafe.Pointer(&data))
+ h.Data = ptr
+ h.Cap = cap
+ h.Len = cap
+ return data
+}
diff --git a/gio/giold/gpu/internal/driver/api.go b/gio/giold/gpu/internal/driver/api.go
new file mode 100644
index 0000000..6e0d846
--- /dev/null
+++ b/gio/giold/gpu/internal/driver/api.go
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package driver
+
+import (
+ "fmt"
+ "unsafe"
+
+ "realy.lol/gio/internal/gl"
+)
+
+// See gpu/api.go for documentation for the API types
+
+type API interface {
+ implementsAPI()
+}
+
+type OpenGL struct {
+ // Context contains the WebGL context for WebAssembly platforms. It is
+ // empty for all other platforms; an OpenGL context is assumed current when
+ // calling NewDevice.
+ Context gl.Context
+}
+
+type Direct3D11 struct {
+ // Device contains a *ID3D11Device.
+ Device unsafe.Pointer
+}
+
+// API specific device constructors.
+var (
+ NewOpenGLDevice func(api OpenGL) (Device, error)
+ NewDirect3D11Device func(api Direct3D11) (Device, error)
+)
+
+// NewDevice creates a new Device given the api.
+//
+// Note that the device does not assume ownership of the resources contained in
+// api; the caller must ensure the resources are valid until the device is
+// released.
+func NewDevice(api API) (Device, error) {
+ switch api := api.(type) {
+ case OpenGL:
+ if NewOpenGLDevice != nil {
+ return NewOpenGLDevice(api)
+ }
+ case Direct3D11:
+ if NewDirect3D11Device != nil {
+ return NewDirect3D11Device(api)
+ }
+ }
+ return nil, fmt.Errorf("driver: no driver available for the API %T", api)
+}
+
+func (OpenGL) implementsAPI() {}
+func (Direct3D11) implementsAPI() {}
diff --git a/gio/giold/gpu/internal/driver/driver.go b/gio/giold/gpu/internal/driver/driver.go
new file mode 100644
index 0000000..14d3d85
--- /dev/null
+++ b/gio/giold/gpu/internal/driver/driver.go
@@ -0,0 +1,270 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package driver
+
+import (
+ "errors"
+ "image"
+ "time"
+)
+
+// Device represents the abstraction of underlying GPU
+// APIs such as OpenGL, Direct3D useful for rendering Gio
+// operations.
+type Device interface {
+ BeginFrame() Framebuffer
+ EndFrame()
+ Caps() Caps
+ NewTimer() Timer
+ // IsContinuousTime reports whether all timer measurements
+ // are valid at the point of call.
+ IsTimeContinuous() bool
+ NewTexture(format TextureFormat, width, height int, minFilter, magFilter TextureFilter, bindings BufferBinding) (Texture, error)
+ NewFramebuffer(tex Texture, depthBits int) (Framebuffer, error)
+ NewImmutableBuffer(typ BufferBinding, data []byte) (Buffer, error)
+ NewBuffer(typ BufferBinding, size int) (Buffer, error)
+ NewComputeProgram(shader ShaderSources) (Program, error)
+ NewProgram(vertexShader, fragmentShader ShaderSources) (Program, error)
+ NewInputLayout(vertexShader ShaderSources, layout []InputDesc) (InputLayout, error)
+
+ DepthFunc(f DepthFunc)
+ ClearDepth(d float32)
+ Clear(r, g, b, a float32)
+ Viewport(x, y, width, height int)
+ DrawArrays(mode DrawMode, off, count int)
+ DrawElements(mode DrawMode, off, count int)
+ SetBlend(enable bool)
+ SetDepthTest(enable bool)
+ DepthMask(mask bool)
+ BlendFunc(sfactor, dfactor BlendFactor)
+
+ BindInputLayout(i InputLayout)
+ BindProgram(p Program)
+ BindFramebuffer(f Framebuffer)
+ BindTexture(unit int, t Texture)
+ BindVertexBuffer(b Buffer, stride, offset int)
+ BindIndexBuffer(b Buffer)
+ BindImageTexture(unit int, texture Texture, access AccessBits, format TextureFormat)
+
+ MemoryBarrier()
+ DispatchCompute(x, y, z int)
+
+ Release()
+}
+
+type ShaderSources struct {
+ Name string
+ GLSL100ES string
+ GLSL300ES string
+ GLSL310ES string
+ GLSL130 string
+ GLSL150 string
+ HLSL string
+ Uniforms UniformsReflection
+ Inputs []InputLocation
+ Textures []TextureBinding
+}
+
+type UniformsReflection struct {
+ Blocks []UniformBlock
+ Locations []UniformLocation
+ Size int
+}
+
+type TextureBinding struct {
+ Name string
+ Binding int
+}
+
+type UniformBlock struct {
+ Name string
+ Binding int
+}
+
+type UniformLocation struct {
+ Name string
+ Type DataType
+ Size int
+ Offset int
+}
+
+type InputLocation struct {
+ // For GLSL.
+ Name string
+ Location int
+ // For HLSL.
+ Semantic string
+ SemanticIndex int
+
+ Type DataType
+ Size int
+}
+
+// InputDesc describes a vertex attribute as laid out in a Buffer.
+type InputDesc struct {
+ Type DataType
+ Size int
+
+ Offset int
+}
+
+// InputLayout is the driver specific representation of the mapping
+// between Buffers and shader attributes.
+type InputLayout interface {
+ Release()
+}
+
+type AccessBits uint8
+
+type BlendFactor uint8
+
+type DrawMode uint8
+
+type TextureFilter uint8
+type TextureFormat uint8
+
+type BufferBinding uint8
+
+type DataType uint8
+
+type DepthFunc uint8
+
+type Features uint
+
+type Caps struct {
+ // BottomLeftOrigin is true if the driver has the origin in the lower left
+ // corner. The OpenGL driver returns true.
+ BottomLeftOrigin bool
+ Features Features
+ MaxTextureSize int
+}
+
+type Program interface {
+ Release()
+ SetStorageBuffer(binding int, buf Buffer)
+ SetVertexUniforms(buf Buffer)
+ SetFragmentUniforms(buf Buffer)
+}
+
+type Buffer interface {
+ Release()
+ Upload(data []byte)
+ Download(data []byte) error
+}
+
+type Framebuffer interface {
+ Invalidate()
+ Release()
+ ReadPixels(src image.Rectangle, pixels []byte) error
+}
+
+type Timer interface {
+ Begin()
+ End()
+ Duration() (time.Duration, bool)
+ Release()
+}
+
+type Texture interface {
+ Upload(offset, size image.Point, pixels []byte)
+ Release()
+}
+
+const (
+ DepthFuncGreater DepthFunc = iota
+ DepthFuncGreaterEqual
+)
+
+const (
+ DataTypeFloat DataType = iota
+ DataTypeInt
+ DataTypeShort
+)
+
+const (
+ BufferBindingIndices BufferBinding = 1 << iota
+ BufferBindingVertices
+ BufferBindingUniforms
+ BufferBindingTexture
+ BufferBindingFramebuffer
+ BufferBindingShaderStorage
+)
+
+const (
+ TextureFormatSRGB TextureFormat = iota
+ TextureFormatFloat
+ TextureFormatRGBA8
+)
+
+const (
+ AccessRead AccessBits = 1 + iota
+ AccessWrite
+)
+
+const (
+ FilterNearest TextureFilter = iota
+ FilterLinear
+)
+
+const (
+ FeatureTimers Features = 1 << iota
+ FeatureFloatRenderTargets
+ FeatureCompute
+)
+
+const (
+ DrawModeTriangleStrip DrawMode = iota
+ DrawModeTriangles
+)
+
+const (
+ BlendFactorOne BlendFactor = iota
+ BlendFactorOneMinusSrcAlpha
+ BlendFactorZero
+ BlendFactorDstColor
+)
+
+var ErrContentLost = errors.New("buffer content lost")
+
+func (f Features) Has(feats Features) bool {
+ return f&feats == feats
+}
+
+func DownloadImage(d Device, f Framebuffer, r image.Rectangle) (*image.RGBA, error) {
+ img := image.NewRGBA(r)
+ if err := f.ReadPixels(r, img.Pix); err != nil {
+ return nil, err
+ }
+ if d.Caps().BottomLeftOrigin {
+ // OpenGL origin is in the lower-left corner. Flip the image to
+ // match.
+ flipImageY(r.Dx()*4, r.Dy(), img.Pix)
+ }
+ return img, nil
+}
+
+func flipImageY(stride, height int, pixels []byte) {
+ // Flip image in y-direction. OpenGL's origin is in the lower
+ // left corner.
+ row := make([]uint8, stride)
+ for y := 0; y < height/2; y++ {
+ y1 := height - y - 1
+ dest := y1 * stride
+ src := y * stride
+ copy(row, pixels[dest:])
+ copy(pixels[dest:], pixels[src:src+len(row)])
+ copy(pixels[src:], row)
+ }
+}
+
+func UploadImage(t Texture, offset image.Point, img *image.RGBA) {
+ var pixels []byte
+ size := img.Bounds().Size()
+ if img.Stride != size.X*4 {
+ panic("unsupported stride")
+ }
+ start := img.PixOffset(0, 0)
+ end := img.PixOffset(size.X, size.Y-1)
+ pixels = img.Pix[start:end]
+ t.Upload(offset, size, pixels)
+}
diff --git a/gio/giold/gpu/internal/opengl/opengl.go b/gio/giold/gpu/internal/opengl/opengl.go
new file mode 100644
index 0000000..e41dbc8
--- /dev/null
+++ b/gio/giold/gpu/internal/opengl/opengl.go
@@ -0,0 +1,998 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package opengl
+
+import (
+ "errors"
+ "fmt"
+ "image"
+ "strings"
+ "time"
+ "unsafe"
+
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/internal/gl"
+)
+
+// Backend implements driver.Device.
+type Backend struct {
+ funcs *gl.Functions
+
+ state glstate
+
+ glver [2]int
+ gles bool
+ ubo bool
+ feats driver.Caps
+ // floatTriple holds the settings for floating point
+ // textures.
+ floatTriple textureTriple
+ // Single channel alpha textures.
+ alphaTriple textureTriple
+ srgbaTriple textureTriple
+}
+
+// State tracking.
+type glstate struct {
+ // nattr is the current number of enabled vertex arrays.
+ nattr int
+ prog *gpuProgram
+ texUnits [4]*gpuTexture
+ layout *gpuInputLayout
+ buffer bufferBinding
+}
+
+type bufferBinding struct {
+ buf *gpuBuffer
+ offset int
+ stride int
+}
+
+type gpuTimer struct {
+ funcs *gl.Functions
+ obj gl.Query
+}
+
+type gpuTexture struct {
+ backend *Backend
+ obj gl.Texture
+ triple textureTriple
+ width int
+ height int
+}
+
+type gpuFramebuffer struct {
+ backend *Backend
+ obj gl.Framebuffer
+ hasDepth bool
+ depthBuf gl.Renderbuffer
+ foreign bool
+}
+
+type gpuBuffer struct {
+ backend *Backend
+ hasBuffer bool
+ obj gl.Buffer
+ typ driver.BufferBinding
+ size int
+ immutable bool
+ version int
+ // For emulation of uniform buffers.
+ data []byte
+}
+
+type gpuProgram struct {
+ backend *Backend
+ obj gl.Program
+ nattr int
+ vertUniforms uniformsTracker
+ fragUniforms uniformsTracker
+ storage [storageBindings]*gpuBuffer
+}
+
+type uniformsTracker struct {
+ locs []uniformLocation
+ size int
+ buf *gpuBuffer
+ version int
+}
+
+type uniformLocation struct {
+ uniform gl.Uniform
+ offset int
+ typ driver.DataType
+ size int
+}
+
+type gpuInputLayout struct {
+ inputs []driver.InputLocation
+ layout []driver.InputDesc
+}
+
+// textureTriple holds the type settings for
+// a TexImage2D call.
+type textureTriple struct {
+ internalFormat gl.Enum
+ format gl.Enum
+ typ gl.Enum
+}
+
+type Context = gl.Context
+
+const (
+ storageBindings = 32
+)
+
+func init() {
+ driver.NewOpenGLDevice = newOpenGLDevice
+}
+
+func newOpenGLDevice(api driver.OpenGL) (driver.Device, error) {
+ f, err := gl.NewFunctions(api.Context)
+ if err != nil {
+ return nil, err
+ }
+ exts := strings.Split(f.GetString(gl.EXTENSIONS), " ")
+ glVer := f.GetString(gl.VERSION)
+ ver, gles, err := gl.ParseGLVersion(glVer)
+ if err != nil {
+ return nil, err
+ }
+ floatTriple, ffboErr := floatTripleFor(f, ver, exts)
+ srgbaTriple, err := srgbaTripleFor(ver, exts)
+ if err != nil {
+ return nil, err
+ }
+ gles30 := gles && ver[0] >= 3
+ gles31 := gles && (ver[0] > 3 || (ver[0] == 3 && ver[1] >= 1))
+ gl40 := !gles && ver[0] >= 4
+ b := &Backend{
+ glver: ver,
+ gles: gles,
+ ubo: gles30 || gl40,
+ funcs: f,
+ floatTriple: floatTriple,
+ alphaTriple: alphaTripleFor(ver),
+ srgbaTriple: srgbaTriple,
+ }
+ b.feats.BottomLeftOrigin = true
+ if ffboErr == nil {
+ b.feats.Features |= driver.FeatureFloatRenderTargets
+ }
+ if gles31 {
+ b.feats.Features |= driver.FeatureCompute
+ }
+ if hasExtension(exts,
+ "GL_EXT_disjoint_timer_query_webgl2") || hasExtension(exts,
+ "GL_EXT_disjoint_timer_query") {
+ b.feats.Features |= driver.FeatureTimers
+ }
+ b.feats.MaxTextureSize = f.GetInteger(gl.MAX_TEXTURE_SIZE)
+ return b, nil
+}
+
+func (b *Backend) BeginFrame() driver.Framebuffer {
+ // Assume GL state is reset between frames.
+ b.state = glstate{}
+ fboID := gl.Framebuffer(b.funcs.GetBinding(gl.FRAMEBUFFER_BINDING))
+ return &gpuFramebuffer{backend: b, obj: fboID, foreign: true}
+}
+
+func (b *Backend) EndFrame() {
+ b.funcs.ActiveTexture(gl.TEXTURE0)
+}
+
+func (b *Backend) Caps() driver.Caps {
+ return b.feats
+}
+
+func (b *Backend) NewTimer() driver.Timer {
+ return &gpuTimer{
+ funcs: b.funcs,
+ obj: b.funcs.CreateQuery(),
+ }
+}
+
+func (b *Backend) IsTimeContinuous() bool {
+ return b.funcs.GetInteger(gl.GPU_DISJOINT_EXT) == gl.FALSE
+}
+
+func (b *Backend) NewFramebuffer(tex driver.Texture,
+ depthBits int) (driver.Framebuffer, error) {
+ glErr(b.funcs)
+ gltex := tex.(*gpuTexture)
+ fb := b.funcs.CreateFramebuffer()
+ fbo := &gpuFramebuffer{backend: b, obj: fb}
+ b.BindFramebuffer(fbo)
+ if err := glErr(b.funcs); err != nil {
+ fbo.Release()
+ return nil, err
+ }
+ b.funcs.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
+ gl.TEXTURE_2D, gltex.obj, 0)
+ if depthBits > 0 {
+ size := gl.Enum(gl.DEPTH_COMPONENT16)
+ switch {
+ case depthBits > 24:
+ size = gl.DEPTH_COMPONENT32F
+ case depthBits > 16:
+ size = gl.DEPTH_COMPONENT24
+ }
+ depthBuf := b.funcs.CreateRenderbuffer()
+ b.funcs.BindRenderbuffer(gl.RENDERBUFFER, depthBuf)
+ b.funcs.RenderbufferStorage(gl.RENDERBUFFER, size, gltex.width,
+ gltex.height)
+ b.funcs.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT,
+ gl.RENDERBUFFER, depthBuf)
+ fbo.depthBuf = depthBuf
+ fbo.hasDepth = true
+ if err := glErr(b.funcs); err != nil {
+ fbo.Release()
+ return nil, err
+ }
+ }
+ if st := b.funcs.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
+ fbo.Release()
+ return nil, fmt.Errorf("incomplete framebuffer, status = 0x%x, err = %d",
+ st, b.funcs.GetError())
+ }
+ return fbo, nil
+}
+
+func (b *Backend) NewTexture(format driver.TextureFormat, width, height int,
+ minFilter, magFilter driver.TextureFilter,
+ binding driver.BufferBinding) (driver.Texture, error) {
+ glErr(b.funcs)
+ tex := &gpuTexture{backend: b, obj: b.funcs.CreateTexture(), width: width,
+ height: height}
+ switch format {
+ case driver.TextureFormatFloat:
+ tex.triple = b.floatTriple
+ case driver.TextureFormatSRGB:
+ tex.triple = b.srgbaTriple
+ case driver.TextureFormatRGBA8:
+ tex.triple = textureTriple{gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE}
+ default:
+ return nil, errors.New("unsupported texture format")
+ }
+ b.BindTexture(0, tex)
+ b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER,
+ toTexFilter(magFilter))
+ b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER,
+ toTexFilter(minFilter))
+ b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
+ b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
+ if b.gles && b.glver[0] >= 3 {
+ // Immutable textures are required for BindImageTexture, and can't hurt otherwise.
+ b.funcs.TexStorage2D(gl.TEXTURE_2D, 1, tex.triple.internalFormat, width,
+ height)
+ } else {
+ b.funcs.TexImage2D(gl.TEXTURE_2D, 0, tex.triple.internalFormat, width,
+ height, tex.triple.format, tex.triple.typ)
+ }
+ if err := glErr(b.funcs); err != nil {
+ tex.Release()
+ return nil, err
+ }
+ return tex, nil
+}
+
+func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer,
+ error) {
+ glErr(b.funcs)
+ buf := &gpuBuffer{backend: b, typ: typ, size: size}
+ if typ&driver.BufferBindingUniforms != 0 {
+ if typ != driver.BufferBindingUniforms {
+ return nil, errors.New("uniforms buffers cannot be bound as anything else")
+ }
+ if !b.ubo {
+ // GLES 2 doesn't support uniform buffers.
+ buf.data = make([]byte, size)
+ }
+ }
+ if typ&^driver.BufferBindingUniforms != 0 || b.ubo {
+ buf.hasBuffer = true
+ buf.obj = b.funcs.CreateBuffer()
+ if err := glErr(b.funcs); err != nil {
+ buf.Release()
+ return nil, err
+ }
+ firstBinding := firstBufferType(typ)
+ b.funcs.BindBuffer(firstBinding, buf.obj)
+ b.funcs.BufferData(firstBinding, size, gl.DYNAMIC_DRAW)
+ }
+ return buf, nil
+}
+
+func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding,
+ data []byte) (driver.Buffer, error) {
+ glErr(b.funcs)
+ obj := b.funcs.CreateBuffer()
+ buf := &gpuBuffer{backend: b, obj: obj, typ: typ, size: len(data),
+ hasBuffer: true}
+ firstBinding := firstBufferType(typ)
+ b.funcs.BindBuffer(firstBinding, buf.obj)
+ b.funcs.BufferData(firstBinding, len(data), gl.STATIC_DRAW)
+ buf.Upload(data)
+ buf.immutable = true
+ if err := glErr(b.funcs); err != nil {
+ buf.Release()
+ return nil, err
+ }
+ return buf, nil
+}
+
+func glErr(f *gl.Functions) error {
+ if st := f.GetError(); st != gl.NO_ERROR {
+ return fmt.Errorf("glGetError: %#x", st)
+ }
+ return nil
+}
+
+func (b *Backend) Release() {
+}
+
+func (b *Backend) MemoryBarrier() {
+ b.funcs.MemoryBarrier(gl.ALL_BARRIER_BITS)
+}
+
+func (b *Backend) DispatchCompute(x, y, z int) {
+ if p := b.state.prog; p != nil {
+ for binding, buf := range p.storage {
+ if buf != nil {
+ b.funcs.BindBufferBase(gl.SHADER_STORAGE_BUFFER, binding,
+ buf.obj)
+ }
+ }
+ }
+ b.funcs.DispatchCompute(x, y, z)
+}
+
+func (b *Backend) BindImageTexture(unit int, tex driver.Texture,
+ access driver.AccessBits, f driver.TextureFormat) {
+ t := tex.(*gpuTexture)
+ var acc gl.Enum
+ switch access {
+ case driver.AccessWrite:
+ acc = gl.WRITE_ONLY
+ case driver.AccessRead:
+ acc = gl.READ_ONLY
+ default:
+ panic("unsupported access bits")
+ }
+ var format gl.Enum
+ switch f {
+ case driver.TextureFormatRGBA8:
+ format = gl.RGBA8
+ default:
+ panic("unsupported format")
+ }
+ b.funcs.BindImageTexture(unit, t.obj, 0, false, 0, acc, format)
+}
+
+func (b *Backend) bindTexture(unit int, t *gpuTexture) {
+ if b.state.texUnits[unit] != t {
+ b.funcs.ActiveTexture(gl.TEXTURE0 + gl.Enum(unit))
+ b.funcs.BindTexture(gl.TEXTURE_2D, t.obj)
+ b.state.texUnits[unit] = t
+ }
+}
+
+func (b *Backend) useProgram(p *gpuProgram) {
+ if b.state.prog != p {
+ p.backend.funcs.UseProgram(p.obj)
+ b.state.prog = p
+ }
+}
+
+func (b *Backend) enableVertexArrays(n int) {
+ // Enable needed arrays.
+ for i := b.state.nattr; i < n; i++ {
+ b.funcs.EnableVertexAttribArray(gl.Attrib(i))
+ }
+ // Disable extra arrays.
+ for i := n; i < b.state.nattr; i++ {
+ b.funcs.DisableVertexAttribArray(gl.Attrib(i))
+ }
+ b.state.nattr = n
+}
+
+func (b *Backend) SetDepthTest(enable bool) {
+ if enable {
+ b.funcs.Enable(gl.DEPTH_TEST)
+ } else {
+ b.funcs.Disable(gl.DEPTH_TEST)
+ }
+}
+
+func (b *Backend) BlendFunc(sfactor, dfactor driver.BlendFactor) {
+ b.funcs.BlendFunc(toGLBlendFactor(sfactor), toGLBlendFactor(dfactor))
+}
+
+func toGLBlendFactor(f driver.BlendFactor) gl.Enum {
+ switch f {
+ case driver.BlendFactorOne:
+ return gl.ONE
+ case driver.BlendFactorOneMinusSrcAlpha:
+ return gl.ONE_MINUS_SRC_ALPHA
+ case driver.BlendFactorZero:
+ return gl.ZERO
+ case driver.BlendFactorDstColor:
+ return gl.DST_COLOR
+ default:
+ panic("unsupported blend factor")
+ }
+}
+
+func (b *Backend) DepthMask(mask bool) {
+ b.funcs.DepthMask(mask)
+}
+
+func (b *Backend) SetBlend(enable bool) {
+ if enable {
+ b.funcs.Enable(gl.BLEND)
+ } else {
+ b.funcs.Disable(gl.BLEND)
+ }
+}
+
+func (b *Backend) DrawElements(mode driver.DrawMode, off, count int) {
+ b.prepareDraw()
+ // off is in 16-bit indices, but DrawElements take a byte offset.
+ byteOff := off * 2
+ b.funcs.DrawElements(toGLDrawMode(mode), count, gl.UNSIGNED_SHORT, byteOff)
+}
+
+func (b *Backend) DrawArrays(mode driver.DrawMode, off, count int) {
+ b.prepareDraw()
+ b.funcs.DrawArrays(toGLDrawMode(mode), off, count)
+}
+
+func (b *Backend) prepareDraw() {
+ nattr := b.state.prog.nattr
+ b.enableVertexArrays(nattr)
+ if nattr > 0 {
+ b.setupVertexArrays()
+ }
+ if p := b.state.prog; p != nil {
+ p.updateUniforms()
+ }
+}
+
+func toGLDrawMode(mode driver.DrawMode) gl.Enum {
+ switch mode {
+ case driver.DrawModeTriangleStrip:
+ return gl.TRIANGLE_STRIP
+ case driver.DrawModeTriangles:
+ return gl.TRIANGLES
+ default:
+ panic("unsupported draw mode")
+ }
+}
+
+func (b *Backend) Viewport(x, y, width, height int) {
+ b.funcs.Viewport(x, y, width, height)
+}
+
+func (b *Backend) Clear(colR, colG, colB, colA float32) {
+ b.funcs.ClearColor(colR, colG, colB, colA)
+ b.funcs.Clear(gl.COLOR_BUFFER_BIT)
+}
+
+func (b *Backend) ClearDepth(d float32) {
+ b.funcs.ClearDepthf(d)
+ b.funcs.Clear(gl.DEPTH_BUFFER_BIT)
+}
+
+func (b *Backend) DepthFunc(f driver.DepthFunc) {
+ var glfunc gl.Enum
+ switch f {
+ case driver.DepthFuncGreater:
+ glfunc = gl.GREATER
+ case driver.DepthFuncGreaterEqual:
+ glfunc = gl.GEQUAL
+ default:
+ panic("unsupported depth func")
+ }
+ b.funcs.DepthFunc(glfunc)
+}
+
+func (b *Backend) NewInputLayout(vs driver.ShaderSources,
+ layout []driver.InputDesc) (driver.InputLayout, error) {
+ if len(vs.Inputs) != len(layout) {
+ return nil, fmt.Errorf("NewInputLayout: got %d inputs, expected %d",
+ len(layout), len(vs.Inputs))
+ }
+ for i, inp := range vs.Inputs {
+ if exp, got := inp.Size, layout[i].Size; exp != got {
+ return nil, fmt.Errorf("NewInputLayout: data size mismatch for %q: got %d expected %d",
+ inp.Name, got, exp)
+ }
+ }
+ return &gpuInputLayout{
+ inputs: vs.Inputs,
+ layout: layout,
+ }, nil
+}
+
+func (b *Backend) NewComputeProgram(src driver.ShaderSources) (driver.Program,
+ error) {
+ p, err := gl.CreateComputeProgram(b.funcs, src.GLSL310ES)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %v", src.Name, err)
+ }
+ gpuProg := &gpuProgram{
+ backend: b,
+ obj: p,
+ }
+ return gpuProg, nil
+}
+
+func (b *Backend) NewProgram(vertShader, fragShader driver.ShaderSources) (driver.Program,
+ error) {
+ attr := make([]string, len(vertShader.Inputs))
+ for _, inp := range vertShader.Inputs {
+ attr[inp.Location] = inp.Name
+ }
+ vsrc, fsrc := vertShader.GLSL100ES, fragShader.GLSL100ES
+ if b.glver[0] >= 3 {
+ // OpenGL (ES) 3.0.
+ switch {
+ case b.gles:
+ vsrc, fsrc = vertShader.GLSL300ES, fragShader.GLSL300ES
+ case b.glver[0] >= 4 || b.glver[1] >= 2:
+ // OpenGL 3.2 Core only accepts glsl 1.50 or newer.
+ vsrc, fsrc = vertShader.GLSL150, fragShader.GLSL150
+ default:
+ vsrc, fsrc = vertShader.GLSL130, fragShader.GLSL130
+ }
+ }
+ p, err := gl.CreateProgram(b.funcs, vsrc, fsrc, attr)
+ if err != nil {
+ return nil, err
+ }
+ gpuProg := &gpuProgram{
+ backend: b,
+ obj: p,
+ nattr: len(attr),
+ }
+ b.BindProgram(gpuProg)
+ // Bind texture uniforms.
+ for _, tex := range vertShader.Textures {
+ u := b.funcs.GetUniformLocation(p, tex.Name)
+ if u.Valid() {
+ b.funcs.Uniform1i(u, tex.Binding)
+ }
+ }
+ for _, tex := range fragShader.Textures {
+ u := b.funcs.GetUniformLocation(p, tex.Name)
+ if u.Valid() {
+ b.funcs.Uniform1i(u, tex.Binding)
+ }
+ }
+ if b.ubo {
+ for _, block := range vertShader.Uniforms.Blocks {
+ blockIdx := b.funcs.GetUniformBlockIndex(p, block.Name)
+ if blockIdx != gl.INVALID_INDEX {
+ b.funcs.UniformBlockBinding(p, blockIdx, uint(block.Binding))
+ }
+ }
+ // To match Direct3D 11 with separate vertex and fragment
+ // shader uniform buffers, offset all fragment blocks to be
+ // located after the vertex blocks.
+ off := len(vertShader.Uniforms.Blocks)
+ for _, block := range fragShader.Uniforms.Blocks {
+ blockIdx := b.funcs.GetUniformBlockIndex(p, block.Name)
+ if blockIdx != gl.INVALID_INDEX {
+ b.funcs.UniformBlockBinding(p, blockIdx,
+ uint(block.Binding+off))
+ }
+ }
+ } else {
+ gpuProg.vertUniforms.setup(b.funcs, p, vertShader.Uniforms.Size,
+ vertShader.Uniforms.Locations)
+ gpuProg.fragUniforms.setup(b.funcs, p, fragShader.Uniforms.Size,
+ fragShader.Uniforms.Locations)
+ }
+ return gpuProg, nil
+}
+
+func lookupUniform(funcs *gl.Functions, p gl.Program,
+ loc driver.UniformLocation) uniformLocation {
+ u := funcs.GetUniformLocation(p, loc.Name)
+ if !u.Valid() {
+ panic(fmt.Errorf("uniform %q not found", loc.Name))
+ }
+ return uniformLocation{uniform: u, offset: loc.Offset, typ: loc.Type,
+ size: loc.Size}
+}
+
+func (p *gpuProgram) SetStorageBuffer(binding int, buffer driver.Buffer) {
+ buf := buffer.(*gpuBuffer)
+ if buf.typ&driver.BufferBindingShaderStorage == 0 {
+ panic("not a shader storage buffer")
+ }
+ p.storage[binding] = buf
+}
+
+func (p *gpuProgram) SetVertexUniforms(buffer driver.Buffer) {
+ p.vertUniforms.setBuffer(buffer)
+}
+
+func (p *gpuProgram) SetFragmentUniforms(buffer driver.Buffer) {
+ p.fragUniforms.setBuffer(buffer)
+}
+
+func (p *gpuProgram) updateUniforms() {
+ f := p.backend.funcs
+ if p.backend.ubo {
+ if b := p.vertUniforms.buf; b != nil {
+ f.BindBufferBase(gl.UNIFORM_BUFFER, 0, b.obj)
+ }
+ if b := p.fragUniforms.buf; b != nil {
+ f.BindBufferBase(gl.UNIFORM_BUFFER, 1, b.obj)
+ }
+ } else {
+ p.vertUniforms.update(f)
+ p.fragUniforms.update(f)
+ }
+}
+
+func (b *Backend) BindProgram(prog driver.Program) {
+ p := prog.(*gpuProgram)
+ b.useProgram(p)
+}
+
+func (p *gpuProgram) Release() {
+ p.backend.funcs.DeleteProgram(p.obj)
+}
+
+func (u *uniformsTracker) setup(funcs *gl.Functions, p gl.Program,
+ uniformSize int, uniforms []driver.UniformLocation) {
+ u.locs = make([]uniformLocation, len(uniforms))
+ for i, uniform := range uniforms {
+ u.locs[i] = lookupUniform(funcs, p, uniform)
+ }
+ u.size = uniformSize
+}
+
+func (u *uniformsTracker) setBuffer(buffer driver.Buffer) {
+ buf := buffer.(*gpuBuffer)
+ if buf.typ&driver.BufferBindingUniforms == 0 {
+ panic("not a uniform buffer")
+ }
+ if buf.size < u.size {
+ panic(fmt.Errorf("uniform buffer too small, got %d need %d", buf.size,
+ u.size))
+ }
+ u.buf = buf
+ // Force update.
+ u.version = buf.version - 1
+}
+
+func (p *uniformsTracker) update(funcs *gl.Functions) {
+ b := p.buf
+ if b == nil || b.version == p.version {
+ return
+ }
+ p.version = b.version
+ data := b.data
+ for _, u := range p.locs {
+ data := data[u.offset:]
+ switch {
+ case u.typ == driver.DataTypeFloat && u.size == 1:
+ data := data[:4]
+ v := *(*[1]float32)(unsafe.Pointer(&data[0]))
+ funcs.Uniform1f(u.uniform, v[0])
+ case u.typ == driver.DataTypeFloat && u.size == 2:
+ data := data[:8]
+ v := *(*[2]float32)(unsafe.Pointer(&data[0]))
+ funcs.Uniform2f(u.uniform, v[0], v[1])
+ case u.typ == driver.DataTypeFloat && u.size == 3:
+ data := data[:12]
+ v := *(*[3]float32)(unsafe.Pointer(&data[0]))
+ funcs.Uniform3f(u.uniform, v[0], v[1], v[2])
+ case u.typ == driver.DataTypeFloat && u.size == 4:
+ data := data[:16]
+ v := *(*[4]float32)(unsafe.Pointer(&data[0]))
+ funcs.Uniform4f(u.uniform, v[0], v[1], v[2], v[3])
+ default:
+ panic("unsupported uniform data type or size")
+ }
+ }
+}
+
+func (b *gpuBuffer) Upload(data []byte) {
+ if b.immutable {
+ panic("immutable buffer")
+ }
+ if len(data) > b.size {
+ panic("buffer size overflow")
+ }
+ b.version++
+ copy(b.data, data)
+ if b.hasBuffer {
+ firstBinding := firstBufferType(b.typ)
+ b.backend.funcs.BindBuffer(firstBinding, b.obj)
+ if len(data) == b.size {
+ // the iOS GL implementation doesn't recognize when BufferSubData
+ // clears the entire buffer. Tell it and avoid GPU stalls.
+ // See also https://github.com/godotengine/godot/issues/23956.
+ b.backend.funcs.BufferData(firstBinding, b.size, gl.DYNAMIC_DRAW)
+ }
+ b.backend.funcs.BufferSubData(firstBinding, 0, data)
+ }
+}
+
+func (b *gpuBuffer) Download(data []byte) error {
+ if len(data) > b.size {
+ panic("buffer size overflow")
+ }
+ if !b.hasBuffer {
+ copy(data, b.data)
+ return nil
+ }
+ firstBinding := firstBufferType(b.typ)
+ b.backend.funcs.BindBuffer(firstBinding, b.obj)
+ bufferMap := b.backend.funcs.MapBufferRange(firstBinding, 0, len(data),
+ gl.MAP_READ_BIT)
+ if bufferMap == nil {
+ return fmt.Errorf("MapBufferRange: error %#x",
+ b.backend.funcs.GetError())
+ }
+ copy(data, bufferMap)
+ if !b.backend.funcs.UnmapBuffer(firstBinding) {
+ return driver.ErrContentLost
+ }
+ return nil
+}
+
+func (b *gpuBuffer) Release() {
+ if b.hasBuffer {
+ b.backend.funcs.DeleteBuffer(b.obj)
+ b.hasBuffer = false
+ }
+}
+
+func (b *Backend) BindVertexBuffer(buf driver.Buffer, stride, offset int) {
+ gbuf := buf.(*gpuBuffer)
+ if gbuf.typ&driver.BufferBindingVertices == 0 {
+ panic("not a vertex buffer")
+ }
+ b.state.buffer = bufferBinding{buf: gbuf, stride: stride, offset: offset}
+}
+
+func (b *Backend) setupVertexArrays() {
+ layout := b.state.layout
+ if layout == nil {
+ return
+ }
+ buf := b.state.buffer
+ b.funcs.BindBuffer(gl.ARRAY_BUFFER, buf.buf.obj)
+ for i, inp := range layout.inputs {
+ l := layout.layout[i]
+ var gltyp gl.Enum
+ switch l.Type {
+ case driver.DataTypeFloat:
+ gltyp = gl.FLOAT
+ case driver.DataTypeShort:
+ gltyp = gl.SHORT
+ default:
+ panic("unsupported data type")
+ }
+ b.funcs.VertexAttribPointer(gl.Attrib(inp.Location), l.Size, gltyp,
+ false, buf.stride, buf.offset+l.Offset)
+ }
+}
+
+func (b *Backend) BindIndexBuffer(buf driver.Buffer) {
+ gbuf := buf.(*gpuBuffer)
+ if gbuf.typ&driver.BufferBindingIndices == 0 {
+ panic("not an index buffer")
+ }
+ b.funcs.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, gbuf.obj)
+}
+
+func (b *Backend) BlitFramebuffer(dst, src driver.Framebuffer,
+ srect, drect image.Rectangle) {
+ b.funcs.BindFramebuffer(gl.DRAW_FRAMEBUFFER, dst.(*gpuFramebuffer).obj)
+ b.funcs.BindFramebuffer(gl.READ_FRAMEBUFFER, src.(*gpuFramebuffer).obj)
+ b.funcs.BlitFramebuffer(
+ srect.Min.X, srect.Min.Y, srect.Max.X, srect.Max.Y,
+ drect.Min.X, drect.Min.Y, drect.Max.X, drect.Max.Y,
+ gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT|gl.STENCIL_BUFFER_BIT,
+ gl.NEAREST)
+}
+
+func (f *gpuFramebuffer) ReadPixels(src image.Rectangle, pixels []byte) error {
+ glErr(f.backend.funcs)
+ f.backend.BindFramebuffer(f)
+ if len(pixels) < src.Dx()*src.Dy()*4 {
+ return errors.New("unexpected RGBA size")
+ }
+ f.backend.funcs.ReadPixels(src.Min.X, src.Min.Y, src.Dx(), src.Dy(),
+ gl.RGBA, gl.UNSIGNED_BYTE, pixels)
+ return glErr(f.backend.funcs)
+}
+
+func (b *Backend) BindFramebuffer(fbo driver.Framebuffer) {
+ b.funcs.BindFramebuffer(gl.FRAMEBUFFER, fbo.(*gpuFramebuffer).obj)
+}
+
+func (f *gpuFramebuffer) Invalidate() {
+ f.backend.BindFramebuffer(f)
+ f.backend.funcs.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0)
+}
+
+func (f *gpuFramebuffer) Release() {
+ if f.foreign {
+ panic("framebuffer not created by NewFramebuffer")
+ }
+ f.backend.funcs.DeleteFramebuffer(f.obj)
+ if f.hasDepth {
+ f.backend.funcs.DeleteRenderbuffer(f.depthBuf)
+ }
+}
+
+func toTexFilter(f driver.TextureFilter) int {
+ switch f {
+ case driver.FilterNearest:
+ return gl.NEAREST
+ case driver.FilterLinear:
+ return gl.LINEAR
+ default:
+ panic("unsupported texture filter")
+ }
+}
+
+func (b *Backend) BindTexture(unit int, t driver.Texture) {
+ b.bindTexture(unit, t.(*gpuTexture))
+}
+
+func (t *gpuTexture) Release() {
+ t.backend.funcs.DeleteTexture(t.obj)
+}
+
+func (t *gpuTexture) Upload(offset, size image.Point, pixels []byte) {
+ if min := size.X * size.Y * 4; min > len(pixels) {
+ panic(fmt.Errorf("size %d larger than data %d", min, len(pixels)))
+ }
+ t.backend.BindTexture(0, t)
+ t.backend.funcs.TexSubImage2D(gl.TEXTURE_2D, 0, offset.X, offset.Y, size.X,
+ size.Y, t.triple.format, t.triple.typ, pixels)
+}
+
+func (t *gpuTimer) Begin() {
+ t.funcs.BeginQuery(gl.TIME_ELAPSED_EXT, t.obj)
+}
+
+func (t *gpuTimer) End() {
+ t.funcs.EndQuery(gl.TIME_ELAPSED_EXT)
+}
+
+func (t *gpuTimer) ready() bool {
+ return t.funcs.GetQueryObjectuiv(t.obj,
+ gl.QUERY_RESULT_AVAILABLE) == gl.TRUE
+}
+
+func (t *gpuTimer) Release() {
+ t.funcs.DeleteQuery(t.obj)
+}
+
+func (t *gpuTimer) Duration() (time.Duration, bool) {
+ if !t.ready() {
+ return 0, false
+ }
+ nanos := t.funcs.GetQueryObjectuiv(t.obj, gl.QUERY_RESULT)
+ return time.Duration(nanos), true
+}
+
+func (b *Backend) BindInputLayout(l driver.InputLayout) {
+ b.state.layout = l.(*gpuInputLayout)
+}
+
+func (l *gpuInputLayout) Release() {}
+
+// floatTripleFor determines the best texture triple for floating point FBOs.
+func floatTripleFor(f *gl.Functions, ver [2]int, exts []string) (textureTriple,
+ error) {
+ var triples []textureTriple
+ if ver[0] >= 3 {
+ triples = append(triples,
+ textureTriple{gl.R16F, gl.Enum(gl.RED), gl.Enum(gl.HALF_FLOAT)})
+ }
+ // According to the OES_texture_half_float specification, EXT_color_buffer_half_float is needed to
+ // render to FBOs. However, the Safari WebGL1 implementation does support half-float FBOs but does not
+ // report EXT_color_buffer_half_float support. The triples are verified below, so it doesn't matter if we're
+ // wrong.
+ if hasExtension(exts, "GL_OES_texture_half_float") || hasExtension(exts,
+ "GL_EXT_color_buffer_half_float") {
+ // Try single channel.
+ triples = append(triples,
+ textureTriple{gl.LUMINANCE, gl.Enum(gl.LUMINANCE),
+ gl.Enum(gl.HALF_FLOAT_OES)})
+ // Fallback to 4 channels.
+ triples = append(triples, textureTriple{gl.RGBA, gl.Enum(gl.RGBA),
+ gl.Enum(gl.HALF_FLOAT_OES)})
+ }
+ if hasExtension(exts, "GL_OES_texture_float") || hasExtension(exts,
+ "GL_EXT_color_buffer_float") {
+ triples = append(triples,
+ textureTriple{gl.RGBA, gl.Enum(gl.RGBA), gl.Enum(gl.FLOAT)})
+ }
+ tex := f.CreateTexture()
+ defer f.DeleteTexture(tex)
+ f.BindTexture(gl.TEXTURE_2D, tex)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
+ fbo := f.CreateFramebuffer()
+ defer f.DeleteFramebuffer(fbo)
+ defFBO := gl.Framebuffer(f.GetBinding(gl.FRAMEBUFFER_BINDING))
+ f.BindFramebuffer(gl.FRAMEBUFFER, fbo)
+ defer f.BindFramebuffer(gl.FRAMEBUFFER, defFBO)
+ var attempts []string
+ for _, tt := range triples {
+ const size = 256
+ f.TexImage2D(gl.TEXTURE_2D, 0, tt.internalFormat, size, size, tt.format,
+ tt.typ)
+ f.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
+ gl.TEXTURE_2D, tex, 0)
+ st := f.CheckFramebufferStatus(gl.FRAMEBUFFER)
+ if st == gl.FRAMEBUFFER_COMPLETE {
+ return tt, nil
+ }
+ attempts = append(attempts,
+ fmt.Sprintf("(0x%x, 0x%x, 0x%x): 0x%x", tt.internalFormat,
+ tt.format, tt.typ, st))
+ }
+ return textureTriple{}, fmt.Errorf("floating point fbos not supported (attempted %s)",
+ attempts)
+}
+
+func srgbaTripleFor(ver [2]int, exts []string) (textureTriple, error) {
+ switch {
+ case ver[0] >= 3:
+ return textureTriple{gl.SRGB8_ALPHA8, gl.Enum(gl.RGBA),
+ gl.Enum(gl.UNSIGNED_BYTE)}, nil
+ case hasExtension(exts, "GL_EXT_sRGB"):
+ return textureTriple{gl.SRGB_ALPHA_EXT, gl.Enum(gl.SRGB_ALPHA_EXT),
+ gl.Enum(gl.UNSIGNED_BYTE)}, nil
+ default:
+ return textureTriple{}, errors.New("no sRGB texture formats found")
+ }
+}
+
+func alphaTripleFor(ver [2]int) textureTriple {
+ intf, f := gl.Enum(gl.R8), gl.Enum(gl.RED)
+ if ver[0] < 3 {
+ // R8, RED not supported on OpenGL ES 2.0.
+ intf, f = gl.LUMINANCE, gl.Enum(gl.LUMINANCE)
+ }
+ return textureTriple{intf, f, gl.UNSIGNED_BYTE}
+}
+
+func hasExtension(exts []string, ext string) bool {
+ for _, e := range exts {
+ if ext == e {
+ return true
+ }
+ }
+ return false
+}
+
+func firstBufferType(typ driver.BufferBinding) gl.Enum {
+ switch {
+ case typ&driver.BufferBindingIndices != 0:
+ return gl.ELEMENT_ARRAY_BUFFER
+ case typ&driver.BufferBindingVertices != 0:
+ return gl.ARRAY_BUFFER
+ case typ&driver.BufferBindingUniforms != 0:
+ return gl.UNIFORM_BUFFER
+ case typ&driver.BufferBindingShaderStorage != 0:
+ return gl.SHADER_STORAGE_BUFFER
+ default:
+ panic("unsupported buffer type")
+ }
+}
diff --git a/gio/giold/gpu/internal/rendertest/bench_test.go b/gio/giold/gpu/internal/rendertest/bench_test.go
new file mode 100644
index 0000000..ac4ec5f
--- /dev/null
+++ b/gio/giold/gpu/internal/rendertest/bench_test.go
@@ -0,0 +1,321 @@
+package rendertest
+
+import (
+ "image"
+ "image/color"
+ "math"
+ "testing"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/font/gofont"
+ "realy.lol/gio/gpu/headless"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/widget/material"
+)
+
+// use some global variables for benchmarking so as to not pollute
+// the reported allocs with allocations that we do not want to count.
+var (
+ c1, c2, c3 = make(chan op.CallOp), make(chan op.CallOp), make(chan op.CallOp)
+ op1, op2, op3 op.Ops
+)
+
+func setupBenchmark(b *testing.B) (layout.Context, *headless.Window,
+ *material.Theme) {
+ sz := image.Point{X: 1024, Y: 1200}
+ w := newWindow(b, sz.X, sz.Y)
+ ops := new(op.Ops)
+ gtx := layout.Context{
+ Ops: ops,
+ Constraints: layout.Exact(sz),
+ }
+ th := material.NewTheme(gofont.Collection())
+ return gtx, w, th
+}
+
+func resetOps(gtx layout.Context) {
+ gtx.Ops.Reset()
+ op1.Reset()
+ op2.Reset()
+ op3.Reset()
+}
+
+func finishBenchmark(b *testing.B, w *headless.Window) {
+ b.StopTimer()
+ if *dumpImages {
+ img, err := w.Screenshot()
+ w.Release()
+ if err != nil {
+ b.Error(err)
+ }
+ if err := saveImage(b.Name()+".png", img); err != nil {
+ b.Error(err)
+ }
+ }
+}
+
+func BenchmarkDrawUICached(b *testing.B) {
+ // As BenchmarkDraw but the same op.Ops every time that is not reset - this
+ // should thus allow for maximal cache usage.
+ gtx, w, th := setupBenchmark(b)
+ drawCore(gtx, th)
+ w.Frame(gtx.Ops)
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ w.Frame(gtx.Ops)
+ }
+ finishBenchmark(b, w)
+}
+
+func BenchmarkDrawUI(b *testing.B) {
+ // BenchmarkDraw is intended as a reasonable overall benchmark for
+ // the drawing performance of the full drawing pipeline, in each iteration
+ // resetting the ops and drawing, similar to how a typical UI would function.
+ // This will allow font caching across frames.
+ gtx, w, th := setupBenchmark(b)
+ drawCore(gtx, th)
+ w.Frame(gtx.Ops)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ resetOps(gtx)
+
+ p := op.Save(gtx.Ops)
+ off := float32(math.Mod(float64(i)/10, 10))
+ op.Offset(f32.Pt(off, off)).Add(gtx.Ops)
+
+ drawCore(gtx, th)
+
+ p.Load()
+ w.Frame(gtx.Ops)
+ }
+ finishBenchmark(b, w)
+}
+
+func BenchmarkDrawUITransformed(b *testing.B) {
+ // Like BenchmarkDraw UI but transformed at every frame
+ gtx, w, th := setupBenchmark(b)
+ drawCore(gtx, th)
+ w.Frame(gtx.Ops)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ resetOps(gtx)
+
+ p := op.Save(gtx.Ops)
+ angle := float32(math.Mod(float64(i)/1000, 0.05))
+ a := f32.Affine2D{}.Shear(f32.Point{}, angle, angle).Rotate(f32.Point{},
+ angle)
+ op.Affine(a).Add(gtx.Ops)
+
+ drawCore(gtx, th)
+
+ p.Load()
+ w.Frame(gtx.Ops)
+ }
+ finishBenchmark(b, w)
+}
+
+func Benchmark1000Circles(b *testing.B) {
+ // Benchmark1000Shapes draws 1000 individual shapes such that no caching between
+ // shapes will be possible and resets buffers on each operation to prevent caching
+ // between frames.
+ gtx, w, _ := setupBenchmark(b)
+ draw1000Circles(gtx)
+ w.Frame(gtx.Ops)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ resetOps(gtx)
+ draw1000Circles(gtx)
+ w.Frame(gtx.Ops)
+ }
+ finishBenchmark(b, w)
+}
+
+func Benchmark1000CirclesInstanced(b *testing.B) {
+ // Like Benchmark1000Circles but will record them and thus allow for caching between
+ // them.
+ gtx, w, _ := setupBenchmark(b)
+ draw1000CirclesInstanced(gtx)
+ w.Frame(gtx.Ops)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ resetOps(gtx)
+ draw1000CirclesInstanced(gtx)
+ w.Frame(gtx.Ops)
+ }
+ finishBenchmark(b, w)
+}
+
+func draw1000Circles(gtx layout.Context) {
+ ops := gtx.Ops
+ for x := 0; x < 100; x++ {
+ p := op.Save(ops)
+ op.Offset(f32.Pt(float32(x*10), 0)).Add(ops)
+ for y := 0; y < 10; y++ {
+ paint.FillShape(ops,
+ color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100,
+ A: 120},
+ clip.RRect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5,
+ NW: 5}.Op(ops),
+ )
+ op.Offset(f32.Pt(0, float32(100))).Add(ops)
+ }
+ p.Load()
+ }
+}
+
+func draw1000CirclesInstanced(gtx layout.Context) {
+ ops := gtx.Ops
+
+ r := op.Record(ops)
+ clip.RRect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5,
+ NW: 5}.Add(ops)
+ paint.PaintOp{}.Add(ops)
+ c := r.Stop()
+
+ for x := 0; x < 100; x++ {
+ p := op.Save(ops)
+ op.Offset(f32.Pt(float32(x*10), 0)).Add(ops)
+ for y := 0; y < 10; y++ {
+ pi := op.Save(ops)
+ paint.ColorOp{Color: color.NRGBA{R: 100 + uint8(x),
+ G: 100 + uint8(y), B: 100, A: 120}}.Add(ops)
+ c.Add(ops)
+ pi.Load()
+ op.Offset(f32.Pt(0, float32(100))).Add(ops)
+ }
+ p.Load()
+ }
+}
+
+func drawCore(gtx layout.Context, th *material.Theme) {
+ c1 := drawIndividualShapes(gtx, th)
+ c2 := drawShapeInstances(gtx, th)
+ c3 := drawText(gtx, th)
+
+ (<-c1).Add(gtx.Ops)
+ (<-c2).Add(gtx.Ops)
+ (<-c3).Add(gtx.Ops)
+}
+
+func drawIndividualShapes(gtx layout.Context,
+ th *material.Theme) chan op.CallOp {
+ // draw 81 rounded rectangles of different solid colors - each one individually
+ go func() {
+ ops := &op1
+ c := op.Record(ops)
+ for x := 0; x < 9; x++ {
+ p := op.Save(ops)
+ op.Offset(f32.Pt(float32(x*50), 0)).Add(ops)
+ for y := 0; y < 9; y++ {
+ paint.FillShape(ops,
+ color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100,
+ A: 120},
+ clip.RRect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10,
+ SW: 10, NW: 10}.Op(ops),
+ )
+ op.Offset(f32.Pt(0, float32(50))).Add(ops)
+ }
+ p.Load()
+ }
+ c1 <- c.Stop()
+ }()
+ return c1
+}
+
+func drawShapeInstances(gtx layout.Context, th *material.Theme) chan op.CallOp {
+ // draw 400 textured circle instances, each with individual transform
+ go func() {
+ ops := &op2
+ co := op.Record(ops)
+
+ r := op.Record(ops)
+ clip.RRect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10,
+ NW: 10}.Add(ops)
+ paint.PaintOp{}.Add(ops)
+ c := r.Stop()
+
+ squares.Add(ops)
+ rad := float32(0)
+ for x := 0; x < 20; x++ {
+ for y := 0; y < 20; y++ {
+ p := op.Save(ops)
+ op.Offset(f32.Pt(float32(x*50+25), float32(y*50+25))).Add(ops)
+ c.Add(ops)
+ p.Load()
+ rad += math.Pi * 2 / 400
+ }
+ }
+ c2 <- co.Stop()
+ }()
+ return c2
+}
+
+func drawText(gtx layout.Context, th *material.Theme) chan op.CallOp {
+ // draw 40 lines of text with different transforms.
+ go func() {
+ ops := &op3
+ c := op.Record(ops)
+
+ txt := material.H6(th, "")
+ for x := 0; x < 40; x++ {
+ txt.Text = textRows[x]
+ p := op.Save(ops)
+ op.Offset(f32.Pt(float32(0), float32(24*x))).Add(ops)
+ gtx.Ops = ops
+ txt.Layout(gtx)
+ p.Load()
+ }
+ c3 <- c.Stop()
+ }()
+ return c3
+}
+
+var textRows = []string{
+ "1. I learned from my grandfather, Verus, to use good manners, and to",
+ "put restraint on anger. 2. In the famous memory of my father I had a",
+ "pattern of modesty and manliness. 3. Of my mother I learned to be",
+ "pious and generous; to keep myself not only from evil deeds, but even",
+ "from evil thoughts; and to live with a simplicity which is far from",
+ "customary among the rich. 4. I owe it to my great-grandfather that I",
+ "did not attend public lectures and discussions, but had good and able",
+ "teachers at home; and I owe him also the knowledge that for things of",
+ "this nature a man should count no expense too great.",
+ "5. My tutor taught me not to favour either green or blue at the",
+ "chariot races, nor, in the contests of gladiators, to be a supporter",
+ "either of light or heavy armed. He taught me also to endure labour;",
+ "not to need many things; to serve myself without troubling others; not",
+ "to intermeddle in the affairs of others, and not easily to listen to",
+ "slanders against them.",
+ "6. Of Diognetus I had the lesson not to busy myself about vain things;",
+ "not to credit the great professions of such as pretend to work",
+ "wonders, or of sorcerers about their charms, and their expelling of",
+ "Demons and the like; not to keep quails (for fighting or divination),",
+ "nor to run after such things; to suffer freedom of speech in others,",
+ "and to apply myself heartily to philosophy. Him also I must thank for",
+ "my hearing first Bacchius, then Tandasis and Marcianus; that I wrote",
+ "dialogues in my youth, and took a liking to the philosopher's pallet",
+ "and skins, and to the other things which, by the Grecian discipline,",
+ "belong to that profession.",
+ "7. To Rusticus I owe my first apprehensions that my nature needed",
+ "reform and cure; and that I did not fall into the ambition of the",
+ "common Sophists, either by composing speculative writings or by",
+ "declaiming harangues of exhortation in public; further, that I never",
+ "strove to be admired by ostentation of great patience in an ascetic",
+ "life, or by display of activity and application; that I gave over the",
+ "study of rhetoric, poetry, and the graces of language; and that I did",
+ "not pace my house in my senatorial robes, or practise any similar",
+ "affectation. I observed also the simplicity of style in his letters,",
+ "particularly in that which he wrote to my mother from Sinuessa. I",
+ "learned from him to be easily appeased, and to be readily reconciled",
+ "with those who had displeased me or given cause of offence, so soon as",
+ "they inclined to make their peace; to read with care; not to rest",
+ "satisfied with a slight and superficial knowledge; nor quickly to",
+ "assent to great talkers. I have him to thank that I met with the",
+}
diff --git a/gio/giold/gpu/internal/rendertest/clip_test.go b/gio/giold/gpu/internal/rendertest/clip_test.go
new file mode 100644
index 0000000..d12bb90
--- /dev/null
+++ b/gio/giold/gpu/internal/rendertest/clip_test.go
@@ -0,0 +1,581 @@
+package rendertest
+
+import (
+ "image"
+ "math"
+ "testing"
+
+ "golang.org/x/image/colornames"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+)
+
+func TestPaintRect(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
+ }, func(r result) {
+ r.expect(0, 0, colornames.Red)
+ r.expect(49, 0, colornames.Red)
+ r.expect(50, 0, transparent)
+ r.expect(10, 50, transparent)
+ })
+}
+
+func TestPaintClippedRect(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ clip.RRect{Rect: f32.Rect(25, 25, 60, 60)}.Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(24, 35, transparent)
+ r.expect(25, 35, colornames.Red)
+ r.expect(50, 0, transparent)
+ r.expect(10, 50, transparent)
+ })
+}
+
+func TestPaintClippedCircle(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ r := float32(10)
+ clip.RRect{Rect: f32.Rect(20, 20, 40, 40), SE: r, SW: r, NW: r,
+ NE: r}.Add(o)
+ clip.Rect(image.Rect(0, 0, 30, 50)).Add(o)
+ paint.Fill(o, red)
+ }, func(r result) {
+ r.expect(21, 21, transparent)
+ r.expect(25, 30, colornames.Red)
+ r.expect(31, 30, transparent)
+ })
+}
+
+func TestPaintArc(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(0, 20))
+ p.Line(f32.Pt(10, 0))
+ p.Arc(f32.Pt(10, 0), f32.Pt(40, 0), math.Pi)
+ p.Line(f32.Pt(30, 0))
+ p.Line(f32.Pt(0, 25))
+ p.Arc(f32.Pt(-10, 5), f32.Pt(10, 15), -math.Pi)
+ p.Line(f32.Pt(0, 25))
+ p.Arc(f32.Pt(10, 10), f32.Pt(10, 10), 2*math.Pi)
+ p.Line(f32.Pt(-10, 0))
+ p.Arc(f32.Pt(-10, 0), f32.Pt(-40, 0), -math.Pi)
+ p.Line(f32.Pt(-10, 0))
+ p.Line(f32.Pt(0, -10))
+ p.Arc(f32.Pt(-10, -20), f32.Pt(10, -5), math.Pi)
+ p.Line(f32.Pt(0, -10))
+ p.Line(f32.Pt(-50, 0))
+ p.Close()
+ clip.Outline{
+ Path: p.End(),
+ }.Op().Add(o)
+
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op())
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(0, 25, colornames.Red)
+ r.expect(0, 15, transparent)
+ })
+}
+
+func TestPaintAbsolute(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(100,
+ 100)) // offset the initial pen position to test "MoveTo"
+
+ p.MoveTo(f32.Pt(20, 20))
+ p.LineTo(f32.Pt(80, 20))
+ p.QuadTo(f32.Pt(80, 80), f32.Pt(20, 80))
+ p.Close()
+ clip.Outline{
+ Path: p.End(),
+ }.Op().Add(o)
+
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op())
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(30, 30, colornames.Red)
+ r.expect(79, 79, transparent)
+ r.expect(90, 90, transparent)
+ })
+}
+
+func TestPaintTexture(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ squares.Add(o)
+ scale(80.0/512, 80.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(0, 0, colornames.Blue)
+ r.expect(79, 10, colornames.Green)
+ r.expect(80, 0, transparent)
+ r.expect(10, 80, transparent)
+ })
+}
+
+func TestTexturedStrokeClipped(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ smallSquares.Add(o)
+ op.Offset(f32.Pt(50, 50)).Add(o)
+ clip.Stroke{
+ Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o),
+ Style: clip.StrokeStyle{
+ Width: 10,
+ },
+ }.Op().Add(o)
+ clip.RRect{Rect: f32.Rect(-30, -30, 60, 60)}.Add(o)
+ op.Offset(f32.Pt(-10, -10)).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ })
+}
+
+func TestTexturedStroke(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ smallSquares.Add(o)
+ op.Offset(f32.Pt(50, 50)).Add(o)
+ clip.Stroke{
+ Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o),
+ Style: clip.StrokeStyle{
+ Width: 10,
+ },
+ }.Op().Add(o)
+ op.Offset(f32.Pt(-10, -10)).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ })
+}
+
+func TestPaintClippedTexture(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ squares.Add(o)
+ clip.RRect{Rect: f32.Rect(0, 0, 40, 40)}.Add(o)
+ scale(80.0/512, 80.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(40, 40, transparent)
+ r.expect(25, 35, colornames.Blue)
+ })
+}
+
+func TestStrokedPathBevelFlat(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ p := newStrokedPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2.5,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+
+ paint.Fill(o, red)
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(10, 50, colornames.Red)
+ })
+}
+
+func TestStrokedPathBevelRound(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ p := newStrokedPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2.5,
+ Cap: clip.RoundCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+
+ paint.Fill(o, red)
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(10, 50, colornames.Red)
+ })
+}
+
+func TestStrokedPathBevelSquare(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ p := newStrokedPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2.5,
+ Cap: clip.SquareCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+
+ paint.Fill(o, red)
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(10, 50, colornames.Red)
+ })
+}
+
+func TestStrokedPathRoundRound(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ p := newStrokedPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2.5,
+ Cap: clip.RoundCap,
+ Join: clip.RoundJoin,
+ },
+ }.Op().Add(o)
+
+ paint.Fill(o, red)
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(10, 50, colornames.Red)
+ })
+}
+
+func TestStrokedPathFlatMiter(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 10,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ Miter: 5,
+ },
+ }.Op().Add(o)
+ paint.Fill(o, red)
+ stk.Load()
+ }
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+ paint.Fill(o, black)
+ stk.Load()
+ }
+
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(40, 10, colornames.Black)
+ r.expect(40, 12, colornames.Red)
+ })
+}
+
+func TestStrokedPathFlatMiterInf(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 10,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ Miter: float32(math.Inf(+1)),
+ },
+ }.Op().Add(o)
+ paint.Fill(o, red)
+ stk.Load()
+ }
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+ paint.Fill(o, black)
+ stk.Load()
+ }
+
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(40, 10, colornames.Black)
+ r.expect(40, 12, colornames.Red)
+ })
+}
+
+func TestStrokedPathZeroWidth(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(10, 50))
+ p.Line(f32.Pt(50, 0))
+ clip.Stroke{
+ Path: p.End(),
+ Style: clip.StrokeStyle{
+ Width: 2,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+
+ paint.Fill(o, black)
+ stk.Load()
+ }
+
+ {
+ stk := op.Save(o)
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(10, 50))
+ p.Line(f32.Pt(30, 0))
+ clip.Stroke{
+ Path: p.End(),
+ }.Op().Add(o) // width=0, disable stroke
+
+ paint.Fill(o, red)
+ stk.Load()
+ }
+
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(10, 50, colornames.Black)
+ r.expect(30, 50, colornames.Black)
+ r.expect(65, 50, transparent)
+ })
+}
+
+func TestDashedPathFlatCapEllipse(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ p := newEllipsePath(o)
+
+ var dash clip.Dash
+ dash.Begin(o)
+ dash.Dash(5)
+ dash.Dash(3)
+
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 10,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ Miter: float32(math.Inf(+1)),
+ },
+ Dashes: dash.End(),
+ }.Op().Add(o)
+
+ paint.Fill(
+ o,
+ red,
+ )
+ stk.Load()
+ }
+ {
+ stk := op.Save(o)
+ p := newEllipsePath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2,
+ },
+ }.Op().Add(o)
+
+ paint.Fill(
+ o,
+ black,
+ )
+ stk.Load()
+ }
+
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(0, 62, colornames.Red)
+ r.expect(0, 65, colornames.Black)
+ })
+}
+
+func TestDashedPathFlatCapZ(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ var dash clip.Dash
+ dash.Begin(o)
+ dash.Dash(5)
+ dash.Dash(3)
+
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 10,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ Miter: float32(math.Inf(+1)),
+ },
+ Dashes: dash.End(),
+ }.Op().Add(o)
+ paint.Fill(o, red)
+ stk.Load()
+ }
+
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+ paint.Fill(o, black)
+ stk.Load()
+ }
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(40, 10, colornames.Black)
+ r.expect(40, 12, colornames.Red)
+ r.expect(46, 12, transparent)
+ })
+}
+
+func TestDashedPathFlatCapZNoDash(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ var dash clip.Dash
+ dash.Begin(o)
+ dash.Phase(1)
+
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 10,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ Miter: float32(math.Inf(+1)),
+ },
+ Dashes: dash.End(),
+ }.Op().Add(o)
+ paint.Fill(o, red)
+ stk.Load()
+ }
+ {
+ stk := op.Save(o)
+ clip.Stroke{
+ Path: newZigZagPath(o),
+ Style: clip.StrokeStyle{
+ Width: 2,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+ paint.Fill(o, black)
+ stk.Load()
+ }
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(40, 10, colornames.Black)
+ r.expect(40, 12, colornames.Red)
+ r.expect(46, 12, colornames.Red)
+ })
+}
+
+func TestDashedPathFlatCapZNoPath(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ var dash clip.Dash
+ dash.Begin(o)
+ dash.Dash(0)
+ clip.Stroke{
+ Path: newZigZagPath(o),
+ Style: clip.StrokeStyle{
+ Width: 10,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ Miter: float32(math.Inf(+1)),
+ },
+ Dashes: dash.End(),
+ }.Op().Add(o)
+ paint.Fill(o, red)
+ stk.Load()
+ }
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+ paint.Fill(o, black)
+ stk.Load()
+ }
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(40, 10, colornames.Black)
+ r.expect(40, 12, transparent)
+ r.expect(46, 12, transparent)
+ })
+}
+
+func newStrokedPath(o *op.Ops) clip.PathSpec {
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(10, 50))
+ p.Line(f32.Pt(10, 0))
+ p.Arc(f32.Pt(10, 0), f32.Pt(20, 0), math.Pi)
+ p.Line(f32.Pt(10, 0))
+ p.Line(f32.Pt(10, 10))
+ p.Arc(f32.Pt(0, 30), f32.Pt(0, 30), 2*math.Pi)
+ p.Line(f32.Pt(-20, 0))
+ p.Quad(f32.Pt(-10, -10), f32.Pt(-30, 30))
+ return p.End()
+}
+
+func newZigZagPath(o *op.Ops) clip.PathSpec {
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(40, 10))
+ p.Line(f32.Pt(50, 0))
+ p.Line(f32.Pt(-50, 50))
+ p.Line(f32.Pt(50, 0))
+ p.Quad(f32.Pt(-50, 20), f32.Pt(-50, 50))
+ p.Line(f32.Pt(50, 0))
+ return p.End()
+}
+
+func newEllipsePath(o *op.Ops) clip.PathSpec {
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(0, 65))
+ p.Line(f32.Pt(20, 0))
+ p.Arc(f32.Pt(20, 0), f32.Pt(70, 0), 2*math.Pi)
+ return p.End()
+}
diff --git a/gio/giold/gpu/internal/rendertest/doc.go b/gio/giold/gpu/internal/rendertest/doc.go
new file mode 100644
index 0000000..9f6948e
--- /dev/null
+++ b/gio/giold/gpu/internal/rendertest/doc.go
@@ -0,0 +1,2 @@
+// Package rendertest is intended for testing of drawing ops only.
+package rendertest
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestBuildOffscreen.png b/gio/giold/gpu/internal/rendertest/refs/TestBuildOffscreen.png
new file mode 100644
index 0000000..fb50427
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestBuildOffscreen.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png b/gio/giold/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png
new file mode 100644
index 0000000..8ff717b
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestClipOffset.png b/gio/giold/gpu/internal/rendertest/refs/TestClipOffset.png
new file mode 100644
index 0000000..6396fb4
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestClipOffset.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestClipPaintOffset.png b/gio/giold/gpu/internal/rendertest/refs/TestClipPaintOffset.png
new file mode 100644
index 0000000..0fe37e6
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestClipPaintOffset.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestClipRotate.png b/gio/giold/gpu/internal/rendertest/refs/TestClipRotate.png
new file mode 100644
index 0000000..e6c15e3
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestClipRotate.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestClipScale.png b/gio/giold/gpu/internal/rendertest/refs/TestClipScale.png
new file mode 100644
index 0000000..6396fb4
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestClipScale.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestComplicatedTransform.png b/gio/giold/gpu/internal/rendertest/refs/TestComplicatedTransform.png
new file mode 100644
index 0000000..4a92e3c
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestComplicatedTransform.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png
new file mode 100644
index 0000000..79bae38
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png
new file mode 100644
index 0000000..12212e9
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png
new file mode 100644
index 0000000..d315f0f
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png
new file mode 100644
index 0000000..94c160e
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestDeferredPaint.png b/gio/giold/gpu/internal/rendertest/refs/TestDeferredPaint.png
new file mode 100644
index 0000000..b562f12
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestDeferredPaint.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestDepthOverlap.png b/gio/giold/gpu/internal/rendertest/refs/TestDepthOverlap.png
new file mode 100644
index 0000000..9d416b9
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestDepthOverlap.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestLinearGradient.png b/gio/giold/gpu/internal/rendertest/refs/TestLinearGradient.png
new file mode 100644
index 0000000..c3c007c
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestLinearGradient.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestLinearGradientAngled.png b/gio/giold/gpu/internal/rendertest/refs/TestLinearGradientAngled.png
new file mode 100644
index 0000000..3ba0734
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestLinearGradientAngled.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestNegativeOverlaps.png b/gio/giold/gpu/internal/rendertest/refs/TestNegativeOverlaps.png
new file mode 100644
index 0000000..fb50427
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestNegativeOverlaps.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestNoClipFromPaint.png b/gio/giold/gpu/internal/rendertest/refs/TestNoClipFromPaint.png
new file mode 100644
index 0000000..e774064
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestNoClipFromPaint.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png b/gio/giold/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png
new file mode 100644
index 0000000..515a4d2
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestOffsetTexture.png b/gio/giold/gpu/internal/rendertest/refs/TestOffsetTexture.png
new file mode 100644
index 0000000..87386e8
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestOffsetTexture.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintAbsolute.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintAbsolute.png
new file mode 100644
index 0000000..dd09760
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintAbsolute.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintArc.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintArc.png
new file mode 100644
index 0000000..f432914
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintArc.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedBorder.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedBorder.png
new file mode 100644
index 0000000..f8fcfbb
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedBorder.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedCircle.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedCircle.png
new file mode 100644
index 0000000..bdf1fce
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedCircle.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedCirle.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedCirle.png
new file mode 100644
index 0000000..c8cf2f6
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedCirle.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedRect.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedRect.png
new file mode 100644
index 0000000..c1dd7a0
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedRect.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedTexture.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedTexture.png
new file mode 100644
index 0000000..ae0e066
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintClippedTexture.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintOffset.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintOffset.png
new file mode 100644
index 0000000..82394d5
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintOffset.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintRect.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintRect.png
new file mode 100644
index 0000000..f942601
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintRect.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintRotate.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintRotate.png
new file mode 100644
index 0000000..fe15d7d
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintRotate.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintShear.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintShear.png
new file mode 100644
index 0000000..6d1a4c9
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintShear.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestPaintTexture.png b/gio/giold/gpu/internal/rendertest/refs/TestPaintTexture.png
new file mode 100644
index 0000000..9120231
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestPaintTexture.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png b/gio/giold/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png
new file mode 100644
index 0000000..da201dc
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestReuseStencil.png b/gio/giold/gpu/internal/rendertest/refs/TestReuseStencil.png
new file mode 100644
index 0000000..349db1f
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestReuseStencil.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestRotateClipTexture.png b/gio/giold/gpu/internal/rendertest/refs/TestRotateClipTexture.png
new file mode 100644
index 0000000..56c3182
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestRotateClipTexture.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestRotateTexture.png b/gio/giold/gpu/internal/rendertest/refs/TestRotateTexture.png
new file mode 100644
index 0000000..e56c972
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestRotateTexture.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png
new file mode 100644
index 0000000..9d442f5
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png
new file mode 100644
index 0000000..a37235c
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png
new file mode 100644
index 0000000..8d2919d
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png
new file mode 100644
index 0000000..ae6472a
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png
new file mode 100644
index 0000000..d315f0f
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png
new file mode 100644
index 0000000..8ef5a94
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png
new file mode 100644
index 0000000..0fc6fe8
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestTexturedStroke.png b/gio/giold/gpu/internal/rendertest/refs/TestTexturedStroke.png
new file mode 100644
index 0000000..637c932
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestTexturedStroke.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png b/gio/giold/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png
new file mode 100644
index 0000000..637c932
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestTransformMacro.png b/gio/giold/gpu/internal/rendertest/refs/TestTransformMacro.png
new file mode 100644
index 0000000..a9cce29
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestTransformMacro.png differ
diff --git a/gio/giold/gpu/internal/rendertest/refs/TestTransformOrder.png b/gio/giold/gpu/internal/rendertest/refs/TestTransformOrder.png
new file mode 100644
index 0000000..720ca3c
Binary files /dev/null and b/gio/giold/gpu/internal/rendertest/refs/TestTransformOrder.png differ
diff --git a/gio/giold/gpu/internal/rendertest/render_test.go b/gio/giold/gpu/internal/rendertest/render_test.go
new file mode 100644
index 0000000..efa60a6
--- /dev/null
+++ b/gio/giold/gpu/internal/rendertest/render_test.go
@@ -0,0 +1,358 @@
+package rendertest
+
+import (
+ "image"
+ "image/color"
+ "math"
+ "testing"
+
+ "golang.org/x/image/colornames"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+)
+
+func TestTransformMacro(t *testing.T) {
+ // testcase resulting from original bug when rendering layout.Stacked
+
+ // Build clip-path.
+ c := constSqPath()
+
+ run(t, func(o *op.Ops) {
+
+ // render the first Stacked item
+ m1 := op.Record(o)
+ dr := image.Rect(0, 0, 128, 50)
+ paint.FillShape(o, black, clip.Rect(dr).Op())
+ c1 := m1.Stop()
+
+ // Render the second stacked item
+ m2 := op.Record(o)
+ paint.ColorOp{Color: red}.Add(o)
+ // Simulate a draw text call
+ stack := op.Save(o)
+ op.Offset(f32.Pt(0, 10)).Add(o)
+
+ // Apply the clip-path.
+ c.Add(o)
+
+ paint.PaintOp{}.Add(o)
+ stack.Load()
+
+ c2 := m2.Stop()
+
+ // Call each of them in a transform
+ s1 := op.Save(o)
+ op.Offset(f32.Pt(0, 0)).Add(o)
+ c1.Add(o)
+ s1.Load()
+ s2 := op.Save(o)
+ op.Offset(f32.Pt(0, 0)).Add(o)
+ c2.Add(o)
+ s2.Load()
+ }, func(r result) {
+ r.expect(5, 15, colornames.Red)
+ r.expect(15, 15, colornames.Black)
+ r.expect(11, 51, transparent)
+ })
+}
+
+func TestRepeatedPaintsZ(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ // Draw a rectangle
+ paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 128, 50)).Op())
+
+ builder := clip.Path{}
+ builder.Begin(o)
+ builder.Move(f32.Pt(0, 0))
+ builder.Line(f32.Pt(10, 0))
+ builder.Line(f32.Pt(0, 10))
+ builder.Line(f32.Pt(-10, 0))
+ builder.Line(f32.Pt(0, -10))
+ p := builder.End()
+ clip.Outline{
+ Path: p,
+ }.Op().Add(o)
+ paint.Fill(o, red)
+ }, func(r result) {
+ r.expect(5, 5, colornames.Red)
+ r.expect(11, 15, colornames.Black)
+ r.expect(11, 51, transparent)
+ })
+}
+
+func TestNoClipFromPaint(t *testing.T) {
+ // ensure that a paint operation does not pollute the state
+ // by leaving any clip paths in place.
+ run(t, func(o *op.Ops) {
+ a := f32.Affine2D{}.Rotate(f32.Pt(20, 20), math.Pi/4)
+ op.Affine(a).Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(10, 10, 30, 30)).Op())
+ a = f32.Affine2D{}.Rotate(f32.Pt(20, 20), -math.Pi/4)
+ op.Affine(a).Add(o)
+
+ paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
+ }, func(r result) {
+ r.expect(1, 1, colornames.Black)
+ r.expect(20, 20, colornames.Black)
+ r.expect(49, 49, colornames.Black)
+ r.expect(51, 51, transparent)
+ })
+}
+
+func TestDeferredPaint(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ state := op.Save(o)
+ clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o)
+ paint.ColorOp{Color: color.NRGBA{A: 0xff, G: 0xff}}.Add(o)
+ paint.PaintOp{}.Add(o)
+
+ op.Affine(f32.Affine2D{}.Offset(f32.Pt(20, 20))).Add(o)
+ m := op.Record(o)
+ clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o)
+ paint.ColorOp{Color: color.NRGBA{A: 0xff, R: 0xff, G: 0xff}}.Add(o)
+ paint.PaintOp{}.Add(o)
+ paintMacro := m.Stop()
+ op.Defer(o, paintMacro)
+
+ state.Load()
+ op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o)
+ clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o)
+ paint.ColorOp{Color: color.NRGBA{A: 0xff, B: 0xff}}.Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ })
+}
+
+func constSqPath() op.CallOp {
+ innerOps := new(op.Ops)
+ m := op.Record(innerOps)
+ builder := clip.Path{}
+ builder.Begin(innerOps)
+ builder.Move(f32.Pt(0, 0))
+ builder.Line(f32.Pt(10, 0))
+ builder.Line(f32.Pt(0, 10))
+ builder.Line(f32.Pt(-10, 0))
+ builder.Line(f32.Pt(0, -10))
+ p := builder.End()
+ clip.Outline{Path: p}.Op().Add(innerOps)
+ return m.Stop()
+}
+
+func constSqCirc() op.CallOp {
+ innerOps := new(op.Ops)
+ m := op.Record(innerOps)
+ clip.RRect{Rect: f32.Rect(0, 0, 40, 40),
+ NW: 20, NE: 20, SW: 20, SE: 20}.Add(innerOps)
+ return m.Stop()
+}
+
+func drawChild(ops *op.Ops, text op.CallOp) op.CallOp {
+ r1 := op.Record(ops)
+ text.Add(ops)
+ paint.PaintOp{}.Add(ops)
+ return r1.Stop()
+}
+
+func TestReuseStencil(t *testing.T) {
+ txt := constSqPath()
+ run(t, func(ops *op.Ops) {
+ c1 := drawChild(ops, txt)
+ c2 := drawChild(ops, txt)
+
+ // lay out the children
+ stack1 := op.Save(ops)
+ c1.Add(ops)
+ stack1.Load()
+
+ stack2 := op.Save(ops)
+ op.Offset(f32.Pt(0, 50)).Add(ops)
+ c2.Add(ops)
+ stack2.Load()
+ }, func(r result) {
+ r.expect(5, 5, colornames.Black)
+ r.expect(5, 55, colornames.Black)
+ })
+}
+
+func TestBuildOffscreen(t *testing.T) {
+ // Check that something we in one frame build outside the screen
+ // still is rendered correctly if moved into the screen in a later
+ // frame.
+
+ txt := constSqCirc()
+ draw := func(off float32, o *op.Ops) {
+ s := op.Save(o)
+ op.Offset(f32.Pt(0, off)).Add(o)
+ txt.Add(o)
+ paint.PaintOp{}.Add(o)
+ s.Load()
+ }
+
+ multiRun(t,
+ frame(
+ func(ops *op.Ops) {
+ draw(-100, ops)
+ }, func(r result) {
+ r.expect(5, 5, transparent)
+ r.expect(20, 20, transparent)
+ }),
+ frame(
+ func(ops *op.Ops) {
+ draw(0, ops)
+ }, func(r result) {
+ r.expect(2, 2, transparent)
+ r.expect(20, 20, colornames.Black)
+ r.expect(38, 38, transparent)
+ }))
+}
+
+func TestNegativeOverlaps(t *testing.T) {
+ run(t, func(ops *op.Ops) {
+ clip.RRect{Rect: f32.Rect(50, 50, 100, 100)}.Add(ops)
+ clip.Rect(image.Rect(0, 120, 100, 122)).Add(ops)
+ paint.PaintOp{}.Add(ops)
+ }, func(r result) {
+ r.expect(60, 60, transparent)
+ r.expect(60, 110, transparent)
+ r.expect(60, 120, transparent)
+ r.expect(60, 122, transparent)
+ })
+}
+
+func TestDepthOverlap(t *testing.T) {
+ run(t, func(ops *op.Ops) {
+ stack := op.Save(ops)
+ paint.FillShape(ops, red, clip.Rect{Max: image.Pt(128, 64)}.Op())
+ stack.Load()
+
+ stack = op.Save(ops)
+ paint.FillShape(ops, green, clip.Rect{Max: image.Pt(64, 128)}.Op())
+ stack.Load()
+ }, func(r result) {
+ r.expect(96, 32, colornames.Red)
+ r.expect(32, 96, colornames.Green)
+ r.expect(32, 32, colornames.Green)
+ })
+}
+
+type Gradient struct {
+ From, To color.NRGBA
+}
+
+var gradients = []Gradient{
+ {From: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF},
+ To: color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}},
+ {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF},
+ To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}},
+ {From: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF},
+ To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}},
+ {From: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF},
+ To: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}},
+ {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0xFF, A: 0xFF},
+ To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}},
+ {From: color.NRGBA{R: 0xFF, G: 0xFF, B: 0x19, A: 0xFF},
+ To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}},
+}
+
+func TestLinearGradient(t *testing.T) {
+ t.Skip("linear gradients don't support transformations")
+
+ const gradienth = 8
+ // 0.5 offset from ends to ensure that the center of the pixel
+ // aligns with gradient from and to colors.
+ pixelAligned := f32.Rect(0.5, 0, 127.5, gradienth)
+ samples := []int{0, 12, 32, 64, 96, 115, 127}
+
+ run(t, func(ops *op.Ops) {
+ gr := f32.Rect(0, 0, 128, gradienth)
+ for _, g := range gradients {
+ paint.LinearGradientOp{
+ Stop1: f32.Pt(gr.Min.X, gr.Min.Y),
+ Color1: g.From,
+ Stop2: f32.Pt(gr.Max.X, gr.Min.Y),
+ Color2: g.To,
+ }.Add(ops)
+ st := op.Save(ops)
+ clip.RRect{Rect: gr}.Add(ops)
+ op.Affine(f32.Affine2D{}.Offset(pixelAligned.Min)).Add(ops)
+ scale(pixelAligned.Dx()/128, 1).Add(ops)
+ paint.PaintOp{}.Add(ops)
+ st.Load()
+ gr = gr.Add(f32.Pt(0, gradienth))
+ }
+ }, func(r result) {
+ gr := pixelAligned
+ for _, g := range gradients {
+ from := f32color.LinearFromSRGB(g.From)
+ to := f32color.LinearFromSRGB(g.To)
+ for _, p := range samples {
+ exp := lerp(from, to, float32(p)/float32(r.img.Bounds().Dx()-1))
+ r.expect(p, int(gr.Min.Y+gradienth/2),
+ f32color.NRGBAToRGBA(exp.SRGB()))
+ }
+ gr = gr.Add(f32.Pt(0, gradienth))
+ }
+ })
+}
+
+func TestLinearGradientAngled(t *testing.T) {
+ run(t, func(ops *op.Ops) {
+ paint.LinearGradientOp{
+ Stop1: f32.Pt(64, 64),
+ Color1: black,
+ Stop2: f32.Pt(0, 0),
+ Color2: red,
+ }.Add(ops)
+ st := op.Save(ops)
+ clip.Rect(image.Rect(0, 0, 64, 64)).Add(ops)
+ paint.PaintOp{}.Add(ops)
+ st.Load()
+
+ paint.LinearGradientOp{
+ Stop1: f32.Pt(64, 64),
+ Color1: white,
+ Stop2: f32.Pt(128, 0),
+ Color2: green,
+ }.Add(ops)
+ st = op.Save(ops)
+ clip.Rect(image.Rect(64, 0, 128, 64)).Add(ops)
+ paint.PaintOp{}.Add(ops)
+ st.Load()
+
+ paint.LinearGradientOp{
+ Stop1: f32.Pt(64, 64),
+ Color1: black,
+ Stop2: f32.Pt(128, 128),
+ Color2: blue,
+ }.Add(ops)
+ st = op.Save(ops)
+ clip.Rect(image.Rect(64, 64, 128, 128)).Add(ops)
+ paint.PaintOp{}.Add(ops)
+ st.Load()
+
+ paint.LinearGradientOp{
+ Stop1: f32.Pt(64, 64),
+ Color1: white,
+ Stop2: f32.Pt(0, 128),
+ Color2: magenta,
+ }.Add(ops)
+ st = op.Save(ops)
+ clip.Rect(image.Rect(0, 64, 64, 128)).Add(ops)
+ paint.PaintOp{}.Add(ops)
+ st.Load()
+ }, func(r result) {})
+}
+
+// lerp calculates linear interpolation with color b and p.
+func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
+ return f32color.RGBA{
+ R: a.R*(1-p) + b.R*p,
+ G: a.G*(1-p) + b.G*p,
+ B: a.B*(1-p) + b.B*p,
+ A: a.A*(1-p) + b.A*p,
+ }
+}
diff --git a/gio/giold/gpu/internal/rendertest/transform_test.go b/gio/giold/gpu/internal/rendertest/transform_test.go
new file mode 100644
index 0000000..b00aa7e
--- /dev/null
+++ b/gio/giold/gpu/internal/rendertest/transform_test.go
@@ -0,0 +1,204 @@
+package rendertest
+
+import (
+ "image"
+ "math"
+ "testing"
+
+ "golang.org/x/image/colornames"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+)
+
+func TestPaintOffset(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ op.Offset(f32.Pt(10, 20)).Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(59, 30, colornames.Red)
+ r.expect(60, 30, transparent)
+ r.expect(10, 70, transparent)
+ })
+}
+
+func TestPaintRotate(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/8)
+ op.Affine(a).Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(20, 20, 60, 60)).Op())
+ }, func(r result) {
+ r.expect(40, 40, colornames.Red)
+ r.expect(50, 19, colornames.Red)
+ r.expect(59, 19, transparent)
+ r.expect(21, 21, transparent)
+ })
+}
+
+func TestPaintShear(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0)
+ op.Affine(a).Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 40, 40)).Op())
+ }, func(r result) {
+ r.expect(10, 30, transparent)
+ })
+}
+
+func TestClipPaintOffset(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ clip.RRect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o)
+ op.Offset(f32.Pt(20, 20)).Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op())
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(19, 19, transparent)
+ r.expect(20, 20, colornames.Red)
+ r.expect(30, 30, transparent)
+ })
+}
+
+func TestClipOffset(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ op.Offset(f32.Pt(20, 20)).Add(o)
+ clip.RRect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op())
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(29, 29, transparent)
+ r.expect(30, 30, colornames.Red)
+ r.expect(49, 49, colornames.Red)
+ r.expect(50, 50, transparent)
+ })
+}
+
+func TestClipScale(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ a := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 2)).Offset(f32.Pt(10,
+ 10))
+ op.Affine(a).Add(o)
+ clip.RRect{Rect: f32.Rect(10, 10, 20, 20)}.Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 1000, 1000)).Op())
+ }, func(r result) {
+ r.expect(19+10, 19+10, transparent)
+ r.expect(20+10, 20+10, colornames.Red)
+ r.expect(39+10, 39+10, colornames.Red)
+ r.expect(40+10, 40+10, transparent)
+ })
+}
+
+func TestClipRotate(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ op.Affine(f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/4)).Add(o)
+ clip.RRect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 40, 100, 100)).Op())
+ }, func(r result) {
+ r.expect(39, 39, transparent)
+ r.expect(41, 41, colornames.Red)
+ r.expect(50, 50, transparent)
+ })
+}
+
+func TestOffsetTexture(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ op.Offset(f32.Pt(15, 15)).Add(o)
+ squares.Add(o)
+ scale(50.0/512, 50.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(14, 20, transparent)
+ r.expect(66, 20, transparent)
+ r.expect(16, 64, colornames.Green)
+ r.expect(64, 16, colornames.Green)
+ })
+}
+
+func TestOffsetScaleTexture(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ op.Offset(f32.Pt(15, 15)).Add(o)
+ squares.Add(o)
+ op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 1))).Add(o)
+ scale(50.0/512, 50.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(114, 64, colornames.Blue)
+ r.expect(116, 64, transparent)
+ })
+}
+
+func TestRotateTexture(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ defer op.Save(o).Load()
+ squares.Add(o)
+ a := f32.Affine2D{}.Offset(f32.Pt(30, 30)).Rotate(f32.Pt(40, 40),
+ math.Pi/4)
+ op.Affine(a).Add(o)
+ scale(20.0/512, 20.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(40, 40-12, colornames.Blue)
+ r.expect(40+12, 40, colornames.Green)
+ })
+}
+
+func TestRotateClipTexture(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ squares.Add(o)
+ a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), math.Pi/8)
+ op.Affine(a).Add(o)
+ clip.RRect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o)
+ op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o)
+ scale(60.0/512, 60.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(37, 39, colornames.Green)
+ r.expect(36, 39, colornames.Green)
+ r.expect(35, 39, colornames.Green)
+ r.expect(34, 39, colornames.Green)
+ r.expect(33, 39, colornames.Green)
+ })
+}
+
+func TestComplicatedTransform(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ squares.Add(o)
+
+ clip.RRect{Rect: f32.Rect(0, 0, 100, 100), SE: 50, SW: 50, NW: 50,
+ NE: 50}.Add(o)
+
+ a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0)
+ op.Affine(a).Add(o)
+ clip.RRect{Rect: f32.Rect(0, 0, 50, 40)}.Add(o)
+
+ scale(50.0/512, 50.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(20, 5, transparent)
+ })
+}
+
+func TestTransformOrder(t *testing.T) {
+ // check the ordering of operations bot in affine and in gpu stack.
+ run(t, func(o *op.Ops) {
+ a := f32.Affine2D{}.Offset(f32.Pt(64, 64))
+ op.Affine(a).Add(o)
+
+ b := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(8, 8))
+ op.Affine(b).Add(o)
+
+ c := f32.Affine2D{}.Offset(f32.Pt(-10, -10)).Scale(f32.Point{},
+ f32.Pt(0.5, 0.5))
+ op.Affine(c).Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 20, 20)).Op())
+ }, func(r result) {
+ // centered and with radius 40
+ r.expect(64-41, 64, transparent)
+ r.expect(64-39, 64, colornames.Red)
+ r.expect(64+39, 64, colornames.Red)
+ r.expect(64+41, 64, transparent)
+ })
+}
diff --git a/gio/giold/gpu/internal/rendertest/util_test.go b/gio/giold/gpu/internal/rendertest/util_test.go
new file mode 100644
index 0000000..74c6f5f
--- /dev/null
+++ b/gio/giold/gpu/internal/rendertest/util_test.go
@@ -0,0 +1,290 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package rendertest
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "image"
+ "image/color"
+ "image/draw"
+ "image/png"
+ "io/ioutil"
+ "path/filepath"
+ "strconv"
+ "testing"
+
+ "golang.org/x/image/colornames"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/gpu/headless"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/paint"
+)
+
+var (
+ dumpImages = flag.Bool("saveimages", false, "save test images")
+ squares paint.ImageOp
+ smallSquares paint.ImageOp
+)
+
+var (
+ red = f32color.RGBAToNRGBA(colornames.Red)
+ green = f32color.RGBAToNRGBA(colornames.Green)
+ blue = f32color.RGBAToNRGBA(colornames.Blue)
+ magenta = f32color.RGBAToNRGBA(colornames.Magenta)
+ black = f32color.RGBAToNRGBA(colornames.Black)
+ white = f32color.RGBAToNRGBA(colornames.White)
+ transparent = color.RGBA{}
+)
+
+func init() {
+ squares = buildSquares(512)
+ smallSquares = buildSquares(50)
+}
+
+func buildSquares(size int) paint.ImageOp {
+ sub := size / 4
+ im := image.NewNRGBA(image.Rect(0, 0, size, size))
+ c1, c2 := image.NewUniform(colornames.Green), image.NewUniform(colornames.Blue)
+ for r := 0; r < 4; r++ {
+ for c := 0; c < 4; c++ {
+ c1, c2 = c2, c1
+ draw.Draw(im, image.Rect(r*sub, c*sub, r*sub+sub, c*sub+sub), c1,
+ image.Point{}, draw.Over)
+ }
+ c1, c2 = c2, c1
+ }
+ return paint.NewImageOp(im)
+}
+
+func drawImage(t *testing.T, size int, ops *op.Ops,
+ draw func(o *op.Ops)) (im *image.RGBA, err error) {
+ sz := image.Point{X: size, Y: size}
+ w := newWindow(t, sz.X, sz.Y)
+ draw(ops)
+ if err := w.Frame(ops); err != nil {
+ return nil, err
+ }
+ return w.Screenshot()
+}
+
+func run(t *testing.T, f func(o *op.Ops), c func(r result)) {
+ // draw a few times and check that it is correct each time, to
+ // ensure any caching effects still generate the correct images.
+ var img *image.RGBA
+ var err error
+ ops := new(op.Ops)
+ for i := 0; i < 3; i++ {
+ ops.Reset()
+ img, err = drawImage(t, 128, ops, f)
+ if err != nil {
+ t.Error("error rendering:", err)
+ return
+ }
+ // check for a reference image and make sure we are identical.
+ if !verifyRef(t, img, 0) {
+ name := fmt.Sprintf("%s-%d-bad.png", t.Name(), i)
+ if err := saveImage(name, img); err != nil {
+ t.Error(err)
+ }
+ }
+ c(result{t: t, img: img})
+ }
+
+ if *dumpImages {
+ if err := saveImage(t.Name()+".png", img); err != nil {
+ t.Error(err)
+ }
+ }
+}
+
+func frame(f func(o *op.Ops), c func(r result)) frameT {
+ return frameT{f: f, c: c}
+}
+
+type frameT struct {
+ f func(o *op.Ops)
+ c func(r result)
+}
+
+// multiRun is used to run test cases over multiple frames, typically
+// to test caching interactions.
+func multiRun(t *testing.T, frames ...frameT) {
+ // draw a few times and check that it is correct each time, to
+ // ensure any caching effects still generate the correct images.
+ var img *image.RGBA
+ var err error
+ sz := image.Point{X: 128, Y: 128}
+ w := newWindow(t, sz.X, sz.Y)
+ ops := new(op.Ops)
+ for i := range frames {
+ ops.Reset()
+ frames[i].f(ops)
+ if err := w.Frame(ops); err != nil {
+ t.Errorf("rendering failed: %v", err)
+ continue
+ }
+ img, err = w.Screenshot()
+ if err != nil {
+ t.Errorf("screenshot failed: %v", err)
+ continue
+ }
+ // Check for a reference image and make sure they are identical.
+ ok := verifyRef(t, img, i)
+ if frames[i].c != nil {
+ frames[i].c(result{t: t, img: img})
+ }
+ if *dumpImages || !ok {
+ name := t.Name() + ".png"
+ if i != 0 {
+ name = t.Name() + "_" + strconv.Itoa(i) + ".png"
+ }
+ if err := saveImage(name, img); err != nil {
+ t.Error(err)
+ }
+ }
+ }
+
+}
+
+func verifyRef(t *testing.T, img *image.RGBA, frame int) (ok bool) {
+ // ensure identical to ref data
+ path := filepath.Join("refs", t.Name()+".png")
+ if frame != 0 {
+ path = filepath.Join("refs", t.Name()+"_"+strconv.Itoa(frame)+".png")
+ }
+ b, err := ioutil.ReadFile(path)
+ if err != nil {
+ t.Error("could not open ref:", err)
+ return
+ }
+ r, err := png.Decode(bytes.NewReader(b))
+ if err != nil {
+ t.Error("could not decode ref:", err)
+ return
+ }
+ if img.Bounds() != r.Bounds() {
+ t.Errorf("reference image is %v, expected %v", r.Bounds(), img.Bounds())
+ return false
+ }
+ var ref *image.RGBA
+ switch r := r.(type) {
+ case *image.RGBA:
+ ref = r
+ case *image.NRGBA:
+ ref = image.NewRGBA(r.Bounds())
+ bnd := r.Bounds()
+ for x := bnd.Min.X; x < bnd.Max.X; x++ {
+ for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
+ ref.SetRGBA(x, y, f32color.NRGBAToRGBA(r.NRGBAAt(x, y)))
+ }
+ }
+ default:
+ t.Fatalf("reference image is a %T, expected *image.NRGBA or *image.RGBA",
+ r)
+ }
+ bnd := img.Bounds()
+ for x := bnd.Min.X; x < bnd.Max.X; x++ {
+ for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
+ exp := ref.RGBAAt(x, y)
+ got := img.RGBAAt(x, y)
+ if !colorsClose(exp, got) {
+ t.Error("not equal to ref at", x, y, " ", got, exp)
+ return false
+ }
+ }
+ }
+ return true
+}
+
+func colorsClose(c1, c2 color.RGBA) bool {
+ const delta = 0.01 // magic value obtained from experimentation.
+ return yiqEqApprox(c1, c2, delta)
+}
+
+// yiqEqApprox compares the colors of 2 pixels, in the NTSC YIQ color space,
+// as described in:
+//
+// Measuring perceived color difference using YIQ NTSC
+// transmission color space in mobile applications.
+// Yuriy Kotsarenko, Fernando Ramos.
+//
+// An electronic version is available at:
+//
+// - http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf
+func yiqEqApprox(c1, c2 color.RGBA, d2 float64) bool {
+ const max = 35215.0 // difference between 2 maximally different pixels.
+
+ var (
+ r1 = float64(c1.R)
+ g1 = float64(c1.G)
+ b1 = float64(c1.B)
+
+ r2 = float64(c2.R)
+ g2 = float64(c2.G)
+ b2 = float64(c2.B)
+
+ y1 = r1*0.29889531 + g1*0.58662247 + b1*0.11448223
+ i1 = r1*0.59597799 - g1*0.27417610 - b1*0.32180189
+ q1 = r1*0.21147017 - g1*0.52261711 + b1*0.31114694
+
+ y2 = r2*0.29889531 + g2*0.58662247 + b2*0.11448223
+ i2 = r2*0.59597799 - g2*0.27417610 - b2*0.32180189
+ q2 = r2*0.21147017 - g2*0.52261711 + b2*0.31114694
+
+ y = y1 - y2
+ i = i1 - i2
+ q = q1 - q2
+
+ diff = 0.5053*y*y + 0.299*i*i + 0.1957*q*q
+ )
+ return diff <= max*d2
+}
+
+func (r result) expect(x, y int, col color.RGBA) {
+ r.t.Helper()
+ if r.img == nil {
+ return
+ }
+ c := r.img.RGBAAt(x, y)
+ if !colorsClose(c, col) {
+ r.t.Error("expected ", col, " at ", "(", x, ",", y, ") but got ", c)
+ }
+}
+
+type result struct {
+ t *testing.T
+ img *image.RGBA
+}
+
+func saveImage(file string, img *image.RGBA) error {
+ // Only NRGBA images are losslessly encoded by png.Encode.
+ nrgba := image.NewNRGBA(img.Bounds())
+ bnd := img.Bounds()
+ for x := bnd.Min.X; x < bnd.Max.X; x++ {
+ for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
+ nrgba.SetNRGBA(x, y, f32color.RGBAToNRGBA(img.RGBAAt(x, y)))
+ }
+ }
+ var buf bytes.Buffer
+ if err := png.Encode(&buf, nrgba); err != nil {
+ return err
+ }
+ return ioutil.WriteFile(file, buf.Bytes(), 0666)
+}
+
+func newWindow(t testing.TB, width, height int) *headless.Window {
+ w, err := headless.NewWindow(width, height)
+ if err != nil {
+ t.Skipf("failed to create headless window, skipping: %v", err)
+ }
+ t.Cleanup(w.Release)
+ return w
+}
+
+func scale(sx, sy float32) op.TransformOp {
+ return op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(sx, sy)))
+}
diff --git a/gio/giold/gpu/pack.go b/gio/giold/gpu/pack.go
new file mode 100644
index 0000000..c4dbaad
--- /dev/null
+++ b/gio/giold/gpu/pack.go
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+import (
+ "image"
+)
+
+// packer packs a set of many smaller rectangles into
+// much fewer larger atlases.
+type packer struct {
+ maxDim int
+ spaces []image.Rectangle
+
+ sizes []image.Point
+ pos image.Point
+}
+
+type placement struct {
+ Idx int
+ Pos image.Point
+}
+
+// add adds the given rectangle to the atlases and
+// return the allocated position.
+func (p *packer) add(s image.Point) (placement, bool) {
+ if place, ok := p.tryAdd(s); ok {
+ return place, true
+ }
+ p.newPage()
+ return p.tryAdd(s)
+}
+
+func (p *packer) clear() {
+ p.sizes = p.sizes[:0]
+ p.spaces = p.spaces[:0]
+}
+
+func (p *packer) newPage() {
+ p.pos = image.Point{}
+ p.sizes = append(p.sizes, image.Point{})
+ p.spaces = p.spaces[:0]
+ p.spaces = append(p.spaces, image.Rectangle{
+ Max: image.Point{X: p.maxDim, Y: p.maxDim},
+ })
+}
+
+func (p *packer) tryAdd(s image.Point) (placement, bool) {
+ // Go backwards to prioritize smaller spaces first.
+ for i := len(p.spaces) - 1; i >= 0; i-- {
+ space := p.spaces[i]
+ rightSpace := space.Dx() - s.X
+ bottomSpace := space.Dy() - s.Y
+ if rightSpace >= 0 && bottomSpace >= 0 {
+ // Remove space.
+ p.spaces[i] = p.spaces[len(p.spaces)-1]
+ p.spaces = p.spaces[:len(p.spaces)-1]
+ // Put s in the top left corner and add the (at most)
+ // two smaller spaces.
+ pos := space.Min
+ if bottomSpace > 0 {
+ p.spaces = append(p.spaces, image.Rectangle{
+ Min: image.Point{X: pos.X, Y: pos.Y + s.Y},
+ Max: image.Point{X: space.Max.X, Y: space.Max.Y},
+ })
+ }
+ if rightSpace > 0 {
+ p.spaces = append(p.spaces, image.Rectangle{
+ Min: image.Point{X: pos.X + s.X, Y: pos.Y},
+ Max: image.Point{X: space.Max.X, Y: pos.Y + s.Y},
+ })
+ }
+ idx := len(p.sizes) - 1
+ size := &p.sizes[idx]
+ if x := pos.X + s.X; x > size.X {
+ size.X = x
+ }
+ if y := pos.Y + s.Y; y > size.Y {
+ size.Y = y
+ }
+ return placement{Idx: idx, Pos: pos}, true
+ }
+ }
+ return placement{}, false
+}
diff --git a/gio/giold/gpu/path.go b/gio/giold/gpu/path.go
new file mode 100644
index 0000000..4670f03
--- /dev/null
+++ b/gio/giold/gpu/path.go
@@ -0,0 +1,438 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+// GPU accelerated path drawing using the algorithms from
+// Pathfinder (https://github.com/servo/pathfinder).
+
+import (
+ "encoding/binary"
+ "image"
+ "math"
+ "unsafe"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/internal/byteslice"
+ "realy.lol/gio/internal/f32color"
+)
+
+type pather struct {
+ ctx driver.Device
+
+ viewport image.Point
+
+ stenciler *stenciler
+ coverer *coverer
+}
+
+type coverer struct {
+ ctx driver.Device
+ prog [3]*program
+ texUniforms *coverTexUniforms
+ colUniforms *coverColUniforms
+ linearGradientUniforms *coverLinearGradientUniforms
+ layout driver.InputLayout
+}
+
+type coverTexUniforms struct {
+ vert struct {
+ coverUniforms
+ _ [12]byte // Padding to multiple of 16.
+ }
+}
+
+type coverColUniforms struct {
+ vert struct {
+ coverUniforms
+ _ [12]byte // Padding to multiple of 16.
+ }
+ frag struct {
+ colorUniforms
+ }
+}
+
+type coverLinearGradientUniforms struct {
+ vert struct {
+ coverUniforms
+ _ [12]byte // Padding to multiple of 16.
+ }
+ frag struct {
+ gradientUniforms
+ }
+}
+
+type coverUniforms struct {
+ transform [4]float32
+ uvCoverTransform [4]float32
+ uvTransformR1 [4]float32
+ uvTransformR2 [4]float32
+ z float32
+}
+
+type stenciler struct {
+ ctx driver.Device
+ prog struct {
+ prog *program
+ uniforms *stencilUniforms
+ layout driver.InputLayout
+ }
+ iprog struct {
+ prog *program
+ uniforms *intersectUniforms
+ layout driver.InputLayout
+ }
+ fbos fboSet
+ intersections fboSet
+ indexBuf driver.Buffer
+}
+
+type stencilUniforms struct {
+ vert struct {
+ transform [4]float32
+ pathOffset [2]float32
+ _ [8]byte // Padding to multiple of 16.
+ }
+}
+
+type intersectUniforms struct {
+ vert struct {
+ uvTransform [4]float32
+ subUVTransform [4]float32
+ }
+}
+
+type fboSet struct {
+ fbos []stencilFBO
+}
+
+type stencilFBO struct {
+ size image.Point
+ fbo driver.Framebuffer
+ tex driver.Texture
+}
+
+type pathData struct {
+ ncurves int
+ data driver.Buffer
+}
+
+// vertex data suitable for passing to vertex programs.
+type vertex struct {
+ // Corner encodes the corner: +0.5 for south, +.25 for east.
+ Corner float32
+ MaxY float32
+ FromX, FromY float32
+ CtrlX, CtrlY float32
+ ToX, ToY float32
+}
+
+func (v vertex) encode(d []byte, maxy uint32) {
+ bo := binary.LittleEndian
+ bo.PutUint32(d[0:], math.Float32bits(v.Corner))
+ bo.PutUint32(d[4:], maxy)
+ bo.PutUint32(d[8:], math.Float32bits(v.FromX))
+ bo.PutUint32(d[12:], math.Float32bits(v.FromY))
+ bo.PutUint32(d[16:], math.Float32bits(v.CtrlX))
+ bo.PutUint32(d[20:], math.Float32bits(v.CtrlY))
+ bo.PutUint32(d[24:], math.Float32bits(v.ToX))
+ bo.PutUint32(d[28:], math.Float32bits(v.ToY))
+}
+
+const (
+ // Number of path quads per draw batch.
+ pathBatchSize = 10000
+ // Size of a vertex as sent to gpu
+ vertStride = 8 * 4
+)
+
+func newPather(ctx driver.Device) *pather {
+ return &pather{
+ ctx: ctx,
+ stenciler: newStenciler(ctx),
+ coverer: newCoverer(ctx),
+ }
+}
+
+func newCoverer(ctx driver.Device) *coverer {
+ c := &coverer{
+ ctx: ctx,
+ }
+ c.colUniforms = new(coverColUniforms)
+ c.texUniforms = new(coverTexUniforms)
+ c.linearGradientUniforms = new(coverLinearGradientUniforms)
+ prog, layout, err := createColorPrograms(ctx, shader_cover_vert,
+ shader_cover_frag,
+ [3]interface{}{&c.colUniforms.vert, &c.linearGradientUniforms.vert,
+ &c.texUniforms.vert},
+ [3]interface{}{&c.colUniforms.frag, &c.linearGradientUniforms.frag,
+ nil},
+ )
+ if err != nil {
+ panic(err)
+ }
+ c.prog = prog
+ c.layout = layout
+ return c
+}
+
+func newStenciler(ctx driver.Device) *stenciler {
+ // Allocate a suitably large index buffer for drawing paths.
+ indices := make([]uint16, pathBatchSize*6)
+ for i := 0; i < pathBatchSize; i++ {
+ i := uint16(i)
+ indices[i*6+0] = i*4 + 0
+ indices[i*6+1] = i*4 + 1
+ indices[i*6+2] = i*4 + 2
+ indices[i*6+3] = i*4 + 2
+ indices[i*6+4] = i*4 + 1
+ indices[i*6+5] = i*4 + 3
+ }
+ indexBuf, err := ctx.NewImmutableBuffer(driver.BufferBindingIndices,
+ byteslice.Slice(indices))
+ if err != nil {
+ panic(err)
+ }
+ progLayout, err := ctx.NewInputLayout(shader_stencil_vert,
+ []driver.InputDesc{
+ {Type: driver.DataTypeFloat, Size: 1,
+ Offset: int(unsafe.Offsetof((*(*vertex)(nil)).Corner))},
+ {Type: driver.DataTypeFloat, Size: 1,
+ Offset: int(unsafe.Offsetof((*(*vertex)(nil)).MaxY))},
+ {Type: driver.DataTypeFloat, Size: 2,
+ Offset: int(unsafe.Offsetof((*(*vertex)(nil)).FromX))},
+ {Type: driver.DataTypeFloat, Size: 2,
+ Offset: int(unsafe.Offsetof((*(*vertex)(nil)).CtrlX))},
+ {Type: driver.DataTypeFloat, Size: 2,
+ Offset: int(unsafe.Offsetof((*(*vertex)(nil)).ToX))},
+ })
+ if err != nil {
+ panic(err)
+ }
+ iprogLayout, err := ctx.NewInputLayout(shader_intersect_vert,
+ []driver.InputDesc{
+ {Type: driver.DataTypeFloat, Size: 2, Offset: 0},
+ {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2},
+ })
+ if err != nil {
+ panic(err)
+ }
+ st := &stenciler{
+ ctx: ctx,
+ indexBuf: indexBuf,
+ }
+ prog, err := ctx.NewProgram(shader_stencil_vert, shader_stencil_frag)
+ if err != nil {
+ panic(err)
+ }
+ st.prog.uniforms = new(stencilUniforms)
+ vertUniforms := newUniformBuffer(ctx, &st.prog.uniforms.vert)
+ st.prog.prog = newProgram(prog, vertUniforms, nil)
+ st.prog.layout = progLayout
+ iprog, err := ctx.NewProgram(shader_intersect_vert, shader_intersect_frag)
+ if err != nil {
+ panic(err)
+ }
+ st.iprog.uniforms = new(intersectUniforms)
+ vertUniforms = newUniformBuffer(ctx, &st.iprog.uniforms.vert)
+ st.iprog.prog = newProgram(iprog, vertUniforms, nil)
+ st.iprog.layout = iprogLayout
+ return st
+}
+
+func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) {
+ // Add fbos.
+ for i := len(s.fbos); i < len(sizes); i++ {
+ s.fbos = append(s.fbos, stencilFBO{})
+ }
+ // Resize fbos.
+ for i, sz := range sizes {
+ f := &s.fbos[i]
+ // Resizing or recreating FBOs can introduce rendering stalls.
+ // Avoid if the space waste is not too high.
+ resize := sz.X > f.size.X || sz.Y > f.size.Y
+ waste := float32(sz.X*sz.Y) / float32(f.size.X*f.size.Y)
+ resize = resize || waste > 1.2
+ if resize {
+ if f.fbo != nil {
+ f.fbo.Release()
+ f.tex.Release()
+ }
+ tex, err := ctx.NewTexture(driver.TextureFormatFloat, sz.X, sz.Y,
+ driver.FilterNearest, driver.FilterNearest,
+ driver.BufferBindingTexture|driver.BufferBindingFramebuffer)
+ if err != nil {
+ panic(err)
+ }
+ fbo, err := ctx.NewFramebuffer(tex, 0)
+ if err != nil {
+ panic(err)
+ }
+ f.size = sz
+ f.tex = tex
+ f.fbo = fbo
+ }
+ }
+ // Delete extra fbos.
+ s.delete(ctx, len(sizes))
+}
+
+func (s *fboSet) invalidate(ctx driver.Device) {
+ for _, f := range s.fbos {
+ f.fbo.Invalidate()
+ }
+}
+
+func (s *fboSet) delete(ctx driver.Device, idx int) {
+ for i := idx; i < len(s.fbos); i++ {
+ f := s.fbos[i]
+ f.fbo.Release()
+ f.tex.Release()
+ }
+ s.fbos = s.fbos[:idx]
+}
+
+func (s *stenciler) release() {
+ s.fbos.delete(s.ctx, 0)
+ s.prog.layout.Release()
+ s.prog.prog.Release()
+ s.iprog.layout.Release()
+ s.iprog.prog.Release()
+ s.indexBuf.Release()
+}
+
+func (p *pather) release() {
+ p.stenciler.release()
+ p.coverer.release()
+}
+
+func (c *coverer) release() {
+ for _, p := range c.prog {
+ p.Release()
+ }
+ c.layout.Release()
+}
+
+func buildPath(ctx driver.Device, p []byte) pathData {
+ buf, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices, p)
+ if err != nil {
+ panic(err)
+ }
+ return pathData{
+ ncurves: len(p) / vertStride,
+ data: buf,
+ }
+}
+
+func (p pathData) release() {
+ p.data.Release()
+}
+
+func (p *pather) begin(sizes []image.Point) {
+ p.stenciler.begin(sizes)
+}
+
+func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point,
+ uv image.Point, data pathData) {
+ p.stenciler.stencilPath(bounds, offset, uv, data)
+}
+
+func (s *stenciler) beginIntersect(sizes []image.Point) {
+ s.ctx.BlendFunc(driver.BlendFactorDstColor, driver.BlendFactorZero)
+ // 8 bit coverage is enough, but OpenGL ES only supports single channel
+ // floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if
+ // no floating point support is available.
+ s.intersections.resize(s.ctx, sizes)
+ s.ctx.BindProgram(s.iprog.prog.prog)
+}
+
+func (s *stenciler) invalidateFBO() {
+ s.intersections.invalidate(s.ctx)
+ s.fbos.invalidate(s.ctx)
+}
+
+func (s *stenciler) cover(idx int) stencilFBO {
+ return s.fbos.fbos[idx]
+}
+
+func (s *stenciler) begin(sizes []image.Point) {
+ s.ctx.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOne)
+ s.fbos.resize(s.ctx, sizes)
+ s.ctx.BindProgram(s.prog.prog.prog)
+ s.ctx.BindInputLayout(s.prog.layout)
+ s.ctx.BindIndexBuffer(s.indexBuf)
+}
+
+func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point,
+ uv image.Point, data pathData) {
+ s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy())
+ // Transform UI coordinates to OpenGL coordinates.
+ texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())}
+ scale := f32.Point{X: 2 / texSize.X, Y: 2 / texSize.Y}
+ orig := f32.Point{X: -1 - float32(bounds.Min.X)*2/texSize.X,
+ Y: -1 - float32(bounds.Min.Y)*2/texSize.Y}
+ s.prog.uniforms.vert.transform = [4]float32{scale.X, scale.Y, orig.X,
+ orig.Y}
+ s.prog.uniforms.vert.pathOffset = [2]float32{offset.X, offset.Y}
+ s.prog.prog.UploadUniforms()
+ // Draw in batches that fit in uint16 indices.
+ start := 0
+ nquads := data.ncurves / 4
+ for start < nquads {
+ batch := nquads - start
+ if max := pathBatchSize; batch > max {
+ batch = max
+ }
+ off := vertStride * start * 4
+ s.ctx.BindVertexBuffer(data.data, vertStride, off)
+ s.ctx.DrawElements(driver.DrawModeTriangles, 0, batch*6)
+ start += batch
+ }
+}
+
+func (p *pather) cover(z float32, mat materialType, col f32color.RGBA,
+ col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D,
+ coverScale, coverOff f32.Point) {
+ p.coverer.cover(z, mat, col, col1, col2, scale, off, uvTrans, coverScale,
+ coverOff)
+}
+
+func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA,
+ col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D,
+ coverScale, coverOff f32.Point) {
+ p := c.prog[mat]
+ c.ctx.BindProgram(p.prog)
+ var uniforms *coverUniforms
+ switch mat {
+ case materialColor:
+ c.colUniforms.frag.color = col
+ uniforms = &c.colUniforms.vert.coverUniforms
+ case materialLinearGradient:
+ c.linearGradientUniforms.frag.color1 = col1
+ c.linearGradientUniforms.frag.color2 = col2
+
+ t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
+ c.linearGradientUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0}
+ c.linearGradientUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0}
+ uniforms = &c.linearGradientUniforms.vert.coverUniforms
+ case materialTexture:
+ t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
+ c.texUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0}
+ c.texUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0}
+ uniforms = &c.texUniforms.vert.coverUniforms
+ }
+ uniforms.z = z
+ uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
+ uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y,
+ coverOff.X, coverOff.Y}
+ p.UploadUniforms()
+ c.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4)
+}
+
+func init() {
+ // Check that struct vertex has the expected size and
+ // that it contains no padding.
+ if unsafe.Sizeof(*(*vertex)(nil)) != vertStride {
+ panic("unexpected struct size")
+ }
+}
diff --git a/gio/giold/gpu/shaders.go b/gio/giold/gpu/shaders.go
new file mode 100644
index 0000000..7df7cb5
--- /dev/null
+++ b/gio/giold/gpu/shaders.go
@@ -0,0 +1,6694 @@
+// Code generated by build.go. DO NOT EDIT.
+
+package gpu
+
+import "realy.lol/gio/gpu/internal/driver"
+
+var (
+ shader_backdrop_comp = driver.ShaderSources{
+ Name: "backdrop.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct AnnotatedRef
+{
+ uint offset;
+};
+
+struct AnnotatedTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct PathRef
+{
+ uint offset;
+};
+
+struct TileRef
+{
+ uint offset;
+};
+
+struct Path
+{
+ uvec4 bbox;
+ TileRef tiles;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _77;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _191;
+
+shared uint sh_row_width[128];
+shared Alloc sh_row_alloc[128];
+shared uint sh_row_count[128];
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+uint read_mem(Alloc alloc, uint offset)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return 0u;
+ }
+ uint v = _77.memory[offset];
+ return v;
+}
+
+AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint tag_and_flags = read_mem(param, param_1);
+ return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+uint fill_mode_from_flags(uint flags)
+{
+ return flags & 1u;
+}
+
+Path Path_read(Alloc a, PathRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Path s;
+ s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16));
+ s.tiles = TileRef(raw2);
+ return s;
+}
+
+Alloc new_alloc(uint offset, uint size)
+{
+ Alloc a;
+ a.offset = offset;
+ return a;
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _77.memory[offset] = val;
+}
+
+void main()
+{
+ if (_77.mem_error != 0u)
+ {
+ return;
+ }
+ uint th_ix = gl_LocalInvocationID.x;
+ uint element_ix = gl_GlobalInvocationID.x;
+ AnnotatedRef ref = AnnotatedRef(_191.conf.anno_alloc.offset + (element_ix * 32u));
+ uint row_count = 0u;
+ if (element_ix < _191.conf.n_elements)
+ {
+ Alloc param;
+ param.offset = _191.conf.anno_alloc.offset;
+ AnnotatedRef param_1 = ref;
+ AnnotatedTag tag = Annotated_tag(param, param_1);
+ switch (tag.tag)
+ {
+ case 2u:
+ case 3u:
+ case 1u:
+ {
+ uint param_2 = tag.flags;
+ if (fill_mode_from_flags(param_2) != 0u)
+ {
+ break;
+ }
+ PathRef path_ref = PathRef(_191.conf.tile_alloc.offset + (element_ix * 12u));
+ Alloc param_3;
+ param_3.offset = _191.conf.tile_alloc.offset;
+ PathRef param_4 = path_ref;
+ Path path = Path_read(param_3, param_4);
+ sh_row_width[th_ix] = path.bbox.z - path.bbox.x;
+ row_count = path.bbox.w - path.bbox.y;
+ bool _267 = row_count == 1u;
+ bool _273;
+ if (_267)
+ {
+ _273 = path.bbox.y > 0u;
+ }
+ else
+ {
+ _273 = _267;
+ }
+ if (_273)
+ {
+ row_count = 0u;
+ }
+ uint param_5 = path.tiles.offset;
+ uint param_6 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u;
+ Alloc path_alloc = new_alloc(param_5, param_6);
+ sh_row_alloc[th_ix] = path_alloc;
+ break;
+ }
+ }
+ }
+ sh_row_count[th_ix] = row_count;
+ for (uint i = 0u; i < 7u; i++)
+ {
+ barrier();
+ if (th_ix >= uint(1 << int(i)))
+ {
+ row_count += sh_row_count[th_ix - uint(1 << int(i))];
+ }
+ barrier();
+ sh_row_count[th_ix] = row_count;
+ }
+ barrier();
+ uint total_rows = sh_row_count[127];
+ uint _395;
+ for (uint row = th_ix; row < total_rows; row += 128u)
+ {
+ uint el_ix = 0u;
+ for (uint i_1 = 0u; i_1 < 7u; i_1++)
+ {
+ uint probe = el_ix + uint(64 >> int(i_1));
+ if (row >= sh_row_count[probe - 1u])
+ {
+ el_ix = probe;
+ }
+ }
+ uint width = sh_row_width[el_ix];
+ if (width > 0u)
+ {
+ Alloc tiles_alloc = sh_row_alloc[el_ix];
+ if (el_ix > 0u)
+ {
+ _395 = sh_row_count[el_ix - 1u];
+ }
+ else
+ {
+ _395 = 0u;
+ }
+ uint seq_ix = row - _395;
+ uint tile_el_ix = ((tiles_alloc.offset >> uint(2)) + 1u) + ((seq_ix * 2u) * width);
+ Alloc param_7 = tiles_alloc;
+ uint param_8 = tile_el_ix;
+ uint sum = read_mem(param_7, param_8);
+ for (uint x = 1u; x < width; x++)
+ {
+ tile_el_ix += 2u;
+ Alloc param_9 = tiles_alloc;
+ uint param_10 = tile_el_ix;
+ sum += read_mem(param_9, param_10);
+ Alloc param_11 = tiles_alloc;
+ uint param_12 = tile_el_ix;
+ uint param_13 = sum;
+ write_mem(param_11, param_12, param_13);
+ }
+ }
+ }
+}
+
+`,
+ }
+ shader_binning_comp = driver.ShaderSources{
+ Name: "binning.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct MallocResult
+{
+ Alloc alloc;
+ bool failed;
+};
+
+struct AnnoEndClipRef
+{
+ uint offset;
+};
+
+struct AnnoEndClip
+{
+ vec4 bbox;
+};
+
+struct AnnotatedRef
+{
+ uint offset;
+};
+
+struct AnnotatedTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct BinInstanceRef
+{
+ uint offset;
+};
+
+struct BinInstance
+{
+ uint element_ix;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _88;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _254;
+
+shared uint bitmaps[4][128];
+shared bool sh_alloc_failed;
+shared uint count[4][128];
+shared Alloc sh_chunk_alloc[128];
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+uint read_mem(Alloc alloc, uint offset)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return 0u;
+ }
+ uint v = _88.memory[offset];
+ return v;
+}
+
+AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint tag_and_flags = read_mem(param, param_1);
+ return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+AnnoEndClip AnnoEndClip_read(Alloc a, AnnoEndClipRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ AnnoEndClip s;
+ s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ return s;
+}
+
+AnnoEndClip Annotated_EndClip_read(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ AnnoEndClipRef param_1 = AnnoEndClipRef(ref.offset + 4u);
+ return AnnoEndClip_read(param, param_1);
+}
+
+Alloc new_alloc(uint offset, uint size)
+{
+ Alloc a;
+ a.offset = offset;
+ return a;
+}
+
+MallocResult malloc(uint size)
+{
+ MallocResult r;
+ r.failed = false;
+ uint _94 = atomicAdd(_88.mem_offset, size);
+ uint offset = _94;
+ uint param = offset;
+ uint param_1 = size;
+ r.alloc = new_alloc(param, param_1);
+ if ((offset + size) > uint(int(uint(_88.memory.length())) * 4))
+ {
+ r.failed = true;
+ uint _115 = atomicMax(_88.mem_error, 1u);
+ return r;
+ }
+ return r;
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _88.memory[offset] = val;
+}
+
+void BinInstance_write(Alloc a, BinInstanceRef ref, BinInstance s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.element_ix;
+ write_mem(param, param_1, param_2);
+}
+
+void main()
+{
+ if (_88.mem_error != 0u)
+ {
+ return;
+ }
+ uint my_n_elements = _254.conf.n_elements;
+ uint my_partition = gl_WorkGroupID.x;
+ for (uint i = 0u; i < 4u; i++)
+ {
+ bitmaps[i][gl_LocalInvocationID.x] = 0u;
+ }
+ if (gl_LocalInvocationID.x == 0u)
+ {
+ sh_alloc_failed = false;
+ }
+ barrier();
+ uint element_ix = (my_partition * 128u) + gl_LocalInvocationID.x;
+ AnnotatedRef ref = AnnotatedRef(_254.conf.anno_alloc.offset + (element_ix * 32u));
+ uint tag = 0u;
+ if (element_ix < my_n_elements)
+ {
+ Alloc param;
+ param.offset = _254.conf.anno_alloc.offset;
+ AnnotatedRef param_1 = ref;
+ tag = Annotated_tag(param, param_1).tag;
+ }
+ int x0 = 0;
+ int y0 = 0;
+ int x1 = 0;
+ int y1 = 0;
+ switch (tag)
+ {
+ case 1u:
+ case 2u:
+ case 3u:
+ case 4u:
+ {
+ Alloc param_2;
+ param_2.offset = _254.conf.anno_alloc.offset;
+ AnnotatedRef param_3 = ref;
+ AnnoEndClip clip = Annotated_EndClip_read(param_2, param_3);
+ x0 = int(floor(clip.bbox.x * 0.001953125));
+ y0 = int(floor(clip.bbox.y * 0.00390625));
+ x1 = int(ceil(clip.bbox.z * 0.001953125));
+ y1 = int(ceil(clip.bbox.w * 0.00390625));
+ break;
+ }
+ }
+ uint width_in_bins = ((_254.conf.width_in_tiles + 16u) - 1u) / 16u;
+ uint height_in_bins = ((_254.conf.height_in_tiles + 8u) - 1u) / 8u;
+ x0 = clamp(x0, 0, int(width_in_bins));
+ x1 = clamp(x1, x0, int(width_in_bins));
+ y0 = clamp(y0, 0, int(height_in_bins));
+ y1 = clamp(y1, y0, int(height_in_bins));
+ if (x0 == x1)
+ {
+ y1 = y0;
+ }
+ int x = x0;
+ int y = y0;
+ uint my_slice = gl_LocalInvocationID.x / 32u;
+ uint my_mask = uint(1 << int(gl_LocalInvocationID.x & 31u));
+ while (y < y1)
+ {
+ uint _438 = atomicOr(bitmaps[my_slice][(uint(y) * width_in_bins) + uint(x)], my_mask);
+ x++;
+ if (x == x1)
+ {
+ x = x0;
+ y++;
+ }
+ }
+ barrier();
+ uint element_count = 0u;
+ for (uint i_1 = 0u; i_1 < 4u; i_1++)
+ {
+ element_count += uint(bitCount(bitmaps[i_1][gl_LocalInvocationID.x]));
+ count[i_1][gl_LocalInvocationID.x] = element_count;
+ }
+ uint param_4 = 0u;
+ uint param_5 = 0u;
+ Alloc chunk_alloc = new_alloc(param_4, param_5);
+ if (element_count != 0u)
+ {
+ uint param_6 = element_count * 4u;
+ MallocResult _487 = malloc(param_6);
+ MallocResult chunk = _487;
+ chunk_alloc = chunk.alloc;
+ sh_chunk_alloc[gl_LocalInvocationID.x] = chunk_alloc;
+ if (chunk.failed)
+ {
+ sh_alloc_failed = true;
+ }
+ }
+ uint out_ix = (_254.conf.bin_alloc.offset >> uint(2)) + (((my_partition * 128u) + gl_LocalInvocationID.x) * 2u);
+ Alloc param_7;
+ param_7.offset = _254.conf.bin_alloc.offset;
+ uint param_8 = out_ix;
+ uint param_9 = element_count;
+ write_mem(param_7, param_8, param_9);
+ Alloc param_10;
+ param_10.offset = _254.conf.bin_alloc.offset;
+ uint param_11 = out_ix + 1u;
+ uint param_12 = chunk_alloc.offset;
+ write_mem(param_10, param_11, param_12);
+ barrier();
+ if (sh_alloc_failed)
+ {
+ return;
+ }
+ x = x0;
+ y = y0;
+ while (y < y1)
+ {
+ uint bin_ix = (uint(y) * width_in_bins) + uint(x);
+ uint out_mask = bitmaps[my_slice][bin_ix];
+ if ((out_mask & my_mask) != 0u)
+ {
+ uint idx = uint(bitCount(out_mask & (my_mask - 1u)));
+ if (my_slice > 0u)
+ {
+ idx += count[my_slice - 1u][bin_ix];
+ }
+ Alloc out_alloc = sh_chunk_alloc[bin_ix];
+ uint out_offset = out_alloc.offset + (idx * 4u);
+ Alloc param_13 = out_alloc;
+ BinInstanceRef param_14 = BinInstanceRef(out_offset);
+ BinInstance param_15 = BinInstance(element_ix);
+ BinInstance_write(param_13, param_14, param_15);
+ }
+ x++;
+ if (x == x1)
+ {
+ x = x0;
+ y++;
+ }
+ }
+}
+
+`,
+ }
+ shader_blit_frag = [...]driver.ShaderSources{
+ {
+ Name: "blit.frag",
+ Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Color", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_color.color", Type: 0x0, Size: 4, Offset: 0}},
+ Size: 16,
+ },
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+struct Color
+{
+ vec4 color;
+};
+
+uniform Color _color;
+
+varying vec2 vUV;
+
+void main()
+{
+ gl_FragData[0] = _color.color;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+layout(std140) uniform Color
+{
+ vec4 color;
+} _color;
+
+layout(location = 0) out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = _color.color;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct Color
+{
+ vec4 color;
+};
+
+uniform Color _color;
+
+out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = _color.color;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0, std140) uniform Color
+{
+ vec4 color;
+} _color;
+
+out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = _color.color;
+}
+
+`,
+ HLSL: "DXBC,\xc1\x9c\x85P\xbc\xab\x8a.\x9e\b\xdd\xf7\xd2\x18\xa2\x01\x00\x00\x00t\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\x84\x00\x00\x00\xcc\x00\x00\x00H\x01\x00\x00\f\x02\x00\x00@\x02\x00\x00Aon9D\x00\x00\x00D\x00\x00\x00\x00\x02\xff\xff\x14\x00\x00\x000\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\xa0\xff\xff\x00\x00SHDR@\x00\x00\x00@\x00\x00\x00\x10\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x006\x00\x00\x06\xf2 \x10\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xbc\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x94\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Color\x00\xab\xab<\x00\x00\x00\x01\x00\x00\x00\\\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00t\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\x84\x00\x00\x00\x00\x00\x00\x00_color_color\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ },
+ {
+ Name: "blit.frag",
+ Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Gradient", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_gradient.color1", Type: 0x0, Size: 4, Offset: 0}, {Name: "_gradient.color2", Type: 0x0, Size: 4, Offset: 16}},
+ Size: 32,
+ },
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+struct Gradient
+{
+ vec4 color1;
+ vec4 color2;
+};
+
+uniform Gradient _gradient;
+
+varying vec2 vUV;
+
+void main()
+{
+ gl_FragData[0] = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+layout(std140) uniform Gradient
+{
+ vec4 color1;
+ vec4 color2;
+} _gradient;
+
+layout(location = 0) out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct Gradient
+{
+ vec4 color1;
+ vec4 color2;
+};
+
+uniform Gradient _gradient;
+
+out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0, std140) uniform Gradient
+{
+ vec4 color1;
+ vec4 color2;
+} _gradient;
+
+out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+}
+
+`,
+ HLSL: "DXBCdZ\xb9AA\xb2\xa5-Ī£c\xb9\xdc\xfd]\xae\x01\x00\x00\x00P\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xcc\x00\x00\x00t\x01\x00\x00\xf0\x01\x00\x00\xe8\x02\x00\x00\x1c\x03\x00\x00Aon9\x8c\x00\x00\x00\x8c\x00\x00\x00\x00\x02\xff\xff\\\x00\x00\x000\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x00\x000\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x01\x00\x00\x02\x00\x00\x18\x80\x00\x00\x00\xb0\x01\x00\x00\x02\x01\x00\x0f\x80\x00\x00\xe4\xa0\x02\x00\x00\x03\x01\x00\x0f\x80\x01\x00\xe4\x81\x01\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\x0f\x80\x00\x00\xff\x80\x01\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xa0\x00\x00\x00@\x00\x00\x00(\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00b\x10\x00\x03\x12\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006 \x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x8e \x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x002\x00\x00\n\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xf0\x00\x00\x00\x01\x00\x00\x00H\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xc5\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Gradient\x00\xab\xab\xab<\x00\x00\x00\x02\x00\x00\x00`\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x00\x00\xb4\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x00\x00_gradient_color1\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_gradient_color2\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x01\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ },
+ {
+ Name: "blit.frag",
+ Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}},
+ Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+
+varying vec2 vUV;
+
+void main()
+{
+ gl_FragData[0] = texture2D(tex, vUV);
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+
+layout(location = 0) out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = texture(tex, vUV);
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+
+out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = texture(tex, vUV);
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+
+out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = texture(tex, vUV);
+}
+
+`,
+ HLSL: "DXBC\xb7?\x1d\xb1\x80Ķ\xa3W\t\xfbZ\x9fV\xd6\xda\x01\x00\x00\x00\x94\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\xa4\x00\x00\x00\x10\x01\x00\x00\x8c\x01\x00\x00,\x02\x00\x00`\x02\x00\x00Aon9d\x00\x00\x00d\x00\x00\x00\x00\x02\xff\xff<\x00\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDRd\x00\x00\x00@\x00\x00\x00\x19\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00E\x00\x00\t\xf2 \x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00m\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00i\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ },
+ }
+ shader_blit_vert = driver.ShaderSources{
+ Name: "blit.vert",
+ Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.uvTransformR1", Type: 0x0, Size: 4, Offset: 16}, {Name: "_block.uvTransformR2", Type: 0x0, Size: 4, Offset: 32}, {Name: "_block.z", Type: 0x0, Size: 1, Offset: 48}},
+ Size: 52,
+ },
+ GLSL100ES: `#version 100
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+struct Block
+{
+ vec4 transform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+};
+
+uniform Block _block;
+
+attribute vec2 pos;
+varying vec2 vUV;
+attribute vec2 uv;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec2 p = (pos * _block.transform.xy) + _block.transform.zw;
+ vec4 param = vec4(p, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+layout(std140) uniform Block
+{
+ vec4 transform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+} _block;
+
+layout(location = 0) in vec2 pos;
+out vec2 vUV;
+layout(location = 1) in vec2 uv;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec2 p = (pos * _block.transform.xy) + _block.transform.zw;
+ vec4 param = vec4(p, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+struct Block
+{
+ vec4 transform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+};
+
+uniform Block _block;
+
+in vec2 pos;
+out vec2 vUV;
+in vec2 uv;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec2 p = (pos * _block.transform.xy) + _block.transform.zw;
+ vec4 param = vec4(p, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+layout(binding = 0, std140) uniform Block
+{
+ vec4 transform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+} _block;
+
+in vec2 pos;
+out vec2 vUV;
+in vec2 uv;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec2 p = (pos * _block.transform.xy) + _block.transform.zw;
+ vec4 param = vec4(p, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+}
+
+`,
+ HLSL: "DXBC\x80\xa7\xa0\x9e\xbb\xa1\xa3\x1b\x85\xac\xb6\xe9\xfb\xe6W\x03\x01\x00\x00\x00\xc8\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00$\x01\x00\x00T\x02\x00\x00\xd0\x02\x00\x00$\x04\x00\x00p\x04\x00\x00Aon9\xe4\x00\x00\x00\xe4\x00\x00\x00\x00\x02\xfe\xff\xb0\x00\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x04\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x05\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\a\x80\x01\x00Ä\x05\x00Š \x05\x00Å \b\x00\x00\x03\x00\x00\x01\xe0\x02\x00\xe4\xa0\x00\x00\xe4\x80\b\x00\x00\x03\x00\x00\x02\xe0\x03\x00\xe4\xa0\x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x90\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\a\x80\x05\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\f\xc0\x04\x00\x00\xa0\x00\x00d\x80\x00\x00$\x80\xff\xff\x00\x00SHDR(\x01\x00\x00@\x00\x01\x00J\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x04\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x006\x00\x00\x052\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x10\x00\x00\b\x12 \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x10\x00\x00\b\" \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x002\x00\x00\v2 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\nB \x10\x00\x01\x00\x00\x00\n\x80 \x00\x00\x00\x00\x00\x03\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\x01@\x00\x00\x00\x00\x00?6\x00\x00\x05\x82 \x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\b\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFL\x01\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00$\x01\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x04\x00\x00\x00\\\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\xf5\x00\x00\x00 \x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\n\x01\x00\x000\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x14\x01\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_uvTransformR1\x00_block_uvTransformR2\x00_block_z\x00\xab\x00\x00\x03\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab",
+ }
+ shader_coarse_comp = driver.ShaderSources{
+ Name: "coarse.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct MallocResult
+{
+ Alloc alloc;
+ bool failed;
+};
+
+struct AnnoImageRef
+{
+ uint offset;
+};
+
+struct AnnoImage
+{
+ vec4 bbox;
+ float linewidth;
+ uint index;
+ ivec2 offset;
+};
+
+struct AnnoColorRef
+{
+ uint offset;
+};
+
+struct AnnoColor
+{
+ vec4 bbox;
+ float linewidth;
+ uint rgba_color;
+};
+
+struct AnnoBeginClipRef
+{
+ uint offset;
+};
+
+struct AnnoBeginClip
+{
+ vec4 bbox;
+ float linewidth;
+};
+
+struct AnnotatedRef
+{
+ uint offset;
+};
+
+struct AnnotatedTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct BinInstanceRef
+{
+ uint offset;
+};
+
+struct BinInstance
+{
+ uint element_ix;
+};
+
+struct PathRef
+{
+ uint offset;
+};
+
+struct TileRef
+{
+ uint offset;
+};
+
+struct Path
+{
+ uvec4 bbox;
+ TileRef tiles;
+};
+
+struct TileSegRef
+{
+ uint offset;
+};
+
+struct Tile
+{
+ TileSegRef tile;
+ int backdrop;
+};
+
+struct CmdStrokeRef
+{
+ uint offset;
+};
+
+struct CmdStroke
+{
+ uint tile_ref;
+ float half_width;
+};
+
+struct CmdFillRef
+{
+ uint offset;
+};
+
+struct CmdFill
+{
+ uint tile_ref;
+ int backdrop;
+};
+
+struct CmdColorRef
+{
+ uint offset;
+};
+
+struct CmdColor
+{
+ uint rgba_color;
+};
+
+struct CmdImageRef
+{
+ uint offset;
+};
+
+struct CmdImage
+{
+ uint index;
+ ivec2 offset;
+};
+
+struct CmdJumpRef
+{
+ uint offset;
+};
+
+struct CmdJump
+{
+ uint new_ref;
+};
+
+struct CmdRef
+{
+ uint offset;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _276;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _1066;
+
+shared uint sh_bitmaps[4][128];
+shared Alloc sh_part_elements[128];
+shared uint sh_part_count[128];
+shared uint sh_elements[128];
+shared uint sh_tile_stride[128];
+shared uint sh_tile_width[128];
+shared uint sh_tile_x0[128];
+shared uint sh_tile_y0[128];
+shared uint sh_tile_base[128];
+shared uint sh_tile_count[128];
+
+Alloc new_alloc(uint offset, uint size)
+{
+ Alloc a;
+ a.offset = offset;
+ return a;
+}
+
+Alloc slice_mem(Alloc a, uint offset, uint size)
+{
+ uint param = a.offset + offset;
+ uint param_1 = size;
+ return new_alloc(param, param_1);
+}
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+uint read_mem(Alloc alloc, uint offset)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return 0u;
+ }
+ uint v = _276.memory[offset];
+ return v;
+}
+
+BinInstanceRef BinInstance_index(BinInstanceRef ref, uint index)
+{
+ return BinInstanceRef(ref.offset + (index * 4u));
+}
+
+BinInstance BinInstance_read(Alloc a, BinInstanceRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ BinInstance s;
+ s.element_ix = raw0;
+ return s;
+}
+
+AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint tag_and_flags = read_mem(param, param_1);
+ return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+Path Path_read(Alloc a, PathRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Path s;
+ s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16));
+ s.tiles = TileRef(raw2);
+ return s;
+}
+
+void write_tile_alloc(uint el_ix, Alloc a)
+{
+}
+
+Alloc read_tile_alloc(uint el_ix)
+{
+ uint param = 0u;
+ uint param_1 = uint(int(uint(_276.memory.length())) * 4);
+ return new_alloc(param, param_1);
+}
+
+Tile Tile_read(Alloc a, TileRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Tile s;
+ s.tile = TileSegRef(raw0);
+ s.backdrop = int(raw1);
+ return s;
+}
+
+AnnoColor AnnoColor_read(Alloc a, AnnoColorRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ Alloc param_8 = a;
+ uint param_9 = ix + 4u;
+ uint raw4 = read_mem(param_8, param_9);
+ Alloc param_10 = a;
+ uint param_11 = ix + 5u;
+ uint raw5 = read_mem(param_10, param_11);
+ AnnoColor s;
+ s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.linewidth = uintBitsToFloat(raw4);
+ s.rgba_color = raw5;
+ return s;
+}
+
+AnnoColor Annotated_Color_read(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ AnnoColorRef param_1 = AnnoColorRef(ref.offset + 4u);
+ return AnnoColor_read(param, param_1);
+}
+
+MallocResult malloc(uint size)
+{
+ MallocResult r;
+ r.failed = false;
+ uint _282 = atomicAdd(_276.mem_offset, size);
+ uint offset = _282;
+ uint param = offset;
+ uint param_1 = size;
+ r.alloc = new_alloc(param, param_1);
+ if ((offset + size) > uint(int(uint(_276.memory.length())) * 4))
+ {
+ r.failed = true;
+ uint _303 = atomicMax(_276.mem_error, 1u);
+ return r;
+ }
+ return r;
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _276.memory[offset] = val;
+}
+
+void CmdJump_write(Alloc a, CmdJumpRef ref, CmdJump s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.new_ref;
+ write_mem(param, param_1, param_2);
+}
+
+void Cmd_Jump_write(Alloc a, CmdRef ref, CmdJump s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 9u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ CmdJumpRef param_4 = CmdJumpRef(ref.offset + 4u);
+ CmdJump param_5 = s;
+ CmdJump_write(param_3, param_4, param_5);
+}
+
+bool alloc_cmd(inout Alloc cmd_alloc, inout CmdRef cmd_ref, inout uint cmd_limit)
+{
+ if (cmd_ref.offset < cmd_limit)
+ {
+ return true;
+ }
+ uint param = 1024u;
+ MallocResult _968 = malloc(param);
+ MallocResult new_cmd = _968;
+ if (new_cmd.failed)
+ {
+ return false;
+ }
+ CmdJump jump = CmdJump(new_cmd.alloc.offset);
+ Alloc param_1 = cmd_alloc;
+ CmdRef param_2 = cmd_ref;
+ CmdJump param_3 = jump;
+ Cmd_Jump_write(param_1, param_2, param_3);
+ cmd_alloc = new_cmd.alloc;
+ cmd_ref = CmdRef(cmd_alloc.offset);
+ cmd_limit = (cmd_alloc.offset + 1024u) - 36u;
+ return true;
+}
+
+uint fill_mode_from_flags(uint flags)
+{
+ return flags & 1u;
+}
+
+void CmdFill_write(Alloc a, CmdFillRef ref, CmdFill s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.tile_ref;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = uint(s.backdrop);
+ write_mem(param_3, param_4, param_5);
+}
+
+void Cmd_Fill_write(Alloc a, CmdRef ref, CmdFill s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 1u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ CmdFillRef param_4 = CmdFillRef(ref.offset + 4u);
+ CmdFill param_5 = s;
+ CmdFill_write(param_3, param_4, param_5);
+}
+
+void Cmd_Solid_write(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 3u;
+ write_mem(param, param_1, param_2);
+}
+
+void CmdStroke_write(Alloc a, CmdStrokeRef ref, CmdStroke s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.tile_ref;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.half_width);
+ write_mem(param_3, param_4, param_5);
+}
+
+void Cmd_Stroke_write(Alloc a, CmdRef ref, CmdStroke s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 2u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ CmdStrokeRef param_4 = CmdStrokeRef(ref.offset + 4u);
+ CmdStroke param_5 = s;
+ CmdStroke_write(param_3, param_4, param_5);
+}
+
+void write_fill(Alloc alloc, inout CmdRef cmd_ref, uint flags, Tile tile, float linewidth)
+{
+ uint param = flags;
+ if (fill_mode_from_flags(param) == 0u)
+ {
+ if (tile.tile.offset != 0u)
+ {
+ CmdFill cmd_fill = CmdFill(tile.tile.offset, tile.backdrop);
+ Alloc param_1 = alloc;
+ CmdRef param_2 = cmd_ref;
+ CmdFill param_3 = cmd_fill;
+ Cmd_Fill_write(param_1, param_2, param_3);
+ cmd_ref.offset += 12u;
+ }
+ else
+ {
+ Alloc param_4 = alloc;
+ CmdRef param_5 = cmd_ref;
+ Cmd_Solid_write(param_4, param_5);
+ cmd_ref.offset += 4u;
+ }
+ }
+ else
+ {
+ CmdStroke cmd_stroke = CmdStroke(tile.tile.offset, 0.5 * linewidth);
+ Alloc param_6 = alloc;
+ CmdRef param_7 = cmd_ref;
+ CmdStroke param_8 = cmd_stroke;
+ Cmd_Stroke_write(param_6, param_7, param_8);
+ cmd_ref.offset += 12u;
+ }
+}
+
+void CmdColor_write(Alloc a, CmdColorRef ref, CmdColor s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.rgba_color;
+ write_mem(param, param_1, param_2);
+}
+
+void Cmd_Color_write(Alloc a, CmdRef ref, CmdColor s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 5u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ CmdColorRef param_4 = CmdColorRef(ref.offset + 4u);
+ CmdColor param_5 = s;
+ CmdColor_write(param_3, param_4, param_5);
+}
+
+AnnoImage AnnoImage_read(Alloc a, AnnoImageRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ Alloc param_8 = a;
+ uint param_9 = ix + 4u;
+ uint raw4 = read_mem(param_8, param_9);
+ Alloc param_10 = a;
+ uint param_11 = ix + 5u;
+ uint raw5 = read_mem(param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 6u;
+ uint raw6 = read_mem(param_12, param_13);
+ AnnoImage s;
+ s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.linewidth = uintBitsToFloat(raw4);
+ s.index = raw5;
+ s.offset = ivec2(int(raw6 << uint(16)) >> 16, int(raw6) >> 16);
+ return s;
+}
+
+AnnoImage Annotated_Image_read(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ AnnoImageRef param_1 = AnnoImageRef(ref.offset + 4u);
+ return AnnoImage_read(param, param_1);
+}
+
+void CmdImage_write(Alloc a, CmdImageRef ref, CmdImage s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.index;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = (uint(s.offset.x) & 65535u) | (uint(s.offset.y) << uint(16));
+ write_mem(param_3, param_4, param_5);
+}
+
+void Cmd_Image_write(Alloc a, CmdRef ref, CmdImage s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 6u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ CmdImageRef param_4 = CmdImageRef(ref.offset + 4u);
+ CmdImage param_5 = s;
+ CmdImage_write(param_3, param_4, param_5);
+}
+
+AnnoBeginClip AnnoBeginClip_read(Alloc a, AnnoBeginClipRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ Alloc param_8 = a;
+ uint param_9 = ix + 4u;
+ uint raw4 = read_mem(param_8, param_9);
+ AnnoBeginClip s;
+ s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.linewidth = uintBitsToFloat(raw4);
+ return s;
+}
+
+AnnoBeginClip Annotated_BeginClip_read(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ AnnoBeginClipRef param_1 = AnnoBeginClipRef(ref.offset + 4u);
+ return AnnoBeginClip_read(param, param_1);
+}
+
+void Cmd_BeginClip_write(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 7u;
+ write_mem(param, param_1, param_2);
+}
+
+void Cmd_EndClip_write(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 8u;
+ write_mem(param, param_1, param_2);
+}
+
+void Cmd_End_write(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 0u;
+ write_mem(param, param_1, param_2);
+}
+
+void alloc_write(Alloc a, uint offset, Alloc alloc)
+{
+ Alloc param = a;
+ uint param_1 = offset >> uint(2);
+ uint param_2 = alloc.offset;
+ write_mem(param, param_1, param_2);
+}
+
+void main()
+{
+ if (_276.mem_error != 0u)
+ {
+ return;
+ }
+ uint width_in_bins = ((_1066.conf.width_in_tiles + 16u) - 1u) / 16u;
+ uint bin_ix = (width_in_bins * gl_WorkGroupID.y) + gl_WorkGroupID.x;
+ uint partition_ix = 0u;
+ uint n_partitions = ((_1066.conf.n_elements + 128u) - 1u) / 128u;
+ uint th_ix = gl_LocalInvocationID.x;
+ uint bin_tile_x = 16u * gl_WorkGroupID.x;
+ uint bin_tile_y = 8u * gl_WorkGroupID.y;
+ uint tile_x = gl_LocalInvocationID.x % 16u;
+ uint tile_y = gl_LocalInvocationID.x / 16u;
+ uint this_tile_ix = (((bin_tile_y + tile_y) * _1066.conf.width_in_tiles) + bin_tile_x) + tile_x;
+ Alloc param;
+ param.offset = _1066.conf.ptcl_alloc.offset;
+ uint param_1 = this_tile_ix * 1024u;
+ uint param_2 = 1024u;
+ Alloc cmd_alloc = slice_mem(param, param_1, param_2);
+ CmdRef cmd_ref = CmdRef(cmd_alloc.offset);
+ uint cmd_limit = (cmd_ref.offset + 1024u) - 36u;
+ uint clip_depth = 0u;
+ uint clip_zero_depth = 0u;
+ uint clip_one_mask = 0u;
+ uint rd_ix = 0u;
+ uint wr_ix = 0u;
+ uint part_start_ix = 0u;
+ uint ready_ix = 0u;
+ Alloc param_3 = cmd_alloc;
+ uint param_4 = 0u;
+ uint param_5 = 8u;
+ Alloc scratch_alloc = slice_mem(param_3, param_4, param_5);
+ cmd_ref.offset += 8u;
+ uint num_begin_slots = 0u;
+ uint begin_slot = 0u;
+ Alloc param_6;
+ Alloc param_8;
+ uint _1354;
+ uint element_ix;
+ AnnotatedRef ref;
+ Alloc param_16;
+ Alloc param_18;
+ uint tile_count;
+ Alloc param_24;
+ uint _1667;
+ bool include_tile;
+ Alloc param_29;
+ Tile tile_1;
+ Alloc param_34;
+ Alloc param_50;
+ Alloc param_66;
+ while (true)
+ {
+ for (uint i = 0u; i < 4u; i++)
+ {
+ sh_bitmaps[i][th_ix] = 0u;
+ }
+ bool _1406;
+ for (;;)
+ {
+ if ((ready_ix == wr_ix) && (partition_ix < n_partitions))
+ {
+ part_start_ix = ready_ix;
+ uint count = 0u;
+ bool _1204 = th_ix < 128u;
+ bool _1212;
+ if (_1204)
+ {
+ _1212 = (partition_ix + th_ix) < n_partitions;
+ }
+ else
+ {
+ _1212 = _1204;
+ }
+ if (_1212)
+ {
+ uint in_ix = (_1066.conf.bin_alloc.offset >> uint(2)) + ((((partition_ix + th_ix) * 128u) + bin_ix) * 2u);
+ param_6.offset = _1066.conf.bin_alloc.offset;
+ uint param_7 = in_ix;
+ count = read_mem(param_6, param_7);
+ param_8.offset = _1066.conf.bin_alloc.offset;
+ uint param_9 = in_ix + 1u;
+ uint offset = read_mem(param_8, param_9);
+ uint param_10 = offset;
+ uint param_11 = count * 4u;
+ sh_part_elements[th_ix] = new_alloc(param_10, param_11);
+ }
+ for (uint i_1 = 0u; i_1 < 7u; i_1++)
+ {
+ if (th_ix < 128u)
+ {
+ sh_part_count[th_ix] = count;
+ }
+ barrier();
+ if (th_ix < 128u)
+ {
+ if (th_ix >= uint(1 << int(i_1)))
+ {
+ count += sh_part_count[th_ix - uint(1 << int(i_1))];
+ }
+ }
+ barrier();
+ }
+ if (th_ix < 128u)
+ {
+ sh_part_count[th_ix] = part_start_ix + count;
+ }
+ barrier();
+ ready_ix = sh_part_count[127];
+ partition_ix += 128u;
+ }
+ uint ix = rd_ix + th_ix;
+ if ((ix >= wr_ix) && (ix < ready_ix))
+ {
+ uint part_ix = 0u;
+ for (uint i_2 = 0u; i_2 < 7u; i_2++)
+ {
+ uint probe = part_ix + uint(64 >> int(i_2));
+ if (ix >= sh_part_count[probe - 1u])
+ {
+ part_ix = probe;
+ }
+ }
+ if (part_ix > 0u)
+ {
+ _1354 = sh_part_count[part_ix - 1u];
+ }
+ else
+ {
+ _1354 = part_start_ix;
+ }
+ ix -= _1354;
+ Alloc bin_alloc = sh_part_elements[part_ix];
+ BinInstanceRef inst_ref = BinInstanceRef(bin_alloc.offset);
+ BinInstanceRef param_12 = inst_ref;
+ uint param_13 = ix;
+ Alloc param_14 = bin_alloc;
+ BinInstanceRef param_15 = BinInstance_index(param_12, param_13);
+ BinInstance inst = BinInstance_read(param_14, param_15);
+ sh_elements[th_ix] = inst.element_ix;
+ }
+ barrier();
+ wr_ix = min((rd_ix + 128u), ready_ix);
+ bool _1396 = (wr_ix - rd_ix) < 128u;
+ if (_1396)
+ {
+ _1406 = (wr_ix < ready_ix) || (partition_ix < n_partitions);
+ }
+ else
+ {
+ _1406 = _1396;
+ }
+ if (_1406)
+ {
+ continue;
+ }
+ else
+ {
+ break;
+ }
+ }
+ uint tag = 0u;
+ if ((th_ix + rd_ix) < wr_ix)
+ {
+ element_ix = sh_elements[th_ix];
+ ref = AnnotatedRef(_1066.conf.anno_alloc.offset + (element_ix * 32u));
+ param_16.offset = _1066.conf.anno_alloc.offset;
+ AnnotatedRef param_17 = ref;
+ tag = Annotated_tag(param_16, param_17).tag;
+ }
+ switch (tag)
+ {
+ case 1u:
+ case 2u:
+ case 3u:
+ case 4u:
+ {
+ uint path_ix = element_ix;
+ param_18.offset = _1066.conf.tile_alloc.offset;
+ PathRef param_19 = PathRef(_1066.conf.tile_alloc.offset + (path_ix * 12u));
+ Path path = Path_read(param_18, param_19);
+ uint stride = path.bbox.z - path.bbox.x;
+ sh_tile_stride[th_ix] = stride;
+ int dx = int(path.bbox.x) - int(bin_tile_x);
+ int dy = int(path.bbox.y) - int(bin_tile_y);
+ int x0 = clamp(dx, 0, 16);
+ int y0 = clamp(dy, 0, 8);
+ int x1 = clamp(int(path.bbox.z) - int(bin_tile_x), 0, 16);
+ int y1 = clamp(int(path.bbox.w) - int(bin_tile_y), 0, 8);
+ sh_tile_width[th_ix] = uint(x1 - x0);
+ sh_tile_x0[th_ix] = uint(x0);
+ sh_tile_y0[th_ix] = uint(y0);
+ tile_count = uint(x1 - x0) * uint(y1 - y0);
+ uint base = path.tiles.offset - (((uint(dy) * stride) + uint(dx)) * 8u);
+ sh_tile_base[th_ix] = base;
+ uint param_20 = path.tiles.offset;
+ uint param_21 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u;
+ Alloc path_alloc = new_alloc(param_20, param_21);
+ uint param_22 = th_ix;
+ Alloc param_23 = path_alloc;
+ write_tile_alloc(param_22, param_23);
+ break;
+ }
+ default:
+ {
+ tile_count = 0u;
+ break;
+ }
+ }
+ sh_tile_count[th_ix] = tile_count;
+ for (uint i_3 = 0u; i_3 < 7u; i_3++)
+ {
+ barrier();
+ if (th_ix >= uint(1 << int(i_3)))
+ {
+ tile_count += sh_tile_count[th_ix - uint(1 << int(i_3))];
+ }
+ barrier();
+ sh_tile_count[th_ix] = tile_count;
+ }
+ barrier();
+ uint total_tile_count = sh_tile_count[127];
+ for (uint ix_1 = th_ix; ix_1 < total_tile_count; ix_1 += 128u)
+ {
+ uint el_ix = 0u;
+ for (uint i_4 = 0u; i_4 < 7u; i_4++)
+ {
+ uint probe_1 = el_ix + uint(64 >> int(i_4));
+ if (ix_1 >= sh_tile_count[probe_1 - 1u])
+ {
+ el_ix = probe_1;
+ }
+ }
+ AnnotatedRef ref_1 = AnnotatedRef(_1066.conf.anno_alloc.offset + (sh_elements[el_ix] * 32u));
+ param_24.offset = _1066.conf.anno_alloc.offset;
+ AnnotatedRef param_25 = ref_1;
+ uint tag_1 = Annotated_tag(param_24, param_25).tag;
+ if (el_ix > 0u)
+ {
+ _1667 = sh_tile_count[el_ix - 1u];
+ }
+ else
+ {
+ _1667 = 0u;
+ }
+ uint seq_ix = ix_1 - _1667;
+ uint width = sh_tile_width[el_ix];
+ uint x = sh_tile_x0[el_ix] + (seq_ix % width);
+ uint y = sh_tile_y0[el_ix] + (seq_ix / width);
+ if ((tag_1 == 3u) || (tag_1 == 4u))
+ {
+ include_tile = true;
+ }
+ else
+ {
+ uint param_26 = el_ix;
+ Alloc param_27 = read_tile_alloc(param_26);
+ TileRef param_28 = TileRef(sh_tile_base[el_ix] + (((sh_tile_stride[el_ix] * y) + x) * 8u));
+ Tile tile = Tile_read(param_27, param_28);
+ bool _1728 = tile.tile.offset != 0u;
+ bool _1735;
+ if (!_1728)
+ {
+ _1735 = tile.backdrop != 0;
+ }
+ else
+ {
+ _1735 = _1728;
+ }
+ include_tile = _1735;
+ }
+ if (include_tile)
+ {
+ uint el_slice = el_ix / 32u;
+ uint el_mask = uint(1 << int(el_ix & 31u));
+ uint _1755 = atomicOr(sh_bitmaps[el_slice][(y * 16u) + x], el_mask);
+ }
+ }
+ barrier();
+ uint slice_ix = 0u;
+ uint bitmap = sh_bitmaps[0][th_ix];
+ while (true)
+ {
+ if (bitmap == 0u)
+ {
+ slice_ix++;
+ if (slice_ix == 4u)
+ {
+ break;
+ }
+ bitmap = sh_bitmaps[slice_ix][th_ix];
+ if (bitmap == 0u)
+ {
+ continue;
+ }
+ }
+ uint element_ref_ix = (slice_ix * 32u) + uint(findLSB(bitmap));
+ uint element_ix_1 = sh_elements[element_ref_ix];
+ bitmap &= (bitmap - 1u);
+ ref = AnnotatedRef(_1066.conf.anno_alloc.offset + (element_ix_1 * 32u));
+ param_29.offset = _1066.conf.anno_alloc.offset;
+ AnnotatedRef param_30 = ref;
+ AnnotatedTag tag_2 = Annotated_tag(param_29, param_30);
+ if (clip_zero_depth == 0u)
+ {
+ switch (tag_2.tag)
+ {
+ case 1u:
+ {
+ uint param_31 = element_ref_ix;
+ Alloc param_32 = read_tile_alloc(param_31);
+ TileRef param_33 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u));
+ tile_1 = Tile_read(param_32, param_33);
+ param_34.offset = _1066.conf.anno_alloc.offset;
+ AnnotatedRef param_35 = ref;
+ AnnoColor fill = Annotated_Color_read(param_34, param_35);
+ Alloc param_36 = cmd_alloc;
+ CmdRef param_37 = cmd_ref;
+ uint param_38 = cmd_limit;
+ bool _1865 = alloc_cmd(param_36, param_37, param_38);
+ cmd_alloc = param_36;
+ cmd_ref = param_37;
+ cmd_limit = param_38;
+ if (!_1865)
+ {
+ break;
+ }
+ Alloc param_39 = cmd_alloc;
+ CmdRef param_40 = cmd_ref;
+ uint param_41 = tag_2.flags;
+ Tile param_42 = tile_1;
+ float param_43 = fill.linewidth;
+ write_fill(param_39, param_40, param_41, param_42, param_43);
+ cmd_ref = param_40;
+ Alloc param_44 = cmd_alloc;
+ CmdRef param_45 = cmd_ref;
+ CmdColor param_46 = CmdColor(fill.rgba_color);
+ Cmd_Color_write(param_44, param_45, param_46);
+ cmd_ref.offset += 8u;
+ break;
+ }
+ case 2u:
+ {
+ uint param_47 = element_ref_ix;
+ Alloc param_48 = read_tile_alloc(param_47);
+ TileRef param_49 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u));
+ tile_1 = Tile_read(param_48, param_49);
+ param_50.offset = _1066.conf.anno_alloc.offset;
+ AnnotatedRef param_51 = ref;
+ AnnoImage fill_img = Annotated_Image_read(param_50, param_51);
+ Alloc param_52 = cmd_alloc;
+ CmdRef param_53 = cmd_ref;
+ uint param_54 = cmd_limit;
+ bool _1935 = alloc_cmd(param_52, param_53, param_54);
+ cmd_alloc = param_52;
+ cmd_ref = param_53;
+ cmd_limit = param_54;
+ if (!_1935)
+ {
+ break;
+ }
+ Alloc param_55 = cmd_alloc;
+ CmdRef param_56 = cmd_ref;
+ uint param_57 = tag_2.flags;
+ Tile param_58 = tile_1;
+ float param_59 = fill_img.linewidth;
+ write_fill(param_55, param_56, param_57, param_58, param_59);
+ cmd_ref = param_56;
+ Alloc param_60 = cmd_alloc;
+ CmdRef param_61 = cmd_ref;
+ CmdImage param_62 = CmdImage(fill_img.index, fill_img.offset);
+ Cmd_Image_write(param_60, param_61, param_62);
+ cmd_ref.offset += 12u;
+ break;
+ }
+ case 3u:
+ {
+ uint param_63 = element_ref_ix;
+ Alloc param_64 = read_tile_alloc(param_63);
+ TileRef param_65 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u));
+ tile_1 = Tile_read(param_64, param_65);
+ bool _1994 = tile_1.tile.offset == 0u;
+ bool _2000;
+ if (_1994)
+ {
+ _2000 = tile_1.backdrop == 0;
+ }
+ else
+ {
+ _2000 = _1994;
+ }
+ if (_2000)
+ {
+ clip_zero_depth = clip_depth + 1u;
+ }
+ else
+ {
+ if ((tile_1.tile.offset == 0u) && (clip_depth < 32u))
+ {
+ clip_one_mask |= uint(1 << int(clip_depth));
+ }
+ else
+ {
+ param_66.offset = _1066.conf.anno_alloc.offset;
+ AnnotatedRef param_67 = ref;
+ AnnoBeginClip begin_clip = Annotated_BeginClip_read(param_66, param_67);
+ Alloc param_68 = cmd_alloc;
+ CmdRef param_69 = cmd_ref;
+ uint param_70 = cmd_limit;
+ bool _2035 = alloc_cmd(param_68, param_69, param_70);
+ cmd_alloc = param_68;
+ cmd_ref = param_69;
+ cmd_limit = param_70;
+ if (!_2035)
+ {
+ break;
+ }
+ Alloc param_71 = cmd_alloc;
+ CmdRef param_72 = cmd_ref;
+ uint param_73 = tag_2.flags;
+ Tile param_74 = tile_1;
+ float param_75 = begin_clip.linewidth;
+ write_fill(param_71, param_72, param_73, param_74, param_75);
+ cmd_ref = param_72;
+ Alloc param_76 = cmd_alloc;
+ CmdRef param_77 = cmd_ref;
+ Cmd_BeginClip_write(param_76, param_77);
+ cmd_ref.offset += 4u;
+ if (clip_depth < 32u)
+ {
+ clip_one_mask &= uint(~(1 << int(clip_depth)));
+ }
+ begin_slot++;
+ num_begin_slots = max(num_begin_slots, begin_slot);
+ }
+ }
+ clip_depth++;
+ break;
+ }
+ case 4u:
+ {
+ clip_depth--;
+ bool _2087 = clip_depth >= 32u;
+ bool _2097;
+ if (!_2087)
+ {
+ _2097 = (clip_one_mask & uint(1 << int(clip_depth))) == 0u;
+ }
+ else
+ {
+ _2097 = _2087;
+ }
+ if (_2097)
+ {
+ Alloc param_78 = cmd_alloc;
+ CmdRef param_79 = cmd_ref;
+ uint param_80 = cmd_limit;
+ bool _2106 = alloc_cmd(param_78, param_79, param_80);
+ cmd_alloc = param_78;
+ cmd_ref = param_79;
+ cmd_limit = param_80;
+ if (!_2106)
+ {
+ break;
+ }
+ Alloc param_81 = cmd_alloc;
+ CmdRef param_82 = cmd_ref;
+ Cmd_Solid_write(param_81, param_82);
+ cmd_ref.offset += 4u;
+ begin_slot--;
+ Alloc param_83 = cmd_alloc;
+ CmdRef param_84 = cmd_ref;
+ Cmd_EndClip_write(param_83, param_84);
+ cmd_ref.offset += 4u;
+ }
+ break;
+ }
+ }
+ }
+ else
+ {
+ switch (tag_2.tag)
+ {
+ case 3u:
+ {
+ clip_depth++;
+ break;
+ }
+ case 4u:
+ {
+ if (clip_depth == clip_zero_depth)
+ {
+ clip_zero_depth = 0u;
+ }
+ clip_depth--;
+ break;
+ }
+ }
+ }
+ }
+ barrier();
+ rd_ix += 128u;
+ if ((rd_ix >= ready_ix) && (partition_ix >= n_partitions))
+ {
+ break;
+ }
+ }
+ bool _2171 = (bin_tile_x + tile_x) < _1066.conf.width_in_tiles;
+ bool _2180;
+ if (_2171)
+ {
+ _2180 = (bin_tile_y + tile_y) < _1066.conf.height_in_tiles;
+ }
+ else
+ {
+ _2180 = _2171;
+ }
+ if (_2180)
+ {
+ Alloc param_85 = cmd_alloc;
+ CmdRef param_86 = cmd_ref;
+ Cmd_End_write(param_85, param_86);
+ if (num_begin_slots > 0u)
+ {
+ uint scratch_size = (((num_begin_slots * 32u) * 32u) * 2u) * 4u;
+ uint param_87 = scratch_size;
+ MallocResult _2201 = malloc(param_87);
+ MallocResult scratch = _2201;
+ Alloc param_88 = scratch_alloc;
+ uint param_89 = scratch_alloc.offset;
+ Alloc param_90 = scratch.alloc;
+ alloc_write(param_88, param_89, param_90);
+ }
+ }
+}
+
+`,
+ }
+ shader_copy_frag = driver.ShaderSources{
+ Name: "copy.frag",
+ Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}},
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+
+layout(location = 0) out highp vec4 fragColor;
+
+highp vec3 sRGBtoRGB(highp vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375));
+ highp vec3 below = rgb / vec3(12.9200000762939453125);
+ highp vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625));
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ highp vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0);
+ highp vec3 param = texel.xyz;
+ highp vec3 rgb = sRGBtoRGB(param);
+ fragColor = vec4(rgb, texel.w);
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+
+out vec4 fragColor;
+
+vec3 sRGBtoRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375));
+ vec3 below = rgb / vec3(12.9200000762939453125);
+ vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625));
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0);
+ vec3 param = texel.xyz;
+ vec3 rgb = sRGBtoRGB(param);
+ fragColor = vec4(rgb, texel.w);
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+
+out vec4 fragColor;
+
+vec3 sRGBtoRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375));
+ vec3 below = rgb / vec3(12.9200000762939453125);
+ vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625));
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0);
+ vec3 param = texel.xyz;
+ vec3 rgb = sRGBtoRGB(param);
+ fragColor = vec4(rgb, texel.w);
+}
+
+`,
+ HLSL: "DXBC\xe6\x89_t\x8b\xfc\xea8\xd9'\xad5.Ćk\x01\x00\x00\x00H\x03\x00\x00\x05\x00\x00\x004\x00\x00\x00\xa4\x00\x00\x00\xd8\x00\x00\x00\f\x01\x00\x00\xcc\x02\x00\x00RDEFh\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00@\x00\x00\x00<\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x03\x00\x00SV_Position\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xabSHDR\xb8\x01\x00\x00@\x00\x00\x00n\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00d \x00\x042\x10\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x00\x1b\x00\x00\x052\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00\x00\a\xf2\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\xaeGa=\xaeGa=\xaeGa=\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00o\xa7r?o\xa7r?o\xa7r?\x00\x00\x00\x00/\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00\x9a\x99\x19@\x9a\x99\x19@\x9a\x99\x19@\x00\x00\x00\x00\x19\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x1d\x00\x00\nr\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\xe6\xae%=\xe6\xae%=\xe6\xae%=\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x00\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x91\x83\x9e=\x91\x83\x9e=\x91\x83\x9e=\x00\x00\x00\x006\x00\x00\x05\x82 \x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x007\x00\x00\tr \x10\x00\x00\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\r\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ }
+ shader_copy_vert = driver.ShaderSources{
+ Name: "copy.vert",
+ GLSL100ES: `#version 100
+
+void main()
+{
+ for (int spvDummy6 = 0; spvDummy6 < 1; spvDummy6++)
+ {
+ if (gl_VertexID == 0)
+ {
+ gl_Position = vec4(-1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ else if (gl_VertexID == 1)
+ {
+ gl_Position = vec4(1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ else if (gl_VertexID == 2)
+ {
+ gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ else if (gl_VertexID == 3)
+ {
+ gl_Position = vec4(1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ }
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+void main()
+{
+ switch (gl_VertexID)
+ {
+ case 0:
+ {
+ gl_Position = vec4(-1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ case 1:
+ {
+ gl_Position = vec4(1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ case 2:
+ {
+ gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ case 3:
+ {
+ gl_Position = vec4(1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ }
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+void main()
+{
+ switch (gl_VertexID)
+ {
+ case 0:
+ {
+ gl_Position = vec4(-1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ case 1:
+ {
+ gl_Position = vec4(1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ case 2:
+ {
+ gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ case 3:
+ {
+ gl_Position = vec4(1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ }
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+void main()
+{
+ switch (gl_VertexID)
+ {
+ case 0:
+ {
+ gl_Position = vec4(-1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ case 1:
+ {
+ gl_Position = vec4(1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ case 2:
+ {
+ gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ case 3:
+ {
+ gl_Position = vec4(1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ }
+}
+
+`,
+ HLSL: "DXBC\x99\xb4[\xef]IX\xa2Qh\x9f\xb6!\x1cR\xe7\x01\x00\x00\x00\xc0\x02\x00\x00\x05\x00\x00\x004\x00\x00\x00\x80\x00\x00\x00\xb4\x00\x00\x00\xe8\x00\x00\x00D\x02\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00SV_VertexID\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00SHDRT\x01\x00\x00@\x00\x01\x00U\x00\x00\x00`\x00\x00\x04\x12\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00L\x00\x00\x03\n\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x03\x01@\x00\x00\x00\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x01\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80?\x00\x00\x80?\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x02\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x03\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80?\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\n\x00\x00\x016\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\x17\x00\x00\x016\x00\x00\x05\xb2 \x10\x00\x00\x00\x00\x00F\b\x10\x00\x00\x00\x00\x006\x00\x00\x05B \x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ }
+ shader_cover_frag = [...]driver.ShaderSources{
+ {
+ Name: "cover.frag",
+ Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Color", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_color.color", Type: 0x0, Size: 4, Offset: 0}},
+ Size: 16,
+ },
+ Textures: []driver.TextureBinding{{Name: "cover", Binding: 1}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+struct Color
+{
+ vec4 color;
+};
+
+uniform Color _color;
+
+uniform mediump sampler2D cover;
+
+varying highp vec2 vCoverUV;
+varying vec2 vUV;
+
+void main()
+{
+ gl_FragData[0] = _color.color;
+ float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0);
+ gl_FragData[0] *= cover_1;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+layout(std140) uniform Color
+{
+ vec4 color;
+} _color;
+
+uniform mediump sampler2D cover;
+
+layout(location = 0) out vec4 fragColor;
+in highp vec2 vCoverUV;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = _color.color;
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct Color
+{
+ vec4 color;
+};
+
+uniform Color _color;
+
+layout(binding = 1) uniform sampler2D cover;
+
+out vec4 fragColor;
+in vec2 vCoverUV;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = _color.color;
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0, std140) uniform Color
+{
+ vec4 color;
+} _color;
+
+layout(binding = 1) uniform sampler2D cover;
+
+out vec4 fragColor;
+in vec2 vCoverUV;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = _color.color;
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ HLSL: "DXBC\x88\x01{\x0f\x94\xca3\xeb\xabßø\xa1\xbfL1\xbf\x01\x00\x00\x00\xa4\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xcc\x00\x00\x00\x90\x01\x00\x00\f\x02\x00\x00$\x03\x00\x00p\x03\x00\x00Aon9\x8c\x00\x00\x00\x8c\x00\x00\x00\x00\x02\xff\xffX\x00\x00\x004\x00\x00\x00\x01\x00(\x00\x00\x004\x00\x00\x004\x00\x01\x00$\x00\x00\x004\x00\x01\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0#\x00\x00\x02\x00\x00\x11\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\x00\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xbc\x00\x00\x00@\x00\x00\x00/\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?8\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x10\x01\x00\x00\x01\x00\x00\x00\x98\x00\x00\x00\x03\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xe8\x00\x00\x00|\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x8b\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\x91\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00_cover_sampler\x00cover\x00Color\x00\xab\x91\x00\x00\x00\x01\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd8\x00\x00\x00\x00\x00\x00\x00_color_color\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ },
+ {
+ Name: "cover.frag",
+ Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Gradient", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_gradient.color1", Type: 0x0, Size: 4, Offset: 0}, {Name: "_gradient.color2", Type: 0x0, Size: 4, Offset: 16}},
+ Size: 32,
+ },
+ Textures: []driver.TextureBinding{{Name: "cover", Binding: 1}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+struct Gradient
+{
+ vec4 color1;
+ vec4 color2;
+};
+
+uniform Gradient _gradient;
+
+uniform mediump sampler2D cover;
+
+varying vec2 vUV;
+varying highp vec2 vCoverUV;
+
+void main()
+{
+ gl_FragData[0] = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+ float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0);
+ gl_FragData[0] *= cover_1;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+layout(std140) uniform Gradient
+{
+ vec4 color1;
+ vec4 color2;
+} _gradient;
+
+uniform mediump sampler2D cover;
+
+layout(location = 0) out vec4 fragColor;
+in vec2 vUV;
+in highp vec2 vCoverUV;
+
+void main()
+{
+ fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct Gradient
+{
+ vec4 color1;
+ vec4 color2;
+};
+
+uniform Gradient _gradient;
+
+layout(binding = 1) uniform sampler2D cover;
+
+out vec4 fragColor;
+in vec2 vUV;
+in vec2 vCoverUV;
+
+void main()
+{
+ fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0, std140) uniform Gradient
+{
+ vec4 color1;
+ vec4 color2;
+} _gradient;
+
+layout(binding = 1) uniform sampler2D cover;
+
+out vec4 fragColor;
+in vec2 vUV;
+in vec2 vCoverUV;
+
+void main()
+{
+ fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ HLSL: "DXBCj\xa0\x9e\x8d\x1eĆO\rJ\xea\x8f\x17\x11o\x98\x01\x00\x00\x00\x80\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00\b\x01\x00\x008\x02\x00\x00\xb4\x02\x00\x00\x00\x04\x00\x00L\x04\x00\x00Aon9\xc8\x00\x00\x00\xc8\x00\x00\x00\x00\x02\xff\xff\x94\x00\x00\x004\x00\x00\x00\x01\x00(\x00\x00\x004\x00\x00\x004\x00\x01\x00$\x00\x00\x004\x00\x01\x01\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x01\x00\x00\x02\x00\x00\x12\x80\x00\x00\xff\xb0\x01\x00\x00\x02\x01\x00\x0f\x80\x00\x00\xe4\xa0\x02\x00\x00\x03\x01\x00\x0f\x80\x01\x00\xe4\x81\x01\x00\xe4\xa0\x04\x00\x00\x04\x01\x00\x0f\x80\x00\x00U\x80\x01\x00\xe4\x80\x00\x00\xe4\xa0#\x00\x00\x02\x00\x00\x11\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\x00\x80\x01\x00\xe4\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR(\x01\x00\x00@\x00\x00\x00J\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03B\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006 \x00\x05\x12\x00\x10\x00\x00\x00\x00\x00*\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x8e \x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x002\x00\x00\n\xf2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?8\x00\x00\a\xf2 \x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x01\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\a\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x01\x00\x00\x01\x00\x00\x00\x9c\x00\x00\x00\x03\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x19\x01\x00\x00|\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x8b\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\x91\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00_cover_sampler\x00cover\x00Gradient\x00\xab\xab\x91\x00\x00\x00\x02\x00\x00\x00\xb4\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x00\x00\b\x01\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x00\x00_gradient_color1\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_gradient_color2\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x04\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ },
+ {
+ Name: "cover.frag",
+ Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}, {Name: "cover", Binding: 1}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+uniform mediump sampler2D cover;
+
+varying vec2 vUV;
+varying highp vec2 vCoverUV;
+
+void main()
+{
+ gl_FragData[0] = texture2D(tex, vUV);
+ float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0);
+ gl_FragData[0] *= cover_1;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+uniform mediump sampler2D cover;
+
+layout(location = 0) out vec4 fragColor;
+in vec2 vUV;
+in highp vec2 vCoverUV;
+
+void main()
+{
+ fragColor = texture(tex, vUV);
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+layout(binding = 1) uniform sampler2D cover;
+
+out vec4 fragColor;
+in vec2 vUV;
+in vec2 vCoverUV;
+
+void main()
+{
+ fragColor = texture(tex, vUV);
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+layout(binding = 1) uniform sampler2D cover;
+
+out vec4 fragColor;
+in vec2 vUV;
+in vec2 vCoverUV;
+
+void main()
+{
+ fragColor = texture(tex, vUV);
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ HLSL: "DXBC\x99\x16l`\xf6:k\xa2Y$\xa1,\xfd\xcdJE\x01\x00\x00\x00\xd8\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xec\x00\x00\x00\xe8\x01\x00\x00d\x02\x00\x00X\x03\x00\x00\xa4\x03\x00\x00Aon9\xac\x00\x00\x00\xac\x00\x00\x00\x00\x02\xff\xff\x80\x00\x00\x00,\x00\x00\x00\x00\x00,\x00\x00\x00,\x00\x00\x00,\x00\x02\x00$\x00\x00\x00,\x00\x00\x00\x00\x00\x01\x01\x01\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0\x1f\x00\x00\x02\x00\x00\x00\x90\x01\b\x0f\xa0\x01\x00\x00\x02\x00\x00\x03\x80\x00\x00\x1b\xb0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\x80\x00\b\xe4\xa0B\x00\x00\x03\x01\x00\x0f\x80\x00\x00\xe4\xb0\x01\b\xe4\xa0#\x00\x00\x02\x01\x00\x11\x80\x01\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\x80\x01\x00\x00\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xf4\x00\x00\x00@\x00\x00\x00=\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03\xc2\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?E\x00\x00\t\xf2\x00\x10\x00\x01\x00\x00\x00\xe6\x1a\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x008\x00\x00\a\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x05\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xec\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xc2\x00\x00\x00\x9c\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xa9\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xb8\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\xbc\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00_cover_sampler\x00tex\x00cover\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\f\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ },
+ }
+ shader_cover_vert = driver.ShaderSources{
+ Name: "cover.vert",
+ Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.uvCoverTransform", Type: 0x0, Size: 4, Offset: 16}, {Name: "_block.uvTransformR1", Type: 0x0, Size: 4, Offset: 32}, {Name: "_block.uvTransformR2", Type: 0x0, Size: 4, Offset: 48}, {Name: "_block.z", Type: 0x0, Size: 1, Offset: 64}},
+ Size: 68,
+ },
+ GLSL100ES: `#version 100
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+struct Block
+{
+ vec4 transform;
+ vec4 uvCoverTransform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+};
+
+uniform Block _block;
+
+attribute vec2 pos;
+varying vec2 vUV;
+attribute vec2 uv;
+varying vec2 vCoverUV;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+ m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_4 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_3, param_4);
+ vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+layout(std140) uniform Block
+{
+ vec4 transform;
+ vec4 uvCoverTransform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+} _block;
+
+layout(location = 0) in vec2 pos;
+out vec2 vUV;
+layout(location = 1) in vec2 uv;
+out vec2 vCoverUV;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+ m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_4 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_3, param_4);
+ vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+struct Block
+{
+ vec4 transform;
+ vec4 uvCoverTransform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+};
+
+uniform Block _block;
+
+in vec2 pos;
+out vec2 vUV;
+in vec2 uv;
+out vec2 vCoverUV;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+ m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_4 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_3, param_4);
+ vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+layout(binding = 0, std140) uniform Block
+{
+ vec4 transform;
+ vec4 uvCoverTransform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+} _block;
+
+in vec2 pos;
+out vec2 vUV;
+in vec2 uv;
+out vec2 vCoverUV;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+ m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_4 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_3, param_4);
+ vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy;
+}
+
+`,
+ HLSL: "DXBCx\xefn{F\v\x88%\xc6\x05\x8f4h\xe4\xaaP\x01\x00\x00\x00\xd8\x05\x00\x00\x06\x00\x00\x008\x00\x00\x00x\x01\x00\x00\x1c\x03\x00\x00\x98\x03\x00\x00\x1c\x05\x00\x00h\x05\x00\x00Aon98\x01\x00\x008\x01\x00\x00\x00\x02\xfe\xff\x04\x01\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x05\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x06\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x80\xbf\x00\x00\x00?\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\a\x80\x01\x00Ä\x06\x00Š \x06\x00Å \b\x00\x00\x03\x00\x00\b\xe0\x03\x00\xe4\xa0\x00\x00\xe4\x80\b\x00\x00\x03\x00\x00\x04\xe0\x04\x00\xe4\xa0\x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\x80\x01\x00\xe1\x90\x06\x00\xe4\xa0\x06\x00\xe1\xa0\x05\x00\x00\x03\x00\x00\x03\x80\x00\x00\xe4\x80\x06\x00\xe2\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x01\x80\x01\x00\x00\x90\x04\x00\x00\x04\x00\x00\x03\xe0\x00\x00\xe4\x80\x02\x00\xe4\xa0\x02\x00\xee\xa0\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x90\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\v\x80\x06\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\f\xc0\x05\x00\x00\xa0\x00\x00t\x80\x00\x004\x80\xff\xff\x00\x00SHDR\x9c\x01\x00\x00@\x00\x01\x00g\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x05\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00e\x00\x00\x03\xc2 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006\x00\x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x006\x00\x00\x052\x00\x10\x00\x01\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x0f\x00\x00\n\"\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x96\x05\x10\x00\x01\x00\x00\x002\x00\x00\v2 \x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x01\x00\x00\x00\x10\x00\x00\bB \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x10\x00\x00\b\x82 \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x03\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x002\x00\x00\v2 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\nB \x10\x00\x01\x00\x00\x00\n\x80 \x00\x00\x00\x00\x00\x04\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\x01@\x00\x00\x00\x00\x00?6\x00\x00\x05\x82 \x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\v\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF|\x01\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00T\x01\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x05\x00\x00\x00\\\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00\xf8\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00\x10\x01\x00\x00 \x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00%\x01\x00\x000\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00:\x01\x00\x00@\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00D\x01\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_uvCoverTransform\x00_block_uvTransformR1\x00_block_uvTransformR2\x00_block_z\x00\xab\x00\x00\x03\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNh\x00\x00\x00\x03\x00\x00\x00\b\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00P\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x03\x00\x00Y\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab",
+ }
+ shader_elements_comp = driver.ShaderSources{
+ Name: "elements.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct ElementRef
+{
+ uint offset;
+};
+
+struct LineSegRef
+{
+ uint offset;
+};
+
+struct LineSeg
+{
+ vec2 p0;
+ vec2 p1;
+};
+
+struct QuadSegRef
+{
+ uint offset;
+};
+
+struct QuadSeg
+{
+ vec2 p0;
+ vec2 p1;
+ vec2 p2;
+};
+
+struct CubicSegRef
+{
+ uint offset;
+};
+
+struct CubicSeg
+{
+ vec2 p0;
+ vec2 p1;
+ vec2 p2;
+ vec2 p3;
+};
+
+struct FillColorRef
+{
+ uint offset;
+};
+
+struct FillColor
+{
+ uint rgba_color;
+};
+
+struct FillImageRef
+{
+ uint offset;
+};
+
+struct FillImage
+{
+ uint index;
+ ivec2 offset;
+};
+
+struct SetLineWidthRef
+{
+ uint offset;
+};
+
+struct SetLineWidth
+{
+ float width;
+};
+
+struct TransformRef
+{
+ uint offset;
+};
+
+struct Transform
+{
+ vec4 mat;
+ vec2 translate;
+};
+
+struct ClipRef
+{
+ uint offset;
+};
+
+struct Clip
+{
+ vec4 bbox;
+};
+
+struct SetFillModeRef
+{
+ uint offset;
+};
+
+struct SetFillMode
+{
+ uint fill_mode;
+};
+
+struct ElementTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct StateRef
+{
+ uint offset;
+};
+
+struct State
+{
+ vec4 mat;
+ vec2 translate;
+ vec4 bbox;
+ float linewidth;
+ uint flags;
+ uint path_count;
+ uint pathseg_count;
+ uint trans_count;
+};
+
+struct AnnoImageRef
+{
+ uint offset;
+};
+
+struct AnnoImage
+{
+ vec4 bbox;
+ float linewidth;
+ uint index;
+ ivec2 offset;
+};
+
+struct AnnoColorRef
+{
+ uint offset;
+};
+
+struct AnnoColor
+{
+ vec4 bbox;
+ float linewidth;
+ uint rgba_color;
+};
+
+struct AnnoBeginClipRef
+{
+ uint offset;
+};
+
+struct AnnoBeginClip
+{
+ vec4 bbox;
+ float linewidth;
+};
+
+struct AnnoEndClipRef
+{
+ uint offset;
+};
+
+struct AnnoEndClip
+{
+ vec4 bbox;
+};
+
+struct AnnotatedRef
+{
+ uint offset;
+};
+
+struct PathCubicRef
+{
+ uint offset;
+};
+
+struct PathCubic
+{
+ vec2 p0;
+ vec2 p1;
+ vec2 p2;
+ vec2 p3;
+ uint path_ix;
+ uint trans_ix;
+ vec2 stroke;
+};
+
+struct PathSegRef
+{
+ uint offset;
+};
+
+struct TransformSegRef
+{
+ uint offset;
+};
+
+struct TransformSeg
+{
+ vec4 mat;
+ vec2 translate;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _294;
+
+layout(binding = 2, std430) readonly buffer SceneBuf
+{
+ uint scene[];
+} _323;
+
+layout(binding = 3, std430) coherent buffer StateBuf
+{
+ uint part_counter;
+ uint state[];
+} _779;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _2435;
+
+shared uint sh_part_ix;
+shared State sh_state[32];
+shared State sh_prefix;
+
+ElementTag Element_tag(ElementRef ref)
+{
+ uint tag_and_flags = _323.scene[ref.offset >> uint(2)];
+ return ElementTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+LineSeg LineSeg_read(LineSegRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ uint raw1 = _323.scene[ix + 1u];
+ uint raw2 = _323.scene[ix + 2u];
+ uint raw3 = _323.scene[ix + 3u];
+ LineSeg s;
+ s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1));
+ s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ return s;
+}
+
+LineSeg Element_Line_read(ElementRef ref)
+{
+ LineSegRef param = LineSegRef(ref.offset + 4u);
+ return LineSeg_read(param);
+}
+
+QuadSeg QuadSeg_read(QuadSegRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ uint raw1 = _323.scene[ix + 1u];
+ uint raw2 = _323.scene[ix + 2u];
+ uint raw3 = _323.scene[ix + 3u];
+ uint raw4 = _323.scene[ix + 4u];
+ uint raw5 = _323.scene[ix + 5u];
+ QuadSeg s;
+ s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1));
+ s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5));
+ return s;
+}
+
+QuadSeg Element_Quad_read(ElementRef ref)
+{
+ QuadSegRef param = QuadSegRef(ref.offset + 4u);
+ return QuadSeg_read(param);
+}
+
+CubicSeg CubicSeg_read(CubicSegRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ uint raw1 = _323.scene[ix + 1u];
+ uint raw2 = _323.scene[ix + 2u];
+ uint raw3 = _323.scene[ix + 3u];
+ uint raw4 = _323.scene[ix + 4u];
+ uint raw5 = _323.scene[ix + 5u];
+ uint raw6 = _323.scene[ix + 6u];
+ uint raw7 = _323.scene[ix + 7u];
+ CubicSeg s;
+ s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1));
+ s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5));
+ s.p3 = vec2(uintBitsToFloat(raw6), uintBitsToFloat(raw7));
+ return s;
+}
+
+CubicSeg Element_Cubic_read(ElementRef ref)
+{
+ CubicSegRef param = CubicSegRef(ref.offset + 4u);
+ return CubicSeg_read(param);
+}
+
+SetLineWidth SetLineWidth_read(SetLineWidthRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ SetLineWidth s;
+ s.width = uintBitsToFloat(raw0);
+ return s;
+}
+
+SetLineWidth Element_SetLineWidth_read(ElementRef ref)
+{
+ SetLineWidthRef param = SetLineWidthRef(ref.offset + 4u);
+ return SetLineWidth_read(param);
+}
+
+Transform Transform_read(TransformRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ uint raw1 = _323.scene[ix + 1u];
+ uint raw2 = _323.scene[ix + 2u];
+ uint raw3 = _323.scene[ix + 3u];
+ uint raw4 = _323.scene[ix + 4u];
+ uint raw5 = _323.scene[ix + 5u];
+ Transform s;
+ s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5));
+ return s;
+}
+
+Transform Element_Transform_read(ElementRef ref)
+{
+ TransformRef param = TransformRef(ref.offset + 4u);
+ return Transform_read(param);
+}
+
+SetFillMode SetFillMode_read(SetFillModeRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ SetFillMode s;
+ s.fill_mode = raw0;
+ return s;
+}
+
+SetFillMode Element_SetFillMode_read(ElementRef ref)
+{
+ SetFillModeRef param = SetFillModeRef(ref.offset + 4u);
+ return SetFillMode_read(param);
+}
+
+State map_element(ElementRef ref)
+{
+ ElementRef param = ref;
+ uint tag = Element_tag(param).tag;
+ State c;
+ c.bbox = vec4(0.0);
+ c.mat = vec4(1.0, 0.0, 0.0, 1.0);
+ c.translate = vec2(0.0);
+ c.linewidth = 1.0;
+ c.flags = 0u;
+ c.path_count = 0u;
+ c.pathseg_count = 0u;
+ c.trans_count = 0u;
+ switch (tag)
+ {
+ case 1u:
+ {
+ ElementRef param_1 = ref;
+ LineSeg line = Element_Line_read(param_1);
+ vec2 _1919 = min(line.p0, line.p1);
+ c.bbox = vec4(_1919.x, _1919.y, c.bbox.z, c.bbox.w);
+ vec2 _1927 = max(line.p0, line.p1);
+ c.bbox = vec4(c.bbox.x, c.bbox.y, _1927.x, _1927.y);
+ c.pathseg_count = 1u;
+ break;
+ }
+ case 2u:
+ {
+ ElementRef param_2 = ref;
+ QuadSeg quad = Element_Quad_read(param_2);
+ vec2 _1944 = min(min(quad.p0, quad.p1), quad.p2);
+ c.bbox = vec4(_1944.x, _1944.y, c.bbox.z, c.bbox.w);
+ vec2 _1955 = max(max(quad.p0, quad.p1), quad.p2);
+ c.bbox = vec4(c.bbox.x, c.bbox.y, _1955.x, _1955.y);
+ c.pathseg_count = 1u;
+ break;
+ }
+ case 3u:
+ {
+ ElementRef param_3 = ref;
+ CubicSeg cubic = Element_Cubic_read(param_3);
+ vec2 _1975 = min(min(cubic.p0, cubic.p1), min(cubic.p2, cubic.p3));
+ c.bbox = vec4(_1975.x, _1975.y, c.bbox.z, c.bbox.w);
+ vec2 _1989 = max(max(cubic.p0, cubic.p1), max(cubic.p2, cubic.p3));
+ c.bbox = vec4(c.bbox.x, c.bbox.y, _1989.x, _1989.y);
+ c.pathseg_count = 1u;
+ break;
+ }
+ case 4u:
+ case 9u:
+ case 7u:
+ {
+ c.flags = 4u;
+ c.path_count = 1u;
+ break;
+ }
+ case 8u:
+ {
+ c.path_count = 1u;
+ break;
+ }
+ case 5u:
+ {
+ ElementRef param_4 = ref;
+ SetLineWidth lw = Element_SetLineWidth_read(param_4);
+ c.linewidth = lw.width;
+ c.flags = 1u;
+ break;
+ }
+ case 6u:
+ {
+ ElementRef param_5 = ref;
+ Transform t = Element_Transform_read(param_5);
+ c.mat = t.mat;
+ c.translate = t.translate;
+ c.trans_count = 1u;
+ break;
+ }
+ case 10u:
+ {
+ ElementRef param_6 = ref;
+ SetFillMode fm = Element_SetFillMode_read(param_6);
+ c.flags = 8u | (fm.fill_mode << uint(4));
+ break;
+ }
+ }
+ return c;
+}
+
+ElementRef Element_index(ElementRef ref, uint index)
+{
+ return ElementRef(ref.offset + (index * 36u));
+}
+
+State combine_state(State a, State b)
+{
+ State c;
+ c.bbox.x = (min(a.mat.x * b.bbox.x, a.mat.x * b.bbox.z) + min(a.mat.z * b.bbox.y, a.mat.z * b.bbox.w)) + a.translate.x;
+ c.bbox.y = (min(a.mat.y * b.bbox.x, a.mat.y * b.bbox.z) + min(a.mat.w * b.bbox.y, a.mat.w * b.bbox.w)) + a.translate.y;
+ c.bbox.z = (max(a.mat.x * b.bbox.x, a.mat.x * b.bbox.z) + max(a.mat.z * b.bbox.y, a.mat.z * b.bbox.w)) + a.translate.x;
+ c.bbox.w = (max(a.mat.y * b.bbox.x, a.mat.y * b.bbox.z) + max(a.mat.w * b.bbox.y, a.mat.w * b.bbox.w)) + a.translate.y;
+ bool _1657 = (a.flags & 4u) == 0u;
+ bool _1665;
+ if (_1657)
+ {
+ _1665 = b.bbox.z <= b.bbox.x;
+ }
+ else
+ {
+ _1665 = _1657;
+ }
+ bool _1673;
+ if (_1665)
+ {
+ _1673 = b.bbox.w <= b.bbox.y;
+ }
+ else
+ {
+ _1673 = _1665;
+ }
+ if (_1673)
+ {
+ c.bbox = a.bbox;
+ }
+ else
+ {
+ bool _1683 = (a.flags & 4u) == 0u;
+ bool _1690;
+ if (_1683)
+ {
+ _1690 = (b.flags & 2u) == 0u;
+ }
+ else
+ {
+ _1690 = _1683;
+ }
+ bool _1707;
+ if (_1690)
+ {
+ bool _1697 = a.bbox.z > a.bbox.x;
+ bool _1706;
+ if (!_1697)
+ {
+ _1706 = a.bbox.w > a.bbox.y;
+ }
+ else
+ {
+ _1706 = _1697;
+ }
+ _1707 = _1706;
+ }
+ else
+ {
+ _1707 = _1690;
+ }
+ if (_1707)
+ {
+ vec2 _1716 = min(a.bbox.xy, c.bbox.xy);
+ c.bbox = vec4(_1716.x, _1716.y, c.bbox.z, c.bbox.w);
+ vec2 _1726 = max(a.bbox.zw, c.bbox.zw);
+ c.bbox = vec4(c.bbox.x, c.bbox.y, _1726.x, _1726.y);
+ }
+ }
+ c.mat.x = (a.mat.x * b.mat.x) + (a.mat.z * b.mat.y);
+ c.mat.y = (a.mat.y * b.mat.x) + (a.mat.w * b.mat.y);
+ c.mat.z = (a.mat.x * b.mat.z) + (a.mat.z * b.mat.w);
+ c.mat.w = (a.mat.y * b.mat.z) + (a.mat.w * b.mat.w);
+ c.translate.x = ((a.mat.x * b.translate.x) + (a.mat.z * b.translate.y)) + a.translate.x;
+ c.translate.y = ((a.mat.y * b.translate.x) + (a.mat.w * b.translate.y)) + a.translate.y;
+ float _1812;
+ if ((b.flags & 1u) == 0u)
+ {
+ _1812 = a.linewidth;
+ }
+ else
+ {
+ _1812 = b.linewidth;
+ }
+ c.linewidth = _1812;
+ c.flags = (a.flags & 11u) | b.flags;
+ c.flags |= ((a.flags & 4u) >> uint(1));
+ uint _1842;
+ if ((b.flags & 8u) == 0u)
+ {
+ _1842 = a.flags;
+ }
+ else
+ {
+ _1842 = b.flags;
+ }
+ uint fill_mode = _1842;
+ fill_mode &= 16u;
+ c.flags = (c.flags & 4294967279u) | fill_mode;
+ c.path_count = a.path_count + b.path_count;
+ c.pathseg_count = a.pathseg_count + b.pathseg_count;
+ c.trans_count = a.trans_count + b.trans_count;
+ return c;
+}
+
+StateRef state_aggregate_ref(uint partition_ix)
+{
+ return StateRef(4u + (partition_ix * 124u));
+}
+
+void State_write(StateRef ref, State s)
+{
+ uint ix = ref.offset >> uint(2);
+ _779.state[ix + 0u] = floatBitsToUint(s.mat.x);
+ _779.state[ix + 1u] = floatBitsToUint(s.mat.y);
+ _779.state[ix + 2u] = floatBitsToUint(s.mat.z);
+ _779.state[ix + 3u] = floatBitsToUint(s.mat.w);
+ _779.state[ix + 4u] = floatBitsToUint(s.translate.x);
+ _779.state[ix + 5u] = floatBitsToUint(s.translate.y);
+ _779.state[ix + 6u] = floatBitsToUint(s.bbox.x);
+ _779.state[ix + 7u] = floatBitsToUint(s.bbox.y);
+ _779.state[ix + 8u] = floatBitsToUint(s.bbox.z);
+ _779.state[ix + 9u] = floatBitsToUint(s.bbox.w);
+ _779.state[ix + 10u] = floatBitsToUint(s.linewidth);
+ _779.state[ix + 11u] = s.flags;
+ _779.state[ix + 12u] = s.path_count;
+ _779.state[ix + 13u] = s.pathseg_count;
+ _779.state[ix + 14u] = s.trans_count;
+}
+
+StateRef state_prefix_ref(uint partition_ix)
+{
+ return StateRef((4u + (partition_ix * 124u)) + 60u);
+}
+
+uint state_flag_index(uint partition_ix)
+{
+ return partition_ix * 31u;
+}
+
+State State_read(StateRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _779.state[ix + 0u];
+ uint raw1 = _779.state[ix + 1u];
+ uint raw2 = _779.state[ix + 2u];
+ uint raw3 = _779.state[ix + 3u];
+ uint raw4 = _779.state[ix + 4u];
+ uint raw5 = _779.state[ix + 5u];
+ uint raw6 = _779.state[ix + 6u];
+ uint raw7 = _779.state[ix + 7u];
+ uint raw8 = _779.state[ix + 8u];
+ uint raw9 = _779.state[ix + 9u];
+ uint raw10 = _779.state[ix + 10u];
+ uint raw11 = _779.state[ix + 11u];
+ uint raw12 = _779.state[ix + 12u];
+ uint raw13 = _779.state[ix + 13u];
+ uint raw14 = _779.state[ix + 14u];
+ State s;
+ s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5));
+ s.bbox = vec4(uintBitsToFloat(raw6), uintBitsToFloat(raw7), uintBitsToFloat(raw8), uintBitsToFloat(raw9));
+ s.linewidth = uintBitsToFloat(raw10);
+ s.flags = raw11;
+ s.path_count = raw12;
+ s.pathseg_count = raw13;
+ s.trans_count = raw14;
+ return s;
+}
+
+uint fill_mode_from_flags(uint flags)
+{
+ return flags & 1u;
+}
+
+vec2 get_linewidth(State st)
+{
+ return vec2(length(st.mat.xz), length(st.mat.yw)) * (0.5 * st.linewidth);
+}
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _294.memory[offset] = val;
+}
+
+void PathCubic_write(Alloc a, PathCubicRef ref, PathCubic s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.p0.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.p0.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.p1.x);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.p1.y);
+ write_mem(param_9, param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 4u;
+ uint param_14 = floatBitsToUint(s.p2.x);
+ write_mem(param_12, param_13, param_14);
+ Alloc param_15 = a;
+ uint param_16 = ix + 5u;
+ uint param_17 = floatBitsToUint(s.p2.y);
+ write_mem(param_15, param_16, param_17);
+ Alloc param_18 = a;
+ uint param_19 = ix + 6u;
+ uint param_20 = floatBitsToUint(s.p3.x);
+ write_mem(param_18, param_19, param_20);
+ Alloc param_21 = a;
+ uint param_22 = ix + 7u;
+ uint param_23 = floatBitsToUint(s.p3.y);
+ write_mem(param_21, param_22, param_23);
+ Alloc param_24 = a;
+ uint param_25 = ix + 8u;
+ uint param_26 = s.path_ix;
+ write_mem(param_24, param_25, param_26);
+ Alloc param_27 = a;
+ uint param_28 = ix + 9u;
+ uint param_29 = s.trans_ix;
+ write_mem(param_27, param_28, param_29);
+ Alloc param_30 = a;
+ uint param_31 = ix + 10u;
+ uint param_32 = floatBitsToUint(s.stroke.x);
+ write_mem(param_30, param_31, param_32);
+ Alloc param_33 = a;
+ uint param_34 = ix + 11u;
+ uint param_35 = floatBitsToUint(s.stroke.y);
+ write_mem(param_33, param_34, param_35);
+}
+
+void PathSeg_Cubic_write(Alloc a, PathSegRef ref, uint flags, PathCubic s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = (flags << uint(16)) | 1u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ PathCubicRef param_4 = PathCubicRef(ref.offset + 4u);
+ PathCubic param_5 = s;
+ PathCubic_write(param_3, param_4, param_5);
+}
+
+FillColor FillColor_read(FillColorRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ FillColor s;
+ s.rgba_color = raw0;
+ return s;
+}
+
+FillColor Element_FillColor_read(ElementRef ref)
+{
+ FillColorRef param = FillColorRef(ref.offset + 4u);
+ return FillColor_read(param);
+}
+
+void AnnoColor_write(Alloc a, AnnoColorRef ref, AnnoColor s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.bbox.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.bbox.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.bbox.z);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.bbox.w);
+ write_mem(param_9, param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 4u;
+ uint param_14 = floatBitsToUint(s.linewidth);
+ write_mem(param_12, param_13, param_14);
+ Alloc param_15 = a;
+ uint param_16 = ix + 5u;
+ uint param_17 = s.rgba_color;
+ write_mem(param_15, param_16, param_17);
+}
+
+void Annotated_Color_write(Alloc a, AnnotatedRef ref, uint flags, AnnoColor s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = (flags << uint(16)) | 1u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ AnnoColorRef param_4 = AnnoColorRef(ref.offset + 4u);
+ AnnoColor param_5 = s;
+ AnnoColor_write(param_3, param_4, param_5);
+}
+
+FillImage FillImage_read(FillImageRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ uint raw1 = _323.scene[ix + 1u];
+ FillImage s;
+ s.index = raw0;
+ s.offset = ivec2(int(raw1 << uint(16)) >> 16, int(raw1) >> 16);
+ return s;
+}
+
+FillImage Element_FillImage_read(ElementRef ref)
+{
+ FillImageRef param = FillImageRef(ref.offset + 4u);
+ return FillImage_read(param);
+}
+
+void AnnoImage_write(Alloc a, AnnoImageRef ref, AnnoImage s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.bbox.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.bbox.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.bbox.z);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.bbox.w);
+ write_mem(param_9, param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 4u;
+ uint param_14 = floatBitsToUint(s.linewidth);
+ write_mem(param_12, param_13, param_14);
+ Alloc param_15 = a;
+ uint param_16 = ix + 5u;
+ uint param_17 = s.index;
+ write_mem(param_15, param_16, param_17);
+ Alloc param_18 = a;
+ uint param_19 = ix + 6u;
+ uint param_20 = (uint(s.offset.x) & 65535u) | (uint(s.offset.y) << uint(16));
+ write_mem(param_18, param_19, param_20);
+}
+
+void Annotated_Image_write(Alloc a, AnnotatedRef ref, uint flags, AnnoImage s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = (flags << uint(16)) | 2u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ AnnoImageRef param_4 = AnnoImageRef(ref.offset + 4u);
+ AnnoImage param_5 = s;
+ AnnoImage_write(param_3, param_4, param_5);
+}
+
+Clip Clip_read(ClipRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ uint raw1 = _323.scene[ix + 1u];
+ uint raw2 = _323.scene[ix + 2u];
+ uint raw3 = _323.scene[ix + 3u];
+ Clip s;
+ s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ return s;
+}
+
+Clip Element_BeginClip_read(ElementRef ref)
+{
+ ClipRef param = ClipRef(ref.offset + 4u);
+ return Clip_read(param);
+}
+
+void AnnoBeginClip_write(Alloc a, AnnoBeginClipRef ref, AnnoBeginClip s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.bbox.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.bbox.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.bbox.z);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.bbox.w);
+ write_mem(param_9, param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 4u;
+ uint param_14 = floatBitsToUint(s.linewidth);
+ write_mem(param_12, param_13, param_14);
+}
+
+void Annotated_BeginClip_write(Alloc a, AnnotatedRef ref, uint flags, AnnoBeginClip s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = (flags << uint(16)) | 3u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ AnnoBeginClipRef param_4 = AnnoBeginClipRef(ref.offset + 4u);
+ AnnoBeginClip param_5 = s;
+ AnnoBeginClip_write(param_3, param_4, param_5);
+}
+
+Clip Element_EndClip_read(ElementRef ref)
+{
+ ClipRef param = ClipRef(ref.offset + 4u);
+ return Clip_read(param);
+}
+
+void AnnoEndClip_write(Alloc a, AnnoEndClipRef ref, AnnoEndClip s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.bbox.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.bbox.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.bbox.z);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.bbox.w);
+ write_mem(param_9, param_10, param_11);
+}
+
+void Annotated_EndClip_write(Alloc a, AnnotatedRef ref, AnnoEndClip s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 4u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ AnnoEndClipRef param_4 = AnnoEndClipRef(ref.offset + 4u);
+ AnnoEndClip param_5 = s;
+ AnnoEndClip_write(param_3, param_4, param_5);
+}
+
+void TransformSeg_write(Alloc a, TransformSegRef ref, TransformSeg s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.mat.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.mat.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.mat.z);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.mat.w);
+ write_mem(param_9, param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 4u;
+ uint param_14 = floatBitsToUint(s.translate.x);
+ write_mem(param_12, param_13, param_14);
+ Alloc param_15 = a;
+ uint param_16 = ix + 5u;
+ uint param_17 = floatBitsToUint(s.translate.y);
+ write_mem(param_15, param_16, param_17);
+}
+
+void main()
+{
+ if (_294.mem_error != 0u)
+ {
+ return;
+ }
+ if (gl_LocalInvocationID.x == 0u)
+ {
+ uint _2069 = atomicAdd(_779.part_counter, 1u);
+ sh_part_ix = _2069;
+ }
+ barrier();
+ uint part_ix = sh_part_ix;
+ uint ix = (part_ix * 128u) + (gl_LocalInvocationID.x * 4u);
+ ElementRef ref = ElementRef(ix * 36u);
+ ElementRef param = ref;
+ State th_state[4];
+ th_state[0] = map_element(param);
+ for (uint i = 1u; i < 4u; i++)
+ {
+ ElementRef param_1 = ref;
+ uint param_2 = i;
+ ElementRef param_3 = Element_index(param_1, param_2);
+ State param_4 = th_state[i - 1u];
+ State param_5 = map_element(param_3);
+ th_state[i] = combine_state(param_4, param_5);
+ }
+ State agg = th_state[3];
+ sh_state[gl_LocalInvocationID.x] = agg;
+ for (uint i_1 = 0u; i_1 < 5u; i_1++)
+ {
+ barrier();
+ if (gl_LocalInvocationID.x >= uint(1 << int(i_1)))
+ {
+ State other = sh_state[gl_LocalInvocationID.x - uint(1 << int(i_1))];
+ State param_6 = other;
+ State param_7 = agg;
+ agg = combine_state(param_6, param_7);
+ }
+ barrier();
+ sh_state[gl_LocalInvocationID.x] = agg;
+ }
+ State exclusive;
+ exclusive.bbox = vec4(0.0);
+ exclusive.mat = vec4(1.0, 0.0, 0.0, 1.0);
+ exclusive.translate = vec2(0.0);
+ exclusive.linewidth = 1.0;
+ exclusive.flags = 0u;
+ exclusive.path_count = 0u;
+ exclusive.pathseg_count = 0u;
+ exclusive.trans_count = 0u;
+ if (gl_LocalInvocationID.x == 31u)
+ {
+ uint param_8 = part_ix;
+ StateRef param_9 = state_aggregate_ref(param_8);
+ State param_10 = agg;
+ State_write(param_9, param_10);
+ uint flag = 1u;
+ memoryBarrierBuffer();
+ if (part_ix == 0u)
+ {
+ uint param_11 = part_ix;
+ StateRef param_12 = state_prefix_ref(param_11);
+ State param_13 = agg;
+ State_write(param_12, param_13);
+ flag = 2u;
+ }
+ uint param_14 = part_ix;
+ _779.state[state_flag_index(param_14)] = flag;
+ if (part_ix != 0u)
+ {
+ uint look_back_ix = part_ix - 1u;
+ uint their_ix = 0u;
+ State their_agg;
+ while (true)
+ {
+ uint param_15 = look_back_ix;
+ flag = _779.state[state_flag_index(param_15)];
+ if (flag == 2u)
+ {
+ uint param_16 = look_back_ix;
+ StateRef param_17 = state_prefix_ref(param_16);
+ State their_prefix = State_read(param_17);
+ State param_18 = their_prefix;
+ State param_19 = exclusive;
+ exclusive = combine_state(param_18, param_19);
+ break;
+ }
+ else
+ {
+ if (flag == 1u)
+ {
+ uint param_20 = look_back_ix;
+ StateRef param_21 = state_aggregate_ref(param_20);
+ their_agg = State_read(param_21);
+ State param_22 = their_agg;
+ State param_23 = exclusive;
+ exclusive = combine_state(param_22, param_23);
+ look_back_ix--;
+ their_ix = 0u;
+ continue;
+ }
+ }
+ ElementRef ref_1 = ElementRef(((look_back_ix * 128u) + their_ix) * 36u);
+ ElementRef param_24 = ref_1;
+ State s = map_element(param_24);
+ if (their_ix == 0u)
+ {
+ their_agg = s;
+ }
+ else
+ {
+ State param_25 = their_agg;
+ State param_26 = s;
+ their_agg = combine_state(param_25, param_26);
+ }
+ their_ix++;
+ if (their_ix == 128u)
+ {
+ State param_27 = their_agg;
+ State param_28 = exclusive;
+ exclusive = combine_state(param_27, param_28);
+ if (look_back_ix == 0u)
+ {
+ break;
+ }
+ look_back_ix--;
+ their_ix = 0u;
+ }
+ }
+ State param_29 = exclusive;
+ State param_30 = agg;
+ State inclusive_prefix = combine_state(param_29, param_30);
+ sh_prefix = exclusive;
+ uint param_31 = part_ix;
+ StateRef param_32 = state_prefix_ref(param_31);
+ State param_33 = inclusive_prefix;
+ State_write(param_32, param_33);
+ memoryBarrierBuffer();
+ flag = 2u;
+ uint param_34 = part_ix;
+ _779.state[state_flag_index(param_34)] = flag;
+ }
+ }
+ barrier();
+ if (part_ix != 0u)
+ {
+ exclusive = sh_prefix;
+ }
+ State row = exclusive;
+ if (gl_LocalInvocationID.x > 0u)
+ {
+ State other_1 = sh_state[gl_LocalInvocationID.x - 1u];
+ State param_35 = row;
+ State param_36 = other_1;
+ row = combine_state(param_35, param_36);
+ }
+ PathCubic path_cubic;
+ PathSegRef path_out_ref;
+ Alloc param_45;
+ Alloc param_51;
+ Alloc param_57;
+ AnnoColor anno_fill;
+ AnnotatedRef out_ref;
+ Alloc param_63;
+ AnnoImage anno_img;
+ Alloc param_69;
+ AnnoBeginClip anno_begin_clip;
+ Alloc param_75;
+ Alloc param_80;
+ Alloc param_83;
+ for (uint i_2 = 0u; i_2 < 4u; i_2++)
+ {
+ State param_37 = row;
+ State param_38 = th_state[i_2];
+ State st = combine_state(param_37, param_38);
+ ElementRef param_39 = ref;
+ uint param_40 = i_2;
+ ElementRef this_ref = Element_index(param_39, param_40);
+ ElementRef param_41 = this_ref;
+ ElementTag tag = Element_tag(param_41);
+ uint param_42 = st.flags >> uint(4);
+ uint fill_mode = fill_mode_from_flags(param_42);
+ bool is_stroke = fill_mode == 1u;
+ switch (tag.tag)
+ {
+ case 1u:
+ {
+ ElementRef param_43 = this_ref;
+ LineSeg line = Element_Line_read(param_43);
+ path_cubic.p0 = line.p0;
+ path_cubic.p1 = mix(line.p0, line.p1, vec2(0.3333333432674407958984375));
+ path_cubic.p2 = mix(line.p1, line.p0, vec2(0.3333333432674407958984375));
+ path_cubic.p3 = line.p1;
+ path_cubic.path_ix = st.path_count;
+ path_cubic.trans_ix = st.trans_count;
+ if (is_stroke)
+ {
+ State param_44 = st;
+ path_cubic.stroke = get_linewidth(param_44);
+ }
+ else
+ {
+ path_cubic.stroke = vec2(0.0);
+ }
+ path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u));
+ param_45.offset = _2435.conf.pathseg_alloc.offset;
+ PathSegRef param_46 = path_out_ref;
+ uint param_47 = fill_mode;
+ PathCubic param_48 = path_cubic;
+ PathSeg_Cubic_write(param_45, param_46, param_47, param_48);
+ break;
+ }
+ case 2u:
+ {
+ ElementRef param_49 = this_ref;
+ QuadSeg quad = Element_Quad_read(param_49);
+ path_cubic.p0 = quad.p0;
+ path_cubic.p1 = mix(quad.p1, quad.p0, vec2(0.3333333432674407958984375));
+ path_cubic.p2 = mix(quad.p1, quad.p2, vec2(0.3333333432674407958984375));
+ path_cubic.p3 = quad.p2;
+ path_cubic.path_ix = st.path_count;
+ path_cubic.trans_ix = st.trans_count;
+ if (is_stroke)
+ {
+ State param_50 = st;
+ path_cubic.stroke = get_linewidth(param_50);
+ }
+ else
+ {
+ path_cubic.stroke = vec2(0.0);
+ }
+ path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u));
+ param_51.offset = _2435.conf.pathseg_alloc.offset;
+ PathSegRef param_52 = path_out_ref;
+ uint param_53 = fill_mode;
+ PathCubic param_54 = path_cubic;
+ PathSeg_Cubic_write(param_51, param_52, param_53, param_54);
+ break;
+ }
+ case 3u:
+ {
+ ElementRef param_55 = this_ref;
+ CubicSeg cubic = Element_Cubic_read(param_55);
+ path_cubic.p0 = cubic.p0;
+ path_cubic.p1 = cubic.p1;
+ path_cubic.p2 = cubic.p2;
+ path_cubic.p3 = cubic.p3;
+ path_cubic.path_ix = st.path_count;
+ path_cubic.trans_ix = st.trans_count;
+ if (is_stroke)
+ {
+ State param_56 = st;
+ path_cubic.stroke = get_linewidth(param_56);
+ }
+ else
+ {
+ path_cubic.stroke = vec2(0.0);
+ }
+ path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u));
+ param_57.offset = _2435.conf.pathseg_alloc.offset;
+ PathSegRef param_58 = path_out_ref;
+ uint param_59 = fill_mode;
+ PathCubic param_60 = path_cubic;
+ PathSeg_Cubic_write(param_57, param_58, param_59, param_60);
+ break;
+ }
+ case 4u:
+ {
+ ElementRef param_61 = this_ref;
+ FillColor fill = Element_FillColor_read(param_61);
+ anno_fill.rgba_color = fill.rgba_color;
+ if (is_stroke)
+ {
+ State param_62 = st;
+ vec2 lw = get_linewidth(param_62);
+ anno_fill.bbox = st.bbox + vec4(-lw, lw);
+ anno_fill.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z)));
+ }
+ else
+ {
+ anno_fill.bbox = st.bbox;
+ anno_fill.linewidth = 0.0;
+ }
+ out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u));
+ param_63.offset = _2435.conf.anno_alloc.offset;
+ AnnotatedRef param_64 = out_ref;
+ uint param_65 = fill_mode;
+ AnnoColor param_66 = anno_fill;
+ Annotated_Color_write(param_63, param_64, param_65, param_66);
+ break;
+ }
+ case 9u:
+ {
+ ElementRef param_67 = this_ref;
+ FillImage fill_img = Element_FillImage_read(param_67);
+ anno_img.index = fill_img.index;
+ anno_img.offset = fill_img.offset;
+ if (is_stroke)
+ {
+ State param_68 = st;
+ vec2 lw_1 = get_linewidth(param_68);
+ anno_img.bbox = st.bbox + vec4(-lw_1, lw_1);
+ anno_img.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z)));
+ }
+ else
+ {
+ anno_img.bbox = st.bbox;
+ anno_img.linewidth = 0.0;
+ }
+ out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u));
+ param_69.offset = _2435.conf.anno_alloc.offset;
+ AnnotatedRef param_70 = out_ref;
+ uint param_71 = fill_mode;
+ AnnoImage param_72 = anno_img;
+ Annotated_Image_write(param_69, param_70, param_71, param_72);
+ break;
+ }
+ case 7u:
+ {
+ ElementRef param_73 = this_ref;
+ Clip begin_clip = Element_BeginClip_read(param_73);
+ anno_begin_clip.bbox = begin_clip.bbox;
+ if (is_stroke)
+ {
+ State param_74 = st;
+ vec2 lw_2 = get_linewidth(param_74);
+ anno_begin_clip.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z)));
+ }
+ else
+ {
+ anno_fill.linewidth = 0.0;
+ }
+ out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u));
+ param_75.offset = _2435.conf.anno_alloc.offset;
+ AnnotatedRef param_76 = out_ref;
+ uint param_77 = fill_mode;
+ AnnoBeginClip param_78 = anno_begin_clip;
+ Annotated_BeginClip_write(param_75, param_76, param_77, param_78);
+ break;
+ }
+ case 8u:
+ {
+ ElementRef param_79 = this_ref;
+ Clip end_clip = Element_EndClip_read(param_79);
+ AnnoEndClip anno_end_clip = AnnoEndClip(end_clip.bbox);
+ out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u));
+ param_80.offset = _2435.conf.anno_alloc.offset;
+ AnnotatedRef param_81 = out_ref;
+ AnnoEndClip param_82 = anno_end_clip;
+ Annotated_EndClip_write(param_80, param_81, param_82);
+ break;
+ }
+ case 6u:
+ {
+ TransformSeg transform = TransformSeg(st.mat, st.translate);
+ TransformSegRef trans_ref = TransformSegRef(_2435.conf.trans_alloc.offset + ((st.trans_count - 1u) * 24u));
+ param_83.offset = _2435.conf.trans_alloc.offset;
+ TransformSegRef param_84 = trans_ref;
+ TransformSeg param_85 = transform;
+ TransformSeg_write(param_83, param_84, param_85);
+ break;
+ }
+ }
+ }
+}
+
+`,
+ }
+ shader_intersect_frag = driver.ShaderSources{
+ Name: "intersect.frag",
+ Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}},
+ Textures: []driver.TextureBinding{{Name: "cover", Binding: 0}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D cover;
+
+varying highp vec2 vUV;
+
+void main()
+{
+ float cover_1 = abs(texture2D(cover, vUV).x);
+ gl_FragData[0].x = cover_1;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D cover;
+
+in highp vec2 vUV;
+layout(location = 0) out vec4 fragColor;
+
+void main()
+{
+ float cover_1 = abs(texture(cover, vUV).x);
+ fragColor.x = cover_1;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D cover;
+
+in vec2 vUV;
+out vec4 fragColor;
+
+void main()
+{
+ float cover_1 = abs(texture(cover, vUV).x);
+ fragColor.x = cover_1;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D cover;
+
+in vec2 vUV;
+out vec4 fragColor;
+
+void main()
+{
+ float cover_1 = abs(texture(cover, vUV).x);
+ fragColor.x = cover_1;
+}
+
+`,
+ HLSL: "DXBC\xe0\xe4\x03\x8c\xacVF\x82l\xe7|\xc3T\xa6'\xef\x01\x00\x00\x00\b\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xd4\x00\x00\x00\x80\x01\x00\x00\xfc\x01\x00\x00\xa0\x02\x00\x00\xd4\x02\x00\x00Aon9\x94\x00\x00\x00\x94\x00\x00\x00\x00\x02\xff\xffl\x00\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0#\x00\x00\x02\x00\x00\x01\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x0e\x80\x00\x00\x00\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xa4\x00\x00\x00@\x00\x00\x00)\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x006\x00\x00\x06\x12 \x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xe2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x9c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00q\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00k\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_cover_sampler\x00cover\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ }
+ shader_intersect_vert = driver.ShaderSources{
+ Name: "intersect.vert",
+ Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_block.uvTransform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.subUVTransform", Type: 0x0, Size: 4, Offset: 16}},
+ Size: 32,
+ },
+ GLSL100ES: `#version 100
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+struct Block
+{
+ vec4 uvTransform;
+ vec4 subUVTransform;
+};
+
+uniform Block _block;
+
+attribute vec2 pos;
+attribute vec2 uv;
+varying vec2 vUV;
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0));
+ vec3 param_1 = vec3(pos, 1.0);
+ vec3 p = transform3x2(param, param_1);
+ gl_Position = vec4(p, 1.0);
+ m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_3 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_2, param_3);
+ vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw;
+ m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_5 = vec3(vUV, 1.0);
+ vUV = transform3x2(param_4, param_5).xy;
+ vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+layout(std140) uniform Block
+{
+ vec4 uvTransform;
+ vec4 subUVTransform;
+} _block;
+
+layout(location = 0) in vec2 pos;
+layout(location = 1) in vec2 uv;
+out vec2 vUV;
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0));
+ vec3 param_1 = vec3(pos, 1.0);
+ vec3 p = transform3x2(param, param_1);
+ gl_Position = vec4(p, 1.0);
+ m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_3 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_2, param_3);
+ vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw;
+ m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_5 = vec3(vUV, 1.0);
+ vUV = transform3x2(param_4, param_5).xy;
+ vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+struct Block
+{
+ vec4 uvTransform;
+ vec4 subUVTransform;
+};
+
+uniform Block _block;
+
+in vec2 pos;
+in vec2 uv;
+out vec2 vUV;
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0));
+ vec3 param_1 = vec3(pos, 1.0);
+ vec3 p = transform3x2(param, param_1);
+ gl_Position = vec4(p, 1.0);
+ m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_3 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_2, param_3);
+ vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw;
+ m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_5 = vec3(vUV, 1.0);
+ vUV = transform3x2(param_4, param_5).xy;
+ vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+layout(binding = 0, std140) uniform Block
+{
+ vec4 uvTransform;
+ vec4 subUVTransform;
+} _block;
+
+in vec2 pos;
+in vec2 uv;
+out vec2 vUV;
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0));
+ vec3 param_1 = vec3(pos, 1.0);
+ vec3 p = transform3x2(param, param_1);
+ gl_Position = vec4(p, 1.0);
+ m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_3 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_2, param_3);
+ vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw;
+ m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_5 = vec3(vUV, 1.0);
+ vUV = transform3x2(param_4, param_5).xy;
+ vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw;
+}
+
+`,
+ HLSL: "DXBCxH\xc4I\xbe\x0f[|\nl\x899\xe0\xb8\xcb?\x01\x00\x00\x00\xdc\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00L\x01\x00\x00\xc4\x02\x00\x00@\x03\x00\x008\x04\x00\x00\x84\x04\x00\x00Aon9\f\x01\x00\x00\f\x01\x00\x00\x00\x02\xfe\xff\xd8\x00\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x03\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x80\xbf\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\x03\x80\x01\x00U\x90\x03\x00\xe4\xa0\x03\x00\xe1\xa0\x05\x00\x00\x03\x00\x00\x03\x80\x00\x00\xe4\x80\x03\x00\xe2\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x01\x80\x01\x00\x00\x90\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x80\x02\x00\xe4\xa0\x02\x00\xee\xa0\x01\x00\x00\x02\x00\x00\x04\x80\x03\x00\x00\xa0\b\x00\x00\x03\x00\x00\b\x80\x03\x00É \x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\xe0\x00\x00\xec\x80\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x90\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\f\xc0\x03\x00\x00\xa0\xff\xff\x00\x00SHDRp\x01\x00\x00@\x00\x01\x00\\\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x006\x00\x00\x05\"\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?6\x00\x00\x05R\x00\x10\x00\x00\x00\x00\x00V\x14\x10\x00\x01\x00\x00\x00\x0f\x00\x00\n\x82\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x002\x00\x00\v2\x00\x10\x00\x00\x00\x00\x00\xe6\n\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x0f\x00\x00\n\x82\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x96\x05\x10\x00\x00\x00\x00\x002\x00\x00\v2 \x10\x00\x00\x00\x00\x00\xc6\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\x052 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x01\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\n\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xf0\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\xc6\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x02\x00\x00\x00\\\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00_block_uvTransform\x00\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_subUVTransform\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab",
+ }
+ shader_kernel4_comp = driver.ShaderSources{
+ Name: "kernel4.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 16, local_size_y = 8, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct CmdStrokeRef
+{
+ uint offset;
+};
+
+struct CmdStroke
+{
+ uint tile_ref;
+ float half_width;
+};
+
+struct CmdFillRef
+{
+ uint offset;
+};
+
+struct CmdFill
+{
+ uint tile_ref;
+ int backdrop;
+};
+
+struct CmdColorRef
+{
+ uint offset;
+};
+
+struct CmdColor
+{
+ uint rgba_color;
+};
+
+struct CmdImageRef
+{
+ uint offset;
+};
+
+struct CmdImage
+{
+ uint index;
+ ivec2 offset;
+};
+
+struct CmdAlphaRef
+{
+ uint offset;
+};
+
+struct CmdAlpha
+{
+ float alpha;
+};
+
+struct CmdJumpRef
+{
+ uint offset;
+};
+
+struct CmdJump
+{
+ uint new_ref;
+};
+
+struct CmdRef
+{
+ uint offset;
+};
+
+struct CmdTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct TileSegRef
+{
+ uint offset;
+};
+
+struct TileSeg
+{
+ vec2 origin;
+ vec2 vector;
+ float y_edge;
+ TileSegRef next;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _196;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _693;
+
+layout(binding = 3, rgba8) uniform readonly highp image2D images[1];
+layout(binding = 2, rgba8) uniform writeonly highp image2D image;
+
+Alloc new_alloc(uint offset, uint size)
+{
+ Alloc a;
+ a.offset = offset;
+ return a;
+}
+
+Alloc slice_mem(Alloc a, uint offset, uint size)
+{
+ uint param = a.offset + offset;
+ uint param_1 = size;
+ return new_alloc(param, param_1);
+}
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+uint read_mem(Alloc alloc, uint offset)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return 0u;
+ }
+ uint v = _196.memory[offset];
+ return v;
+}
+
+Alloc alloc_read(Alloc a, uint offset)
+{
+ Alloc param = a;
+ uint param_1 = offset >> uint(2);
+ Alloc alloc;
+ alloc.offset = read_mem(param, param_1);
+ return alloc;
+}
+
+CmdTag Cmd_tag(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint tag_and_flags = read_mem(param, param_1);
+ return CmdTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+CmdStroke CmdStroke_read(Alloc a, CmdStrokeRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ CmdStroke s;
+ s.tile_ref = raw0;
+ s.half_width = uintBitsToFloat(raw1);
+ return s;
+}
+
+CmdStroke Cmd_Stroke_read(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ CmdStrokeRef param_1 = CmdStrokeRef(ref.offset + 4u);
+ return CmdStroke_read(param, param_1);
+}
+
+TileSeg TileSeg_read(Alloc a, TileSegRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ Alloc param_8 = a;
+ uint param_9 = ix + 4u;
+ uint raw4 = read_mem(param_8, param_9);
+ Alloc param_10 = a;
+ uint param_11 = ix + 5u;
+ uint raw5 = read_mem(param_10, param_11);
+ TileSeg s;
+ s.origin = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1));
+ s.vector = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.y_edge = uintBitsToFloat(raw4);
+ s.next = TileSegRef(raw5);
+ return s;
+}
+
+uvec2 chunk_offset(uint i)
+{
+ return uvec2((i % 2u) * 16u, (i / 2u) * 8u);
+}
+
+CmdFill CmdFill_read(Alloc a, CmdFillRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ CmdFill s;
+ s.tile_ref = raw0;
+ s.backdrop = int(raw1);
+ return s;
+}
+
+CmdFill Cmd_Fill_read(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ CmdFillRef param_1 = CmdFillRef(ref.offset + 4u);
+ return CmdFill_read(param, param_1);
+}
+
+CmdAlpha CmdAlpha_read(Alloc a, CmdAlphaRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ CmdAlpha s;
+ s.alpha = uintBitsToFloat(raw0);
+ return s;
+}
+
+CmdAlpha Cmd_Alpha_read(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ CmdAlphaRef param_1 = CmdAlphaRef(ref.offset + 4u);
+ return CmdAlpha_read(param, param_1);
+}
+
+CmdColor CmdColor_read(Alloc a, CmdColorRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ CmdColor s;
+ s.rgba_color = raw0;
+ return s;
+}
+
+CmdColor Cmd_Color_read(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ CmdColorRef param_1 = CmdColorRef(ref.offset + 4u);
+ return CmdColor_read(param, param_1);
+}
+
+vec3 fromsRGB(vec3 srgb)
+{
+ bvec3 cutoff = greaterThanEqual(srgb, vec3(0.040449999272823333740234375));
+ vec3 below = srgb / vec3(12.9200000762939453125);
+ vec3 above = pow((srgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625));
+ return mix(below, above, cutoff);
+}
+
+vec4 unpacksRGB(uint srgba)
+{
+ vec4 color = unpackUnorm4x8(srgba).wzyx;
+ vec3 param = color.xyz;
+ return vec4(fromsRGB(param), color.w);
+}
+
+CmdImage CmdImage_read(Alloc a, CmdImageRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ CmdImage s;
+ s.index = raw0;
+ s.offset = ivec2(int(raw1 << uint(16)) >> 16, int(raw1) >> 16);
+ return s;
+}
+
+CmdImage Cmd_Image_read(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ CmdImageRef param_1 = CmdImageRef(ref.offset + 4u);
+ return CmdImage_read(param, param_1);
+}
+
+vec4[8] fillImage(uvec2 xy, CmdImage cmd_img)
+{
+ vec4 rgba[8];
+ for (uint i = 0u; i < 8u; i++)
+ {
+ uint param = i;
+ ivec2 uv = ivec2(xy + chunk_offset(param)) + cmd_img.offset;
+ vec4 fg_rgba = imageLoad(images[0], uv);
+ vec3 param_1 = fg_rgba.xyz;
+ vec3 _663 = fromsRGB(param_1);
+ fg_rgba = vec4(_663.x, _663.y, _663.z, fg_rgba.w);
+ rgba[i] = fg_rgba;
+ }
+ return rgba;
+}
+
+vec3 tosRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375));
+ vec3 below = vec3(12.9200000762939453125) * rgb;
+ vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875);
+ return mix(below, above, cutoff);
+}
+
+uint packsRGB(inout vec4 rgba)
+{
+ vec3 param = rgba.xyz;
+ rgba = vec4(tosRGB(param), rgba.w);
+ return packUnorm4x8(rgba.wzyx);
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _196.memory[offset] = val;
+}
+
+CmdJump CmdJump_read(Alloc a, CmdJumpRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ CmdJump s;
+ s.new_ref = raw0;
+ return s;
+}
+
+CmdJump Cmd_Jump_read(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ CmdJumpRef param_1 = CmdJumpRef(ref.offset + 4u);
+ return CmdJump_read(param, param_1);
+}
+
+void main()
+{
+ if (_196.mem_error != 0u)
+ {
+ return;
+ }
+ uint tile_ix = (gl_WorkGroupID.y * _693.conf.width_in_tiles) + gl_WorkGroupID.x;
+ Alloc param;
+ param.offset = _693.conf.ptcl_alloc.offset;
+ uint param_1 = tile_ix * 1024u;
+ uint param_2 = 1024u;
+ Alloc cmd_alloc = slice_mem(param, param_1, param_2);
+ CmdRef cmd_ref = CmdRef(cmd_alloc.offset);
+ Alloc param_3 = cmd_alloc;
+ uint param_4 = cmd_ref.offset;
+ Alloc scratch_alloc = alloc_read(param_3, param_4);
+ cmd_ref.offset += 8u;
+ uvec2 xy_uint = uvec2(gl_LocalInvocationID.x + (32u * gl_WorkGroupID.x), gl_LocalInvocationID.y + (32u * gl_WorkGroupID.y));
+ vec2 xy = vec2(xy_uint);
+ vec4 rgba[8];
+ for (uint i = 0u; i < 8u; i++)
+ {
+ rgba[i] = vec4(0.0);
+ }
+ uint clip_depth = 0u;
+ float df[8];
+ TileSegRef tile_seg_ref;
+ float area[8];
+ uint base_ix;
+ while (true)
+ {
+ Alloc param_5 = cmd_alloc;
+ CmdRef param_6 = cmd_ref;
+ uint tag = Cmd_tag(param_5, param_6).tag;
+ if (tag == 0u)
+ {
+ break;
+ }
+ switch (tag)
+ {
+ case 2u:
+ {
+ Alloc param_7 = cmd_alloc;
+ CmdRef param_8 = cmd_ref;
+ CmdStroke stroke = Cmd_Stroke_read(param_7, param_8);
+ for (uint k = 0u; k < 8u; k++)
+ {
+ df[k] = 1000000000.0;
+ }
+ tile_seg_ref = TileSegRef(stroke.tile_ref);
+ do
+ {
+ uint param_9 = tile_seg_ref.offset;
+ uint param_10 = 24u;
+ Alloc param_11 = new_alloc(param_9, param_10);
+ TileSegRef param_12 = tile_seg_ref;
+ TileSeg seg = TileSeg_read(param_11, param_12);
+ vec2 line_vec = seg.vector;
+ for (uint k_1 = 0u; k_1 < 8u; k_1++)
+ {
+ vec2 dpos = (xy + vec2(0.5)) - seg.origin;
+ uint param_13 = k_1;
+ dpos += vec2(chunk_offset(param_13));
+ float t = clamp(dot(line_vec, dpos) / dot(line_vec, line_vec), 0.0, 1.0);
+ df[k_1] = min(df[k_1], length((line_vec * t) - dpos));
+ }
+ tile_seg_ref = seg.next;
+ } while (tile_seg_ref.offset != 0u);
+ for (uint k_2 = 0u; k_2 < 8u; k_2++)
+ {
+ area[k_2] = clamp((stroke.half_width + 0.5) - df[k_2], 0.0, 1.0);
+ }
+ cmd_ref.offset += 12u;
+ break;
+ }
+ case 1u:
+ {
+ Alloc param_14 = cmd_alloc;
+ CmdRef param_15 = cmd_ref;
+ CmdFill fill = Cmd_Fill_read(param_14, param_15);
+ for (uint k_3 = 0u; k_3 < 8u; k_3++)
+ {
+ area[k_3] = float(fill.backdrop);
+ }
+ tile_seg_ref = TileSegRef(fill.tile_ref);
+ do
+ {
+ uint param_16 = tile_seg_ref.offset;
+ uint param_17 = 24u;
+ Alloc param_18 = new_alloc(param_16, param_17);
+ TileSegRef param_19 = tile_seg_ref;
+ TileSeg seg_1 = TileSeg_read(param_18, param_19);
+ for (uint k_4 = 0u; k_4 < 8u; k_4++)
+ {
+ uint param_20 = k_4;
+ vec2 my_xy = xy + vec2(chunk_offset(param_20));
+ vec2 start = seg_1.origin - my_xy;
+ vec2 end = start + seg_1.vector;
+ vec2 window = clamp(vec2(start.y, end.y), vec2(0.0), vec2(1.0));
+ if (!(window.x == window.y))
+ {
+ vec2 t_1 = (window - vec2(start.y)) / vec2(seg_1.vector.y);
+ vec2 xs = vec2(mix(start.x, end.x, t_1.x), mix(start.x, end.x, t_1.y));
+ float xmin = min(min(xs.x, xs.y), 1.0) - 9.9999999747524270787835121154785e-07;
+ float xmax = max(xs.x, xs.y);
+ float b = min(xmax, 1.0);
+ float c = max(b, 0.0);
+ float d = max(xmin, 0.0);
+ float a = ((b + (0.5 * ((d * d) - (c * c)))) - xmin) / (xmax - xmin);
+ area[k_4] += (a * (window.x - window.y));
+ }
+ area[k_4] += (sign(seg_1.vector.x) * clamp((my_xy.y - seg_1.y_edge) + 1.0, 0.0, 1.0));
+ }
+ tile_seg_ref = seg_1.next;
+ } while (tile_seg_ref.offset != 0u);
+ for (uint k_5 = 0u; k_5 < 8u; k_5++)
+ {
+ area[k_5] = min(abs(area[k_5]), 1.0);
+ }
+ cmd_ref.offset += 12u;
+ break;
+ }
+ case 3u:
+ {
+ for (uint k_6 = 0u; k_6 < 8u; k_6++)
+ {
+ area[k_6] = 1.0;
+ }
+ cmd_ref.offset += 4u;
+ break;
+ }
+ case 4u:
+ {
+ Alloc param_21 = cmd_alloc;
+ CmdRef param_22 = cmd_ref;
+ CmdAlpha alpha = Cmd_Alpha_read(param_21, param_22);
+ for (uint k_7 = 0u; k_7 < 8u; k_7++)
+ {
+ area[k_7] = alpha.alpha;
+ }
+ cmd_ref.offset += 8u;
+ break;
+ }
+ case 5u:
+ {
+ Alloc param_23 = cmd_alloc;
+ CmdRef param_24 = cmd_ref;
+ CmdColor color = Cmd_Color_read(param_23, param_24);
+ uint param_25 = color.rgba_color;
+ vec4 fg = unpacksRGB(param_25);
+ for (uint k_8 = 0u; k_8 < 8u; k_8++)
+ {
+ vec4 fg_k = fg * area[k_8];
+ rgba[k_8] = (rgba[k_8] * (1.0 - fg_k.w)) + fg_k;
+ }
+ cmd_ref.offset += 8u;
+ break;
+ }
+ case 6u:
+ {
+ Alloc param_26 = cmd_alloc;
+ CmdRef param_27 = cmd_ref;
+ CmdImage fill_img = Cmd_Image_read(param_26, param_27);
+ uvec2 param_28 = xy_uint;
+ CmdImage param_29 = fill_img;
+ vec4 img[8] = fillImage(param_28, param_29);
+ for (uint k_9 = 0u; k_9 < 8u; k_9++)
+ {
+ vec4 fg_k_1 = img[k_9] * area[k_9];
+ rgba[k_9] = (rgba[k_9] * (1.0 - fg_k_1.w)) + fg_k_1;
+ }
+ cmd_ref.offset += 12u;
+ break;
+ }
+ case 7u:
+ {
+ base_ix = (scratch_alloc.offset >> uint(2)) + (2u * ((((clip_depth * 32u) * 32u) + gl_LocalInvocationID.x) + (32u * gl_LocalInvocationID.y)));
+ for (uint k_10 = 0u; k_10 < 8u; k_10++)
+ {
+ uint param_30 = k_10;
+ uvec2 offset = chunk_offset(param_30);
+ vec4 param_31 = vec4(rgba[k_10]);
+ uint _1286 = packsRGB(param_31);
+ uint srgb = _1286;
+ float alpha_1 = clamp(abs(area[k_10]), 0.0, 1.0);
+ Alloc param_32 = scratch_alloc;
+ uint param_33 = (base_ix + 0u) + (2u * (offset.x + (offset.y * 32u)));
+ uint param_34 = srgb;
+ write_mem(param_32, param_33, param_34);
+ Alloc param_35 = scratch_alloc;
+ uint param_36 = (base_ix + 1u) + (2u * (offset.x + (offset.y * 32u)));
+ uint param_37 = floatBitsToUint(alpha_1);
+ write_mem(param_35, param_36, param_37);
+ rgba[k_10] = vec4(0.0);
+ }
+ clip_depth++;
+ cmd_ref.offset += 4u;
+ break;
+ }
+ case 8u:
+ {
+ clip_depth--;
+ base_ix = (scratch_alloc.offset >> uint(2)) + (2u * ((((clip_depth * 32u) * 32u) + gl_LocalInvocationID.x) + (32u * gl_LocalInvocationID.y)));
+ for (uint k_11 = 0u; k_11 < 8u; k_11++)
+ {
+ uint param_38 = k_11;
+ uvec2 offset_1 = chunk_offset(param_38);
+ Alloc param_39 = scratch_alloc;
+ uint param_40 = (base_ix + 0u) + (2u * (offset_1.x + (offset_1.y * 32u)));
+ uint srgb_1 = read_mem(param_39, param_40);
+ Alloc param_41 = scratch_alloc;
+ uint param_42 = (base_ix + 1u) + (2u * (offset_1.x + (offset_1.y * 32u)));
+ uint alpha_2 = read_mem(param_41, param_42);
+ uint param_43 = srgb_1;
+ vec4 bg = unpacksRGB(param_43);
+ vec4 fg_1 = (rgba[k_11] * area[k_11]) * uintBitsToFloat(alpha_2);
+ rgba[k_11] = (bg * (1.0 - fg_1.w)) + fg_1;
+ }
+ cmd_ref.offset += 4u;
+ break;
+ }
+ case 9u:
+ {
+ Alloc param_44 = cmd_alloc;
+ CmdRef param_45 = cmd_ref;
+ cmd_ref = CmdRef(Cmd_Jump_read(param_44, param_45).new_ref);
+ cmd_alloc.offset = cmd_ref.offset;
+ break;
+ }
+ }
+ }
+ for (uint i_1 = 0u; i_1 < 8u; i_1++)
+ {
+ uint param_46 = i_1;
+ vec3 param_47 = rgba[i_1].xyz;
+ imageStore(image, ivec2(xy_uint + chunk_offset(param_46)), vec4(tosRGB(param_47), rgba[i_1].w));
+ }
+}
+
+`,
+ }
+ shader_material_frag = driver.ShaderSources{
+ Name: "material.frag",
+ Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}},
+ Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+
+varying vec2 vUV;
+
+vec3 RGBtosRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375));
+ vec3 below = vec3(12.9200000762939453125) * rgb;
+ vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875);
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ vec4 texel = texture2D(tex, vUV);
+ vec3 param = texel.xyz;
+ vec3 _59 = RGBtosRGB(param);
+ texel = vec4(_59.x, _59.y, _59.z, texel.w);
+ gl_FragData[0] = texel;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+
+in vec2 vUV;
+layout(location = 0) out vec4 fragColor;
+
+vec3 RGBtosRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375));
+ vec3 below = vec3(12.9200000762939453125) * rgb;
+ vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875);
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ vec4 texel = texture(tex, vUV);
+ vec3 param = texel.xyz;
+ vec3 _59 = RGBtosRGB(param);
+ texel = vec4(_59.x, _59.y, _59.z, texel.w);
+ fragColor = texel;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+
+in vec2 vUV;
+out vec4 fragColor;
+
+vec3 RGBtosRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375));
+ vec3 below = vec3(12.9200000762939453125) * rgb;
+ vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875);
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ vec4 texel = texture(tex, vUV);
+ vec3 param = texel.xyz;
+ vec3 _59 = RGBtosRGB(param);
+ texel = vec4(_59.x, _59.y, _59.z, texel.w);
+ fragColor = texel;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+
+in vec2 vUV;
+out vec4 fragColor;
+
+vec3 RGBtosRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375));
+ vec3 below = vec3(12.9200000762939453125) * rgb;
+ vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875);
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ vec4 texel = texture(tex, vUV);
+ vec3 param = texel.xyz;
+ vec3 _59 = RGBtosRGB(param);
+ texel = vec4(_59.x, _59.y, _59.z, texel.w);
+ fragColor = texel;
+}
+
+`,
+ HLSL: "DXBC\x9e\x87LD\xf3\x17\n\x06\\\xb7\x98\x94\xa9PKe\x01\x00\x00\x00\xc8\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00\xbc\x01\x00\x00D\x03\x00\x00\xc0\x03\x00\x00`\x04\x00\x00\x94\x04\x00\x00Aon9|\x01\x00\x00|\x01\x00\x00\x00\x02\xff\xffT\x01\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0=\n\x87?\xaeGa\xbd\x00\x00\x00\x00\x00\x00\x00\x00Q\x00\x00\x05\x01\x00\x0f\xa0\x1c.M\xbbR\xb8NAvT\xd5>\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x0f\x00\x00\x02\x01\x00\x01\x80\x00\x00\x00\x80\x0f\x00\x00\x02\x01\x00\x02\x80\x00\x00U\x80\x0f\x00\x00\x02\x01\x00\x04\x80\x00\x00\xaa\x80\x05\x00\x00\x03\x01\x00\a\x80\x01\x00\xe4\x80\x01\x00\xaa\xa0\x0e\x00\x00\x02\x02\x00\x01\x80\x01\x00\x00\x80\x0e\x00\x00\x02\x02\x00\x02\x80\x01\x00U\x80\x0e\x00\x00\x02\x02\x00\x04\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\a\x80\x02\x00\xe4\x80\x00\x00\x00\xa0\x00\x00U\xa0\x02\x00\x00\x03\x01\x00\b\x80\x00\x00\x00\x80\x01\x00\x00\xa0\x05\x00\x00\x03\x02\x00\a\x80\x00\x00\xe4\x80\x01\x00U\xa0X\x00\x00\x04\x00\x00\x01\x80\x01\x00\xff\x80\x01\x00\x00\x80\x02\x00\x00\x80\x02\x00\x00\x03\x01\x00\x01\x80\x00\x00U\x80\x01\x00\x00\xa0X\x00\x00\x04\x00\x00\x02\x80\x01\x00\x00\x80\x01\x00U\x80\x02\x00U\x80\x02\x00\x00\x03\x01\x00\x01\x80\x00\x00\xaa\x80\x01\x00\x00\xa0X\x00\x00\x04\x00\x00\x04\x80\x01\x00\x00\x80\x01\x00\xaa\x80\x02\x00\xaa\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\x80\x01\x00\x00@\x00\x00\x00`\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x00/\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00vT\xd5>vT\xd5>vT\xd5>\x00\x00\x00\x00\x19\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x002\x00\x00\x0fr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00=\n\x87?=\n\x87?=\n\x87?\x00\x00\x00\x00\x02@\x00\x00\xaeGa\xbd\xaeGa\xbd\xaeGa\xbd\x00\x00\x00\x00\x1d\x00\x00\nr\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x1c.M;\x1c.M;\x1c.M;\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x00\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00R\xb8NAR\xb8NAR\xb8NA\x00\x00\x00\x006\x00\x00\x05\x82 \x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x007\x00\x00\tr \x10\x00\x00\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\n\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00m\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00i\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ }
+ shader_material_vert = driver.ShaderSources{
+ Name: "material.vert",
+ Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ GLSL100ES: `#version 100
+
+varying vec2 vUV;
+attribute vec2 uv;
+attribute vec2 pos;
+
+void main()
+{
+ vUV = uv;
+ gl_Position = vec4(pos, 0.0, 1.0);
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+out vec2 vUV;
+layout(location = 1) in vec2 uv;
+layout(location = 0) in vec2 pos;
+
+void main()
+{
+ vUV = uv;
+ gl_Position = vec4(pos, 0.0, 1.0);
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+out vec2 vUV;
+in vec2 uv;
+in vec2 pos;
+
+void main()
+{
+ vUV = uv;
+ gl_Position = vec4(pos, 0.0, 1.0);
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+out vec2 vUV;
+in vec2 uv;
+in vec2 pos;
+
+void main()
+{
+ vUV = uv;
+ gl_Position = vec4(pos, 0.0, 1.0);
+}
+
+`,
+ HLSL: "DXBCg\xc0\xae\x16\xd8\xe1\xbdl~Å\xf1\xc4\xf6dV\x01\x00\x00\x00\xc4\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\xc8\x00\x00\x00X\x01\x00\x00\xd4\x01\x00\x00 \x02\x00\x00l\x02\x00\x00Aon9\x88\x00\x00\x00\x88\x00\x00\x00\x00\x02\xfe\xff`\x00\x00\x00(\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x01\x00$\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x01\x00\x0f\xa0\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x90\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\x03\xe0\x01\x00\xe4\x90\x01\x00\x00\x02\x00\x00\f\xc0\x01\x00D\xa0\xff\xff\x00\x00SHDR\x88\x00\x00\x00@\x00\x01\x00\"\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x006\x00\x00\x052 \x10\x00\x00\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x052 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x01\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab",
+ }
+ shader_path_coarse_comp = driver.ShaderSources{
+ Name: "path_coarse.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct MallocResult
+{
+ Alloc alloc;
+ bool failed;
+};
+
+struct PathCubicRef
+{
+ uint offset;
+};
+
+struct PathCubic
+{
+ vec2 p0;
+ vec2 p1;
+ vec2 p2;
+ vec2 p3;
+ uint path_ix;
+ uint trans_ix;
+ vec2 stroke;
+};
+
+struct PathSegRef
+{
+ uint offset;
+};
+
+struct PathSegTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct TileRef
+{
+ uint offset;
+};
+
+struct PathRef
+{
+ uint offset;
+};
+
+struct Path
+{
+ uvec4 bbox;
+ TileRef tiles;
+};
+
+struct TileSegRef
+{
+ uint offset;
+};
+
+struct TileSeg
+{
+ vec2 origin;
+ vec2 vector;
+ float y_edge;
+ TileSegRef next;
+};
+
+struct TransformSegRef
+{
+ uint offset;
+};
+
+struct TransformSeg
+{
+ vec4 mat;
+ vec2 translate;
+};
+
+struct SubdivResult
+{
+ float val;
+ float a0;
+ float a2;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _149;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _788;
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+uint read_mem(Alloc alloc, uint offset)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return 0u;
+ }
+ uint v = _149.memory[offset];
+ return v;
+}
+
+PathSegTag PathSeg_tag(Alloc a, PathSegRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint tag_and_flags = read_mem(param, param_1);
+ return PathSegTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+PathCubic PathCubic_read(Alloc a, PathCubicRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ Alloc param_8 = a;
+ uint param_9 = ix + 4u;
+ uint raw4 = read_mem(param_8, param_9);
+ Alloc param_10 = a;
+ uint param_11 = ix + 5u;
+ uint raw5 = read_mem(param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 6u;
+ uint raw6 = read_mem(param_12, param_13);
+ Alloc param_14 = a;
+ uint param_15 = ix + 7u;
+ uint raw7 = read_mem(param_14, param_15);
+ Alloc param_16 = a;
+ uint param_17 = ix + 8u;
+ uint raw8 = read_mem(param_16, param_17);
+ Alloc param_18 = a;
+ uint param_19 = ix + 9u;
+ uint raw9 = read_mem(param_18, param_19);
+ Alloc param_20 = a;
+ uint param_21 = ix + 10u;
+ uint raw10 = read_mem(param_20, param_21);
+ Alloc param_22 = a;
+ uint param_23 = ix + 11u;
+ uint raw11 = read_mem(param_22, param_23);
+ PathCubic s;
+ s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1));
+ s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5));
+ s.p3 = vec2(uintBitsToFloat(raw6), uintBitsToFloat(raw7));
+ s.path_ix = raw8;
+ s.trans_ix = raw9;
+ s.stroke = vec2(uintBitsToFloat(raw10), uintBitsToFloat(raw11));
+ return s;
+}
+
+PathCubic PathSeg_Cubic_read(Alloc a, PathSegRef ref)
+{
+ Alloc param = a;
+ PathCubicRef param_1 = PathCubicRef(ref.offset + 4u);
+ return PathCubic_read(param, param_1);
+}
+
+TransformSeg TransformSeg_read(Alloc a, TransformSegRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ Alloc param_8 = a;
+ uint param_9 = ix + 4u;
+ uint raw4 = read_mem(param_8, param_9);
+ Alloc param_10 = a;
+ uint param_11 = ix + 5u;
+ uint raw5 = read_mem(param_10, param_11);
+ TransformSeg s;
+ s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5));
+ return s;
+}
+
+vec2 eval_cubic(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t)
+{
+ float mt = 1.0 - t;
+ return (p0 * ((mt * mt) * mt)) + (((p1 * ((mt * mt) * 3.0)) + (((p2 * (mt * 3.0)) + (p3 * t)) * t)) * t);
+}
+
+float approx_parabola_integral(float x)
+{
+ return x * inversesqrt(sqrt(0.3300000131130218505859375 + (0.201511204242706298828125 + ((0.25 * x) * x))));
+}
+
+SubdivResult estimate_subdiv(vec2 p0, vec2 p1, vec2 p2, float sqrt_tol)
+{
+ vec2 d01 = p1 - p0;
+ vec2 d12 = p2 - p1;
+ vec2 dd = d01 - d12;
+ float _cross = ((p2.x - p0.x) * dd.y) - ((p2.y - p0.y) * dd.x);
+ float x0 = ((d01.x * dd.x) + (d01.y * dd.y)) / _cross;
+ float x2 = ((d12.x * dd.x) + (d12.y * dd.y)) / _cross;
+ float scale = abs(_cross / (length(dd) * (x2 - x0)));
+ float param = x0;
+ float a0 = approx_parabola_integral(param);
+ float param_1 = x2;
+ float a2 = approx_parabola_integral(param_1);
+ float val = 0.0;
+ if (scale < 1000000000.0)
+ {
+ float da = abs(a2 - a0);
+ float sqrt_scale = sqrt(scale);
+ if (sign(x0) == sign(x2))
+ {
+ val = da * sqrt_scale;
+ }
+ else
+ {
+ float xmin = sqrt_tol / sqrt_scale;
+ float param_2 = xmin;
+ val = (sqrt_tol * da) / approx_parabola_integral(param_2);
+ }
+ }
+ return SubdivResult(val, a0, a2);
+}
+
+uint fill_mode_from_flags(uint flags)
+{
+ return flags & 1u;
+}
+
+Path Path_read(Alloc a, PathRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Path s;
+ s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16));
+ s.tiles = TileRef(raw2);
+ return s;
+}
+
+Alloc new_alloc(uint offset, uint size)
+{
+ Alloc a;
+ a.offset = offset;
+ return a;
+}
+
+float approx_parabola_inv_integral(float x)
+{
+ return x * sqrt(0.61000001430511474609375 + (0.1520999968051910400390625 + ((0.25 * x) * x)));
+}
+
+vec2 eval_quad(vec2 p0, vec2 p1, vec2 p2, float t)
+{
+ float mt = 1.0 - t;
+ return (p0 * (mt * mt)) + (((p1 * (mt * 2.0)) + (p2 * t)) * t);
+}
+
+MallocResult malloc(uint size)
+{
+ MallocResult r;
+ r.failed = false;
+ uint _155 = atomicAdd(_149.mem_offset, size);
+ uint offset = _155;
+ uint param = offset;
+ uint param_1 = size;
+ r.alloc = new_alloc(param, param_1);
+ if ((offset + size) > uint(int(uint(_149.memory.length())) * 4))
+ {
+ r.failed = true;
+ uint _176 = atomicMax(_149.mem_error, 1u);
+ return r;
+ }
+ return r;
+}
+
+TileRef Tile_index(TileRef ref, uint index)
+{
+ return TileRef(ref.offset + (index * 8u));
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _149.memory[offset] = val;
+}
+
+void TileSeg_write(Alloc a, TileSegRef ref, TileSeg s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.origin.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.origin.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.vector.x);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.vector.y);
+ write_mem(param_9, param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 4u;
+ uint param_14 = floatBitsToUint(s.y_edge);
+ write_mem(param_12, param_13, param_14);
+ Alloc param_15 = a;
+ uint param_16 = ix + 5u;
+ uint param_17 = s.next.offset;
+ write_mem(param_15, param_16, param_17);
+}
+
+void main()
+{
+ if (_149.mem_error != 0u)
+ {
+ return;
+ }
+ uint element_ix = gl_GlobalInvocationID.x;
+ PathSegRef ref = PathSegRef(_788.conf.pathseg_alloc.offset + (element_ix * 52u));
+ PathSegTag tag = PathSegTag(0u, 0u);
+ if (element_ix < _788.conf.n_pathseg)
+ {
+ Alloc param;
+ param.offset = _788.conf.pathseg_alloc.offset;
+ PathSegRef param_1 = ref;
+ tag = PathSeg_tag(param, param_1);
+ }
+ switch (tag.tag)
+ {
+ case 1u:
+ {
+ Alloc param_2;
+ param_2.offset = _788.conf.pathseg_alloc.offset;
+ PathSegRef param_3 = ref;
+ PathCubic cubic = PathSeg_Cubic_read(param_2, param_3);
+ uint trans_ix = cubic.trans_ix;
+ if (trans_ix > 0u)
+ {
+ TransformSegRef trans_ref = TransformSegRef(_788.conf.trans_alloc.offset + ((trans_ix - 1u) * 24u));
+ Alloc param_4;
+ param_4.offset = _788.conf.trans_alloc.offset;
+ TransformSegRef param_5 = trans_ref;
+ TransformSeg trans = TransformSeg_read(param_4, param_5);
+ cubic.p0 = ((trans.mat.xy * cubic.p0.x) + (trans.mat.zw * cubic.p0.y)) + trans.translate;
+ cubic.p1 = ((trans.mat.xy * cubic.p1.x) + (trans.mat.zw * cubic.p1.y)) + trans.translate;
+ cubic.p2 = ((trans.mat.xy * cubic.p2.x) + (trans.mat.zw * cubic.p2.y)) + trans.translate;
+ cubic.p3 = ((trans.mat.xy * cubic.p3.x) + (trans.mat.zw * cubic.p3.y)) + trans.translate;
+ }
+ vec2 err_v = (((cubic.p2 - cubic.p1) * 3.0) + cubic.p0) - cubic.p3;
+ float err = (err_v.x * err_v.x) + (err_v.y * err_v.y);
+ uint n_quads = max(uint(ceil(pow(err * 3.7037036418914794921875, 0.16666667163372039794921875))), 1u);
+ float val = 0.0;
+ vec2 qp0 = cubic.p0;
+ float _step = 1.0 / float(n_quads);
+ for (uint i = 0u; i < n_quads; i++)
+ {
+ float t = float(i + 1u) * _step;
+ vec2 param_6 = cubic.p0;
+ vec2 param_7 = cubic.p1;
+ vec2 param_8 = cubic.p2;
+ vec2 param_9 = cubic.p3;
+ float param_10 = t;
+ vec2 qp2 = eval_cubic(param_6, param_7, param_8, param_9, param_10);
+ vec2 param_11 = cubic.p0;
+ vec2 param_12 = cubic.p1;
+ vec2 param_13 = cubic.p2;
+ vec2 param_14 = cubic.p3;
+ float param_15 = t - (0.5 * _step);
+ vec2 qp1 = eval_cubic(param_11, param_12, param_13, param_14, param_15);
+ qp1 = (qp1 * 2.0) - ((qp0 + qp2) * 0.5);
+ vec2 param_16 = qp0;
+ vec2 param_17 = qp1;
+ vec2 param_18 = qp2;
+ float param_19 = 0.4743416607379913330078125;
+ SubdivResult params = estimate_subdiv(param_16, param_17, param_18, param_19);
+ val += params.val;
+ qp0 = qp2;
+ }
+ uint n = max(uint(ceil((val * 0.5) / 0.4743416607379913330078125)), 1u);
+ uint param_20 = tag.flags;
+ bool is_stroke = fill_mode_from_flags(param_20) == 1u;
+ uint path_ix = cubic.path_ix;
+ Alloc param_21;
+ param_21.offset = _788.conf.tile_alloc.offset;
+ PathRef param_22 = PathRef(_788.conf.tile_alloc.offset + (path_ix * 12u));
+ Path path = Path_read(param_21, param_22);
+ uint param_23 = path.tiles.offset;
+ uint param_24 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u;
+ Alloc path_alloc = new_alloc(param_23, param_24);
+ ivec4 bbox = ivec4(path.bbox);
+ vec2 p0 = cubic.p0;
+ qp0 = cubic.p0;
+ float v_step = val / float(n);
+ int n_out = 1;
+ float val_sum = 0.0;
+ vec2 p1;
+ float _1309;
+ TileSeg tile_seg;
+ for (uint i_1 = 0u; i_1 < n_quads; i_1++)
+ {
+ float t_1 = float(i_1 + 1u) * _step;
+ vec2 param_25 = cubic.p0;
+ vec2 param_26 = cubic.p1;
+ vec2 param_27 = cubic.p2;
+ vec2 param_28 = cubic.p3;
+ float param_29 = t_1;
+ vec2 qp2_1 = eval_cubic(param_25, param_26, param_27, param_28, param_29);
+ vec2 param_30 = cubic.p0;
+ vec2 param_31 = cubic.p1;
+ vec2 param_32 = cubic.p2;
+ vec2 param_33 = cubic.p3;
+ float param_34 = t_1 - (0.5 * _step);
+ vec2 qp1_1 = eval_cubic(param_30, param_31, param_32, param_33, param_34);
+ qp1_1 = (qp1_1 * 2.0) - ((qp0 + qp2_1) * 0.5);
+ vec2 param_35 = qp0;
+ vec2 param_36 = qp1_1;
+ vec2 param_37 = qp2_1;
+ float param_38 = 0.4743416607379913330078125;
+ SubdivResult params_1 = estimate_subdiv(param_35, param_36, param_37, param_38);
+ float param_39 = params_1.a0;
+ float u0 = approx_parabola_inv_integral(param_39);
+ float param_40 = params_1.a2;
+ float u2 = approx_parabola_inv_integral(param_40);
+ float uscale = 1.0 / (u2 - u0);
+ float target = float(n_out) * v_step;
+ for (;;)
+ {
+ bool _1202 = uint(n_out) == n;
+ bool _1212;
+ if (!_1202)
+ {
+ _1212 = target < (val_sum + params_1.val);
+ }
+ else
+ {
+ _1212 = _1202;
+ }
+ if (_1212)
+ {
+ if (uint(n_out) == n)
+ {
+ p1 = cubic.p3;
+ }
+ else
+ {
+ float u = (target - val_sum) / params_1.val;
+ float a = mix(params_1.a0, params_1.a2, u);
+ float param_41 = a;
+ float au = approx_parabola_inv_integral(param_41);
+ float t_2 = (au - u0) * uscale;
+ vec2 param_42 = qp0;
+ vec2 param_43 = qp1_1;
+ vec2 param_44 = qp2_1;
+ float param_45 = t_2;
+ p1 = eval_quad(param_42, param_43, param_44, param_45);
+ }
+ float xmin = min(p0.x, p1.x) - cubic.stroke.x;
+ float xmax = max(p0.x, p1.x) + cubic.stroke.x;
+ float ymin = min(p0.y, p1.y) - cubic.stroke.y;
+ float ymax = max(p0.y, p1.y) + cubic.stroke.y;
+ float dx = p1.x - p0.x;
+ float dy = p1.y - p0.y;
+ if (abs(dy) < 9.999999717180685365747194737196e-10)
+ {
+ _1309 = 1000000000.0;
+ }
+ else
+ {
+ _1309 = dx / dy;
+ }
+ float invslope = _1309;
+ float c = (cubic.stroke.x + (abs(invslope) * (16.0 + cubic.stroke.y))) * 0.03125;
+ float b = invslope;
+ float a_1 = (p0.x - ((p0.y - 16.0) * b)) * 0.03125;
+ int x0 = int(floor(xmin * 0.03125));
+ int x1 = int(floor(xmax * 0.03125) + 1.0);
+ int y0 = int(floor(ymin * 0.03125));
+ int y1 = int(floor(ymax * 0.03125) + 1.0);
+ x0 = clamp(x0, bbox.x, bbox.z);
+ y0 = clamp(y0, bbox.y, bbox.w);
+ x1 = clamp(x1, bbox.x, bbox.z);
+ y1 = clamp(y1, bbox.y, bbox.w);
+ float xc = a_1 + (b * float(y0));
+ int stride = bbox.z - bbox.x;
+ int base = ((y0 - bbox.y) * stride) - bbox.x;
+ uint n_tile_alloc = uint((x1 - x0) * (y1 - y0));
+ uint param_46 = n_tile_alloc * 24u;
+ MallocResult _1424 = malloc(param_46);
+ MallocResult tile_alloc = _1424;
+ if (tile_alloc.failed)
+ {
+ return;
+ }
+ uint tile_offset = tile_alloc.alloc.offset;
+ int xray = int(floor(p0.x * 0.03125));
+ int last_xray = int(floor(p1.x * 0.03125));
+ if (p0.y > p1.y)
+ {
+ int tmp = xray;
+ xray = last_xray;
+ last_xray = tmp;
+ }
+ for (int y = y0; y < y1; y++)
+ {
+ float tile_y0 = float(y * 32);
+ int xbackdrop = max((xray + 1), bbox.x);
+ bool _1478 = !is_stroke;
+ bool _1488;
+ if (_1478)
+ {
+ _1488 = min(p0.y, p1.y) < tile_y0;
+ }
+ else
+ {
+ _1488 = _1478;
+ }
+ bool _1495;
+ if (_1488)
+ {
+ _1495 = xbackdrop < bbox.z;
+ }
+ else
+ {
+ _1495 = _1488;
+ }
+ if (_1495)
+ {
+ int backdrop = (p1.y < p0.y) ? 1 : (-1);
+ TileRef param_47 = path.tiles;
+ uint param_48 = uint(base + xbackdrop);
+ TileRef tile_ref = Tile_index(param_47, param_48);
+ uint tile_el = tile_ref.offset >> uint(2);
+ Alloc param_49 = path_alloc;
+ uint param_50 = tile_el + 1u;
+ if (touch_mem(param_49, param_50))
+ {
+ uint _1533 = atomicAdd(_149.memory[tile_el + 1u], uint(backdrop));
+ }
+ }
+ int next_xray = last_xray;
+ if (y < (y1 - 1))
+ {
+ float tile_y1 = float((y + 1) * 32);
+ float x_edge = mix(p0.x, p1.x, (tile_y1 - p0.y) / dy);
+ next_xray = int(floor(x_edge * 0.03125));
+ }
+ int min_xray = min(xray, next_xray);
+ int max_xray = max(xray, next_xray);
+ int xx0 = min(int(floor(xc - c)), min_xray);
+ int xx1 = max(int(ceil(xc + c)), (max_xray + 1));
+ xx0 = clamp(xx0, x0, x1);
+ xx1 = clamp(xx1, x0, x1);
+ for (int x = xx0; x < xx1; x++)
+ {
+ float tile_x0 = float(x * 32);
+ TileRef param_51 = TileRef(path.tiles.offset);
+ uint param_52 = uint(base + x);
+ TileRef tile_ref_1 = Tile_index(param_51, param_52);
+ uint tile_el_1 = tile_ref_1.offset >> uint(2);
+ uint old = 0u;
+ Alloc param_53 = path_alloc;
+ uint param_54 = tile_el_1;
+ if (touch_mem(param_53, param_54))
+ {
+ uint _1636 = atomicExchange(_149.memory[tile_el_1], tile_offset);
+ old = _1636;
+ }
+ tile_seg.origin = p0;
+ tile_seg.vector = p1 - p0;
+ float y_edge = 0.0;
+ if (!is_stroke)
+ {
+ y_edge = mix(p0.y, p1.y, (tile_x0 - p0.x) / dx);
+ if (min(p0.x, p1.x) < tile_x0)
+ {
+ vec2 p = vec2(tile_x0, y_edge);
+ if (p0.x > p1.x)
+ {
+ tile_seg.vector = p - p0;
+ }
+ else
+ {
+ tile_seg.origin = p;
+ tile_seg.vector = p1 - p;
+ }
+ if (tile_seg.vector.x == 0.0)
+ {
+ tile_seg.vector.x = sign(p1.x - p0.x) * 9.999999717180685365747194737196e-10;
+ }
+ }
+ if ((x <= min_xray) || (max_xray < x))
+ {
+ y_edge = 1000000000.0;
+ }
+ }
+ tile_seg.y_edge = y_edge;
+ tile_seg.next.offset = old;
+ Alloc param_55 = tile_alloc.alloc;
+ TileSegRef param_56 = TileSegRef(tile_offset);
+ TileSeg param_57 = tile_seg;
+ TileSeg_write(param_55, param_56, param_57);
+ tile_offset += 24u;
+ }
+ xc += b;
+ base += stride;
+ xray = next_xray;
+ }
+ n_out++;
+ target += v_step;
+ p0 = p1;
+ continue;
+ }
+ else
+ {
+ break;
+ }
+ }
+ val_sum += params_1.val;
+ qp0 = qp2_1;
+ }
+ break;
+ }
+ }
+}
+
+`,
+ }
+ shader_stencil_frag = driver.ShaderSources{
+ Name: "stencil.frag",
+ Inputs: []driver.InputLocation{{Name: "vFrom", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vCtrl", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}, {Name: "vTo", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+varying vec2 vTo;
+varying vec2 vFrom;
+varying vec2 vCtrl;
+
+void main()
+{
+ float dx = vTo.x - vFrom.x;
+ bool increasing = vTo.x >= vFrom.x;
+ bvec2 _35 = bvec2(increasing);
+ vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y);
+ bvec2 _41 = bvec2(increasing);
+ vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y);
+ vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5));
+ float midx = mix(extent.x, extent.y, 0.5);
+ float x0 = midx - left.x;
+ vec2 p1 = vCtrl - left;
+ vec2 v = right - vCtrl;
+ float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0)));
+ float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t);
+ vec2 d_half = mix(p1, v, vec2(t));
+ float dy = d_half.y / d_half.x;
+ float width = extent.y - extent.x;
+ dy = abs(dy * width);
+ vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy);
+ sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0));
+ float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w));
+ area *= width;
+ if (width == 0.0)
+ {
+ area = 0.0;
+ }
+ gl_FragData[0].x = area;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+in vec2 vTo;
+in vec2 vFrom;
+in vec2 vCtrl;
+layout(location = 0) out vec4 fragCover;
+
+void main()
+{
+ float dx = vTo.x - vFrom.x;
+ bool increasing = vTo.x >= vFrom.x;
+ bvec2 _35 = bvec2(increasing);
+ vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y);
+ bvec2 _41 = bvec2(increasing);
+ vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y);
+ vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5));
+ float midx = mix(extent.x, extent.y, 0.5);
+ float x0 = midx - left.x;
+ vec2 p1 = vCtrl - left;
+ vec2 v = right - vCtrl;
+ float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0)));
+ float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t);
+ vec2 d_half = mix(p1, v, vec2(t));
+ float dy = d_half.y / d_half.x;
+ float width = extent.y - extent.x;
+ dy = abs(dy * width);
+ vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy);
+ sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0));
+ float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w));
+ area *= width;
+ if (width == 0.0)
+ {
+ area = 0.0;
+ }
+ fragCover.x = area;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+in vec2 vTo;
+in vec2 vFrom;
+in vec2 vCtrl;
+out vec4 fragCover;
+
+void main()
+{
+ float dx = vTo.x - vFrom.x;
+ bool increasing = vTo.x >= vFrom.x;
+ bvec2 _35 = bvec2(increasing);
+ vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y);
+ bvec2 _41 = bvec2(increasing);
+ vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y);
+ vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5));
+ float midx = mix(extent.x, extent.y, 0.5);
+ float x0 = midx - left.x;
+ vec2 p1 = vCtrl - left;
+ vec2 v = right - vCtrl;
+ float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0)));
+ float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t);
+ vec2 d_half = mix(p1, v, vec2(t));
+ float dy = d_half.y / d_half.x;
+ float width = extent.y - extent.x;
+ dy = abs(dy * width);
+ vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy);
+ sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0));
+ float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w));
+ area *= width;
+ if (width == 0.0)
+ {
+ area = 0.0;
+ }
+ fragCover.x = area;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+in vec2 vTo;
+in vec2 vFrom;
+in vec2 vCtrl;
+out vec4 fragCover;
+
+void main()
+{
+ float dx = vTo.x - vFrom.x;
+ bool increasing = vTo.x >= vFrom.x;
+ bvec2 _35 = bvec2(increasing);
+ vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y);
+ bvec2 _41 = bvec2(increasing);
+ vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y);
+ vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5));
+ float midx = mix(extent.x, extent.y, 0.5);
+ float x0 = midx - left.x;
+ vec2 p1 = vCtrl - left;
+ vec2 v = right - vCtrl;
+ float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0)));
+ float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t);
+ vec2 d_half = mix(p1, v, vec2(t));
+ float dy = d_half.y / d_half.x;
+ float width = extent.y - extent.x;
+ dy = abs(dy * width);
+ vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy);
+ sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0));
+ float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w));
+ area *= width;
+ if (width == 0.0)
+ {
+ area = 0.0;
+ }
+ fragCover.x = area;
+}
+
+`,
+ HLSL: "DXBC\x94!\xb9\x13L\xba\r\x11\x8f\xc7\xce\x0eAs\xec\xe1\x01\x00\x00\x00\\\n\x00\x00\x06\x00\x00\x008\x00\x00\x00\x9c\x03\x00\x00\xfc\b\x00\x00x\t\x00\x00\xc4\t\x00\x00(\n\x00\x00Aon9\\\x03\x00\x00\\\x03\x00\x00\x00\x02\xff\xff8\x03\x00\x00$\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x00\xbf\x00\x00\x00?\x00\x00\x80?\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x80\x01\x00\x03\xb0\v\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\xb0\x00\x00\x00\xa0\v\x00\x00\x03\x00\x00\x02\x80\x01\x00\x00\xb0\x00\x00\x00\xa0\n\x00\x00\x03\x01\x00\x03\x80\x00\x00\xe4\x80\x00\x00U\xa0\x02\x00\x00\x03\x00\x00\x01\x80\x01\x00\x00\x81\x01\x00U\x80\x04\x00\x00\x04\x00\x00\x02\x80\x00\x00\x00\x80\x00\x00U\xa0\x01\x00\x00\x80\x01\x00\x00\x02\x01\x00\x03\x80\x00\x00\xe4\xb0\n\x00\x00\x03\x02\x00\x01\x80\x01\x00\x00\x80\x01\x00\x00\xb0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x02\x00\x00\x81\v\x00\x00\x03\x03\x00\x01\x80\x01\x00\x00\xb0\x01\x00\x00\x80\x02\x00\x00\x03\x00\x00\x04\x80\x01\x00\x00\x81\x01\x00\x00\xb0X\x00\x00\x04\x03\x00\x02\x80\x00\x00\xaa\x80\x01\x00U\xb0\x01\x00U\x80X\x00\x00\x04\x02\x00\x02\x80\x00\x00\xaa\x80\x01\x00U\x80\x01\x00U\xb0\x02\x00\x00\x03\x00\x00\f\x80\x03\x00\x1b\x80\x00\x00\xe4\xb1\x02\x00\x00\x03\x01\x00\x03\x80\x02\x00\xe4\x81\x00\x00\x1b\xb0\x02\x00\x00\x03\x01\x00\x04\x80\x00\x00\xff\x80\x01\x00\x00\x81\x05\x00\x00\x03\x01\x00\x04\x80\x00\x00U\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x04\x80\x01\x00\x00\x80\x01\x00\x00\x80\x01\x00\xaa\x80\a\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x06\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x02\x00\x00\x03\x01\x00\x04\x80\x01\x00\xaa\x80\x01\x00\x00\x80\x06\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x04\x80\x00\x00U\x80\x01\x00U\x80\x02\x00U\x80\x12\x00\x00\x04\x02\x00\x03\x80\x00\x00U\x80\x00\x00\x1b\x80\x01\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x00\x00\xaa\xb0\x12\x00\x00\x04\x02\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x01\x00\xaa\x80\x06\x00\x00\x02\x00\x00\x02\x80\x02\x00\x00\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x02\x00U\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00\x00\x80\x00\x00U\x80#\x00\x00\x02\x00\x00\x02\x80\x00\x00U\x80\x04\x00\x00\x04\x01\x00\x01\x80\x00\x00U\x80\x00\x00U\xa0\x02\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x02\x80\x00\x00U\x80\x00\x00\x00\xa0\x02\x00\xaa\x80\x06\x00\x00\x02\x00\x00\x02\x80\x00\x00U\x80\x02\x00\x00\x03\x00\x00\f\x80\x02\x00\xaa\x81\x00\x00\x1b\xa0\x05\x00\x00\x03\x01\x00\b\x80\x00\x00U\x80\x00\x00\xff\x80\x05\x00\x00\x03\x01\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x02\x00\x00\x03\x01\x00\x1f\x80\x01\x00\xe4\x80\x00\x00U\xa0\x04\x00\x00\x04\x00\x00\x02\x80\x01\x00\xaa\x80\x01\x00U\x81\x01\x00\xaa\x80\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\xaa\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x01\x00\x00\x81\x00\x00U\x80\x04\x00\x00\x04\x00\x00\x02\x80\x01\x00\x00\x80\x01\x00\xff\x80\x00\x00U\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00U\xa0X\x00\x00\x04\x00\x00\x01\x80\x00\x00\x00\x81\x00\x00\xff\xa0\x00\x00U\x80\x01\x00\x00\x02\x00\x00\x0e\x80\x00\x00\xff\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDRX\x05\x00\x00@\x00\x00\x00V\x01\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03\xc2\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x006\x00\x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x006\x00\x00\x05\"\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x004\x00\x00\n2\x00\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x003\x00\x00\n2\x00\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\"\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\n\x00\x10\x00\x00\x00\x00\x003\x00\x00\a2\x00\x10\x00\x01\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x01\x00\x00\x00\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x004\x00\x00\a2\x00\x10\x00\x02\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x01\x00\x00\x00\x1d\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x007\x00\x00\tB\x00\x10\x00\x02\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x01\x00\x00\x00\x1a\x10\x10\x00\x00\x00\x00\x007\x00\x00\tB\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x01\x00\x00\x00\x00\x00\x00\br\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00\xa6\x1b\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x00\x00\x00\x00V\t\x10\x80A\x00\x00\x00\x01\x00\x00\x00\xa6\x1e\x10\x00\x00\x00\x00\x00\x00\x00\x00\b\xb2\x00\x10\x00\x01\x00\x00\x00\xa6\x0e\x10\x80A\x00\x00\x00\x00\x00\x00\x00F\b\x10\x00\x02\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00K\x00\x00\x05\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\a\x12\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x0e\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\xc2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00V\r\x10\x00\x01\x00\x00\x00\xa6\x0e\x10\x00\x00\x00\x00\x00\x0e\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x008\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x82\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x02\x00\x00\x00:\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\b\x82\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\v2\x00\x10\x00\x01\x00\x00\x00\x06\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\r2\x00\x10\x00\x02\x00\x00\x00\xa6\n\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00\x0e\x00\x00\b\xc2\x00\x10\x00\x02\x00\x00\x00\x06\x04\x10\x00\x01\x00\x00\x00\xa6\n\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x00 \x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x0e\x10\x00\x02\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00?\x00\x00\x00?\x00\x00\x00?2\x00\x00\n\x12\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00:\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x18\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00?7\x00\x00\t\x12 \x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x006\x00\x00\b\xe2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00)\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\\\x00\x00\x00\x03\x00\x00\x00\b\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00P\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\f\x00\x00P\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ }
+ shader_stencil_vert = driver.ShaderSources{
+ Name: "stencil.vert",
+ Inputs: []driver.InputLocation{{Name: "corner", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 1}, {Name: "maxy", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 1}, {Name: "from", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}, {Name: "ctrl", Location: 3, Semantic: "TEXCOORD", SemanticIndex: 3, Type: 0x0, Size: 2}, {Name: "to", Location: 4, Semantic: "TEXCOORD", SemanticIndex: 4, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.pathOffset", Type: 0x0, Size: 2, Offset: 16}},
+ Size: 24,
+ },
+ GLSL100ES: `#version 100
+
+struct Block
+{
+ vec4 transform;
+ vec2 pathOffset;
+};
+
+uniform Block _block;
+
+attribute vec2 from;
+attribute vec2 ctrl;
+attribute vec2 to;
+attribute float maxy;
+attribute float corner;
+varying vec2 vFrom;
+varying vec2 vCtrl;
+varying vec2 vTo;
+
+void main()
+{
+ vec2 from_1 = from + _block.pathOffset;
+ vec2 ctrl_1 = ctrl + _block.pathOffset;
+ vec2 to_1 = to + _block.pathOffset;
+ float maxy_1 = maxy + _block.pathOffset.y;
+ float c = corner;
+ vec2 pos;
+ if (c >= 0.375)
+ {
+ c -= 0.5;
+ pos.y = maxy_1 + 1.0;
+ }
+ else
+ {
+ pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;
+ }
+ if (c >= 0.125)
+ {
+ pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;
+ }
+ else
+ {
+ pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;
+ }
+ vFrom = from_1 - pos;
+ vCtrl = ctrl_1 - pos;
+ vTo = to_1 - pos;
+ pos = (pos * _block.transform.xy) + _block.transform.zw;
+ gl_Position = vec4(pos, 1.0, 1.0);
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+layout(std140) uniform Block
+{
+ vec4 transform;
+ vec2 pathOffset;
+} _block;
+
+layout(location = 2) in vec2 from;
+layout(location = 3) in vec2 ctrl;
+layout(location = 4) in vec2 to;
+layout(location = 1) in float maxy;
+layout(location = 0) in float corner;
+out vec2 vFrom;
+out vec2 vCtrl;
+out vec2 vTo;
+
+void main()
+{
+ vec2 from_1 = from + _block.pathOffset;
+ vec2 ctrl_1 = ctrl + _block.pathOffset;
+ vec2 to_1 = to + _block.pathOffset;
+ float maxy_1 = maxy + _block.pathOffset.y;
+ float c = corner;
+ vec2 pos;
+ if (c >= 0.375)
+ {
+ c -= 0.5;
+ pos.y = maxy_1 + 1.0;
+ }
+ else
+ {
+ pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;
+ }
+ if (c >= 0.125)
+ {
+ pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;
+ }
+ else
+ {
+ pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;
+ }
+ vFrom = from_1 - pos;
+ vCtrl = ctrl_1 - pos;
+ vTo = to_1 - pos;
+ pos = (pos * _block.transform.xy) + _block.transform.zw;
+ gl_Position = vec4(pos, 1.0, 1.0);
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct Block
+{
+ vec4 transform;
+ vec2 pathOffset;
+};
+
+uniform Block _block;
+
+in vec2 from;
+in vec2 ctrl;
+in vec2 to;
+in float maxy;
+in float corner;
+out vec2 vFrom;
+out vec2 vCtrl;
+out vec2 vTo;
+
+void main()
+{
+ vec2 from_1 = from + _block.pathOffset;
+ vec2 ctrl_1 = ctrl + _block.pathOffset;
+ vec2 to_1 = to + _block.pathOffset;
+ float maxy_1 = maxy + _block.pathOffset.y;
+ float c = corner;
+ vec2 pos;
+ if (c >= 0.375)
+ {
+ c -= 0.5;
+ pos.y = maxy_1 + 1.0;
+ }
+ else
+ {
+ pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;
+ }
+ if (c >= 0.125)
+ {
+ pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;
+ }
+ else
+ {
+ pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;
+ }
+ vFrom = from_1 - pos;
+ vCtrl = ctrl_1 - pos;
+ vTo = to_1 - pos;
+ pos = (pos * _block.transform.xy) + _block.transform.zw;
+ gl_Position = vec4(pos, 1.0, 1.0);
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0, std140) uniform Block
+{
+ vec4 transform;
+ vec2 pathOffset;
+} _block;
+
+in vec2 from;
+in vec2 ctrl;
+in vec2 to;
+in float maxy;
+in float corner;
+out vec2 vFrom;
+out vec2 vCtrl;
+out vec2 vTo;
+
+void main()
+{
+ vec2 from_1 = from + _block.pathOffset;
+ vec2 ctrl_1 = ctrl + _block.pathOffset;
+ vec2 to_1 = to + _block.pathOffset;
+ float maxy_1 = maxy + _block.pathOffset.y;
+ float c = corner;
+ vec2 pos;
+ if (c >= 0.375)
+ {
+ c -= 0.5;
+ pos.y = maxy_1 + 1.0;
+ }
+ else
+ {
+ pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;
+ }
+ if (c >= 0.125)
+ {
+ pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;
+ }
+ else
+ {
+ pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;
+ }
+ vFrom = from_1 - pos;
+ vCtrl = ctrl_1 - pos;
+ vTo = to_1 - pos;
+ pos = (pos * _block.transform.xy) + _block.transform.zw;
+ gl_Position = vec4(pos, 1.0, 1.0);
+}
+
+`,
+ HLSL: "DXBC\xa5!\xd8\x10\xb4n\x90\xe3\xd9U\xdb\xe2\xb6~I0\x01\x00\x00\x00\x10\b\x00\x00\x06\x00\x00\x008\x00\x00\x00L\x02\x00\x00t\x05\x00\x00\xf0\x05\x00\x00\xf4\x06\x00\x00\x88\a\x00\x00Aon9\f\x02\x00\x00\f\x02\x00\x00\x00\x02\xfe\xff\xd8\x01\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x03\x00\x0f\xa0\x00\x00\xc0>\x00\x00\x80?\x00\x00\x80\xbf\x00\x00\x00\xbfQ\x00\x00\x05\x04\x00\x0f\xa0\x00\x00\x00>\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x02\x80\x02\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x03\x80\x03\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x04\x80\x04\x00\x0f\x90\x02\x00\x00\x03\x00\x00\x01\x80\x01\x00\x00\x90\x02\x00U\xa0\x02\x00\x00\x03\x00\x00\x04\x80\x00\x00\x00\x80\x03\x00U\xa0\r\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\x90\x03\x00\x00\xa0\x01\x00\x00\x02\x01\x00\x04\x80\x00\x00\x00\x90\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00\x00\x90\x03\x00\xff\xa0\x02\x00\x00\x03\x02\x00\x03\x80\x02\x00\xe4\x90\x02\x00\xe4\xa0\x02\x00\x00\x03\x02\x00\f\x80\x03\x00\x14\x90\x02\x00\x14\xa0\n\x00\x00\x03\x03\x00\x03\x80\x02\x00\xee\x80\x02\x00\xe1\x80\x02\x00\x00\x03\x03\x00\f\x80\x04\x00D\x90\x02\x00D\xa0\n\x00\x00\x03\x03\x00\x03\x80\x03\x00\xeb\x80\x03\x00\xe4\x80\x02\x00\x00\x03\x01\x00\x03\x80\x03\x00\xe4\x80\x03\x00\xaa\xa0\x12\x00\x00\x04\x04\x00\x06\x80\x00\x00\x00\x80\x00\x00\xe4\x80\x01\x00Č\r\x00\x00\x03\x00\x00\x01\x80\x04\x00U\x80\x04\x00\x00\xa0\v\x00\x00\x03\x00\x00\x02\x80\x02\x00\xff\x80\x02\x00\x00\x80\v\x00\x00\x03\x00\x00\x02\x80\x03\x00\xaa\x80\x00\x00U\x80\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x03\x00U\xa0\x12\x00\x00\x04\x04\x00\x01\x80\x00\x00\x00\x80\x00\x00U\x80\x01\x00U\x80\x02\x00\x00\x03\x00\x00\x0f\xe0\x02\x00\xe4\x80\x04\x00(\x81\x02\x00\x00\x03\x01\x00\x03\xe0\x03\x00\xee\x80\x04\x00\xe8\x81\x04\x00\x00\x04\x00\x00\x03\x80\x04\x00\xe8\x80\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\f\xc0\x03\x00U\xa0\xff\xff\x00\x00SHDR \x03\x00\x00@\x00\x01\x00\xc8\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00_\x00\x00\x03\x12\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x03\x12\x10\x10\x00\x01\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x02\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x03\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x04\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00e\x00\x00\x03\xc2 \x10\x00\x00\x00\x00\x00e\x00\x00\x032 \x10\x00\x01\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x02\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x00\x1a\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x1d\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\xc0>\x00\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\xbf6\x00\x00\x05B\x00\x10\x00\x01\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\b2\x00\x10\x00\x02\x00\x00\x00F\x10\x10\x00\x02\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x02\x00\x00\x00\x06\x14\x10\x00\x03\x00\x00\x00\x06\x84 \x00\x00\x00\x00\x00\x01\x00\x00\x003\x00\x00\a2\x00\x10\x00\x03\x00\x00\x00\xb6\x0f\x10\x00\x02\x00\x00\x00\x16\x05\x10\x00\x02\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x03\x00\x00\x00\x06\x14\x10\x00\x04\x00\x00\x00\x06\x84 \x00\x00\x00\x00\x00\x01\x00\x00\x003\x00\x00\a2\x00\x10\x00\x03\x00\x00\x00\xb6\x0f\x10\x00\x03\x00\x00\x00F\x00\x10\x00\x03\x00\x00\x00\x00\x00\x00\n2\x00\x10\x00\x01\x00\x00\x00F\x00\x10\x00\x03\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80\xbf\x00\x00\x00\x00\x00\x00\x00\x007\x00\x00\tb\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00V\x06\x10\x00\x00\x00\x00\x00\xa6\b\x10\x00\x01\x00\x00\x00\x1d\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00>4\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x02\x00\x00\x00\n\x00\x10\x00\x02\x00\x00\x004\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x03\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?7\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x86\b\x10\x80A\x00\x00\x00\x00\x00\x00\x00F\x0e\x10\x00\x02\x00\x00\x00\x00\x00\x00\b2 \x10\x00\x01\x00\x00\x00\x86\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\xe6\n\x10\x00\x03\x00\x00\x002\x00\x00\v2 \x10\x00\x02\x00\x00\x00\x86\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x02\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x16\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xfc\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\xd4\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x02\x00\x00\x00\\\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\b\x00\x00\x00\x02\x00\x00\x00\xc4\x00\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_pathOffset\x00\xab\xab\x01\x00\x03\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\x8c\x00\x00\x00\x05\x00\x00\x00\b\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x80\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x01\x01\x00\x00\x80\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x03\x03\x00\x00\x80\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x03\x00\x00\x00\x03\x03\x00\x00\x80\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN\x80\x00\x00\x00\x04\x00\x00\x00\b\x00\x00\x00h\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00h\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x03\x00\x00h\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\f\x00\x00q\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab",
+ }
+ shader_tile_alloc_comp = driver.ShaderSources{
+ Name: "tile_alloc.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct MallocResult
+{
+ Alloc alloc;
+ bool failed;
+};
+
+struct AnnoEndClipRef
+{
+ uint offset;
+};
+
+struct AnnoEndClip
+{
+ vec4 bbox;
+};
+
+struct AnnotatedRef
+{
+ uint offset;
+};
+
+struct AnnotatedTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct PathRef
+{
+ uint offset;
+};
+
+struct TileRef
+{
+ uint offset;
+};
+
+struct Path
+{
+ uvec4 bbox;
+ TileRef tiles;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _96;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _309;
+
+shared uint sh_tile_count[128];
+shared MallocResult sh_tile_alloc;
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+uint read_mem(Alloc alloc, uint offset)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return 0u;
+ }
+ uint v = _96.memory[offset];
+ return v;
+}
+
+AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint tag_and_flags = read_mem(param, param_1);
+ return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+AnnoEndClip AnnoEndClip_read(Alloc a, AnnoEndClipRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ AnnoEndClip s;
+ s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ return s;
+}
+
+AnnoEndClip Annotated_EndClip_read(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ AnnoEndClipRef param_1 = AnnoEndClipRef(ref.offset + 4u);
+ return AnnoEndClip_read(param, param_1);
+}
+
+Alloc new_alloc(uint offset, uint size)
+{
+ Alloc a;
+ a.offset = offset;
+ return a;
+}
+
+MallocResult malloc(uint size)
+{
+ MallocResult r;
+ r.failed = false;
+ uint _102 = atomicAdd(_96.mem_offset, size);
+ uint offset = _102;
+ uint param = offset;
+ uint param_1 = size;
+ r.alloc = new_alloc(param, param_1);
+ if ((offset + size) > uint(int(uint(_96.memory.length())) * 4))
+ {
+ r.failed = true;
+ uint _123 = atomicMax(_96.mem_error, 1u);
+ return r;
+ }
+ return r;
+}
+
+Alloc slice_mem(Alloc a, uint offset, uint size)
+{
+ uint param = a.offset + offset;
+ uint param_1 = size;
+ return new_alloc(param, param_1);
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _96.memory[offset] = val;
+}
+
+void Path_write(Alloc a, PathRef ref, Path s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.bbox.x | (s.bbox.y << uint(16));
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = s.bbox.z | (s.bbox.w << uint(16));
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = s.tiles.offset;
+ write_mem(param_6, param_7, param_8);
+}
+
+void main()
+{
+ if (_96.mem_error != 0u)
+ {
+ return;
+ }
+ uint th_ix = gl_LocalInvocationID.x;
+ uint element_ix = gl_GlobalInvocationID.x;
+ PathRef path_ref = PathRef(_309.conf.tile_alloc.offset + (element_ix * 12u));
+ AnnotatedRef ref = AnnotatedRef(_309.conf.anno_alloc.offset + (element_ix * 32u));
+ uint tag = 0u;
+ if (element_ix < _309.conf.n_elements)
+ {
+ Alloc param;
+ param.offset = _309.conf.anno_alloc.offset;
+ AnnotatedRef param_1 = ref;
+ tag = Annotated_tag(param, param_1).tag;
+ }
+ int x0 = 0;
+ int y0 = 0;
+ int x1 = 0;
+ int y1 = 0;
+ switch (tag)
+ {
+ case 1u:
+ case 2u:
+ case 3u:
+ case 4u:
+ {
+ Alloc param_2;
+ param_2.offset = _309.conf.anno_alloc.offset;
+ AnnotatedRef param_3 = ref;
+ AnnoEndClip clip = Annotated_EndClip_read(param_2, param_3);
+ x0 = int(floor(clip.bbox.x * 0.03125));
+ y0 = int(floor(clip.bbox.y * 0.03125));
+ x1 = int(ceil(clip.bbox.z * 0.03125));
+ y1 = int(ceil(clip.bbox.w * 0.03125));
+ break;
+ }
+ }
+ x0 = clamp(x0, 0, int(_309.conf.width_in_tiles));
+ y0 = clamp(y0, 0, int(_309.conf.height_in_tiles));
+ x1 = clamp(x1, 0, int(_309.conf.width_in_tiles));
+ y1 = clamp(y1, 0, int(_309.conf.height_in_tiles));
+ Path path;
+ path.bbox = uvec4(uint(x0), uint(y0), uint(x1), uint(y1));
+ uint tile_count = uint((x1 - x0) * (y1 - y0));
+ if (tag == 4u)
+ {
+ tile_count = 0u;
+ }
+ sh_tile_count[th_ix] = tile_count;
+ uint total_tile_count = tile_count;
+ for (uint i = 0u; i < 7u; i++)
+ {
+ barrier();
+ if (th_ix >= uint(1 << int(i)))
+ {
+ total_tile_count += sh_tile_count[th_ix - uint(1 << int(i))];
+ }
+ barrier();
+ sh_tile_count[th_ix] = total_tile_count;
+ }
+ if (th_ix == 127u)
+ {
+ uint param_4 = total_tile_count * 8u;
+ MallocResult _482 = malloc(param_4);
+ sh_tile_alloc = _482;
+ }
+ barrier();
+ MallocResult alloc_start = sh_tile_alloc;
+ if (alloc_start.failed)
+ {
+ return;
+ }
+ if (element_ix < _309.conf.n_elements)
+ {
+ uint _499;
+ if (th_ix > 0u)
+ {
+ _499 = sh_tile_count[th_ix - 1u];
+ }
+ else
+ {
+ _499 = 0u;
+ }
+ uint tile_subix = _499;
+ Alloc param_5 = alloc_start.alloc;
+ uint param_6 = 8u * tile_subix;
+ uint param_7 = 8u * tile_count;
+ Alloc tiles_alloc = slice_mem(param_5, param_6, param_7);
+ path.tiles = TileRef(tiles_alloc.offset);
+ Alloc param_8;
+ param_8.offset = _309.conf.tile_alloc.offset;
+ PathRef param_9 = path_ref;
+ Path param_10 = path;
+ Path_write(param_8, param_9, param_10);
+ }
+ uint total_count = sh_tile_count[127] * 2u;
+ uint start_ix = alloc_start.alloc.offset >> uint(2);
+ for (uint i_1 = th_ix; i_1 < total_count; i_1 += 128u)
+ {
+ Alloc param_11 = alloc_start.alloc;
+ uint param_12 = start_ix + i_1;
+ uint param_13 = 0u;
+ write_mem(param_11, param_12, param_13);
+ }
+}
+
+`,
+ }
+)
diff --git a/gio/giold/gpu/timer.go b/gio/giold/gpu/timer.go
new file mode 100644
index 0000000..6e0bd4a
--- /dev/null
+++ b/gio/giold/gpu/timer.go
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+import (
+ "time"
+
+ "realy.lol/gio/gpu/internal/driver"
+)
+
+type timers struct {
+ backend driver.Device
+ timers []*timer
+}
+
+type timer struct {
+ Elapsed time.Duration
+ backend driver.Device
+ timer driver.Timer
+ state timerState
+}
+
+type timerState uint8
+
+const (
+ timerIdle timerState = iota
+ timerRunning
+ timerWaiting
+)
+
+func newTimers(b driver.Device) *timers {
+ return &timers{
+ backend: b,
+ }
+}
+
+func (t *timers) newTimer() *timer {
+ if t == nil {
+ return nil
+ }
+ tt := &timer{
+ backend: t.backend,
+ timer: t.backend.NewTimer(),
+ }
+ t.timers = append(t.timers, tt)
+ return tt
+}
+
+func (t *timer) begin() {
+ if t == nil || t.state != timerIdle {
+ return
+ }
+ t.timer.Begin()
+ t.state = timerRunning
+}
+
+func (t *timer) end() {
+ if t == nil || t.state != timerRunning {
+ return
+ }
+ t.timer.End()
+ t.state = timerWaiting
+}
+
+func (t *timers) ready() bool {
+ if t == nil {
+ return false
+ }
+ for _, tt := range t.timers {
+ switch tt.state {
+ case timerIdle:
+ continue
+ case timerRunning:
+ return false
+ }
+ d, ok := tt.timer.Duration()
+ if !ok {
+ return false
+ }
+ tt.state = timerIdle
+ tt.Elapsed = d
+ }
+ return t.backend.IsTimeContinuous()
+}
+
+func (t *timers) release() {
+ if t == nil {
+ return
+ }
+ for _, tt := range t.timers {
+ tt.timer.Release()
+ }
+ t.timers = nil
+}
diff --git a/gio/giold/internal/byteslice/byteslice.go b/gio/giold/internal/byteslice/byteslice.go
new file mode 100644
index 0000000..26ebdb2
--- /dev/null
+++ b/gio/giold/internal/byteslice/byteslice.go
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package byteslice provides byte slice views of other Go values such as
+// slices and structs.
+package byteslice
+
+import (
+ "reflect"
+ "unsafe"
+)
+
+// Struct returns a byte slice view of a struct.
+func Struct(s interface{}) []byte {
+ v := reflect.ValueOf(s).Elem()
+ sz := int(v.Type().Size())
+ var res []byte
+ h := (*reflect.SliceHeader)(unsafe.Pointer(&res))
+ h.Data = uintptr(unsafe.Pointer(v.UnsafeAddr()))
+ h.Cap = sz
+ h.Len = sz
+ return res
+}
+
+// Uint32 returns a byte slice view of a uint32 slice.
+func Uint32(s []uint32) []byte {
+ n := len(s)
+ if n == 0 {
+ return nil
+ }
+ blen := n * int(unsafe.Sizeof(s[0]))
+ return (*[1 << 30]byte)(unsafe.Pointer(&s[0]))[:blen:blen]
+}
+
+// Slice returns a byte slice view of a slice.
+func Slice(s interface{}) []byte {
+ v := reflect.ValueOf(s)
+ first := v.Index(0)
+ sz := int(first.Type().Size())
+ var res []byte
+ h := (*reflect.SliceHeader)(unsafe.Pointer(&res))
+ h.Data = first.UnsafeAddr()
+ h.Cap = v.Cap() * sz
+ h.Len = v.Len() * sz
+ return res
+}
diff --git a/gio/giold/internal/cocoainit/cocoa_darwin.go b/gio/giold/internal/cocoainit/cocoa_darwin.go
new file mode 100644
index 0000000..2a34e57
--- /dev/null
+++ b/gio/giold/internal/cocoainit/cocoa_darwin.go
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package cocoainit initializes support for multithreaded
+// programs in Cocoa.
+package cocoainit
+
+/*
+#cgo CFLAGS: -xobjective-c -fmodules -fobjc-arc
+#import
+
+static inline void activate_cocoa_multithreading() {
+ [[NSThread new] start];
+}
+#pragma GCC visibility push(hidden)
+*/
+import "C"
+
+func init() {
+ C.activate_cocoa_multithreading()
+}
diff --git a/gio/giold/internal/d3d11/d3d11_windows.go b/gio/giold/internal/d3d11/d3d11_windows.go
new file mode 100644
index 0000000..f33eb61
--- /dev/null
+++ b/gio/giold/internal/d3d11/d3d11_windows.go
@@ -0,0 +1,1470 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package d3d11
+
+import (
+ "fmt"
+ "math"
+ "syscall"
+ "unsafe"
+
+ "realy.lol/gio/internal/f32color"
+
+ "golang.org/x/sys/windows"
+)
+
+type DXGI_SWAP_CHAIN_DESC struct {
+ BufferDesc DXGI_MODE_DESC
+ SampleDesc DXGI_SAMPLE_DESC
+ BufferUsage uint32
+ BufferCount uint32
+ OutputWindow windows.Handle
+ Windowed uint32
+ SwapEffect uint32
+ Flags uint32
+}
+
+type DXGI_SAMPLE_DESC struct {
+ Count uint32
+ Quality uint32
+}
+
+type DXGI_MODE_DESC struct {
+ Width uint32
+ Height uint32
+ RefreshRate DXGI_RATIONAL
+ Format uint32
+ ScanlineOrdering uint32
+ Scaling uint32
+}
+
+type DXGI_RATIONAL struct {
+ Numerator uint32
+ Denominator uint32
+}
+
+type TEXTURE2D_DESC struct {
+ Width uint32
+ Height uint32
+ MipLevels uint32
+ ArraySize uint32
+ Format uint32
+ SampleDesc DXGI_SAMPLE_DESC
+ Usage uint32
+ BindFlags uint32
+ CPUAccessFlags uint32
+ MiscFlags uint32
+}
+
+type SAMPLER_DESC struct {
+ Filter uint32
+ AddressU uint32
+ AddressV uint32
+ AddressW uint32
+ MipLODBias float32
+ MaxAnisotropy uint32
+ ComparisonFunc uint32
+ BorderColor [4]float32
+ MinLOD float32
+ MaxLOD float32
+}
+
+type SHADER_RESOURCE_VIEW_DESC_TEX2D struct {
+ SHADER_RESOURCE_VIEW_DESC
+ Texture2D TEX2D_SRV
+}
+
+type SHADER_RESOURCE_VIEW_DESC struct {
+ Format uint32
+ ViewDimension uint32
+}
+
+type TEX2D_SRV struct {
+ MostDetailedMip uint32
+ MipLevels uint32
+}
+
+type INPUT_ELEMENT_DESC struct {
+ SemanticName *byte
+ SemanticIndex uint32
+ Format uint32
+ InputSlot uint32
+ AlignedByteOffset uint32
+ InputSlotClass uint32
+ InstanceDataStepRate uint32
+}
+
+type IDXGISwapChain struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ GetPrivateData uintptr
+ GetParent uintptr
+ GetDevice uintptr
+ Present uintptr
+ GetBuffer uintptr
+ SetFullscreenState uintptr
+ GetFullscreenState uintptr
+ GetDesc uintptr
+ ResizeBuffers uintptr
+ ResizeTarget uintptr
+ GetContainingOutput uintptr
+ GetFrameStatistics uintptr
+ GetLastPresentCount uintptr
+ }
+}
+
+type Device struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ CreateBuffer uintptr
+ CreateTexture1D uintptr
+ CreateTexture2D uintptr
+ CreateTexture3D uintptr
+ CreateShaderResourceView uintptr
+ CreateUnorderedAccessView uintptr
+ CreateRenderTargetView uintptr
+ CreateDepthStencilView uintptr
+ CreateInputLayout uintptr
+ CreateVertexShader uintptr
+ CreateGeometryShader uintptr
+ CreateGeometryShaderWithStreamOutput uintptr
+ CreatePixelShader uintptr
+ CreateHullShader uintptr
+ CreateDomainShader uintptr
+ CreateComputeShader uintptr
+ CreateClassLinkage uintptr
+ CreateBlendState uintptr
+ CreateDepthStencilState uintptr
+ CreateRasterizerState uintptr
+ CreateSamplerState uintptr
+ CreateQuery uintptr
+ CreatePredicate uintptr
+ CreateCounter uintptr
+ CreateDeferredContext uintptr
+ OpenSharedResource uintptr
+ CheckFormatSupport uintptr
+ CheckMultisampleQualityLevels uintptr
+ CheckCounterInfo uintptr
+ CheckCounter uintptr
+ CheckFeatureSupport uintptr
+ GetPrivateData uintptr
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ GetFeatureLevel uintptr
+ GetCreationFlags uintptr
+ GetDeviceRemovedReason uintptr
+ GetImmediateContext uintptr
+ SetExceptionMode uintptr
+ GetExceptionMode uintptr
+ }
+}
+
+type DeviceContext struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ GetDevice uintptr
+ GetPrivateData uintptr
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ VSSetConstantBuffers uintptr
+ PSSetShaderResources uintptr
+ PSSetShader uintptr
+ PSSetSamplers uintptr
+ VSSetShader uintptr
+ DrawIndexed uintptr
+ Draw uintptr
+ Map uintptr
+ Unmap uintptr
+ PSSetConstantBuffers uintptr
+ IASetInputLayout uintptr
+ IASetVertexBuffers uintptr
+ IASetIndexBuffer uintptr
+ DrawIndexedInstanced uintptr
+ DrawInstanced uintptr
+ GSSetConstantBuffers uintptr
+ GSSetShader uintptr
+ IASetPrimitiveTopology uintptr
+ VSSetShaderResources uintptr
+ VSSetSamplers uintptr
+ Begin uintptr
+ End uintptr
+ GetData uintptr
+ SetPredication uintptr
+ GSSetShaderResources uintptr
+ GSSetSamplers uintptr
+ OMSetRenderTargets uintptr
+ OMSetRenderTargetsAndUnorderedAccessViews uintptr
+ OMSetBlendState uintptr
+ OMSetDepthStencilState uintptr
+ SOSetTargets uintptr
+ DrawAuto uintptr
+ DrawIndexedInstancedIndirect uintptr
+ DrawInstancedIndirect uintptr
+ Dispatch uintptr
+ DispatchIndirect uintptr
+ RSSetState uintptr
+ RSSetViewports uintptr
+ RSSetScissorRects uintptr
+ CopySubresourceRegion uintptr
+ CopyResource uintptr
+ UpdateSubresource uintptr
+ CopyStructureCount uintptr
+ ClearRenderTargetView uintptr
+ ClearUnorderedAccessViewUint uintptr
+ ClearUnorderedAccessViewFloat uintptr
+ ClearDepthStencilView uintptr
+ GenerateMips uintptr
+ SetResourceMinLOD uintptr
+ GetResourceMinLOD uintptr
+ ResolveSubresource uintptr
+ ExecuteCommandList uintptr
+ HSSetShaderResources uintptr
+ HSSetShader uintptr
+ HSSetSamplers uintptr
+ HSSetConstantBuffers uintptr
+ DSSetShaderResources uintptr
+ DSSetShader uintptr
+ DSSetSamplers uintptr
+ DSSetConstantBuffers uintptr
+ CSSetShaderResources uintptr
+ CSSetUnorderedAccessViews uintptr
+ CSSetShader uintptr
+ CSSetSamplers uintptr
+ CSSetConstantBuffers uintptr
+ VSGetConstantBuffers uintptr
+ PSGetShaderResources uintptr
+ PSGetShader uintptr
+ PSGetSamplers uintptr
+ VSGetShader uintptr
+ PSGetConstantBuffers uintptr
+ IAGetInputLayout uintptr
+ IAGetVertexBuffers uintptr
+ IAGetIndexBuffer uintptr
+ GSGetConstantBuffers uintptr
+ GSGetShader uintptr
+ IAGetPrimitiveTopology uintptr
+ VSGetShaderResources uintptr
+ VSGetSamplers uintptr
+ GetPredication uintptr
+ GSGetShaderResources uintptr
+ GSGetSamplers uintptr
+ OMGetRenderTargets uintptr
+ OMGetRenderTargetsAndUnorderedAccessViews uintptr
+ OMGetBlendState uintptr
+ OMGetDepthStencilState uintptr
+ SOGetTargets uintptr
+ RSGetState uintptr
+ RSGetViewports uintptr
+ RSGetScissorRects uintptr
+ HSGetShaderResources uintptr
+ HSGetShader uintptr
+ HSGetSamplers uintptr
+ HSGetConstantBuffers uintptr
+ DSGetShaderResources uintptr
+ DSGetShader uintptr
+ DSGetSamplers uintptr
+ DSGetConstantBuffers uintptr
+ CSGetShaderResources uintptr
+ CSGetUnorderedAccessViews uintptr
+ CSGetShader uintptr
+ CSGetSamplers uintptr
+ CSGetConstantBuffers uintptr
+ ClearState uintptr
+ Flush uintptr
+ GetType uintptr
+ GetContextFlags uintptr
+ FinishCommandList uintptr
+ }
+}
+
+type RenderTargetView struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type Resource struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type Texture2D struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type Buffer struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type SamplerState struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type PixelShader struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type ShaderResourceView struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type DepthStencilView struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type BlendState struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type DepthStencilState struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type VertexShader struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type RasterizerState struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type InputLayout struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ GetBufferPointer uintptr
+ GetBufferSize uintptr
+ }
+}
+
+type DEPTH_STENCIL_DESC struct {
+ DepthEnable uint32
+ DepthWriteMask uint32
+ DepthFunc uint32
+ StencilEnable uint32
+ StencilReadMask uint8
+ StencilWriteMask uint8
+ FrontFace DEPTH_STENCILOP_DESC
+ BackFace DEPTH_STENCILOP_DESC
+}
+
+type DEPTH_STENCILOP_DESC struct {
+ StencilFailOp uint32
+ StencilDepthFailOp uint32
+ StencilPassOp uint32
+ StencilFunc uint32
+}
+
+type DEPTH_STENCIL_VIEW_DESC_TEX2D struct {
+ Format uint32
+ ViewDimension uint32
+ Flags uint32
+ Texture2D TEX2D_DSV
+}
+
+type TEX2D_DSV struct {
+ MipSlice uint32
+}
+
+type BLEND_DESC struct {
+ AlphaToCoverageEnable uint32
+ IndependentBlendEnable uint32
+ RenderTarget [8]RENDER_TARGET_BLEND_DESC
+}
+
+type RENDER_TARGET_BLEND_DESC struct {
+ BlendEnable uint32
+ SrcBlend uint32
+ DestBlend uint32
+ BlendOp uint32
+ SrcBlendAlpha uint32
+ DestBlendAlpha uint32
+ BlendOpAlpha uint32
+ RenderTargetWriteMask uint8
+}
+
+type IDXGIObject struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ GetPrivateData uintptr
+ GetParent uintptr
+ }
+}
+
+type IDXGIAdapter struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ GetPrivateData uintptr
+ GetParent uintptr
+ EnumOutputs uintptr
+ GetDesc uintptr
+ CheckInterfaceSupport uintptr
+ GetDesc1 uintptr
+ }
+}
+
+type IDXGIFactory struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ GetPrivateData uintptr
+ GetParent uintptr
+ EnumAdapters uintptr
+ MakeWindowAssociation uintptr
+ GetWindowAssociation uintptr
+ CreateSwapChain uintptr
+ CreateSoftwareAdapter uintptr
+ }
+}
+
+type IDXGIDevice struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ GetPrivateData uintptr
+ GetParent uintptr
+ GetAdapter uintptr
+ CreateSurface uintptr
+ QueryResourceResidency uintptr
+ SetGPUThreadPriority uintptr
+ GetGPUThreadPriority uintptr
+ }
+}
+
+type IUnknown struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type _IUnknownVTbl struct {
+ QueryInterface uintptr
+ AddRef uintptr
+ Release uintptr
+}
+
+type BUFFER_DESC struct {
+ ByteWidth uint32
+ Usage uint32
+ BindFlags uint32
+ CPUAccessFlags uint32
+ MiscFlags uint32
+ StructureByteStride uint32
+}
+
+type GUID struct {
+ Data1 uint32
+ Data2 uint16
+ Data3 uint16
+ Data4_0 uint8
+ Data4_1 uint8
+ Data4_2 uint8
+ Data4_3 uint8
+ Data4_4 uint8
+ Data4_5 uint8
+ Data4_6 uint8
+ Data4_7 uint8
+}
+
+type VIEWPORT struct {
+ TopLeftX float32
+ TopLeftY float32
+ Width float32
+ Height float32
+ MinDepth float32
+ MaxDepth float32
+}
+
+type SUBRESOURCE_DATA struct {
+ pSysMem *byte
+}
+
+type BOX struct {
+ Left uint32
+ Top uint32
+ Front uint32
+ Right uint32
+ Bottom uint32
+ Back uint32
+}
+
+type MAPPED_SUBRESOURCE struct {
+ PData uintptr
+ RowPitch uint32
+ DepthPitch uint32
+}
+
+type ErrorCode struct {
+ Name string
+ Code uint32
+}
+
+type RASTERIZER_DESC struct {
+ FillMode uint32
+ CullMode uint32
+ FrontCounterClockwise uint32
+ DepthBias int32
+ DepthBiasClamp float32
+ SlopeScaledDepthBias float32
+ DepthClipEnable uint32
+ ScissorEnable uint32
+ MultisampleEnable uint32
+ AntialiasedLineEnable uint32
+}
+
+var (
+ IID_Texture2D = GUID{0x6f15aaf2, 0xd208, 0x4e89, 0x9a, 0xb4, 0x48, 0x95,
+ 0x35, 0xd3, 0x4f, 0x9c}
+ IID_IDXGIDevice = GUID{0x54ec77fa, 0x1377, 0x44e6, 0x8c, 0x32, 0x88, 0xfd,
+ 0x5f, 0x44, 0xc8, 0x4c}
+ IID_IDXGIFactory = GUID{0x7b7166ec, 0x21c7, 0x44ae, 0xb2, 0x1a, 0xc9, 0xae,
+ 0x32, 0x1a, 0xe3, 0x69}
+)
+
+var (
+ d3d11 = windows.NewLazySystemDLL("d3d11.dll")
+
+ _D3D11CreateDevice = d3d11.NewProc("D3D11CreateDevice")
+ _D3D11CreateDeviceAndSwapChain = d3d11.NewProc("D3D11CreateDeviceAndSwapChain")
+)
+
+const (
+ SDK_VERSION = 7
+ DRIVER_TYPE_HARDWARE = 1
+
+ DXGI_FORMAT_UNKNOWN = 0
+ DXGI_FORMAT_R16_FLOAT = 54
+ DXGI_FORMAT_R32_FLOAT = 41
+ DXGI_FORMAT_R32G32_FLOAT = 16
+ DXGI_FORMAT_R32G32B32_FLOAT = 6
+ DXGI_FORMAT_R32G32B32A32_FLOAT = 2
+ DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29
+ DXGI_FORMAT_R16_SINT = 59
+ DXGI_FORMAT_R16G16_SINT = 38
+ DXGI_FORMAT_R16_UINT = 57
+ DXGI_FORMAT_D24_UNORM_S8_UINT = 45
+ DXGI_FORMAT_R16G16_FLOAT = 34
+ DXGI_FORMAT_R16G16B16A16_FLOAT = 10
+
+ FORMAT_SUPPORT_TEXTURE2D = 0x20
+ FORMAT_SUPPORT_RENDER_TARGET = 0x4000
+
+ DXGI_USAGE_RENDER_TARGET_OUTPUT = 1 << (1 + 4)
+
+ CPU_ACCESS_READ = 0x20000
+
+ MAP_READ = 1
+
+ DXGI_SWAP_EFFECT_DISCARD = 0
+
+ FEATURE_LEVEL_9_1 = 0x9100
+ FEATURE_LEVEL_9_3 = 0x9300
+ FEATURE_LEVEL_11_0 = 0xb000
+
+ USAGE_IMMUTABLE = 1
+ USAGE_STAGING = 3
+
+ BIND_VERTEX_BUFFER = 0x1
+ BIND_INDEX_BUFFER = 0x2
+ BIND_CONSTANT_BUFFER = 0x4
+ BIND_SHADER_RESOURCE = 0x8
+ BIND_RENDER_TARGET = 0x20
+ BIND_DEPTH_STENCIL = 0x40
+
+ PRIMITIVE_TOPOLOGY_TRIANGLELIST = 4
+ PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5
+
+ FILTER_MIN_MAG_LINEAR_MIP_POINT = 0x14
+ FILTER_MIN_MAG_MIP_POINT = 0
+
+ TEXTURE_ADDRESS_MIRROR = 2
+ TEXTURE_ADDRESS_CLAMP = 3
+ TEXTURE_ADDRESS_WRAP = 1
+
+ SRV_DIMENSION_TEXTURE2D = 4
+
+ CREATE_DEVICE_DEBUG = 0x2
+
+ FILL_SOLID = 3
+
+ CULL_NONE = 1
+
+ CLEAR_DEPTH = 0x1
+ CLEAR_STENCIL = 0x2
+
+ DSV_DIMENSION_TEXTURE2D = 3
+
+ DEPTH_WRITE_MASK_ALL = 1
+
+ COMPARISON_GREATER = 5
+ COMPARISON_GREATER_EQUAL = 7
+
+ BLEND_OP_ADD = 1
+ BLEND_ONE = 2
+ BLEND_INV_SRC_ALPHA = 6
+ BLEND_ZERO = 1
+ BLEND_DEST_COLOR = 9
+ BLEND_DEST_ALPHA = 7
+
+ COLOR_WRITE_ENABLE_ALL = 1 | 2 | 4 | 8
+
+ DXGI_STATUS_OCCLUDED = 0x087A0001
+ DXGI_ERROR_DEVICE_RESET = 0x887A0007
+ DXGI_ERROR_DEVICE_REMOVED = 0x887A0005
+ D3DDDIERR_DEVICEREMOVED = 1<<31 | 0x876<<16 | 2160
+)
+
+func CreateDevice(driverType uint32, flags uint32) (*Device, *DeviceContext,
+ uint32, error) {
+ var (
+ dev *Device
+ ctx *DeviceContext
+ featLvl uint32
+ )
+ r, _, _ := _D3D11CreateDevice.Call(
+ 0, // pAdapter
+ uintptr(driverType), // driverType
+ 0, // Software
+ uintptr(flags), // Flags
+ 0, // pFeatureLevels
+ 0, // FeatureLevels
+ SDK_VERSION, // SDKVersion
+ uintptr(unsafe.Pointer(&dev)), // ppDevice
+ uintptr(unsafe.Pointer(&featLvl)), // pFeatureLevel
+ uintptr(unsafe.Pointer(&ctx)), // ppImmediateContext
+ )
+ if r != 0 {
+ return nil, nil, 0, ErrorCode{Name: "D3D11CreateDevice",
+ Code: uint32(r)}
+ }
+ return dev, ctx, featLvl, nil
+}
+
+func CreateDeviceAndSwapChain(driverType uint32, flags uint32,
+ swapDesc *DXGI_SWAP_CHAIN_DESC) (*Device, *DeviceContext, *IDXGISwapChain,
+ uint32, error) {
+ var (
+ dev *Device
+ ctx *DeviceContext
+ swchain *IDXGISwapChain
+ featLvl uint32
+ )
+ r, _, _ := _D3D11CreateDeviceAndSwapChain.Call(
+ 0, // pAdapter
+ uintptr(driverType), // driverType
+ 0, // Software
+ uintptr(flags), // Flags
+ 0, // pFeatureLevels
+ 0, // FeatureLevels
+ SDK_VERSION, // SDKVersion
+ uintptr(unsafe.Pointer(swapDesc)), // pSwapChainDesc
+ uintptr(unsafe.Pointer(&swchain)), // ppSwapChain
+ uintptr(unsafe.Pointer(&dev)), // ppDevice
+ uintptr(unsafe.Pointer(&featLvl)), // pFeatureLevel
+ uintptr(unsafe.Pointer(&ctx)), // ppImmediateContext
+ )
+ if r != 0 {
+ return nil, nil, nil, 0, ErrorCode{Name: "D3D11CreateDeviceAndSwapChain",
+ Code: uint32(r)}
+ }
+ return dev, ctx, swchain, featLvl, nil
+}
+
+func (d *Device) CheckFormatSupport(format uint32) (uint32, error) {
+ var support uint32
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.CheckFormatSupport,
+ 3,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(format),
+ uintptr(unsafe.Pointer(&support)),
+ )
+ if r != 0 {
+ return 0, ErrorCode{Name: "DeviceCheckFormatSupport", Code: uint32(r)}
+ }
+ return support, nil
+}
+
+func (d *Device) CreateBuffer(desc *BUFFER_DESC, data []byte) (*Buffer, error) {
+ var dataDesc *SUBRESOURCE_DATA
+ if len(data) > 0 {
+ dataDesc = &SUBRESOURCE_DATA{
+ pSysMem: &data[0],
+ }
+ }
+ var buf *Buffer
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateBuffer,
+ 4,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(dataDesc)),
+ uintptr(unsafe.Pointer(&buf)),
+ 0, 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateBuffer", Code: uint32(r)}
+ }
+ return buf, nil
+}
+
+func (d *Device) CreateDepthStencilViewTEX2D(res *Resource,
+ desc *DEPTH_STENCIL_VIEW_DESC_TEX2D) (*DepthStencilView, error) {
+ var view *DepthStencilView
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateDepthStencilView,
+ 4,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(res)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&view)),
+ 0, 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateDepthStencilView",
+ Code: uint32(r)}
+ }
+ return view, nil
+}
+
+func (d *Device) CreatePixelShader(bytecode []byte) (*PixelShader, error) {
+ var shader *PixelShader
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreatePixelShader,
+ 5,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(&bytecode[0])),
+ uintptr(len(bytecode)),
+ 0, // pClassLinkage
+ uintptr(unsafe.Pointer(&shader)),
+ 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreatePixelShader", Code: uint32(r)}
+ }
+ return shader, nil
+}
+
+func (d *Device) CreateVertexShader(bytecode []byte) (*VertexShader, error) {
+ var shader *VertexShader
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateVertexShader,
+ 5,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(&bytecode[0])),
+ uintptr(len(bytecode)),
+ 0, // pClassLinkage
+ uintptr(unsafe.Pointer(&shader)),
+ 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateVertexShader", Code: uint32(r)}
+ }
+ return shader, nil
+}
+
+func (d *Device) CreateShaderResourceViewTEX2D(res *Resource,
+ desc *SHADER_RESOURCE_VIEW_DESC_TEX2D) (*ShaderResourceView, error) {
+ var resView *ShaderResourceView
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateShaderResourceView,
+ 4,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(res)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&resView)),
+ 0, 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateShaderResourceView",
+ Code: uint32(r)}
+ }
+ return resView, nil
+}
+
+func (d *Device) CreateRasterizerState(desc *RASTERIZER_DESC) (*RasterizerState,
+ error) {
+ var state *RasterizerState
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.CreateRasterizerState,
+ 3,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&state)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateRasterizerState",
+ Code: uint32(r)}
+ }
+ return state, nil
+}
+
+func (d *Device) CreateInputLayout(descs []INPUT_ELEMENT_DESC,
+ bytecode []byte) (*InputLayout, error) {
+ var pdesc *INPUT_ELEMENT_DESC
+ if len(descs) > 0 {
+ pdesc = &descs[0]
+ }
+ var layout *InputLayout
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateInputLayout,
+ 6,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(pdesc)),
+ uintptr(len(descs)),
+ uintptr(unsafe.Pointer(&bytecode[0])),
+ uintptr(len(bytecode)),
+ uintptr(unsafe.Pointer(&layout)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateInputLayout", Code: uint32(r)}
+ }
+ return layout, nil
+}
+
+func (d *Device) CreateSamplerState(desc *SAMPLER_DESC) (*SamplerState, error) {
+ var sampler *SamplerState
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.CreateSamplerState,
+ 3,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&sampler)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateSamplerState", Code: uint32(r)}
+ }
+ return sampler, nil
+}
+
+func (d *Device) CreateTexture2D(desc *TEXTURE2D_DESC) (*Texture2D, error) {
+ var tex *Texture2D
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateTexture2D,
+ 4,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(desc)),
+ 0, // pInitialData
+ uintptr(unsafe.Pointer(&tex)),
+ 0, 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "CreateTexture2D", Code: uint32(r)}
+ }
+ return tex, nil
+}
+
+func (d *Device) CreateRenderTargetView(res *Resource) (*RenderTargetView,
+ error) {
+ var target *RenderTargetView
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateRenderTargetView,
+ 4,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(res)),
+ 0, // pDesc
+ uintptr(unsafe.Pointer(&target)),
+ 0, 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateRenderTargetView",
+ Code: uint32(r)}
+ }
+ return target, nil
+}
+
+func (d *Device) CreateBlendState(desc *BLEND_DESC) (*BlendState, error) {
+ var state *BlendState
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.CreateBlendState,
+ 3,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&state)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateBlendState", Code: uint32(r)}
+ }
+ return state, nil
+}
+
+func (d *Device) CreateDepthStencilState(desc *DEPTH_STENCIL_DESC) (*DepthStencilState,
+ error) {
+ var state *DepthStencilState
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.CreateDepthStencilState,
+ 3,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&state)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateDepthStencilState",
+ Code: uint32(r)}
+ }
+ return state, nil
+}
+
+func (d *Device) GetFeatureLevel() int {
+ lvl, _, _ := syscall.Syscall(
+ d.Vtbl.GetFeatureLevel,
+ 1,
+ uintptr(unsafe.Pointer(d)),
+ 0, 0,
+ )
+ return int(lvl)
+}
+
+func (d *Device) GetImmediateContext() *DeviceContext {
+ var ctx *DeviceContext
+ syscall.Syscall(
+ d.Vtbl.GetImmediateContext,
+ 2,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(&ctx)),
+ 0,
+ )
+ return ctx
+}
+
+func (s *IDXGISwapChain) GetDesc() (DXGI_SWAP_CHAIN_DESC, error) {
+ var desc DXGI_SWAP_CHAIN_DESC
+ r, _, _ := syscall.Syscall(
+ s.Vtbl.GetDesc,
+ 2,
+ uintptr(unsafe.Pointer(s)),
+ uintptr(unsafe.Pointer(&desc)),
+ 0,
+ )
+ if r != 0 {
+ return DXGI_SWAP_CHAIN_DESC{}, ErrorCode{Name: "IDXGISwapChainGetDesc",
+ Code: uint32(r)}
+ }
+ return desc, nil
+}
+
+func (s *IDXGISwapChain) ResizeBuffers(buffers, width, height, newFormat, flags uint32) error {
+ r, _, _ := syscall.Syscall6(
+ s.Vtbl.ResizeBuffers,
+ 6,
+ uintptr(unsafe.Pointer(s)),
+ uintptr(buffers),
+ uintptr(width),
+ uintptr(height),
+ uintptr(newFormat),
+ uintptr(flags),
+ )
+ if r != 0 {
+ return ErrorCode{Name: "IDXGISwapChainResizeBuffers", Code: uint32(r)}
+ }
+ return nil
+}
+
+func (s *IDXGISwapChain) Present(SyncInterval int, Flags uint32) error {
+ r, _, _ := syscall.Syscall(
+ s.Vtbl.Present,
+ 3,
+ uintptr(unsafe.Pointer(s)),
+ uintptr(SyncInterval),
+ uintptr(Flags),
+ )
+ if r != 0 {
+ return ErrorCode{Name: "IDXGISwapChainPresent", Code: uint32(r)}
+ }
+ return nil
+}
+
+func (s *IDXGISwapChain) GetBuffer(index int, riid *GUID) (*IUnknown, error) {
+ var buf *IUnknown
+ r, _, _ := syscall.Syscall6(
+ s.Vtbl.GetBuffer,
+ 4,
+ uintptr(unsafe.Pointer(s)),
+ uintptr(index),
+ uintptr(unsafe.Pointer(riid)),
+ uintptr(unsafe.Pointer(&buf)),
+ 0,
+ 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "IDXGISwapChainGetBuffer", Code: uint32(r)}
+ }
+ return buf, nil
+}
+
+func (c *DeviceContext) Unmap(resource *Resource, subResource uint32) {
+ syscall.Syscall(
+ c.Vtbl.Unmap,
+ 3,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(resource)),
+ uintptr(subResource),
+ )
+}
+
+func (c *DeviceContext) Map(resource *Resource,
+ subResource, mapType, mapFlags uint32) (MAPPED_SUBRESOURCE, error) {
+ var resMap MAPPED_SUBRESOURCE
+ r, _, _ := syscall.Syscall6(
+ c.Vtbl.Map,
+ 6,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(resource)),
+ uintptr(subResource),
+ uintptr(mapType),
+ uintptr(mapFlags),
+ uintptr(unsafe.Pointer(&resMap)),
+ )
+ if r != 0 {
+ return resMap, ErrorCode{Name: "DeviceContextMap", Code: uint32(r)}
+ }
+ return resMap, nil
+}
+
+func (c *DeviceContext) CopySubresourceRegion(dst *Resource,
+ dstSubresource, dstX, dstY, dstZ uint32, src *Resource,
+ srcSubresource uint32, srcBox *BOX) {
+ syscall.Syscall9(
+ c.Vtbl.CopySubresourceRegion,
+ 9,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(dst)),
+ uintptr(dstSubresource),
+ uintptr(dstX),
+ uintptr(dstY),
+ uintptr(dstZ),
+ uintptr(unsafe.Pointer(src)),
+ uintptr(srcSubresource),
+ uintptr(unsafe.Pointer(srcBox)),
+ )
+}
+
+func (c *DeviceContext) ClearDepthStencilView(target *DepthStencilView,
+ flags uint32, depth float32, stencil uint8) {
+ syscall.Syscall6(
+ c.Vtbl.ClearDepthStencilView,
+ 5,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(target)),
+ uintptr(flags),
+ uintptr(math.Float32bits(depth)),
+ uintptr(stencil),
+ 0,
+ )
+}
+
+func (c *DeviceContext) ClearRenderTargetView(target *RenderTargetView,
+ color *[4]float32) {
+ syscall.Syscall(
+ c.Vtbl.ClearRenderTargetView,
+ 3,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(target)),
+ uintptr(unsafe.Pointer(color)),
+ )
+}
+
+func (c *DeviceContext) RSSetViewports(viewport *VIEWPORT) {
+ syscall.Syscall(
+ c.Vtbl.RSSetViewports,
+ 3,
+ uintptr(unsafe.Pointer(c)),
+ 1, // NumViewports
+ uintptr(unsafe.Pointer(viewport)),
+ )
+}
+
+func (c *DeviceContext) VSSetShader(s *VertexShader) {
+ syscall.Syscall6(
+ c.Vtbl.VSSetShader,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(s)),
+ 0, // ppClassInstances
+ 0, // NumClassInstances
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) VSSetConstantBuffers(b *Buffer) {
+ syscall.Syscall6(
+ c.Vtbl.VSSetConstantBuffers,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ 0, // StartSlot
+ 1, // NumBuffers
+ uintptr(unsafe.Pointer(&b)),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) PSSetConstantBuffers(b *Buffer) {
+ syscall.Syscall6(
+ c.Vtbl.PSSetConstantBuffers,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ 0, // StartSlot
+ 1, // NumBuffers
+ uintptr(unsafe.Pointer(&b)),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) PSSetShaderResources(startSlot uint32,
+ s *ShaderResourceView) {
+ syscall.Syscall6(
+ c.Vtbl.PSSetShaderResources,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(startSlot),
+ 1, // NumViews
+ uintptr(unsafe.Pointer(&s)),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) PSSetSamplers(startSlot uint32, s *SamplerState) {
+ syscall.Syscall6(
+ c.Vtbl.PSSetSamplers,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(startSlot),
+ 1, // NumSamplers
+ uintptr(unsafe.Pointer(&s)),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) PSSetShader(s *PixelShader) {
+ syscall.Syscall6(
+ c.Vtbl.PSSetShader,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(s)),
+ 0, // ppClassInstances
+ 0, // NumClassInstances
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) UpdateSubresource(res *Resource, dstBox *BOX,
+ rowPitch, depthPitch uint32, data []byte) {
+ syscall.Syscall9(
+ c.Vtbl.UpdateSubresource,
+ 7,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(res)),
+ 0, // DstSubresource
+ uintptr(unsafe.Pointer(dstBox)),
+ uintptr(unsafe.Pointer(&data[0])),
+ uintptr(rowPitch),
+ uintptr(depthPitch),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) RSSetState(state *RasterizerState) {
+ syscall.Syscall(
+ c.Vtbl.RSSetState,
+ 2,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(state)),
+ 0,
+ )
+}
+
+func (c *DeviceContext) IASetInputLayout(layout *InputLayout) {
+ syscall.Syscall(
+ c.Vtbl.IASetInputLayout,
+ 2,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(layout)),
+ 0,
+ )
+}
+
+func (c *DeviceContext) IASetIndexBuffer(buf *Buffer, format, offset uint32) {
+ syscall.Syscall6(
+ c.Vtbl.IASetIndexBuffer,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(buf)),
+ uintptr(format),
+ uintptr(offset),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) IASetVertexBuffers(buf *Buffer, stride, offset uint32) {
+ syscall.Syscall6(
+ c.Vtbl.IASetVertexBuffers,
+ 6,
+ uintptr(unsafe.Pointer(c)),
+ 0, // StartSlot
+ 1, // NumBuffers,
+ uintptr(unsafe.Pointer(&buf)),
+ uintptr(unsafe.Pointer(&stride)),
+ uintptr(unsafe.Pointer(&offset)),
+ )
+}
+
+func (c *DeviceContext) IASetPrimitiveTopology(mode uint32) {
+ syscall.Syscall(
+ c.Vtbl.IASetPrimitiveTopology,
+ 2,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(mode),
+ 0,
+ )
+}
+
+func (c *DeviceContext) OMGetRenderTargets() (*RenderTargetView,
+ *DepthStencilView) {
+ var (
+ target *RenderTargetView
+ depthStencilView *DepthStencilView
+ )
+ syscall.Syscall6(
+ c.Vtbl.OMGetRenderTargets,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ 1, // NumViews
+ uintptr(unsafe.Pointer(&target)),
+ uintptr(unsafe.Pointer(&depthStencilView)),
+ 0, 0,
+ )
+ return target, depthStencilView
+}
+
+func (c *DeviceContext) OMSetRenderTargets(target *RenderTargetView,
+ depthStencil *DepthStencilView) {
+ syscall.Syscall6(
+ c.Vtbl.OMSetRenderTargets,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ 1, // NumViews
+ uintptr(unsafe.Pointer(&target)),
+ uintptr(unsafe.Pointer(depthStencil)),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) Draw(count, start uint32) {
+ syscall.Syscall(
+ c.Vtbl.Draw,
+ 3,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(count),
+ uintptr(start),
+ )
+}
+
+func (c *DeviceContext) DrawIndexed(count, start uint32, base int32) {
+ syscall.Syscall6(
+ c.Vtbl.DrawIndexed,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(count),
+ uintptr(start),
+ uintptr(base),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) OMSetBlendState(state *BlendState,
+ factor *f32color.RGBA, sampleMask uint32) {
+ syscall.Syscall6(
+ c.Vtbl.OMSetBlendState,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(state)),
+ uintptr(unsafe.Pointer(factor)),
+ uintptr(sampleMask),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) OMSetDepthStencilState(state *DepthStencilState,
+ stencilRef uint32) {
+ syscall.Syscall(
+ c.Vtbl.OMSetDepthStencilState,
+ 3,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(state)),
+ uintptr(stencilRef),
+ )
+}
+
+func (d *IDXGIObject) GetParent(guid *GUID) (*IDXGIObject, error) {
+ var parent *IDXGIObject
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.GetParent,
+ 3,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(guid)),
+ uintptr(unsafe.Pointer(&parent)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "IDXGIObjectGetParent", Code: uint32(r)}
+ }
+ return parent, nil
+}
+
+func (d *IDXGIFactory) CreateSwapChain(device *IUnknown,
+ desc *DXGI_SWAP_CHAIN_DESC) (*IDXGISwapChain, error) {
+ var swchain *IDXGISwapChain
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateSwapChain,
+ 4,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(device)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&swchain)),
+ 0, 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "IDXGIFactory", Code: uint32(r)}
+ }
+ return swchain, nil
+}
+
+func (d *IDXGIDevice) GetAdapter() (*IDXGIAdapter, error) {
+ var adapter *IDXGIAdapter
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.GetAdapter,
+ 2,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(&adapter)),
+ 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "IDXGIDeviceGetAdapter", Code: uint32(r)}
+ }
+ return adapter, nil
+}
+
+func IUnknownQueryInterface(obj unsafe.Pointer, queryInterfaceMethod uintptr,
+ guid *GUID) (*IUnknown, error) {
+ var ref *IUnknown
+ r, _, _ := syscall.Syscall(
+ queryInterfaceMethod,
+ 3,
+ uintptr(obj),
+ uintptr(unsafe.Pointer(guid)),
+ uintptr(unsafe.Pointer(&ref)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "IUnknownQueryInterface", Code: uint32(r)}
+ }
+ return ref, nil
+}
+
+func IUnknownRelease(obj unsafe.Pointer, releaseMethod uintptr) {
+ syscall.Syscall(
+ releaseMethod,
+ 1,
+ uintptr(obj),
+ 0,
+ 0,
+ )
+}
+
+func (e ErrorCode) Error() string {
+ return fmt.Sprintf("%s: %#x", e.Name, e.Code)
+}
+
+func CreateSwapChain(dev *Device, hwnd windows.Handle) (*IDXGISwapChain,
+ error) {
+ dxgiDev, err := IUnknownQueryInterface(unsafe.Pointer(dev),
+ dev.Vtbl.QueryInterface, &IID_IDXGIDevice)
+ if err != nil {
+ return nil, fmt.Errorf("NewContext: %v", err)
+ }
+ adapter, err := (*IDXGIDevice)(unsafe.Pointer(dxgiDev)).GetAdapter()
+ IUnknownRelease(unsafe.Pointer(dxgiDev), dxgiDev.Vtbl.Release)
+ if err != nil {
+ return nil, fmt.Errorf("NewContext: %v", err)
+ }
+ dxgiFactory, err := (*IDXGIObject)(unsafe.Pointer(adapter)).GetParent(&IID_IDXGIFactory)
+ IUnknownRelease(unsafe.Pointer(adapter), adapter.Vtbl.Release)
+ if err != nil {
+ return nil, fmt.Errorf("NewContext: %v", err)
+ }
+ swchain, err := (*IDXGIFactory)(unsafe.Pointer(dxgiFactory)).CreateSwapChain(
+ (*IUnknown)(unsafe.Pointer(dev)),
+ &DXGI_SWAP_CHAIN_DESC{
+ BufferDesc: DXGI_MODE_DESC{
+ Format: DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
+ },
+ SampleDesc: DXGI_SAMPLE_DESC{
+ Count: 1,
+ },
+ BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
+ BufferCount: 1,
+ OutputWindow: hwnd,
+ Windowed: 1,
+ SwapEffect: DXGI_SWAP_EFFECT_DISCARD,
+ },
+ )
+ IUnknownRelease(unsafe.Pointer(dxgiFactory), dxgiFactory.Vtbl.Release)
+ if err != nil {
+ return nil, fmt.Errorf("NewContext: %v", err)
+ }
+ return swchain, nil
+}
+
+func CreateDepthView(d *Device,
+ width, height, depthBits int) (*DepthStencilView, error) {
+ depthTex, err := d.CreateTexture2D(&TEXTURE2D_DESC{
+ Width: uint32(width),
+ Height: uint32(height),
+ MipLevels: 1,
+ ArraySize: 1,
+ Format: DXGI_FORMAT_D24_UNORM_S8_UINT,
+ SampleDesc: DXGI_SAMPLE_DESC{
+ Count: 1,
+ Quality: 0,
+ },
+ BindFlags: BIND_DEPTH_STENCIL,
+ })
+ if err != nil {
+ return nil, err
+ }
+ depthView, err := d.CreateDepthStencilViewTEX2D(
+ (*Resource)(unsafe.Pointer(depthTex)),
+ &DEPTH_STENCIL_VIEW_DESC_TEX2D{
+ Format: DXGI_FORMAT_D24_UNORM_S8_UINT,
+ ViewDimension: DSV_DIMENSION_TEXTURE2D,
+ },
+ )
+ IUnknownRelease(unsafe.Pointer(depthTex), depthTex.Vtbl.Release)
+ return depthView, err
+}
diff --git a/gio/giold/internal/egl/egl.go b/gio/giold/internal/egl/egl.go
new file mode 100644
index 0000000..5a23650
--- /dev/null
+++ b/gio/giold/internal/egl/egl.go
@@ -0,0 +1,293 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build linux || windows || freebsd || openbsd
+// +build linux windows freebsd openbsd
+
+package egl
+
+import (
+ "errors"
+ "fmt"
+ "runtime"
+ "strings"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/internal/gl"
+ "realy.lol/gio/internal/srgb"
+)
+
+type Context struct {
+ c *gl.Functions
+ disp _EGLDisplay
+ eglCtx *eglContext
+ eglSurf _EGLSurface
+ width, height int
+ refreshFBO bool
+ // For sRGB emulation.
+ srgbFBO *srgb.FBO
+}
+
+type eglContext struct {
+ config _EGLConfig
+ ctx _EGLContext
+ visualID int
+ srgb bool
+ surfaceless bool
+}
+
+var (
+ nilEGLDisplay _EGLDisplay
+ nilEGLSurface _EGLSurface
+ nilEGLContext _EGLContext
+ nilEGLConfig _EGLConfig
+ EGL_DEFAULT_DISPLAY NativeDisplayType
+)
+
+const (
+ _EGL_ALPHA_SIZE = 0x3021
+ _EGL_BLUE_SIZE = 0x3022
+ _EGL_CONFIG_CAVEAT = 0x3027
+ _EGL_CONTEXT_CLIENT_VERSION = 0x3098
+ _EGL_DEPTH_SIZE = 0x3025
+ _EGL_GL_COLORSPACE_KHR = 0x309d
+ _EGL_GL_COLORSPACE_SRGB_KHR = 0x3089
+ _EGL_GREEN_SIZE = 0x3023
+ _EGL_EXTENSIONS = 0x3055
+ _EGL_NATIVE_VISUAL_ID = 0x302e
+ _EGL_NONE = 0x3038
+ _EGL_OPENGL_ES2_BIT = 0x4
+ _EGL_RED_SIZE = 0x3024
+ _EGL_RENDERABLE_TYPE = 0x3040
+ _EGL_SURFACE_TYPE = 0x3033
+ _EGL_WINDOW_BIT = 0x4
+)
+
+func (c *Context) Release() {
+ if c.srgbFBO != nil {
+ c.srgbFBO.Release()
+ c.srgbFBO = nil
+ }
+ c.ReleaseSurface()
+ if c.eglCtx != nil {
+ eglDestroyContext(c.disp, c.eglCtx.ctx)
+ c.eglCtx = nil
+ }
+ c.disp = nilEGLDisplay
+}
+
+func (c *Context) Present() error {
+ if c.srgbFBO != nil {
+ c.srgbFBO.Blit()
+ }
+ if !eglSwapBuffers(c.disp, c.eglSurf) {
+ return fmt.Errorf("eglSwapBuffers failed (%x)", eglGetError())
+ }
+ if c.srgbFBO != nil {
+ c.srgbFBO.AfterPresent()
+ }
+ return nil
+}
+
+func NewContext(disp NativeDisplayType) (*Context, error) {
+ if err := loadEGL(); err != nil {
+ return nil, err
+ }
+ eglDisp := eglGetDisplay(disp)
+ // eglGetDisplay can return EGL_NO_DISPLAY yet no error
+ // (EGL_SUCCESS), in which case a default EGL display might be
+ // available.
+ if eglDisp == nilEGLDisplay {
+ eglDisp = eglGetDisplay(EGL_DEFAULT_DISPLAY)
+ }
+ if eglDisp == nilEGLDisplay {
+ return nil, fmt.Errorf("eglGetDisplay failed: 0x%x", eglGetError())
+ }
+ eglCtx, err := createContext(eglDisp)
+ if err != nil {
+ return nil, err
+ }
+ f, err := gl.NewFunctions(nil)
+ if err != nil {
+ return nil, err
+ }
+ c := &Context{
+ disp: eglDisp,
+ eglCtx: eglCtx,
+ c: f,
+ }
+ return c, nil
+}
+
+func (c *Context) API() gpu.API {
+ return gpu.OpenGL{}
+}
+
+func (c *Context) ReleaseSurface() {
+ if c.eglSurf == nilEGLSurface {
+ return
+ }
+ // Make sure any in-flight GL commands are complete.
+ c.c.Finish()
+ c.ReleaseCurrent()
+ eglDestroySurface(c.disp, c.eglSurf)
+ c.eglSurf = nilEGLSurface
+}
+
+func (c *Context) VisualID() int {
+ return c.eglCtx.visualID
+}
+
+func (c *Context) CreateSurface(win NativeWindowType, width, height int) error {
+ eglSurf, err := createSurface(c.disp, c.eglCtx, win)
+ c.eglSurf = eglSurf
+ c.width = width
+ c.height = height
+ c.refreshFBO = true
+ return err
+}
+
+func (c *Context) ReleaseCurrent() {
+ if c.disp != nilEGLDisplay {
+ eglMakeCurrent(c.disp, nilEGLSurface, nilEGLSurface, nilEGLContext)
+ }
+}
+
+func (c *Context) MakeCurrent() error {
+ if c.eglSurf == nilEGLSurface && !c.eglCtx.surfaceless {
+ return errors.New("no surface created yet EGL_KHR_surfaceless_context is not supported")
+ }
+ if !eglMakeCurrent(c.disp, c.eglSurf, c.eglSurf, c.eglCtx.ctx) {
+ return fmt.Errorf("eglMakeCurrent error 0x%x", eglGetError())
+ }
+ if c.eglCtx.srgb || c.eglSurf == nilEGLSurface {
+ return nil
+ }
+ if c.srgbFBO == nil {
+ var err error
+ c.srgbFBO, err = srgb.New(nil)
+ if err != nil {
+ c.ReleaseCurrent()
+ return err
+ }
+ }
+ if c.refreshFBO {
+ c.refreshFBO = false
+ if err := c.srgbFBO.Refresh(c.width, c.height); err != nil {
+ c.ReleaseCurrent()
+ return err
+ }
+ }
+ return nil
+}
+
+func (c *Context) EnableVSync(enable bool) {
+ if enable {
+ eglSwapInterval(c.disp, 1)
+ } else {
+ eglSwapInterval(c.disp, 0)
+ }
+}
+
+func hasExtension(exts []string, ext string) bool {
+ for _, e := range exts {
+ if ext == e {
+ return true
+ }
+ }
+ return false
+}
+
+func createContext(disp _EGLDisplay) (*eglContext, error) {
+ major, minor, ret := eglInitialize(disp)
+ if !ret {
+ return nil, fmt.Errorf("eglInitialize failed: 0x%x", eglGetError())
+ }
+ // sRGB framebuffer support on EGL 1.5 or if EGL_KHR_gl_colorspace is supported.
+ exts := strings.Split(eglQueryString(disp, _EGL_EXTENSIONS), " ")
+ srgb := major > 1 || minor >= 5 || hasExtension(exts,
+ "EGL_KHR_gl_colorspace")
+ attribs := []_EGLint{
+ _EGL_RENDERABLE_TYPE, _EGL_OPENGL_ES2_BIT,
+ _EGL_SURFACE_TYPE, _EGL_WINDOW_BIT,
+ _EGL_BLUE_SIZE, 8,
+ _EGL_GREEN_SIZE, 8,
+ _EGL_RED_SIZE, 8,
+ _EGL_CONFIG_CAVEAT, _EGL_NONE,
+ }
+ if srgb {
+ if runtime.GOOS == "linux" || runtime.GOOS == "android" {
+ // Some Mesa drivers crash if an sRGB framebuffer is requested without alpha.
+ // https://bugs.freedesktop.org/show_bug.cgi?id=107782.
+ //
+ // Also, some Android devices (Samsung S9) needs alpha for sRGB to work.
+ attribs = append(attribs, _EGL_ALPHA_SIZE, 8)
+ }
+ // Only request a depth buffer if we're going to render directly to the framebuffer.
+ attribs = append(attribs, _EGL_DEPTH_SIZE, 16)
+ }
+ attribs = append(attribs, _EGL_NONE)
+ eglCfg, ret := eglChooseConfig(disp, attribs)
+ if !ret {
+ return nil, fmt.Errorf("eglChooseConfig failed: 0x%x", eglGetError())
+ }
+ if eglCfg == nilEGLConfig {
+ supportsNoCfg := hasExtension(exts, "EGL_KHR_no_config_context")
+ if !supportsNoCfg {
+ return nil, errors.New("eglChooseConfig returned no configs")
+ }
+ }
+ var visID _EGLint
+ if eglCfg != nilEGLConfig {
+ var ok bool
+ visID, ok = eglGetConfigAttrib(disp, eglCfg, _EGL_NATIVE_VISUAL_ID)
+ if !ok {
+ return nil, errors.New("newContext: eglGetConfigAttrib for _EGL_NATIVE_VISUAL_ID failed")
+ }
+ }
+ ctxAttribs := []_EGLint{
+ _EGL_CONTEXT_CLIENT_VERSION, 3,
+ _EGL_NONE,
+ }
+ eglCtx := eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs)
+ if eglCtx == nilEGLContext {
+ // Fall back to OpenGL ES 2 and rely on extensions.
+ ctxAttribs := []_EGLint{
+ _EGL_CONTEXT_CLIENT_VERSION, 2,
+ _EGL_NONE,
+ }
+ eglCtx = eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs)
+ if eglCtx == nilEGLContext {
+ return nil, fmt.Errorf("eglCreateContext failed: 0x%x",
+ eglGetError())
+ }
+ }
+ return &eglContext{
+ config: _EGLConfig(eglCfg),
+ ctx: _EGLContext(eglCtx),
+ visualID: int(visID),
+ srgb: srgb,
+ surfaceless: hasExtension(exts, "EGL_KHR_surfaceless_context"),
+ }, nil
+}
+
+func createSurface(disp _EGLDisplay, eglCtx *eglContext,
+ win NativeWindowType) (_EGLSurface, error) {
+ var surfAttribs []_EGLint
+ if eglCtx.srgb {
+ surfAttribs = append(surfAttribs, _EGL_GL_COLORSPACE_KHR,
+ _EGL_GL_COLORSPACE_SRGB_KHR)
+ }
+ surfAttribs = append(surfAttribs, _EGL_NONE)
+ eglSurf := eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs)
+ if eglSurf == nilEGLSurface && eglCtx.srgb {
+ // Try again without sRGB
+ eglCtx.srgb = false
+ surfAttribs = []_EGLint{_EGL_NONE}
+ eglSurf = eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs)
+ }
+ if eglSurf == nilEGLSurface {
+ return nilEGLSurface, fmt.Errorf("newContext: eglCreateWindowSurface failed 0x%x (sRGB=%v)",
+ eglGetError(), eglCtx.srgb)
+ }
+ return eglSurf, nil
+}
diff --git a/gio/giold/internal/egl/egl_unix.go b/gio/giold/internal/egl/egl_unix.go
new file mode 100644
index 0000000..059dd55
--- /dev/null
+++ b/gio/giold/internal/egl/egl_unix.go
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build linux freebsd openbsd
+
+package egl
+
+/*
+#cgo linux,!android pkg-config: egl
+#cgo freebsd openbsd android LDFLAGS: -lEGL
+#cgo freebsd CFLAGS: -I/usr/local/include
+#cgo freebsd LDFLAGS: -L/usr/local/lib
+#cgo openbsd CFLAGS: -I/usr/X11R6/include
+#cgo openbsd LDFLAGS: -L/usr/X11R6/lib
+#cgo CFLAGS: -DEGL_NO_X11
+
+#include
+#include
+*/
+import "C"
+
+type (
+ _EGLint = C.EGLint
+ _EGLDisplay = C.EGLDisplay
+ _EGLConfig = C.EGLConfig
+ _EGLContext = C.EGLContext
+ _EGLSurface = C.EGLSurface
+ NativeDisplayType = C.EGLNativeDisplayType
+ NativeWindowType = C.EGLNativeWindowType
+)
+
+func loadEGL() error {
+ return nil
+}
+
+func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) {
+ var cfg C.EGLConfig
+ var ncfg C.EGLint
+ if C.eglChooseConfig(disp, &attribs[0], &cfg, 1, &ncfg) != C.EGL_TRUE {
+ return nilEGLConfig, false
+ }
+ return _EGLConfig(cfg), true
+}
+
+func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, attribs []_EGLint) _EGLContext {
+ ctx := C.eglCreateContext(disp, cfg, shareCtx, &attribs[0])
+ return _EGLContext(ctx)
+}
+
+func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool {
+ return C.eglDestroySurface(disp, surf) == C.EGL_TRUE
+}
+
+func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool {
+ return C.eglDestroyContext(disp, ctx) == C.EGL_TRUE
+}
+
+func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, attr _EGLint) (_EGLint, bool) {
+ var val _EGLint
+ ret := C.eglGetConfigAttrib(disp, cfg, attr, &val)
+ return val, ret == C.EGL_TRUE
+}
+
+func eglGetError() _EGLint {
+ return C.eglGetError()
+}
+
+func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) {
+ var maj, min _EGLint
+ ret := C.eglInitialize(disp, &maj, &min)
+ return maj, min, ret == C.EGL_TRUE
+}
+
+func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, ctx _EGLContext) bool {
+ return C.eglMakeCurrent(disp, draw, read, ctx) == C.EGL_TRUE
+}
+
+func eglReleaseThread() bool {
+ return C.eglReleaseThread() == C.EGL_TRUE
+}
+
+func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool {
+ return C.eglSwapBuffers(disp, surf) == C.EGL_TRUE
+}
+
+func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool {
+ return C.eglSwapInterval(disp, interval) == C.EGL_TRUE
+}
+
+func eglTerminate(disp _EGLDisplay) bool {
+ return C.eglTerminate(disp) == C.EGL_TRUE
+}
+
+func eglQueryString(disp _EGLDisplay, name _EGLint) string {
+ return C.GoString(C.eglQueryString(disp, name))
+}
+
+func eglGetDisplay(disp NativeDisplayType) _EGLDisplay {
+ return C.eglGetDisplay(disp)
+}
+
+func eglCreateWindowSurface(disp _EGLDisplay, conf _EGLConfig, win NativeWindowType, attribs []_EGLint) _EGLSurface {
+ eglSurf := C.eglCreateWindowSurface(disp, conf, win, &attribs[0])
+ return eglSurf
+}
diff --git a/gio/giold/internal/egl/egl_windows.go b/gio/giold/internal/egl/egl_windows.go
new file mode 100644
index 0000000..5df5c65
--- /dev/null
+++ b/gio/giold/internal/egl/egl_windows.go
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package egl
+
+import (
+ "fmt"
+ "runtime"
+ "sync"
+ "unsafe"
+
+ syscall "golang.org/x/sys/windows"
+
+ "realy.lol/gio/internal/gl"
+)
+
+type (
+ _EGLint int32
+ _EGLDisplay uintptr
+ _EGLConfig uintptr
+ _EGLContext uintptr
+ _EGLSurface uintptr
+ NativeDisplayType uintptr
+ NativeWindowType uintptr
+)
+
+var (
+ libEGL = syscall.NewLazyDLL("libEGL.dll")
+ _eglChooseConfig = libEGL.NewProc("eglChooseConfig")
+ _eglCreateContext = libEGL.NewProc("eglCreateContext")
+ _eglCreateWindowSurface = libEGL.NewProc("eglCreateWindowSurface")
+ _eglDestroyContext = libEGL.NewProc("eglDestroyContext")
+ _eglDestroySurface = libEGL.NewProc("eglDestroySurface")
+ _eglGetConfigAttrib = libEGL.NewProc("eglGetConfigAttrib")
+ _eglGetDisplay = libEGL.NewProc("eglGetDisplay")
+ _eglGetError = libEGL.NewProc("eglGetError")
+ _eglInitialize = libEGL.NewProc("eglInitialize")
+ _eglMakeCurrent = libEGL.NewProc("eglMakeCurrent")
+ _eglReleaseThread = libEGL.NewProc("eglReleaseThread")
+ _eglSwapInterval = libEGL.NewProc("eglSwapInterval")
+ _eglSwapBuffers = libEGL.NewProc("eglSwapBuffers")
+ _eglTerminate = libEGL.NewProc("eglTerminate")
+ _eglQueryString = libEGL.NewProc("eglQueryString")
+)
+
+var loadOnce sync.Once
+
+func loadEGL() error {
+ var err error
+ loadOnce.Do(func() {
+ err = loadDLLs()
+ })
+ return err
+}
+
+func loadDLLs() error {
+ if err := loadDLL(libEGL, "libEGL.dll"); err != nil {
+ return err
+ }
+ if err := loadDLL(gl.LibGLESv2, "libGLESv2.dll"); err != nil {
+ return err
+ }
+ // d3dcompiler_47.dll is needed internally for shader compilation to function.
+ return loadDLL(syscall.NewLazyDLL("d3dcompiler_47.dll"),
+ "d3dcompiler_47.dll")
+}
+
+func loadDLL(dll *syscall.LazyDLL, name string) error {
+ err := dll.Load()
+ if err != nil {
+ return fmt.Errorf("egl: failed to load %s: %v", name, err)
+ }
+ return nil
+}
+
+func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) {
+ var cfg _EGLConfig
+ var ncfg _EGLint
+ a := &attribs[0]
+ r, _, _ := _eglChooseConfig.Call(uintptr(disp), uintptr(unsafe.Pointer(a)),
+ uintptr(unsafe.Pointer(&cfg)), 1, uintptr(unsafe.Pointer(&ncfg)))
+ issue34474KeepAlive(a)
+ return cfg, r != 0
+}
+
+func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext,
+ attribs []_EGLint) _EGLContext {
+ a := &attribs[0]
+ c, _, _ := _eglCreateContext.Call(uintptr(disp), uintptr(cfg),
+ uintptr(shareCtx), uintptr(unsafe.Pointer(a)))
+ issue34474KeepAlive(a)
+ return _EGLContext(c)
+}
+
+func eglCreateWindowSurface(disp _EGLDisplay, cfg _EGLConfig,
+ win NativeWindowType, attribs []_EGLint) _EGLSurface {
+ a := &attribs[0]
+ s, _, _ := _eglCreateWindowSurface.Call(uintptr(disp), uintptr(cfg),
+ uintptr(win), uintptr(unsafe.Pointer(a)))
+ issue34474KeepAlive(a)
+ return _EGLSurface(s)
+}
+
+func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool {
+ r, _, _ := _eglDestroySurface.Call(uintptr(disp), uintptr(surf))
+ return r != 0
+}
+
+func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool {
+ r, _, _ := _eglDestroyContext.Call(uintptr(disp), uintptr(ctx))
+ return r != 0
+}
+
+func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig,
+ attr _EGLint) (_EGLint, bool) {
+ var val uintptr
+ r, _, _ := _eglGetConfigAttrib.Call(uintptr(disp), uintptr(cfg),
+ uintptr(attr), uintptr(unsafe.Pointer(&val)))
+ return _EGLint(val), r != 0
+}
+
+func eglGetDisplay(disp NativeDisplayType) _EGLDisplay {
+ d, _, _ := _eglGetDisplay.Call(uintptr(disp))
+ return _EGLDisplay(d)
+}
+
+func eglGetError() _EGLint {
+ e, _, _ := _eglGetError.Call()
+ return _EGLint(e)
+}
+
+func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) {
+ var maj, min uintptr
+ r, _, _ := _eglInitialize.Call(uintptr(disp), uintptr(unsafe.Pointer(&maj)),
+ uintptr(unsafe.Pointer(&min)))
+ return _EGLint(maj), _EGLint(min), r != 0
+}
+
+func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface,
+ ctx _EGLContext) bool {
+ r, _, _ := _eglMakeCurrent.Call(uintptr(disp), uintptr(draw), uintptr(read),
+ uintptr(ctx))
+ return r != 0
+}
+
+func eglReleaseThread() bool {
+ r, _, _ := _eglReleaseThread.Call()
+ return r != 0
+}
+
+func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool {
+ r, _, _ := _eglSwapInterval.Call(uintptr(disp), uintptr(interval))
+ return r != 0
+}
+
+func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool {
+ r, _, _ := _eglSwapBuffers.Call(uintptr(disp), uintptr(surf))
+ return r != 0
+}
+
+func eglTerminate(disp _EGLDisplay) bool {
+ r, _, _ := _eglTerminate.Call(uintptr(disp))
+ return r != 0
+}
+
+func eglQueryString(disp _EGLDisplay, name _EGLint) string {
+ r, _, _ := _eglQueryString.Call(uintptr(disp), uintptr(name))
+ return syscall.BytePtrToString((*byte)(unsafe.Pointer(r)))
+}
+
+// issue34474KeepAlive calls runtime.KeepAlive as a
+// workaround for golang.org/issue/34474.
+func issue34474KeepAlive(v interface{}) {
+ runtime.KeepAlive(v)
+}
diff --git a/gio/giold/internal/f32color/rgba.go b/gio/giold/internal/f32color/rgba.go
new file mode 100644
index 0000000..eecf018
--- /dev/null
+++ b/gio/giold/internal/f32color/rgba.go
@@ -0,0 +1,177 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package f32color
+
+import (
+ "image/color"
+ "math"
+)
+
+// RGBA is a 32 bit floating point linear premultiplied color space.
+type RGBA struct {
+ R, G, B, A float32
+}
+
+// Array returns rgba values in a [4]float32 array.
+func (rgba RGBA) Array() [4]float32 {
+ return [4]float32{rgba.R, rgba.G, rgba.B, rgba.A}
+}
+
+// Float32 returns r, g, b, a values.
+func (col RGBA) Float32() (r, g, b, a float32) {
+ return col.R, col.G, col.B, col.A
+}
+
+// SRGBA converts from linear to sRGB color space.
+func (col RGBA) SRGB() color.NRGBA {
+ if col.A == 0 {
+ return color.NRGBA{}
+ }
+ return color.NRGBA{
+ R: uint8(linearTosRGB(col.R/col.A)*255 + .5),
+ G: uint8(linearTosRGB(col.G/col.A)*255 + .5),
+ B: uint8(linearTosRGB(col.B/col.A)*255 + .5),
+ A: uint8(col.A*255 + .5),
+ }
+}
+
+// Luminance calculates the relative luminance of a linear RGBA color.
+// Normalized to 0 for black and 1 for white.
+//
+// See https://www.w3.org/TR/WCAG20/#relativeluminancedef for more details
+func (col RGBA) Luminance() float32 {
+ return 0.2126*col.R + 0.7152*col.G + 0.0722*col.B
+}
+
+// Opaque returns the color without alpha component.
+func (col RGBA) Opaque() RGBA {
+ col.A = 1.0
+ return col
+}
+
+// LinearFromSRGB converts from col in the sRGB colorspace to RGBA.
+func LinearFromSRGB(col color.NRGBA) RGBA {
+ af := float32(col.A) / 0xFF
+ return RGBA{
+ R: sRGBToLinear(float32(col.R)/0xff) * af,
+ G: sRGBToLinear(float32(col.G)/0xff) * af,
+ B: sRGBToLinear(float32(col.B)/0xff) * af,
+ A: af,
+ }
+}
+
+// NRGBAToRGBA converts from non-premultiplied sRGB color to premultiplied sRGB color.
+//
+// Each component in the result is `sRGBToLinear(c * alpha)`, where `c`
+// is the linear color.
+func NRGBAToRGBA(col color.NRGBA) color.RGBA {
+ if col.A == 0xFF {
+ return color.RGBA(col)
+ }
+ c := LinearFromSRGB(col)
+ return color.RGBA{
+ R: uint8(linearTosRGB(c.R)*255 + .5),
+ G: uint8(linearTosRGB(c.G)*255 + .5),
+ B: uint8(linearTosRGB(c.B)*255 + .5),
+ A: col.A,
+ }
+}
+
+// NRGBAToLinearRGBA converts from non-premultiplied sRGB color to premultiplied linear RGBA color.
+//
+// Each component in the result is `c * alpha`, where `c` is the linear color.
+func NRGBAToLinearRGBA(col color.NRGBA) color.RGBA {
+ if col.A == 0xFF {
+ return color.RGBA(col)
+ }
+ c := LinearFromSRGB(col)
+ return color.RGBA{
+ R: uint8(c.R*255 + .5),
+ G: uint8(c.G*255 + .5),
+ B: uint8(c.B*255 + .5),
+ A: col.A,
+ }
+}
+
+// RGBAToNRGBA converts from premultiplied sRGB color to non-premultiplied sRGB color.
+func RGBAToNRGBA(col color.RGBA) color.NRGBA {
+ if col.A == 0xFF {
+ return color.NRGBA(col)
+ }
+
+ linear := RGBA{
+ R: sRGBToLinear(float32(col.R) / 0xff),
+ G: sRGBToLinear(float32(col.G) / 0xff),
+ B: sRGBToLinear(float32(col.B) / 0xff),
+ A: float32(col.A) / 0xff,
+ }
+
+ return linear.SRGB()
+}
+
+// linearTosRGB transforms color value from linear to sRGB.
+func linearTosRGB(c float32) float32 {
+ // Formula from EXT_sRGB.
+ switch {
+ case c <= 0:
+ return 0
+ case 0 < c && c < 0.0031308:
+ return 12.92 * c
+ case 0.0031308 <= c && c < 1:
+ return 1.055*float32(math.Pow(float64(c), 0.41666)) - 0.055
+ }
+
+ return 1
+}
+
+// sRGBToLinear transforms color value from sRGB to linear.
+func sRGBToLinear(c float32) float32 {
+ // Formula from EXT_sRGB.
+ if c <= 0.04045 {
+ return c / 12.92
+ } else {
+ return float32(math.Pow(float64((c+0.055)/1.055), 2.4))
+ }
+}
+
+// MulAlpha applies the alpha to the color.
+func MulAlpha(c color.NRGBA, alpha uint8) color.NRGBA {
+ c.A = uint8(uint32(c.A) * uint32(alpha) / 0xFF)
+ return c
+}
+
+// Disabled blends color towards the luminance and multiplies alpha.
+// Blending towards luminance will desaturate the color.
+// Multiplying alpha blends the color together more with the background.
+func Disabled(c color.NRGBA) (d color.NRGBA) {
+ const r = 80 // blend ratio
+ lum := approxLuminance(c)
+ return color.NRGBA{
+ R: byte((int(c.R)*r + int(lum)*(256-r)) / 256),
+ G: byte((int(c.G)*r + int(lum)*(256-r)) / 256),
+ B: byte((int(c.B)*r + int(lum)*(256-r)) / 256),
+ A: byte(int(c.A) * (128 + 32) / 256),
+ }
+}
+
+// Hovered blends color towards a brighter color.
+func Hovered(c color.NRGBA) (d color.NRGBA) {
+ const r = 0x20 // lighten ratio
+ return color.NRGBA{
+ R: byte(255 - int(255-c.R)*(255-r)/256),
+ G: byte(255 - int(255-c.G)*(255-r)/256),
+ B: byte(255 - int(255-c.B)*(255-r)/256),
+ A: c.A,
+ }
+}
+
+// approxLuminance is a fast approximate version of RGBA.Luminance.
+func approxLuminance(c color.NRGBA) byte {
+ const (
+ r = 13933 // 0.2126 * 256 * 256
+ g = 46871 // 0.7152 * 256 * 256
+ b = 4732 // 0.0722 * 256 * 256
+ t = r + g + b
+ )
+ return byte((r*int(c.R) + g*int(c.G) + b*int(c.B)) / t)
+}
diff --git a/gio/giold/internal/f32color/rgba_test.go b/gio/giold/internal/f32color/rgba_test.go
new file mode 100644
index 0000000..ea0f871
--- /dev/null
+++ b/gio/giold/internal/f32color/rgba_test.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package f32color
+
+import (
+ "image/color"
+ "testing"
+)
+
+func TestNRGBAToLinearRGBA_Boundary(t *testing.T) {
+ for col := 0; col <= 0xFF; col++ {
+ for alpha := 0; alpha <= 0xFF; alpha++ {
+ in := color.NRGBA{R: uint8(col), A: uint8(alpha)}
+ premul := NRGBAToLinearRGBA(in)
+ if premul.A != uint8(alpha) {
+ t.Errorf("%v: got %v expected %v", in, premul.A, alpha)
+ }
+ if premul.R > premul.A {
+ t.Errorf("%v: R=%v > A=%v", in, premul.R, premul.A)
+ }
+ }
+ }
+}
+
+func TestLinearToRGBARoundtrip(t *testing.T) {
+ for col := 0; col <= 0xFF; col++ {
+ for alpha := 0; alpha <= 0xFF; alpha++ {
+ want := color.NRGBA{R: uint8(col), A: uint8(alpha)}
+ if alpha == 0 {
+ want.R = 0
+ }
+ got := LinearFromSRGB(want).SRGB()
+ if want != got {
+ t.Errorf("got %v expected %v", got, want)
+ }
+ }
+ }
+}
diff --git a/gio/giold/internal/fling/animation.go b/gio/giold/internal/fling/animation.go
new file mode 100644
index 0000000..82a2b8e
--- /dev/null
+++ b/gio/giold/internal/fling/animation.go
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package fling
+
+import (
+ "math"
+ "runtime"
+ "time"
+
+ "realy.lol/gio/unit"
+)
+
+type Animation struct {
+ // Current offset in pixels.
+ x float32
+ // Initial time.
+ t0 time.Time
+ // Initial velocity in pixels pr second.
+ v0 float32
+}
+
+var (
+ // Pixels/second.
+ minFlingVelocity = unit.Dp(50)
+ maxFlingVelocity = unit.Dp(8000)
+)
+
+const (
+ thresholdVelocity = 1
+)
+
+// Start a fling given a starting velocity. Returns whether a
+// fling was started.
+func (f *Animation) Start(c unit.Metric, now time.Time, velocity float32) bool {
+ min := float32(c.Px(minFlingVelocity))
+ v := velocity
+ if -min <= v && v <= min {
+ return false
+ }
+ max := float32(c.Px(maxFlingVelocity))
+ if v > max {
+ v = max
+ } else if v < -max {
+ v = -max
+ }
+ f.init(now, v)
+ return true
+}
+
+func (f *Animation) init(now time.Time, v0 float32) {
+ f.t0 = now
+ f.v0 = v0
+ f.x = 0
+}
+
+func (f *Animation) Active() bool {
+ return f.v0 != 0
+}
+
+// Tick computes and returns a fling distance since
+// the last time Tick was called.
+func (f *Animation) Tick(now time.Time) int {
+ if !f.Active() {
+ return 0
+ }
+ var k float32
+ if runtime.GOOS == "darwin" {
+ k = -2 // iOS
+ } else {
+ k = -4.2 // Android and default
+ }
+ t := now.Sub(f.t0)
+ // The acceleration x''(t) of a point mass with a drag
+ // force, f, proportional with velocity, x'(t), is
+ // governed by the equation
+ //
+ // x''(t) = kx'(t)
+ //
+ // Given the starting position x(0) = 0, the starting
+ // velocity x'(0) = v0, the position is then
+ // given by
+ //
+ // x(t) = v0*e^(k*t)/k - v0/k
+ //
+ ekt := float32(math.Exp(float64(k) * t.Seconds()))
+ x := f.v0*ekt/k - f.v0/k
+ dist := x - f.x
+ idist := int(dist)
+ f.x += float32(idist)
+ // Solving for the velocity x'(t) gives us
+ //
+ // x'(t) = v0*e^(k*t)
+ v := f.v0 * ekt
+ if -thresholdVelocity < v && v < thresholdVelocity {
+ f.v0 = 0
+ }
+ return idist
+}
diff --git a/gio/giold/internal/fling/extrapolation.go b/gio/giold/internal/fling/extrapolation.go
new file mode 100644
index 0000000..655ef84
--- /dev/null
+++ b/gio/giold/internal/fling/extrapolation.go
@@ -0,0 +1,332 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package fling
+
+import (
+ "math"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Extrapolation computes a 1-dimensional velocity estimate
+// for a set of timestamped points using the least squares
+// fit of a 2nd order polynomial. The same method is used
+// by Android.
+type Extrapolation struct {
+ // Index into points.
+ idx int
+ // Circular buffer of samples.
+ samples []sample
+ lastValue float32
+ // Pre-allocated cache for samples.
+ cache [historySize]sample
+
+ // Filtered values and times
+ values [historySize]float32
+ times [historySize]float32
+}
+
+type sample struct {
+ t time.Duration
+ v float32
+}
+
+type matrix struct {
+ rows, cols int
+ data []float32
+}
+
+type Estimate struct {
+ Velocity float32
+ Distance float32
+}
+
+type coefficients [degree + 1]float32
+
+const (
+ degree = 2
+ historySize = 20
+ maxAge = 100 * time.Millisecond
+ maxSampleGap = 40 * time.Millisecond
+)
+
+// SampleDelta adds a relative sample to the estimation.
+func (e *Extrapolation) SampleDelta(t time.Duration, delta float32) {
+ val := delta + e.lastValue
+ e.Sample(t, val)
+}
+
+// Sample adds an absolute sample to the estimation.
+func (e *Extrapolation) Sample(t time.Duration, val float32) {
+ e.lastValue = val
+ if e.samples == nil {
+ e.samples = e.cache[:0]
+ }
+ s := sample{
+ t: t,
+ v: val,
+ }
+ if e.idx == len(e.samples) && e.idx < cap(e.samples) {
+ e.samples = append(e.samples, s)
+ } else {
+ e.samples[e.idx] = s
+ }
+ e.idx++
+ if e.idx == cap(e.samples) {
+ e.idx = 0
+ }
+}
+
+// Velocity returns an estimate of the implied velocity and
+// distance for the points sampled, or zero if the estimation method
+// failed.
+func (e *Extrapolation) Estimate() Estimate {
+ if len(e.samples) == 0 {
+ return Estimate{}
+ }
+ values := e.values[:0]
+ times := e.times[:0]
+ first := e.get(0)
+ t := first.t
+ // Walk backwards collecting samples.
+ for i := 0; i < len(e.samples); i++ {
+ p := e.get(-i)
+ age := first.t - p.t
+ if age >= maxAge || t-p.t >= maxSampleGap {
+ // If the samples are too old or
+ // too much time passed between samples
+ // assume they're not part of the fling.
+ break
+ }
+ t = p.t
+ values = append(values, first.v-p.v)
+ times = append(times, float32((-age).Seconds()))
+ }
+ coef, ok := polyFit(times, values)
+ if !ok {
+ return Estimate{}
+ }
+ dist := values[len(values)-1] - values[0]
+ return Estimate{
+ Velocity: coef[1],
+ Distance: dist,
+ }
+}
+
+func (e *Extrapolation) get(i int) sample {
+ idx := (e.idx + i - 1 + len(e.samples)) % len(e.samples)
+ return e.samples[idx]
+}
+
+// fit computes the least squares polynomial fit for
+// the set of points in X, Y. If the fitting fails
+// because of contradicting or insufficient data,
+// fit returns false.
+func polyFit(X, Y []float32) (coefficients, bool) {
+ if len(X) != len(Y) {
+ panic("X and Y lengths differ")
+ }
+ if len(X) <= degree {
+ // Not enough points to fit a curve.
+ return coefficients{}, false
+ }
+
+ // Use a method similar to Android's VelocityTracker.cpp:
+ // https://android.googlesource.com/platform/frameworks/base/+/56a2301/libs/androidfw/VelocityTracker.cpp
+ // where all weights are 1.
+
+ // First, expand the X vector to the matrix A in column-major order.
+ A := newMatrix(degree+1, len(X))
+ for i, x := range X {
+ A.set(0, i, 1)
+ for j := 1; j < A.rows; j++ {
+ A.set(j, i, A.get(j-1, i)*x)
+ }
+ }
+
+ Q, Rt, ok := decomposeQR(A)
+ if !ok {
+ return coefficients{}, false
+ }
+ // Solve R*B = Qt*Y for B, which is then the polynomial coefficients.
+ // Since R is upper triangular, we can proceed from bottom right to
+ // upper left.
+ // https://en.wikipedia.org/wiki/Non-linear_least_squares
+ var B coefficients
+ for i := Q.rows - 1; i >= 0; i-- {
+ B[i] = dot(Q.col(i), Y)
+ for j := Q.rows - 1; j > i; j-- {
+ B[i] -= Rt.get(i, j) * B[j]
+ }
+ B[i] /= Rt.get(i, i)
+ }
+ return B, true
+}
+
+// decomposeQR computes and returns Q, Rt where Q*transpose(Rt) = A, if
+// possible. R is guaranteed to be upper triangular and only the square
+// part of Rt is returned.
+func decomposeQR(A *matrix) (*matrix, *matrix, bool) {
+ // Gram-Schmidt QR decompose A where Q*R = A.
+ // https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process
+ Q := newMatrix(A.rows, A.cols) // Column-major.
+ Rt := newMatrix(A.rows, A.rows) // R transposed, row-major.
+ for i := 0; i < Q.rows; i++ {
+ // Copy A column.
+ for j := 0; j < Q.cols; j++ {
+ Q.set(i, j, A.get(i, j))
+ }
+ // Subtract projections. Note that int the projection
+ //
+ // proju a = / u
+ //
+ // the normalized column e replaces u, where = 1:
+ //
+ // proje a = / e = e
+ for j := 0; j < i; j++ {
+ d := dot(Q.col(j), Q.col(i))
+ for k := 0; k < Q.cols; k++ {
+ Q.set(i, k, Q.get(i, k)-d*Q.get(j, k))
+ }
+ }
+ // Normalize Q columns.
+ n := norm(Q.col(i))
+ if n < 0.000001 {
+ // Degenerate data, no solution.
+ return nil, nil, false
+ }
+ invNorm := 1 / n
+ for j := 0; j < Q.cols; j++ {
+ Q.set(i, j, Q.get(i, j)*invNorm)
+ }
+ // Update Rt.
+ for j := i; j < Rt.cols; j++ {
+ Rt.set(i, j, dot(Q.col(i), A.col(j)))
+ }
+ }
+ return Q, Rt, true
+}
+
+func norm(V []float32) float32 {
+ var n float32
+ for _, v := range V {
+ n += v * v
+ }
+ return float32(math.Sqrt(float64(n)))
+}
+
+func dot(V1, V2 []float32) float32 {
+ var d float32
+ for i, v1 := range V1 {
+ d += v1 * V2[i]
+ }
+ return d
+}
+
+func newMatrix(rows, cols int) *matrix {
+ return &matrix{
+ rows: rows,
+ cols: cols,
+ data: make([]float32, rows*cols),
+ }
+}
+
+func (m *matrix) set(row, col int, v float32) {
+ if row < 0 || row >= m.rows {
+ panic("row out of range")
+ }
+ if col < 0 || col >= m.cols {
+ panic("col out of range")
+ }
+ m.data[row*m.cols+col] = v
+}
+
+func (m *matrix) get(row, col int) float32 {
+ if row < 0 || row >= m.rows {
+ panic("row out of range")
+ }
+ if col < 0 || col >= m.cols {
+ panic("col out of range")
+ }
+ return m.data[row*m.cols+col]
+}
+
+func (m *matrix) col(c int) []float32 {
+ return m.data[c*m.cols : (c+1)*m.cols]
+}
+
+func (m *matrix) approxEqual(m2 *matrix) bool {
+ if m.rows != m2.rows || m.cols != m2.cols {
+ return false
+ }
+ const epsilon = 0.00001
+ for row := 0; row < m.rows; row++ {
+ for col := 0; col < m.cols; col++ {
+ d := m2.get(row, col) - m.get(row, col)
+ if d < -epsilon || d > epsilon {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+func (m *matrix) transpose() *matrix {
+ t := &matrix{
+ rows: m.cols,
+ cols: m.rows,
+ data: make([]float32, len(m.data)),
+ }
+ for i := 0; i < m.rows; i++ {
+ for j := 0; j < m.cols; j++ {
+ t.set(j, i, m.get(i, j))
+ }
+ }
+ return t
+}
+
+func (m *matrix) mul(m2 *matrix) *matrix {
+ if m.rows != m2.cols {
+ panic("mismatched matrices")
+ }
+ mm := &matrix{
+ rows: m.rows,
+ cols: m2.cols,
+ data: make([]float32, m.rows*m2.cols),
+ }
+ for i := 0; i < mm.rows; i++ {
+ for j := 0; j < mm.cols; j++ {
+ var v float32
+ for k := 0; k < m.rows; k++ {
+ v += m.get(k, j) * m2.get(i, k)
+ }
+ mm.set(i, j, v)
+ }
+ }
+ return mm
+}
+
+func (m *matrix) String() string {
+ var b strings.Builder
+ for i := 0; i < m.rows; i++ {
+ for j := 0; j < m.cols; j++ {
+ v := m.get(i, j)
+ b.WriteString(strconv.FormatFloat(float64(v), 'g', -1, 32))
+ b.WriteString(", ")
+ }
+ b.WriteString("\n")
+ }
+ return b.String()
+}
+
+func (c coefficients) approxEqual(c2 coefficients) bool {
+ const epsilon = 0.00001
+ for i, v := range c {
+ d := v - c2[i]
+ if d < -epsilon || d > epsilon {
+ return false
+ }
+ }
+ return true
+}
diff --git a/gio/giold/internal/fling/extrapolation_test.go b/gio/giold/internal/fling/extrapolation_test.go
new file mode 100644
index 0000000..3f9d982
--- /dev/null
+++ b/gio/giold/internal/fling/extrapolation_test.go
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package fling
+
+import "testing"
+
+func TestDecomposeQR(t *testing.T) {
+ A := &matrix{
+ rows: 3, cols: 3,
+ data: []float32{
+ 12, 6, -4,
+ -51, 167, 24,
+ 4, -68, -41,
+ },
+ }
+ Q, Rt, ok := decomposeQR(A)
+ if !ok {
+ t.Fatal("decomposeQR failed")
+ }
+ R := Rt.transpose()
+ QR := Q.mul(R)
+ if !A.approxEqual(QR) {
+ t.Log("A\n", A)
+ t.Log("Q\n", Q)
+ t.Log("R\n", R)
+ t.Log("QR\n", QR)
+ t.Fatal("Q*R not approximately equal to A")
+ }
+}
+
+func TestFit(t *testing.T) {
+ X := []float32{-1, 0, 1}
+ Y := []float32{2, 0, 2}
+
+ got, ok := polyFit(X, Y)
+ if !ok {
+ t.Fatal("polyFit failed")
+ }
+ want := coefficients{0, 0, 2}
+ if !got.approxEqual(want) {
+ t.Fatalf("polyFit: got %v want %v", got, want)
+ }
+}
diff --git a/gio/giold/internal/gl/gl.go b/gio/giold/internal/gl/gl.go
new file mode 100644
index 0000000..9696c71
--- /dev/null
+++ b/gio/giold/internal/gl/gl.go
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gl
+
+type (
+ Attrib uint
+ Enum uint
+)
+
+const (
+ ALL_BARRIER_BITS = 0xffffffff
+ ARRAY_BUFFER = 0x8892
+ BLEND = 0xbe2
+ CLAMP_TO_EDGE = 0x812f
+ COLOR_ATTACHMENT0 = 0x8ce0
+ COLOR_BUFFER_BIT = 0x4000
+ COMPILE_STATUS = 0x8b81
+ COMPUTE_SHADER = 0x91B9
+ DEPTH_BUFFER_BIT = 0x100
+ DEPTH_ATTACHMENT = 0x8d00
+ DEPTH_COMPONENT16 = 0x81a5
+ DEPTH_COMPONENT24 = 0x81A6
+ DEPTH_COMPONENT32F = 0x8CAC
+ DEPTH_TEST = 0xb71
+ DRAW_FRAMEBUFFER = 0x8CA9
+ DST_COLOR = 0x306
+ DYNAMIC_DRAW = 0x88E8
+ DYNAMIC_READ = 0x88E9
+ ELEMENT_ARRAY_BUFFER = 0x8893
+ EXTENSIONS = 0x1f03
+ FALSE = 0
+ FLOAT = 0x1406
+ FRAGMENT_SHADER = 0x8b30
+ FRAMEBUFFER = 0x8d40
+ FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING = 0x8210
+ FRAMEBUFFER_BINDING = 0x8ca6
+ FRAMEBUFFER_COMPLETE = 0x8cd5
+ HALF_FLOAT = 0x140b
+ HALF_FLOAT_OES = 0x8d61
+ INFO_LOG_LENGTH = 0x8B84
+ INVALID_INDEX = ^uint(0)
+ GREATER = 0x204
+ GEQUAL = 0x206
+ LINEAR = 0x2601
+ LINK_STATUS = 0x8b82
+ LUMINANCE = 0x1909
+ MAP_READ_BIT = 0x0001
+ MAX_TEXTURE_SIZE = 0xd33
+ NEAREST = 0x2600
+ NO_ERROR = 0x0
+ NUM_EXTENSIONS = 0x821D
+ ONE = 0x1
+ ONE_MINUS_SRC_ALPHA = 0x303
+ PROGRAM_BINARY_LENGTH = 0x8741
+ QUERY_RESULT = 0x8866
+ QUERY_RESULT_AVAILABLE = 0x8867
+ R16F = 0x822d
+ R8 = 0x8229
+ READ_FRAMEBUFFER = 0x8ca8
+ READ_ONLY = 0x88B8
+ READ_WRITE = 0x88BA
+ RED = 0x1903
+ RENDERER = 0x1F01
+ RENDERBUFFER = 0x8d41
+ RENDERBUFFER_BINDING = 0x8ca7
+ RENDERBUFFER_HEIGHT = 0x8d43
+ RENDERBUFFER_WIDTH = 0x8d42
+ RGB = 0x1907
+ RGBA = 0x1908
+ RGBA8 = 0x8058
+ SHADER_STORAGE_BUFFER = 0x90D2
+ SHORT = 0x1402
+ SRGB = 0x8c40
+ SRGB_ALPHA_EXT = 0x8c42
+ SRGB8 = 0x8c41
+ SRGB8_ALPHA8 = 0x8c43
+ STATIC_DRAW = 0x88e4
+ STENCIL_BUFFER_BIT = 0x00000400
+ TEXTURE_2D = 0xde1
+ TEXTURE_MAG_FILTER = 0x2800
+ TEXTURE_MIN_FILTER = 0x2801
+ TEXTURE_WRAP_S = 0x2802
+ TEXTURE_WRAP_T = 0x2803
+ TEXTURE0 = 0x84c0
+ TEXTURE1 = 0x84c1
+ TRIANGLE_STRIP = 0x5
+ TRIANGLES = 0x4
+ TRUE = 1
+ UNIFORM_BUFFER = 0x8A11
+ UNPACK_ALIGNMENT = 0xcf5
+ UNSIGNED_BYTE = 0x1401
+ UNSIGNED_SHORT = 0x1403
+ VERSION = 0x1f02
+ VERTEX_SHADER = 0x8b31
+ WRITE_ONLY = 0x88B9
+ ZERO = 0x0
+
+ // EXT_disjoint_timer_query
+ TIME_ELAPSED_EXT = 0x88BF
+ GPU_DISJOINT_EXT = 0x8FBB
+)
+
+var _ interface {
+ ActiveTexture(texture Enum)
+ AttachShader(p Program, s Shader)
+ BeginQuery(target Enum, query Query)
+ BindAttribLocation(p Program, a Attrib, name string)
+ BindBuffer(target Enum, b Buffer)
+ BindBufferBase(target Enum, index int, buffer Buffer)
+ BindFramebuffer(target Enum, fb Framebuffer)
+ BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum)
+ BindRenderbuffer(target Enum, fb Renderbuffer)
+ BindTexture(target Enum, t Texture)
+ BlendEquation(mode Enum)
+ BlendFunc(sfactor, dfactor Enum)
+ BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum)
+ BufferData(target Enum, size int, usage Enum)
+ BufferSubData(target Enum, offset int, src []byte)
+ CheckFramebufferStatus(target Enum) Enum
+ Clear(mask Enum)
+ ClearColor(red, green, blue, alpha float32)
+ ClearDepthf(d float32)
+ CompileShader(s Shader)
+ CreateBuffer() Buffer
+ CreateFramebuffer() Framebuffer
+ CreateProgram() Program
+ CreateQuery() Query
+ CreateRenderbuffer() Renderbuffer
+ CreateShader(ty Enum) Shader
+ CreateTexture() Texture
+ DeleteBuffer(v Buffer)
+ DeleteFramebuffer(v Framebuffer)
+ DeleteProgram(p Program)
+ DeleteQuery(query Query)
+ DeleteRenderbuffer(r Renderbuffer)
+ DeleteShader(s Shader)
+ DeleteTexture(v Texture)
+ DepthFunc(f Enum)
+ DepthMask(mask bool)
+ DisableVertexAttribArray(a Attrib)
+ Disable(cap Enum)
+ DispatchCompute(x, y, z int)
+ DrawArrays(mode Enum, first, count int)
+ DrawElements(mode Enum, count int, ty Enum, offset int)
+ Enable(cap Enum)
+ EnableVertexAttribArray(a Attrib)
+ EndQuery(target Enum)
+ FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int)
+ FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer)
+ GetBinding(pname Enum) Object
+ GetError() Enum
+ GetInteger(pname Enum) int
+ GetProgrami(p Program, pname Enum) int
+ GetProgramInfoLog(p Program) string
+ GetQueryObjectuiv(query Query, pname Enum) uint
+ GetShaderi(s Shader, pname Enum) int
+ GetShaderInfoLog(s Shader) string
+ GetString(pname Enum) string
+ GetUniformBlockIndex(p Program, name string) uint
+ GetUniformLocation(p Program, name string) Uniform
+ InvalidateFramebuffer(target, attachment Enum)
+ LinkProgram(p Program)
+ MapBufferRange(target Enum, offset, length int, access Enum) []byte
+ MemoryBarrier(barriers Enum)
+ ReadPixels(x, y, width, height int, format, ty Enum, data []byte)
+ RenderbufferStorage(target, internalformat Enum, width, height int)
+ ShaderSource(s Shader, src string)
+ TexImage2D(target Enum, level int, internalFormat Enum, width, height int, format, ty Enum)
+ TexParameteri(target, pname Enum, param int)
+ TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int)
+ TexSubImage2D(target Enum, level, xoff, yoff int, width, height int, format, ty Enum, data []byte)
+ UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint)
+ Uniform1f(dst Uniform, v float32)
+ Uniform1i(dst Uniform, v int)
+ Uniform2f(dst Uniform, v0, v1 float32)
+ Uniform3f(dst Uniform, v0, v1, v2 float32)
+ Uniform4f(dst Uniform, v0, v1, v2, v3 float32)
+ UseProgram(p Program)
+ UnmapBuffer(target Enum) bool
+ VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int)
+ Viewport(x, y, width, height int)
+} = (*Functions)(nil)
diff --git a/gio/giold/internal/gl/gl_js.go b/gio/giold/internal/gl/gl_js.go
new file mode 100644
index 0000000..13890a7
--- /dev/null
+++ b/gio/giold/internal/gl/gl_js.go
@@ -0,0 +1,381 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gl
+
+import (
+ "errors"
+ "strings"
+ "syscall/js"
+)
+
+type Functions struct {
+ Ctx js.Value
+ EXT_disjoint_timer_query js.Value
+ EXT_disjoint_timer_query_webgl2 js.Value
+
+ // Cached reference to the Uint8Array JS type.
+ uint8Array js.Value
+
+ // Cached JS arrays.
+ arrayBuf js.Value
+ int32Buf js.Value
+}
+
+type Context js.Value
+
+func NewFunctions(ctx Context) (*Functions, error) {
+ f := &Functions{
+ Ctx: js.Value(ctx),
+ uint8Array: js.Global().Get("Uint8Array"),
+ }
+ if err := f.Init(); err != nil {
+ return nil, err
+ }
+ return f, nil
+}
+
+func (f *Functions) Init() error {
+ webgl2Class := js.Global().Get("WebGL2RenderingContext")
+ iswebgl2 := !webgl2Class.IsUndefined() && f.Ctx.InstanceOf(webgl2Class)
+ if !iswebgl2 {
+ f.EXT_disjoint_timer_query = f.getExtension("EXT_disjoint_timer_query")
+ if f.getExtension("OES_texture_half_float").IsNull() && f.getExtension("OES_texture_float").IsNull() {
+ return errors.New("gl: no support for neither OES_texture_half_float nor OES_texture_float")
+ }
+ if f.getExtension("EXT_sRGB").IsNull() {
+ return errors.New("gl: EXT_sRGB not supported")
+ }
+ } else {
+ // WebGL2 extensions.
+ f.EXT_disjoint_timer_query_webgl2 = f.getExtension("EXT_disjoint_timer_query_webgl2")
+ if f.getExtension("EXT_color_buffer_half_float").IsNull() && f.getExtension("EXT_color_buffer_float").IsNull() {
+ return errors.New("gl: no support for neither EXT_color_buffer_half_float nor EXT_color_buffer_float")
+ }
+ }
+ return nil
+}
+
+func (f *Functions) getExtension(name string) js.Value {
+ return f.Ctx.Call("getExtension", name)
+}
+
+func (f *Functions) ActiveTexture(t Enum) {
+ f.Ctx.Call("activeTexture", int(t))
+}
+func (f *Functions) AttachShader(p Program, s Shader) {
+ f.Ctx.Call("attachShader", js.Value(p), js.Value(s))
+}
+func (f *Functions) BeginQuery(target Enum, query Query) {
+ if !f.EXT_disjoint_timer_query_webgl2.IsNull() {
+ f.Ctx.Call("beginQuery", int(target), js.Value(query))
+ } else {
+ f.EXT_disjoint_timer_query.Call("beginQueryEXT", int(target), js.Value(query))
+ }
+}
+func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) {
+ f.Ctx.Call("bindAttribLocation", js.Value(p), int(a), name)
+}
+func (f *Functions) BindBuffer(target Enum, b Buffer) {
+ f.Ctx.Call("bindBuffer", int(target), js.Value(b))
+}
+func (f *Functions) BindBufferBase(target Enum, index int, b Buffer) {
+ f.Ctx.Call("bindBufferBase", int(target), index, js.Value(b))
+}
+func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) {
+ f.Ctx.Call("bindFramebuffer", int(target), js.Value(fb))
+}
+func (f *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) {
+ f.Ctx.Call("bindRenderbuffer", int(target), js.Value(rb))
+}
+func (f *Functions) BindTexture(target Enum, t Texture) {
+ f.Ctx.Call("bindTexture", int(target), js.Value(t))
+}
+func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) {
+ panic("not implemented")
+}
+func (f *Functions) BlendEquation(mode Enum) {
+ f.Ctx.Call("blendEquation", int(mode))
+}
+func (f *Functions) BlendFunc(sfactor, dfactor Enum) {
+ f.Ctx.Call("blendFunc", int(sfactor), int(dfactor))
+}
+func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) {
+ panic("not implemented")
+}
+func (f *Functions) BufferData(target Enum, size int, usage Enum) {
+ f.Ctx.Call("bufferData", int(target), size, int(usage))
+}
+func (f *Functions) BufferSubData(target Enum, offset int, src []byte) {
+ f.Ctx.Call("bufferSubData", int(target), offset, f.byteArrayOf(src))
+}
+func (f *Functions) CheckFramebufferStatus(target Enum) Enum {
+ return Enum(f.Ctx.Call("checkFramebufferStatus", int(target)).Int())
+}
+func (f *Functions) Clear(mask Enum) {
+ f.Ctx.Call("clear", int(mask))
+}
+func (f *Functions) ClearColor(red, green, blue, alpha float32) {
+ f.Ctx.Call("clearColor", red, green, blue, alpha)
+}
+func (f *Functions) ClearDepthf(d float32) {
+ f.Ctx.Call("clearDepth", d)
+}
+func (f *Functions) CompileShader(s Shader) {
+ f.Ctx.Call("compileShader", js.Value(s))
+}
+func (f *Functions) CreateBuffer() Buffer {
+ return Buffer(f.Ctx.Call("createBuffer"))
+}
+func (f *Functions) CreateFramebuffer() Framebuffer {
+ return Framebuffer(f.Ctx.Call("createFramebuffer"))
+}
+func (f *Functions) CreateProgram() Program {
+ return Program(f.Ctx.Call("createProgram"))
+}
+func (f *Functions) CreateQuery() Query {
+ return Query(f.Ctx.Call("createQuery"))
+}
+func (f *Functions) CreateRenderbuffer() Renderbuffer {
+ return Renderbuffer(f.Ctx.Call("createRenderbuffer"))
+}
+func (f *Functions) CreateShader(ty Enum) Shader {
+ return Shader(f.Ctx.Call("createShader", int(ty)))
+}
+func (f *Functions) CreateTexture() Texture {
+ return Texture(f.Ctx.Call("createTexture"))
+}
+func (f *Functions) DeleteBuffer(v Buffer) {
+ f.Ctx.Call("deleteBuffer", js.Value(v))
+}
+func (f *Functions) DeleteFramebuffer(v Framebuffer) {
+ f.Ctx.Call("deleteFramebuffer", js.Value(v))
+}
+func (f *Functions) DeleteProgram(p Program) {
+ f.Ctx.Call("deleteProgram", js.Value(p))
+}
+func (f *Functions) DeleteQuery(query Query) {
+ if !f.EXT_disjoint_timer_query_webgl2.IsNull() {
+ f.Ctx.Call("deleteQuery", js.Value(query))
+ } else {
+ f.EXT_disjoint_timer_query.Call("deleteQueryEXT", js.Value(query))
+ }
+}
+func (f *Functions) DeleteShader(s Shader) {
+ f.Ctx.Call("deleteShader", js.Value(s))
+}
+func (f *Functions) DeleteRenderbuffer(v Renderbuffer) {
+ f.Ctx.Call("deleteRenderbuffer", js.Value(v))
+}
+func (f *Functions) DeleteTexture(v Texture) {
+ f.Ctx.Call("deleteTexture", js.Value(v))
+}
+func (f *Functions) DepthFunc(fn Enum) {
+ f.Ctx.Call("depthFunc", int(fn))
+}
+func (f *Functions) DepthMask(mask bool) {
+ f.Ctx.Call("depthMask", mask)
+}
+func (f *Functions) DisableVertexAttribArray(a Attrib) {
+ f.Ctx.Call("disableVertexAttribArray", int(a))
+}
+func (f *Functions) Disable(cap Enum) {
+ f.Ctx.Call("disable", int(cap))
+}
+func (f *Functions) DrawArrays(mode Enum, first, count int) {
+ f.Ctx.Call("drawArrays", int(mode), first, count)
+}
+func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) {
+ f.Ctx.Call("drawElements", int(mode), count, int(ty), offset)
+}
+func (f *Functions) DispatchCompute(x, y, z int) {
+ panic("not implemented")
+}
+func (f *Functions) Enable(cap Enum) {
+ f.Ctx.Call("enable", int(cap))
+}
+func (f *Functions) EnableVertexAttribArray(a Attrib) {
+ f.Ctx.Call("enableVertexAttribArray", int(a))
+}
+func (f *Functions) EndQuery(target Enum) {
+ if !f.EXT_disjoint_timer_query_webgl2.IsNull() {
+ f.Ctx.Call("endQuery", int(target))
+ } else {
+ f.EXT_disjoint_timer_query.Call("endQueryEXT", int(target))
+ }
+}
+func (f *Functions) Finish() {
+ f.Ctx.Call("finish")
+}
+func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) {
+ f.Ctx.Call("framebufferRenderbuffer", int(target), int(attachment), int(renderbuffertarget), js.Value(renderbuffer))
+}
+func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) {
+ f.Ctx.Call("framebufferTexture2D", int(target), int(attachment), int(texTarget), js.Value(t), level)
+}
+func (f *Functions) GetError() Enum {
+ // Avoid slow getError calls. See gio#179.
+ return 0
+}
+func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int {
+ return paramVal(f.Ctx.Call("getRenderbufferParameteri", int(pname)))
+}
+func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int {
+ return paramVal(f.Ctx.Call("getFramebufferAttachmentParameter", int(target), int(attachment), int(pname)))
+}
+func (f *Functions) GetBinding(pname Enum) Object {
+ return Object(f.Ctx.Call("getParameter", int(pname)))
+}
+func (f *Functions) GetInteger(pname Enum) int {
+ return paramVal(f.Ctx.Call("getParameter", int(pname)))
+}
+func (f *Functions) GetProgrami(p Program, pname Enum) int {
+ return paramVal(f.Ctx.Call("getProgramParameter", js.Value(p), int(pname)))
+}
+func (f *Functions) GetProgramInfoLog(p Program) string {
+ return f.Ctx.Call("getProgramInfoLog", js.Value(p)).String()
+}
+func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint {
+ if !f.EXT_disjoint_timer_query_webgl2.IsNull() {
+ return uint(paramVal(f.Ctx.Call("getQueryParameter", js.Value(query), int(pname))))
+ } else {
+ return uint(paramVal(f.EXT_disjoint_timer_query.Call("getQueryObjectEXT", js.Value(query), int(pname))))
+ }
+}
+func (f *Functions) GetShaderi(s Shader, pname Enum) int {
+ return paramVal(f.Ctx.Call("getShaderParameter", js.Value(s), int(pname)))
+}
+func (f *Functions) GetShaderInfoLog(s Shader) string {
+ return f.Ctx.Call("getShaderInfoLog", js.Value(s)).String()
+}
+func (f *Functions) GetString(pname Enum) string {
+ switch pname {
+ case EXTENSIONS:
+ extsjs := f.Ctx.Call("getSupportedExtensions")
+ var exts []string
+ for i := 0; i < extsjs.Length(); i++ {
+ exts = append(exts, "GL_"+extsjs.Index(i).String())
+ }
+ return strings.Join(exts, " ")
+ default:
+ return f.Ctx.Call("getParameter", int(pname)).String()
+ }
+}
+func (f *Functions) GetUniformBlockIndex(p Program, name string) uint {
+ return uint(paramVal(f.Ctx.Call("getUniformBlockIndex", js.Value(p), name)))
+}
+func (f *Functions) GetUniformLocation(p Program, name string) Uniform {
+ return Uniform(f.Ctx.Call("getUniformLocation", js.Value(p), name))
+}
+func (f *Functions) InvalidateFramebuffer(target, attachment Enum) {
+ fn := f.Ctx.Get("invalidateFramebuffer")
+ if !fn.IsUndefined() {
+ if f.int32Buf.IsUndefined() {
+ f.int32Buf = js.Global().Get("Int32Array").New(1)
+ }
+ f.int32Buf.SetIndex(0, int32(attachment))
+ f.Ctx.Call("invalidateFramebuffer", int(target), f.int32Buf)
+ }
+}
+func (f *Functions) LinkProgram(p Program) {
+ f.Ctx.Call("linkProgram", js.Value(p))
+}
+func (f *Functions) PixelStorei(pname Enum, param int32) {
+ f.Ctx.Call("pixelStorei", int(pname), param)
+}
+func (f *Functions) MemoryBarrier(barriers Enum) {
+ panic("not implemented")
+}
+func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte {
+ panic("not implemented")
+}
+func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) {
+ f.Ctx.Call("renderbufferStorage", int(target), int(internalformat), width, height)
+}
+func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) {
+ ba := f.byteArrayOf(data)
+ f.Ctx.Call("readPixels", x, y, width, height, int(format), int(ty), ba)
+ js.CopyBytesToGo(data, ba)
+}
+func (f *Functions) Scissor(x, y, width, height int32) {
+ f.Ctx.Call("scissor", x, y, width, height)
+}
+func (f *Functions) ShaderSource(s Shader, src string) {
+ f.Ctx.Call("shaderSource", js.Value(s), src)
+}
+func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width, height int, format, ty Enum) {
+ f.Ctx.Call("texImage2D", int(target), int(level), int(internalFormat), int(width), int(height), 0, int(format), int(ty), nil)
+}
+func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) {
+ f.Ctx.Call("texStorage2D", int(target), levels, int(internalFormat), width, height)
+}
+func (f *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) {
+ f.Ctx.Call("texSubImage2D", int(target), level, x, y, width, height, int(format), int(ty), f.byteArrayOf(data))
+}
+func (f *Functions) TexParameteri(target, pname Enum, param int) {
+ f.Ctx.Call("texParameteri", int(target), int(pname), int(param))
+}
+func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) {
+ f.Ctx.Call("uniformBlockBinding", js.Value(p), int(uniformBlockIndex), int(uniformBlockBinding))
+}
+func (f *Functions) Uniform1f(dst Uniform, v float32) {
+ f.Ctx.Call("uniform1f", js.Value(dst), v)
+}
+func (f *Functions) Uniform1i(dst Uniform, v int) {
+ f.Ctx.Call("uniform1i", js.Value(dst), v)
+}
+func (f *Functions) Uniform2f(dst Uniform, v0, v1 float32) {
+ f.Ctx.Call("uniform2f", js.Value(dst), v0, v1)
+}
+func (f *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) {
+ f.Ctx.Call("uniform3f", js.Value(dst), v0, v1, v2)
+}
+func (f *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) {
+ f.Ctx.Call("uniform4f", js.Value(dst), v0, v1, v2, v3)
+}
+func (f *Functions) UseProgram(p Program) {
+ f.Ctx.Call("useProgram", js.Value(p))
+}
+func (f *Functions) UnmapBuffer(target Enum) bool {
+ panic("not implemented")
+}
+func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) {
+ f.Ctx.Call("vertexAttribPointer", int(dst), size, int(ty), normalized, stride, offset)
+}
+func (f *Functions) Viewport(x, y, width, height int) {
+ f.Ctx.Call("viewport", x, y, width, height)
+}
+
+func (f *Functions) byteArrayOf(data []byte) js.Value {
+ if len(data) == 0 {
+ return js.Null()
+ }
+ f.resizeByteBuffer(len(data))
+ ba := f.uint8Array.New(f.arrayBuf, int(0), int(len(data)))
+ js.CopyBytesToJS(ba, data)
+ return ba
+}
+
+func (f *Functions) resizeByteBuffer(n int) {
+ if n == 0 {
+ return
+ }
+ if !f.arrayBuf.IsUndefined() && f.arrayBuf.Length() >= n {
+ return
+ }
+ f.arrayBuf = js.Global().Get("ArrayBuffer").New(n)
+}
+
+func paramVal(v js.Value) int {
+ switch v.Type() {
+ case js.TypeBoolean:
+ if b := v.Bool(); b {
+ return 1
+ } else {
+ return 0
+ }
+ case js.TypeNumber:
+ return v.Int()
+ default:
+ panic("unknown parameter type")
+ }
+}
diff --git a/gio/giold/internal/gl/gl_unix.go b/gio/giold/internal/gl/gl_unix.go
new file mode 100644
index 0000000..a1d017a
--- /dev/null
+++ b/gio/giold/internal/gl/gl_unix.go
@@ -0,0 +1,635 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build darwin linux freebsd openbsd
+
+package gl
+
+import (
+ "runtime"
+ "strings"
+ "unsafe"
+)
+
+/*
+#cgo CFLAGS: -Werror
+#cgo linux,!android pkg-config: glesv2
+#cgo linux freebsd LDFLAGS: -ldl
+#cgo freebsd openbsd android LDFLAGS: -lGLESv2
+#cgo freebsd CFLAGS: -I/usr/local/include
+#cgo freebsd LDFLAGS: -L/usr/local/lib
+#cgo openbsd CFLAGS: -I/usr/X11R6/include
+#cgo openbsd LDFLAGS: -L/usr/X11R6/lib
+#cgo darwin,!ios CFLAGS: -DGL_SILENCE_DEPRECATION
+#cgo darwin,!ios LDFLAGS: -framework OpenGL
+#cgo darwin,ios CFLAGS: -DGLES_SILENCE_DEPRECATION
+#cgo darwin,ios LDFLAGS: -framework OpenGLES
+
+#include
+#define __USE_GNU
+#include
+
+#ifdef __APPLE__
+ #include "TargetConditionals.h"
+ #if TARGET_OS_IPHONE
+ #include
+ #else
+ #include
+ #endif
+#else
+#include
+#include
+#endif
+
+static void (*_glBindBufferBase)(GLenum target, GLuint index, GLuint buffer);
+static GLuint (*_glGetUniformBlockIndex)(GLuint program, const GLchar *uniformBlockName);
+static void (*_glUniformBlockBinding)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);
+static void (*_glInvalidateFramebuffer)(GLenum target, GLsizei numAttachments, const GLenum *attachments);
+
+static void (*_glBeginQuery)(GLenum target, GLuint id);
+static void (*_glDeleteQueries)(GLsizei n, const GLuint *ids);
+static void (*_glEndQuery)(GLenum target);
+static void (*_glGenQueries)(GLsizei n, GLuint *ids);
+static void (*_glGetProgramBinary)(GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary);
+static void (*_glGetQueryObjectuiv)(GLuint id, GLenum pname, GLuint *params);
+static const GLubyte* (*_glGetStringi)(GLenum name, GLuint index);
+static void (*_glMemoryBarrier)(GLbitfield barriers);
+static void (*_glDispatchCompute)(GLuint x, GLuint y, GLuint z);
+static void* (*_glMapBufferRange)(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);
+static GLboolean (*_glUnmapBuffer)(GLenum target);
+static void (*_glBindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format);
+static void (*_glTexStorage2D)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
+static void (*_glBlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
+
+// The pointer-free version of glVertexAttribPointer, to avoid the Cgo pointer checks.
+__attribute__ ((visibility ("hidden"))) void gio_glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, uintptr_t offset) {
+ glVertexAttribPointer(index, size, type, normalized, stride, (const GLvoid *)offset);
+}
+
+// The pointer-free version of glDrawElements, to avoid the Cgo pointer checks.
+__attribute__ ((visibility ("hidden"))) void gio_glDrawElements(GLenum mode, GLsizei count, GLenum type, const uintptr_t offset) {
+ glDrawElements(mode, count, type, (const GLvoid *)offset);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glBindBufferBase(GLenum target, GLuint index, GLuint buffer) {
+ _glBindBufferBase(target, index, buffer);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding) {
+ _glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding);
+}
+
+__attribute__ ((visibility ("hidden"))) GLuint gio_glGetUniformBlockIndex(GLuint program, const GLchar *uniformBlockName) {
+ return _glGetUniformBlockIndex(program, uniformBlockName);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glInvalidateFramebuffer(GLenum target, GLenum attachment) {
+ // Framebuffer invalidation is just a hint and can safely be ignored.
+ if (_glInvalidateFramebuffer != NULL) {
+ _glInvalidateFramebuffer(target, 1, &attachment);
+ }
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glBeginQuery(GLenum target, GLenum attachment) {
+ _glBeginQuery(target, attachment);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glDeleteQueries(GLsizei n, const GLuint *ids) {
+ _glDeleteQueries(n, ids);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glEndQuery(GLenum target) {
+ _glEndQuery(target);
+}
+
+__attribute__ ((visibility ("hidden"))) const GLubyte* gio_glGetStringi(GLenum name, GLuint index) {
+ if (_glGetStringi == NULL) {
+ return NULL;
+ }
+ return _glGetStringi(name, index);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glGenQueries(GLsizei n, GLuint *ids) {
+ _glGenQueries(n, ids);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glGetProgramBinary(GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary) {
+ _glGetProgramBinary(program, bufsize, length, binaryFormat, binary);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glGetQueryObjectuiv(GLuint id, GLenum pname, GLuint *params) {
+ _glGetQueryObjectuiv(id, pname, params);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glMemoryBarrier(GLbitfield barriers) {
+ _glMemoryBarrier(barriers);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glDispatchCompute(GLuint x, GLuint y, GLuint z) {
+ _glDispatchCompute(x, y, z);
+}
+
+__attribute__ ((visibility ("hidden"))) void *gio_glMapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) {
+ return _glMapBufferRange(target, offset, length, access);
+}
+
+__attribute__ ((visibility ("hidden"))) GLboolean gio_glUnmapBuffer(GLenum target) {
+ return _glUnmapBuffer(target);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format) {
+ _glBindImageTexture(unit, texture, level, layered, layer, access, format);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glTexStorage2D(GLenum target, GLsizei levels, GLenum internalFormat, GLsizei width, GLsizei height) {
+ _glTexStorage2D(target, levels, internalFormat, width, height);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
+ _glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
+}
+
+__attribute__((constructor)) static void gio_loadGLFunctions() {
+ // Load libGLESv3 if available.
+ dlopen("libGLESv3.so", RTLD_NOW | RTLD_GLOBAL);
+
+ _glBindBufferBase = dlsym(RTLD_DEFAULT, "glBindBufferBase");
+ _glGetUniformBlockIndex = dlsym(RTLD_DEFAULT, "glGetUniformBlockIndex");
+ _glUniformBlockBinding = dlsym(RTLD_DEFAULT, "glUniformBlockBinding");
+ _glInvalidateFramebuffer = dlsym(RTLD_DEFAULT, "glInvalidateFramebuffer");
+ _glGetStringi = dlsym(RTLD_DEFAULT, "glGetStringi");
+ // Fall back to EXT_invalidate_framebuffer if available.
+ if (_glInvalidateFramebuffer == NULL) {
+ _glInvalidateFramebuffer = dlsym(RTLD_DEFAULT, "glDiscardFramebufferEXT");
+ }
+
+ _glBeginQuery = dlsym(RTLD_DEFAULT, "glBeginQuery");
+ if (_glBeginQuery == NULL)
+ _glBeginQuery = dlsym(RTLD_DEFAULT, "glBeginQueryEXT");
+ _glDeleteQueries = dlsym(RTLD_DEFAULT, "glDeleteQueries");
+ if (_glDeleteQueries == NULL)
+ _glDeleteQueries = dlsym(RTLD_DEFAULT, "glDeleteQueriesEXT");
+ _glEndQuery = dlsym(RTLD_DEFAULT, "glEndQuery");
+ if (_glEndQuery == NULL)
+ _glEndQuery = dlsym(RTLD_DEFAULT, "glEndQueryEXT");
+ _glGenQueries = dlsym(RTLD_DEFAULT, "glGenQueries");
+ if (_glGenQueries == NULL)
+ _glGenQueries = dlsym(RTLD_DEFAULT, "glGenQueriesEXT");
+ _glGetQueryObjectuiv = dlsym(RTLD_DEFAULT, "glGetQueryObjectuiv");
+ if (_glGetQueryObjectuiv == NULL)
+ _glGetQueryObjectuiv = dlsym(RTLD_DEFAULT, "glGetQueryObjectuivEXT");
+
+ _glMemoryBarrier = dlsym(RTLD_DEFAULT, "glMemoryBarrier");
+ _glDispatchCompute = dlsym(RTLD_DEFAULT, "glDispatchCompute");
+ _glMapBufferRange = dlsym(RTLD_DEFAULT, "glMapBufferRange");
+ _glUnmapBuffer = dlsym(RTLD_DEFAULT, "glUnmapBuffer");
+ _glBindImageTexture = dlsym(RTLD_DEFAULT, "glBindImageTexture");
+ _glTexStorage2D = dlsym(RTLD_DEFAULT, "glTexStorage2D");
+ _glBlitFramebuffer = dlsym(RTLD_DEFAULT, "glBlitFramebuffer");
+ _glGetProgramBinary = dlsym(RTLD_DEFAULT, "glGetProgramBinary");
+}
+*/
+import "C"
+
+type Context interface{}
+
+type Functions struct {
+ // Query caches.
+ uints [100]C.GLuint
+ ints [100]C.GLint
+}
+
+func NewFunctions(ctx Context) (*Functions, error) {
+ if ctx != nil {
+ panic("non-nil context")
+ }
+ return new(Functions), nil
+}
+
+func (f *Functions) ActiveTexture(texture Enum) {
+ C.glActiveTexture(C.GLenum(texture))
+}
+
+func (f *Functions) AttachShader(p Program, s Shader) {
+ C.glAttachShader(C.GLuint(p.V), C.GLuint(s.V))
+}
+
+func (f *Functions) BeginQuery(target Enum, query Query) {
+ C.gio_glBeginQuery(C.GLenum(target), C.GLenum(query.V))
+}
+
+func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) {
+ cname := C.CString(name)
+ defer C.free(unsafe.Pointer(cname))
+ C.glBindAttribLocation(C.GLuint(p.V), C.GLuint(a), cname)
+}
+
+func (f *Functions) BindBufferBase(target Enum, index int, b Buffer) {
+ C.gio_glBindBufferBase(C.GLenum(target), C.GLuint(index), C.GLuint(b.V))
+}
+
+func (f *Functions) BindBuffer(target Enum, b Buffer) {
+ C.glBindBuffer(C.GLenum(target), C.GLuint(b.V))
+}
+
+func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) {
+ C.glBindFramebuffer(C.GLenum(target), C.GLuint(fb.V))
+}
+
+func (f *Functions) BindRenderbuffer(target Enum, fb Renderbuffer) {
+ C.glBindRenderbuffer(C.GLenum(target), C.GLuint(fb.V))
+}
+
+func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) {
+ l := C.GLboolean(C.GL_FALSE)
+ if layered {
+ l = C.GL_TRUE
+ }
+ C.gio_glBindImageTexture(C.GLuint(unit), C.GLuint(t.V), C.GLint(level), l, C.GLint(layer), C.GLenum(access), C.GLenum(format))
+}
+
+func (f *Functions) BindTexture(target Enum, t Texture) {
+ C.glBindTexture(C.GLenum(target), C.GLuint(t.V))
+}
+
+func (f *Functions) BlendEquation(mode Enum) {
+ C.glBlendEquation(C.GLenum(mode))
+}
+
+func (f *Functions) BlendFunc(sfactor, dfactor Enum) {
+ C.glBlendFunc(C.GLenum(sfactor), C.GLenum(dfactor))
+}
+
+func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) {
+ C.gio_glBlitFramebuffer(
+ C.GLint(sx0), C.GLint(sy0), C.GLint(sx1), C.GLint(sy1),
+ C.GLint(dx0), C.GLint(dy0), C.GLint(dx1), C.GLint(dy1),
+ C.GLenum(mask), C.GLenum(filter),
+ )
+}
+
+func (f *Functions) BufferData(target Enum, size int, usage Enum) {
+ C.glBufferData(C.GLenum(target), C.GLsizeiptr(size), nil, C.GLenum(usage))
+}
+
+func (f *Functions) BufferSubData(target Enum, offset int, src []byte) {
+ var p unsafe.Pointer
+ if len(src) > 0 {
+ p = unsafe.Pointer(&src[0])
+ }
+ C.glBufferSubData(C.GLenum(target), C.GLintptr(offset), C.GLsizeiptr(len(src)), p)
+}
+
+func (f *Functions) CheckFramebufferStatus(target Enum) Enum {
+ return Enum(C.glCheckFramebufferStatus(C.GLenum(target)))
+}
+
+func (f *Functions) Clear(mask Enum) {
+ C.glClear(C.GLbitfield(mask))
+}
+
+func (f *Functions) ClearColor(red float32, green float32, blue float32, alpha float32) {
+ C.glClearColor(C.GLfloat(red), C.GLfloat(green), C.GLfloat(blue), C.GLfloat(alpha))
+}
+
+func (f *Functions) ClearDepthf(d float32) {
+ C.glClearDepthf(C.GLfloat(d))
+}
+
+func (f *Functions) CompileShader(s Shader) {
+ C.glCompileShader(C.GLuint(s.V))
+}
+
+func (f *Functions) CreateBuffer() Buffer {
+ C.glGenBuffers(1, &f.uints[0])
+ return Buffer{uint(f.uints[0])}
+}
+
+func (f *Functions) CreateFramebuffer() Framebuffer {
+ C.glGenFramebuffers(1, &f.uints[0])
+ return Framebuffer{uint(f.uints[0])}
+}
+
+func (f *Functions) CreateProgram() Program {
+ return Program{uint(C.glCreateProgram())}
+}
+
+func (f *Functions) CreateQuery() Query {
+ C.gio_glGenQueries(1, &f.uints[0])
+ return Query{uint(f.uints[0])}
+}
+
+func (f *Functions) CreateRenderbuffer() Renderbuffer {
+ C.glGenRenderbuffers(1, &f.uints[0])
+ return Renderbuffer{uint(f.uints[0])}
+}
+
+func (f *Functions) CreateShader(ty Enum) Shader {
+ return Shader{uint(C.glCreateShader(C.GLenum(ty)))}
+}
+
+func (f *Functions) CreateTexture() Texture {
+ C.glGenTextures(1, &f.uints[0])
+ return Texture{uint(f.uints[0])}
+}
+
+func (f *Functions) DeleteBuffer(v Buffer) {
+ f.uints[0] = C.GLuint(v.V)
+ C.glDeleteBuffers(1, &f.uints[0])
+}
+
+func (f *Functions) DeleteFramebuffer(v Framebuffer) {
+ f.uints[0] = C.GLuint(v.V)
+ C.glDeleteFramebuffers(1, &f.uints[0])
+}
+
+func (f *Functions) DeleteProgram(p Program) {
+ C.glDeleteProgram(C.GLuint(p.V))
+}
+
+func (f *Functions) DeleteQuery(query Query) {
+ f.uints[0] = C.GLuint(query.V)
+ C.gio_glDeleteQueries(1, &f.uints[0])
+}
+
+func (f *Functions) DeleteRenderbuffer(v Renderbuffer) {
+ f.uints[0] = C.GLuint(v.V)
+ C.glDeleteRenderbuffers(1, &f.uints[0])
+}
+
+func (f *Functions) DeleteShader(s Shader) {
+ C.glDeleteShader(C.GLuint(s.V))
+}
+
+func (f *Functions) DeleteTexture(v Texture) {
+ f.uints[0] = C.GLuint(v.V)
+ C.glDeleteTextures(1, &f.uints[0])
+}
+
+func (f *Functions) DepthFunc(v Enum) {
+ C.glDepthFunc(C.GLenum(v))
+}
+
+func (f *Functions) DepthMask(mask bool) {
+ m := C.GLboolean(C.GL_FALSE)
+ if mask {
+ m = C.GLboolean(C.GL_TRUE)
+ }
+ C.glDepthMask(m)
+}
+
+func (f *Functions) DisableVertexAttribArray(a Attrib) {
+ C.glDisableVertexAttribArray(C.GLuint(a))
+}
+
+func (f *Functions) Disable(cap Enum) {
+ C.glDisable(C.GLenum(cap))
+}
+
+func (f *Functions) DrawArrays(mode Enum, first int, count int) {
+ C.glDrawArrays(C.GLenum(mode), C.GLint(first), C.GLsizei(count))
+}
+
+func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) {
+ C.gio_glDrawElements(C.GLenum(mode), C.GLsizei(count), C.GLenum(ty), C.uintptr_t(offset))
+}
+
+func (f *Functions) DispatchCompute(x, y, z int) {
+ C.gio_glDispatchCompute(C.GLuint(x), C.GLuint(y), C.GLuint(z))
+}
+
+func (f *Functions) Enable(cap Enum) {
+ C.glEnable(C.GLenum(cap))
+}
+
+func (f *Functions) EndQuery(target Enum) {
+ C.gio_glEndQuery(C.GLenum(target))
+}
+
+func (f *Functions) EnableVertexAttribArray(a Attrib) {
+ C.glEnableVertexAttribArray(C.GLuint(a))
+}
+
+func (f *Functions) Finish() {
+ C.glFinish()
+}
+
+func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) {
+ C.glFramebufferRenderbuffer(C.GLenum(target), C.GLenum(attachment), C.GLenum(renderbuffertarget), C.GLuint(renderbuffer.V))
+}
+
+func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) {
+ C.glFramebufferTexture2D(C.GLenum(target), C.GLenum(attachment), C.GLenum(texTarget), C.GLuint(t.V), C.GLint(level))
+}
+
+func (c *Functions) GetBinding(pname Enum) Object {
+ return Object{uint(c.GetInteger(pname))}
+}
+
+func (f *Functions) GetError() Enum {
+ return Enum(C.glGetError())
+}
+
+func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int {
+ C.glGetRenderbufferParameteriv(C.GLenum(target), C.GLenum(pname), &f.ints[0])
+ return int(f.ints[0])
+}
+
+func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int {
+ C.glGetFramebufferAttachmentParameteriv(C.GLenum(target), C.GLenum(attachment), C.GLenum(pname), &f.ints[0])
+ return int(f.ints[0])
+}
+
+func (f *Functions) GetInteger(pname Enum) int {
+ C.glGetIntegerv(C.GLenum(pname), &f.ints[0])
+ return int(f.ints[0])
+}
+
+func (f *Functions) GetProgrami(p Program, pname Enum) int {
+ C.glGetProgramiv(C.GLuint(p.V), C.GLenum(pname), &f.ints[0])
+ return int(f.ints[0])
+}
+
+func (f *Functions) GetProgramBinary(p Program) []byte {
+ sz := f.GetProgrami(p, PROGRAM_BINARY_LENGTH)
+ if sz == 0 {
+ return nil
+ }
+ buf := make([]byte, sz)
+ var format C.GLenum
+ C.gio_glGetProgramBinary(C.GLuint(p.V), C.GLsizei(sz), nil, &format, unsafe.Pointer(&buf[0]))
+ return buf
+}
+
+func (f *Functions) GetProgramInfoLog(p Program) string {
+ n := f.GetProgrami(p, INFO_LOG_LENGTH)
+ buf := make([]byte, n)
+ C.glGetProgramInfoLog(C.GLuint(p.V), C.GLsizei(len(buf)), nil, (*C.GLchar)(unsafe.Pointer(&buf[0])))
+ return string(buf)
+}
+
+func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint {
+ C.gio_glGetQueryObjectuiv(C.GLuint(query.V), C.GLenum(pname), &f.uints[0])
+ return uint(f.uints[0])
+}
+
+func (f *Functions) GetShaderi(s Shader, pname Enum) int {
+ C.glGetShaderiv(C.GLuint(s.V), C.GLenum(pname), &f.ints[0])
+ return int(f.ints[0])
+}
+
+func (f *Functions) GetShaderInfoLog(s Shader) string {
+ n := f.GetShaderi(s, INFO_LOG_LENGTH)
+ buf := make([]byte, n)
+ C.glGetShaderInfoLog(C.GLuint(s.V), C.GLsizei(len(buf)), nil, (*C.GLchar)(unsafe.Pointer(&buf[0])))
+ return string(buf)
+}
+
+func (f *Functions) GetStringi(pname Enum, index int) string {
+ str := C.gio_glGetStringi(C.GLenum(pname), C.GLuint(index))
+ if str == nil {
+ return ""
+ }
+ return C.GoString((*C.char)(unsafe.Pointer(str)))
+}
+
+func (f *Functions) GetString(pname Enum) string {
+ switch {
+ case runtime.GOOS == "darwin" && pname == EXTENSIONS:
+ // macOS OpenGL 3 core profile doesn't support glGetString(GL_EXTENSIONS).
+ // Use glGetStringi(GL_EXTENSIONS, ).
+ var exts []string
+ nexts := f.GetInteger(NUM_EXTENSIONS)
+ for i := 0; i < nexts; i++ {
+ ext := f.GetStringi(EXTENSIONS, i)
+ exts = append(exts, ext)
+ }
+ return strings.Join(exts, " ")
+ default:
+ str := C.glGetString(C.GLenum(pname))
+ return C.GoString((*C.char)(unsafe.Pointer(str)))
+ }
+}
+
+func (f *Functions) GetUniformBlockIndex(p Program, name string) uint {
+ cname := C.CString(name)
+ defer C.free(unsafe.Pointer(cname))
+ return uint(C.gio_glGetUniformBlockIndex(C.GLuint(p.V), cname))
+}
+
+func (f *Functions) GetUniformLocation(p Program, name string) Uniform {
+ cname := C.CString(name)
+ defer C.free(unsafe.Pointer(cname))
+ return Uniform{int(C.glGetUniformLocation(C.GLuint(p.V), cname))}
+}
+
+func (f *Functions) InvalidateFramebuffer(target, attachment Enum) {
+ C.gio_glInvalidateFramebuffer(C.GLenum(target), C.GLenum(attachment))
+}
+
+func (f *Functions) LinkProgram(p Program) {
+ C.glLinkProgram(C.GLuint(p.V))
+}
+
+func (f *Functions) PixelStorei(pname Enum, param int32) {
+ C.glPixelStorei(C.GLenum(pname), C.GLint(param))
+}
+
+func (f *Functions) MemoryBarrier(barriers Enum) {
+ C.gio_glMemoryBarrier(C.GLbitfield(barriers))
+}
+
+func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte {
+ p := C.gio_glMapBufferRange(C.GLenum(target), C.GLintptr(offset), C.GLsizeiptr(length), C.GLbitfield(access))
+ if p == nil {
+ return nil
+ }
+ return (*[1 << 30]byte)(p)[:length:length]
+}
+
+func (f *Functions) Scissor(x, y, width, height int32) {
+ C.glScissor(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height))
+}
+
+func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) {
+ var p unsafe.Pointer
+ if len(data) > 0 {
+ p = unsafe.Pointer(&data[0])
+ }
+ C.glReadPixels(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p)
+}
+
+func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) {
+ C.glRenderbufferStorage(C.GLenum(target), C.GLenum(internalformat), C.GLsizei(width), C.GLsizei(height))
+}
+
+func (f *Functions) ShaderSource(s Shader, src string) {
+ csrc := C.CString(src)
+ defer C.free(unsafe.Pointer(csrc))
+ strlen := C.GLint(len(src))
+ C.glShaderSource(C.GLuint(s.V), 1, &csrc, &strlen)
+}
+
+func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width int, height int, format Enum, ty Enum) {
+ C.glTexImage2D(C.GLenum(target), C.GLint(level), C.GLint(internalFormat), C.GLsizei(width), C.GLsizei(height), 0, C.GLenum(format), C.GLenum(ty), nil)
+}
+
+func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) {
+ C.gio_glTexStorage2D(C.GLenum(target), C.GLsizei(levels), C.GLenum(internalFormat), C.GLsizei(width), C.GLsizei(height))
+}
+
+func (f *Functions) TexSubImage2D(target Enum, level int, x int, y int, width int, height int, format Enum, ty Enum, data []byte) {
+ var p unsafe.Pointer
+ if len(data) > 0 {
+ p = unsafe.Pointer(&data[0])
+ }
+ C.glTexSubImage2D(C.GLenum(target), C.GLint(level), C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p)
+}
+
+func (f *Functions) TexParameteri(target, pname Enum, param int) {
+ C.glTexParameteri(C.GLenum(target), C.GLenum(pname), C.GLint(param))
+}
+
+func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) {
+ C.gio_glUniformBlockBinding(C.GLuint(p.V), C.GLuint(uniformBlockIndex), C.GLuint(uniformBlockBinding))
+}
+
+func (f *Functions) Uniform1f(dst Uniform, v float32) {
+ C.glUniform1f(C.GLint(dst.V), C.GLfloat(v))
+}
+
+func (f *Functions) Uniform1i(dst Uniform, v int) {
+ C.glUniform1i(C.GLint(dst.V), C.GLint(v))
+}
+
+func (f *Functions) Uniform2f(dst Uniform, v0 float32, v1 float32) {
+ C.glUniform2f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1))
+}
+
+func (f *Functions) Uniform3f(dst Uniform, v0 float32, v1 float32, v2 float32) {
+ C.glUniform3f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2))
+}
+
+func (f *Functions) Uniform4f(dst Uniform, v0 float32, v1 float32, v2 float32, v3 float32) {
+ C.glUniform4f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2), C.GLfloat(v3))
+}
+
+func (f *Functions) UseProgram(p Program) {
+ C.glUseProgram(C.GLuint(p.V))
+}
+
+func (f *Functions) UnmapBuffer(target Enum) bool {
+ r := C.gio_glUnmapBuffer(C.GLenum(target))
+ return r == C.GL_TRUE
+}
+
+func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride int, offset int) {
+ var n C.GLboolean = C.GL_FALSE
+ if normalized {
+ n = C.GL_TRUE
+ }
+ C.gio_glVertexAttribPointer(C.GLuint(dst), C.GLint(size), C.GLenum(ty), n, C.GLsizei(stride), C.uintptr_t(offset))
+}
+
+func (f *Functions) Viewport(x int, y int, width int, height int) {
+ C.glViewport(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height))
+}
diff --git a/gio/giold/internal/gl/gl_windows.go b/gio/giold/internal/gl/gl_windows.go
new file mode 100644
index 0000000..099c82b
--- /dev/null
+++ b/gio/giold/internal/gl/gl_windows.go
@@ -0,0 +1,430 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gl
+
+import (
+ "math"
+ "runtime"
+ "syscall"
+ "unsafe"
+
+ "golang.org/x/sys/windows"
+)
+
+var (
+ LibGLESv2 = windows.NewLazyDLL("libGLESv2.dll")
+ _glActiveTexture = LibGLESv2.NewProc("glActiveTexture")
+ _glAttachShader = LibGLESv2.NewProc("glAttachShader")
+ _glBeginQuery = LibGLESv2.NewProc("glBeginQuery")
+ _glBindAttribLocation = LibGLESv2.NewProc("glBindAttribLocation")
+ _glBindBuffer = LibGLESv2.NewProc("glBindBuffer")
+ _glBindBufferBase = LibGLESv2.NewProc("glBindBufferBase")
+ _glBindFramebuffer = LibGLESv2.NewProc("glBindFramebuffer")
+ _glBindRenderbuffer = LibGLESv2.NewProc("glBindRenderbuffer")
+ _glBindTexture = LibGLESv2.NewProc("glBindTexture")
+ _glBlendEquation = LibGLESv2.NewProc("glBlendEquation")
+ _glBlendFunc = LibGLESv2.NewProc("glBlendFunc")
+ _glBufferData = LibGLESv2.NewProc("glBufferData")
+ _glBufferSubData = LibGLESv2.NewProc("glBufferSubData")
+ _glCheckFramebufferStatus = LibGLESv2.NewProc("glCheckFramebufferStatus")
+ _glClear = LibGLESv2.NewProc("glClear")
+ _glClearColor = LibGLESv2.NewProc("glClearColor")
+ _glClearDepthf = LibGLESv2.NewProc("glClearDepthf")
+ _glDeleteQueries = LibGLESv2.NewProc("glDeleteQueries")
+ _glCompileShader = LibGLESv2.NewProc("glCompileShader")
+ _glGenBuffers = LibGLESv2.NewProc("glGenBuffers")
+ _glGenFramebuffers = LibGLESv2.NewProc("glGenFramebuffers")
+ _glGetUniformBlockIndex = LibGLESv2.NewProc("glGetUniformBlockIndex")
+ _glCreateProgram = LibGLESv2.NewProc("glCreateProgram")
+ _glGenRenderbuffers = LibGLESv2.NewProc("glGenRenderbuffers")
+ _glCreateShader = LibGLESv2.NewProc("glCreateShader")
+ _glGenTextures = LibGLESv2.NewProc("glGenTextures")
+ _glDeleteBuffers = LibGLESv2.NewProc("glDeleteBuffers")
+ _glDeleteFramebuffers = LibGLESv2.NewProc("glDeleteFramebuffers")
+ _glDeleteProgram = LibGLESv2.NewProc("glDeleteProgram")
+ _glDeleteShader = LibGLESv2.NewProc("glDeleteShader")
+ _glDeleteRenderbuffers = LibGLESv2.NewProc("glDeleteRenderbuffers")
+ _glDeleteTextures = LibGLESv2.NewProc("glDeleteTextures")
+ _glDepthFunc = LibGLESv2.NewProc("glDepthFunc")
+ _glDepthMask = LibGLESv2.NewProc("glDepthMask")
+ _glDisableVertexAttribArray = LibGLESv2.NewProc("glDisableVertexAttribArray")
+ _glDisable = LibGLESv2.NewProc("glDisable")
+ _glDrawArrays = LibGLESv2.NewProc("glDrawArrays")
+ _glDrawElements = LibGLESv2.NewProc("glDrawElements")
+ _glEnable = LibGLESv2.NewProc("glEnable")
+ _glEnableVertexAttribArray = LibGLESv2.NewProc("glEnableVertexAttribArray")
+ _glEndQuery = LibGLESv2.NewProc("glEndQuery")
+ _glFinish = LibGLESv2.NewProc("glFinish")
+ _glFramebufferRenderbuffer = LibGLESv2.NewProc("glFramebufferRenderbuffer")
+ _glFramebufferTexture2D = LibGLESv2.NewProc("glFramebufferTexture2D")
+ _glGenQueries = LibGLESv2.NewProc("glGenQueries")
+ _glGetError = LibGLESv2.NewProc("glGetError")
+ _glGetRenderbufferParameteri = LibGLESv2.NewProc("glGetRenderbufferParameteri")
+ _glGetFramebufferAttachmentParameteri = LibGLESv2.NewProc("glGetFramebufferAttachmentParameteri")
+ _glGetIntegerv = LibGLESv2.NewProc("glGetIntegerv")
+ _glGetProgramiv = LibGLESv2.NewProc("glGetProgramiv")
+ _glGetProgramInfoLog = LibGLESv2.NewProc("glGetProgramInfoLog")
+ _glGetQueryObjectuiv = LibGLESv2.NewProc("glGetQueryObjectuiv")
+ _glGetShaderiv = LibGLESv2.NewProc("glGetShaderiv")
+ _glGetShaderInfoLog = LibGLESv2.NewProc("glGetShaderInfoLog")
+ _glGetString = LibGLESv2.NewProc("glGetString")
+ _glGetUniformLocation = LibGLESv2.NewProc("glGetUniformLocation")
+ _glInvalidateFramebuffer = LibGLESv2.NewProc("glInvalidateFramebuffer")
+ _glLinkProgram = LibGLESv2.NewProc("glLinkProgram")
+ _glPixelStorei = LibGLESv2.NewProc("glPixelStorei")
+ _glReadPixels = LibGLESv2.NewProc("glReadPixels")
+ _glRenderbufferStorage = LibGLESv2.NewProc("glRenderbufferStorage")
+ _glScissor = LibGLESv2.NewProc("glScissor")
+ _glShaderSource = LibGLESv2.NewProc("glShaderSource")
+ _glTexImage2D = LibGLESv2.NewProc("glTexImage2D")
+ _glTexStorage2D = LibGLESv2.NewProc("glTexStorage2D")
+ _glTexSubImage2D = LibGLESv2.NewProc("glTexSubImage2D")
+ _glTexParameteri = LibGLESv2.NewProc("glTexParameteri")
+ _glUniformBlockBinding = LibGLESv2.NewProc("glUniformBlockBinding")
+ _glUniform1f = LibGLESv2.NewProc("glUniform1f")
+ _glUniform1i = LibGLESv2.NewProc("glUniform1i")
+ _glUniform2f = LibGLESv2.NewProc("glUniform2f")
+ _glUniform3f = LibGLESv2.NewProc("glUniform3f")
+ _glUniform4f = LibGLESv2.NewProc("glUniform4f")
+ _glUseProgram = LibGLESv2.NewProc("glUseProgram")
+ _glVertexAttribPointer = LibGLESv2.NewProc("glVertexAttribPointer")
+ _glViewport = LibGLESv2.NewProc("glViewport")
+)
+
+type Functions struct {
+ // Query caches.
+ int32s [100]int32
+}
+
+type Context interface{}
+
+func NewFunctions(ctx Context) (*Functions, error) {
+ if ctx != nil {
+ panic("non-nil context")
+ }
+ return new(Functions), nil
+}
+
+func (c *Functions) ActiveTexture(t Enum) {
+ syscall.Syscall(_glActiveTexture.Addr(), 1, uintptr(t), 0, 0)
+}
+func (c *Functions) AttachShader(p Program, s Shader) {
+ syscall.Syscall(_glAttachShader.Addr(), 2, uintptr(p.V), uintptr(s.V), 0)
+}
+func (f *Functions) BeginQuery(target Enum, query Query) {
+ syscall.Syscall(_glBeginQuery.Addr(), 2, uintptr(target), uintptr(query.V), 0)
+}
+func (c *Functions) BindAttribLocation(p Program, a Attrib, name string) {
+ cname := cString(name)
+ c0 := &cname[0]
+ syscall.Syscall(_glBindAttribLocation.Addr(), 3, uintptr(p.V), uintptr(a), uintptr(unsafe.Pointer(c0)))
+ issue34474KeepAlive(c)
+}
+func (c *Functions) BindBuffer(target Enum, b Buffer) {
+ syscall.Syscall(_glBindBuffer.Addr(), 2, uintptr(target), uintptr(b.V), 0)
+}
+func (c *Functions) BindBufferBase(target Enum, index int, b Buffer) {
+ syscall.Syscall(_glBindBufferBase.Addr(), 3, uintptr(target), uintptr(index), uintptr(b.V))
+}
+func (c *Functions) BindFramebuffer(target Enum, fb Framebuffer) {
+ syscall.Syscall(_glBindFramebuffer.Addr(), 2, uintptr(target), uintptr(fb.V), 0)
+}
+func (c *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) {
+ syscall.Syscall(_glBindRenderbuffer.Addr(), 2, uintptr(target), uintptr(rb.V), 0)
+}
+func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) {
+ panic("not implemented")
+}
+func (c *Functions) BindTexture(target Enum, t Texture) {
+ syscall.Syscall(_glBindTexture.Addr(), 2, uintptr(target), uintptr(t.V), 0)
+}
+func (c *Functions) BlendEquation(mode Enum) {
+ syscall.Syscall(_glBlendEquation.Addr(), 1, uintptr(mode), 0, 0)
+}
+func (c *Functions) BlendFunc(sfactor, dfactor Enum) {
+ syscall.Syscall(_glBlendFunc.Addr(), 2, uintptr(sfactor), uintptr(dfactor), 0)
+}
+func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) {
+ panic("not implemented")
+}
+func (c *Functions) BufferData(target Enum, size int, usage Enum) {
+ syscall.Syscall6(_glBufferData.Addr(), 4, uintptr(target), uintptr(size), 0, uintptr(usage), 0, 0)
+}
+func (f *Functions) BufferSubData(target Enum, offset int, src []byte) {
+ if n := len(src); n > 0 {
+ s0 := &src[0]
+ syscall.Syscall6(_glBufferSubData.Addr(), 4, uintptr(target), uintptr(offset), uintptr(n), uintptr(unsafe.Pointer(s0)), 0, 0)
+ issue34474KeepAlive(s0)
+ }
+}
+func (c *Functions) CheckFramebufferStatus(target Enum) Enum {
+ s, _, _ := syscall.Syscall(_glCheckFramebufferStatus.Addr(), 1, uintptr(target), 0, 0)
+ return Enum(s)
+}
+func (c *Functions) Clear(mask Enum) {
+ syscall.Syscall(_glClear.Addr(), 1, uintptr(mask), 0, 0)
+}
+func (c *Functions) ClearColor(red, green, blue, alpha float32) {
+ syscall.Syscall6(_glClearColor.Addr(), 4, uintptr(math.Float32bits(red)), uintptr(math.Float32bits(green)), uintptr(math.Float32bits(blue)), uintptr(math.Float32bits(alpha)), 0, 0)
+}
+func (c *Functions) ClearDepthf(d float32) {
+ syscall.Syscall(_glClearDepthf.Addr(), 1, uintptr(math.Float32bits(d)), 0, 0)
+}
+func (c *Functions) CompileShader(s Shader) {
+ syscall.Syscall(_glCompileShader.Addr(), 1, uintptr(s.V), 0, 0)
+}
+func (c *Functions) CreateBuffer() Buffer {
+ var buf uintptr
+ syscall.Syscall(_glGenBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&buf)), 0)
+ return Buffer{uint(buf)}
+}
+func (c *Functions) CreateFramebuffer() Framebuffer {
+ var fb uintptr
+ syscall.Syscall(_glGenFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&fb)), 0)
+ return Framebuffer{uint(fb)}
+}
+func (c *Functions) CreateProgram() Program {
+ p, _, _ := syscall.Syscall(_glCreateProgram.Addr(), 0, 0, 0, 0)
+ return Program{uint(p)}
+}
+func (f *Functions) CreateQuery() Query {
+ var q uintptr
+ syscall.Syscall(_glGenQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&q)), 0)
+ return Query{uint(q)}
+}
+func (c *Functions) CreateRenderbuffer() Renderbuffer {
+ var rb uintptr
+ syscall.Syscall(_glGenRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&rb)), 0)
+ return Renderbuffer{uint(rb)}
+}
+func (c *Functions) CreateShader(ty Enum) Shader {
+ s, _, _ := syscall.Syscall(_glCreateShader.Addr(), 1, uintptr(ty), 0, 0)
+ return Shader{uint(s)}
+}
+func (c *Functions) CreateTexture() Texture {
+ var t uintptr
+ syscall.Syscall(_glGenTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&t)), 0)
+ return Texture{uint(t)}
+}
+func (c *Functions) DeleteBuffer(v Buffer) {
+ syscall.Syscall(_glDeleteBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v)), 0)
+}
+func (c *Functions) DeleteFramebuffer(v Framebuffer) {
+ syscall.Syscall(_glDeleteFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0)
+}
+func (c *Functions) DeleteProgram(p Program) {
+ syscall.Syscall(_glDeleteProgram.Addr(), 1, uintptr(p.V), 0, 0)
+}
+func (f *Functions) DeleteQuery(query Query) {
+ syscall.Syscall(_glDeleteQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&query.V)), 0)
+}
+func (c *Functions) DeleteShader(s Shader) {
+ syscall.Syscall(_glDeleteShader.Addr(), 1, uintptr(s.V), 0, 0)
+}
+func (c *Functions) DeleteRenderbuffer(v Renderbuffer) {
+ syscall.Syscall(_glDeleteRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0)
+}
+func (c *Functions) DeleteTexture(v Texture) {
+ syscall.Syscall(_glDeleteTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0)
+}
+func (c *Functions) DepthFunc(f Enum) {
+ syscall.Syscall(_glDepthFunc.Addr(), 1, uintptr(f), 0, 0)
+}
+func (c *Functions) DepthMask(mask bool) {
+ var m uintptr
+ if mask {
+ m = 1
+ }
+ syscall.Syscall(_glDepthMask.Addr(), 1, m, 0, 0)
+}
+func (c *Functions) DisableVertexAttribArray(a Attrib) {
+ syscall.Syscall(_glDisableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0)
+}
+func (c *Functions) Disable(cap Enum) {
+ syscall.Syscall(_glDisable.Addr(), 1, uintptr(cap), 0, 0)
+}
+func (c *Functions) DrawArrays(mode Enum, first, count int) {
+ syscall.Syscall(_glDrawArrays.Addr(), 3, uintptr(mode), uintptr(first), uintptr(count))
+}
+func (c *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) {
+ syscall.Syscall6(_glDrawElements.Addr(), 4, uintptr(mode), uintptr(count), uintptr(ty), uintptr(offset), 0, 0)
+}
+func (f *Functions) DispatchCompute(x, y, z int) {
+ panic("not implemented")
+}
+func (c *Functions) Enable(cap Enum) {
+ syscall.Syscall(_glEnable.Addr(), 1, uintptr(cap), 0, 0)
+}
+func (c *Functions) EnableVertexAttribArray(a Attrib) {
+ syscall.Syscall(_glEnableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0)
+}
+func (f *Functions) EndQuery(target Enum) {
+ syscall.Syscall(_glEndQuery.Addr(), 1, uintptr(target), 0, 0)
+}
+func (c *Functions) Finish() {
+ syscall.Syscall(_glFinish.Addr(), 0, 0, 0, 0)
+}
+func (c *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) {
+ syscall.Syscall6(_glFramebufferRenderbuffer.Addr(), 4, uintptr(target), uintptr(attachment), uintptr(renderbuffertarget), uintptr(renderbuffer.V), 0, 0)
+}
+func (c *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) {
+ syscall.Syscall6(_glFramebufferTexture2D.Addr(), 5, uintptr(target), uintptr(attachment), uintptr(texTarget), uintptr(t.V), uintptr(level), 0)
+}
+func (f *Functions) GetUniformBlockIndex(p Program, name string) uint {
+ cname := cString(name)
+ c0 := &cname[0]
+ u, _, _ := syscall.Syscall(_glGetUniformBlockIndex.Addr(), 2, uintptr(p.V), uintptr(unsafe.Pointer(c0)), 0)
+ issue34474KeepAlive(c0)
+ return uint(u)
+}
+func (c *Functions) GetBinding(pname Enum) Object {
+ return Object{uint(c.GetInteger(pname))}
+}
+func (c *Functions) GetError() Enum {
+ e, _, _ := syscall.Syscall(_glGetError.Addr(), 0, 0, 0, 0)
+ return Enum(e)
+}
+func (c *Functions) GetRenderbufferParameteri(target, pname Enum) int {
+ p, _, _ := syscall.Syscall(_glGetRenderbufferParameteri.Addr(), 2, uintptr(target), uintptr(pname), 0)
+ return int(p)
+}
+func (c *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int {
+ p, _, _ := syscall.Syscall(_glGetFramebufferAttachmentParameteri.Addr(), 3, uintptr(target), uintptr(attachment), uintptr(pname))
+ return int(p)
+}
+func (c *Functions) GetInteger(pname Enum) int {
+ syscall.Syscall(_glGetIntegerv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])), 0)
+ return int(c.int32s[0])
+}
+func (c *Functions) GetProgrami(p Program, pname Enum) int {
+ syscall.Syscall(_glGetProgramiv.Addr(), 3, uintptr(p.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])))
+ return int(c.int32s[0])
+}
+func (c *Functions) GetProgramInfoLog(p Program) string {
+ n := c.GetProgrami(p, INFO_LOG_LENGTH)
+ buf := make([]byte, n)
+ syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0)
+ return string(buf)
+}
+func (c *Functions) GetQueryObjectuiv(query Query, pname Enum) uint {
+ syscall.Syscall(_glGetQueryObjectuiv.Addr(), 3, uintptr(query.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])))
+ return uint(c.int32s[0])
+}
+func (c *Functions) GetShaderi(s Shader, pname Enum) int {
+ syscall.Syscall(_glGetShaderiv.Addr(), 3, uintptr(s.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])))
+ return int(c.int32s[0])
+}
+func (c *Functions) GetShaderInfoLog(s Shader) string {
+ n := c.GetShaderi(s, INFO_LOG_LENGTH)
+ buf := make([]byte, n)
+ syscall.Syscall6(_glGetShaderInfoLog.Addr(), 4, uintptr(s.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0)
+ return string(buf)
+}
+func (c *Functions) GetString(pname Enum) string {
+ s, _, _ := syscall.Syscall(_glGetString.Addr(), 1, uintptr(pname), 0, 0)
+ return windows.BytePtrToString((*byte)(unsafe.Pointer(s)))
+}
+func (c *Functions) GetUniformLocation(p Program, name string) Uniform {
+ cname := cString(name)
+ c0 := &cname[0]
+ u, _, _ := syscall.Syscall(_glGetUniformLocation.Addr(), 2, uintptr(p.V), uintptr(unsafe.Pointer(c0)), 0)
+ issue34474KeepAlive(c0)
+ return Uniform{int(u)}
+}
+func (c *Functions) InvalidateFramebuffer(target, attachment Enum) {
+ addr := _glInvalidateFramebuffer.Addr()
+ if addr == 0 {
+ // InvalidateFramebuffer is just a hint. Skip it if not supported.
+ return
+ }
+ syscall.Syscall(addr, 3, uintptr(target), 1, uintptr(unsafe.Pointer(&attachment)))
+}
+func (c *Functions) LinkProgram(p Program) {
+ syscall.Syscall(_glLinkProgram.Addr(), 1, uintptr(p.V), 0, 0)
+}
+func (c *Functions) PixelStorei(pname Enum, param int32) {
+ syscall.Syscall(_glPixelStorei.Addr(), 2, uintptr(pname), uintptr(param), 0)
+}
+func (f *Functions) MemoryBarrier(barriers Enum) {
+ panic("not implemented")
+}
+func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte {
+ panic("not implemented")
+}
+func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) {
+ d0 := &data[0]
+ syscall.Syscall9(_glReadPixels.Addr(), 7, uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(d0)), 0, 0)
+ issue34474KeepAlive(d0)
+}
+func (c *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) {
+ syscall.Syscall6(_glRenderbufferStorage.Addr(), 4, uintptr(target), uintptr(internalformat), uintptr(width), uintptr(height), 0, 0)
+}
+func (c *Functions) Scissor(x, y, width, height int32) {
+ syscall.Syscall6(_glScissor.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0)
+}
+func (c *Functions) ShaderSource(s Shader, src string) {
+ var n uintptr = uintptr(len(src))
+ psrc := &src
+ syscall.Syscall6(_glShaderSource.Addr(), 4, uintptr(s.V), 1, uintptr(unsafe.Pointer(psrc)), uintptr(unsafe.Pointer(&n)), 0, 0)
+ issue34474KeepAlive(psrc)
+}
+func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width int, height int, format Enum, ty Enum) {
+ syscall.Syscall9(_glTexImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(internalFormat), uintptr(width), uintptr(height), 0, uintptr(format), uintptr(ty), 0)
+}
+func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) {
+ syscall.Syscall6(_glTexStorage2D.Addr(), 5, uintptr(target), uintptr(levels), uintptr(internalFormat), uintptr(width), uintptr(height), 0)
+}
+func (c *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) {
+ d0 := &data[0]
+ syscall.Syscall9(_glTexSubImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(d0)))
+ issue34474KeepAlive(d0)
+}
+func (c *Functions) TexParameteri(target, pname Enum, param int) {
+ syscall.Syscall(_glTexParameteri.Addr(), 3, uintptr(target), uintptr(pname), uintptr(param))
+}
+func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) {
+ syscall.Syscall(_glUniformBlockBinding.Addr(), 3, uintptr(p.V), uintptr(uniformBlockIndex), uintptr(uniformBlockBinding))
+}
+func (c *Functions) Uniform1f(dst Uniform, v float32) {
+ syscall.Syscall(_glUniform1f.Addr(), 2, uintptr(dst.V), uintptr(math.Float32bits(v)), 0)
+}
+func (c *Functions) Uniform1i(dst Uniform, v int) {
+ syscall.Syscall(_glUniform1i.Addr(), 2, uintptr(dst.V), uintptr(v), 0)
+}
+func (c *Functions) Uniform2f(dst Uniform, v0, v1 float32) {
+ syscall.Syscall(_glUniform2f.Addr(), 3, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)))
+}
+func (c *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) {
+ syscall.Syscall6(_glUniform3f.Addr(), 4, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), 0, 0)
+}
+func (c *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) {
+ syscall.Syscall6(_glUniform4f.Addr(), 5, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), uintptr(math.Float32bits(v3)), 0)
+}
+func (c *Functions) UseProgram(p Program) {
+ syscall.Syscall(_glUseProgram.Addr(), 1, uintptr(p.V), 0, 0)
+}
+func (f *Functions) UnmapBuffer(target Enum) bool {
+ panic("not implemented")
+}
+func (c *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) {
+ var norm uintptr
+ if normalized {
+ norm = 1
+ }
+ syscall.Syscall6(_glVertexAttribPointer.Addr(), 6, uintptr(dst), uintptr(size), uintptr(ty), norm, uintptr(stride), uintptr(offset))
+}
+func (c *Functions) Viewport(x, y, width, height int) {
+ syscall.Syscall6(_glViewport.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0)
+}
+
+func cString(s string) []byte {
+ b := make([]byte, len(s)+1)
+ copy(b, s)
+ return b
+}
+
+// issue34474KeepAlive calls runtime.KeepAlive as a
+// workaround for golang.org/issue/34474.
+func issue34474KeepAlive(v interface{}) {
+ runtime.KeepAlive(v)
+}
diff --git a/gio/giold/internal/gl/types.go b/gio/giold/internal/gl/types.go
new file mode 100644
index 0000000..45db3be
--- /dev/null
+++ b/gio/giold/internal/gl/types.go
@@ -0,0 +1,27 @@
+// +build !js
+
+package gl
+
+type (
+ Buffer struct{ V uint }
+ Framebuffer struct{ V uint }
+ Program struct{ V uint }
+ Renderbuffer struct{ V uint }
+ Shader struct{ V uint }
+ Texture struct{ V uint }
+ Query struct{ V uint }
+ Uniform struct{ V int }
+ Object struct{ V uint }
+)
+
+func (u Uniform) Valid() bool {
+ return u.V != -1
+}
+
+func (p Program) Valid() bool {
+ return p.V != 0
+}
+
+func (s Shader) Valid() bool {
+ return s.V != 0
+}
diff --git a/gio/giold/internal/gl/types_js.go b/gio/giold/internal/gl/types_js.go
new file mode 100644
index 0000000..584c2af
--- /dev/null
+++ b/gio/giold/internal/gl/types_js.go
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gl
+
+import "syscall/js"
+
+type (
+ Buffer js.Value
+ Framebuffer js.Value
+ Program js.Value
+ Renderbuffer js.Value
+ Shader js.Value
+ Texture js.Value
+ Query js.Value
+ Uniform js.Value
+ Object js.Value
+)
+
+func (p Program) Valid() bool {
+ return !js.Value(p).IsUndefined() && !js.Value(p).IsNull()
+}
+
+func (s Shader) Valid() bool {
+ return !js.Value(s).IsUndefined() && !js.Value(s).IsNull()
+}
+
+func (u Uniform) Valid() bool {
+ return !js.Value(u).IsUndefined() && !js.Value(u).IsNull()
+}
diff --git a/gio/giold/internal/gl/util.go b/gio/giold/internal/gl/util.go
new file mode 100644
index 0000000..3d5b44b
--- /dev/null
+++ b/gio/giold/internal/gl/util.go
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gl
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+)
+
+func CreateProgram(ctx *Functions, vsSrc, fsSrc string, attribs []string) (Program, error) {
+ vs, err := createShader(ctx, VERTEX_SHADER, vsSrc)
+ if err != nil {
+ return Program{}, err
+ }
+ defer ctx.DeleteShader(vs)
+ fs, err := createShader(ctx, FRAGMENT_SHADER, fsSrc)
+ if err != nil {
+ return Program{}, err
+ }
+ defer ctx.DeleteShader(fs)
+ prog := ctx.CreateProgram()
+ if !prog.Valid() {
+ return Program{}, errors.New("glCreateProgram failed")
+ }
+ ctx.AttachShader(prog, vs)
+ ctx.AttachShader(prog, fs)
+ for i, a := range attribs {
+ ctx.BindAttribLocation(prog, Attrib(i), a)
+ }
+ ctx.LinkProgram(prog)
+ if ctx.GetProgrami(prog, LINK_STATUS) == 0 {
+ log := ctx.GetProgramInfoLog(prog)
+ ctx.DeleteProgram(prog)
+ return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log))
+ }
+ return prog, nil
+}
+
+func CreateComputeProgram(ctx *Functions, src string) (Program, error) {
+ cs, err := createShader(ctx, COMPUTE_SHADER, src)
+ if err != nil {
+ return Program{}, err
+ }
+ defer ctx.DeleteShader(cs)
+ prog := ctx.CreateProgram()
+ if !prog.Valid() {
+ return Program{}, errors.New("glCreateProgram failed")
+ }
+ ctx.AttachShader(prog, cs)
+ ctx.LinkProgram(prog)
+ if ctx.GetProgrami(prog, LINK_STATUS) == 0 {
+ log := ctx.GetProgramInfoLog(prog)
+ ctx.DeleteProgram(prog)
+ return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log))
+ }
+ return prog, nil
+}
+
+func createShader(ctx *Functions, typ Enum, src string) (Shader, error) {
+ sh := ctx.CreateShader(typ)
+ if !sh.Valid() {
+ return Shader{}, errors.New("glCreateShader failed")
+ }
+ ctx.ShaderSource(sh, src)
+ ctx.CompileShader(sh)
+ if ctx.GetShaderi(sh, COMPILE_STATUS) == 0 {
+ log := ctx.GetShaderInfoLog(sh)
+ ctx.DeleteShader(sh)
+ return Shader{}, fmt.Errorf("shader compilation failed: %s", strings.TrimSpace(log))
+ }
+ return sh, nil
+}
+
+func ParseGLVersion(glVer string) (version [2]int, gles bool, err error) {
+ var ver [2]int
+ if _, err := fmt.Sscanf(glVer, "OpenGL ES %d.%d", &ver[0], &ver[1]); err == nil {
+ return ver, true, nil
+ } else if _, err := fmt.Sscanf(glVer, "WebGL %d.%d", &ver[0], &ver[1]); err == nil {
+ // WebGL major version v corresponds to OpenGL ES version v + 1
+ ver[0]++
+ return ver, true, nil
+ } else if _, err := fmt.Sscanf(glVer, "%d.%d", &ver[0], &ver[1]); err == nil {
+ return ver, false, nil
+ }
+ return ver, false, fmt.Errorf("failed to parse OpenGL ES version (%s)", glVer)
+}
diff --git a/gio/giold/internal/opconst/ops.go b/gio/giold/internal/opconst/ops.go
new file mode 100644
index 0000000..db9dd8d
--- /dev/null
+++ b/gio/giold/internal/opconst/ops.go
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package opconst
+
+type OpType byte
+
+// Start at a high number for easier debugging.
+const firstOpIndex = 200
+
+const (
+ TypeMacro OpType = iota + firstOpIndex
+ TypeCall
+ TypeDefer
+ TypeTransform
+ TypeInvalidate
+ TypeImage
+ TypePaint
+ TypeColor
+ TypeLinearGradient
+ TypeArea
+ TypePointerInput
+ TypePass
+ TypeClipboardRead
+ TypeClipboardWrite
+ TypeKeyInput
+ TypeKeyFocus
+ TypeKeySoftKeyboard
+ TypeSave
+ TypeLoad
+ TypeAux
+ TypeClip
+ TypeProfile
+ TypeCursor
+ TypePath
+ TypeStroke
+)
+
+const (
+ TypeMacroLen = 1 + 4 + 4
+ TypeCallLen = 1 + 4 + 4
+ TypeDeferLen = 1
+ TypeTransformLen = 1 + 4*6
+ TypeRedrawLen = 1 + 8
+ TypeImageLen = 1
+ TypePaintLen = 1
+ TypeColorLen = 1 + 4
+ TypeLinearGradientLen = 1 + 8*2 + 4*2
+ TypeAreaLen = 1 + 1 + 4*4
+ TypePointerInputLen = 1 + 1 + 1 + 2*4 + 2*4
+ TypePassLen = 1 + 1
+ TypeClipboardReadLen = 1
+ TypeClipboardWriteLen = 1
+ TypeKeyInputLen = 1
+ TypeKeyFocusLen = 1
+ TypeKeySoftKeyboardLen = 1 + 1
+ TypeSaveLen = 1 + 4
+ TypeLoadLen = 1 + 1 + 4
+ TypeAuxLen = 1
+ TypeClipLen = 1 + 4*4 + 1
+ TypeProfileLen = 1
+ TypeCursorLen = 1 + 1
+ TypePathLen = 1
+ TypeStrokeLen = 1 + 4
+)
+
+// StateMask is a bitmask of state types a load operation
+// should restore.
+type StateMask uint8
+
+const (
+ TransformState StateMask = 1 << iota
+
+ AllState = ^StateMask(0)
+)
+
+// InitialStateID is the ID for saving and loading
+// the initial operation state.
+const InitialStateID = 0
+
+func (t OpType) Size() int {
+ return [...]int{
+ TypeMacroLen,
+ TypeCallLen,
+ TypeDeferLen,
+ TypeTransformLen,
+ TypeRedrawLen,
+ TypeImageLen,
+ TypePaintLen,
+ TypeColorLen,
+ TypeLinearGradientLen,
+ TypeAreaLen,
+ TypePointerInputLen,
+ TypePassLen,
+ TypeClipboardReadLen,
+ TypeClipboardWriteLen,
+ TypeKeyInputLen,
+ TypeKeyFocusLen,
+ TypeKeySoftKeyboardLen,
+ TypeSaveLen,
+ TypeLoadLen,
+ TypeAuxLen,
+ TypeClipLen,
+ TypeProfileLen,
+ TypeCursorLen,
+ TypePathLen,
+ TypeStrokeLen,
+ }[t-firstOpIndex]
+}
+
+func (t OpType) NumRefs() int {
+ switch t {
+ case TypeKeyInput, TypeKeyFocus, TypePointerInput, TypeProfile, TypeCall, TypeClipboardRead, TypeClipboardWrite, TypeCursor:
+ return 1
+ case TypeImage:
+ return 2
+ default:
+ return 0
+ }
+}
diff --git a/gio/giold/internal/ops/ops.go b/gio/giold/internal/ops/ops.go
new file mode 100644
index 0000000..a25839f
--- /dev/null
+++ b/gio/giold/internal/ops/ops.go
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package ops
+
+import (
+ "encoding/binary"
+ "math"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/byteslice"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/scene"
+)
+
+func DecodeCommand(d []byte) scene.Command {
+ var cmd scene.Command
+ copy(byteslice.Uint32(cmd[:]), d)
+ return cmd
+}
+
+func EncodeCommand(out []byte, cmd scene.Command) {
+ copy(out, byteslice.Uint32(cmd[:]))
+}
+
+func DecodeTransform(data []byte) (t f32.Affine2D) {
+ if opconst.OpType(data[0]) != opconst.TypeTransform {
+ panic("invalid op")
+ }
+ data = data[1:]
+ data = data[:4*6]
+
+ bo := binary.LittleEndian
+ a := math.Float32frombits(bo.Uint32(data))
+ b := math.Float32frombits(bo.Uint32(data[4*1:]))
+ c := math.Float32frombits(bo.Uint32(data[4*2:]))
+ d := math.Float32frombits(bo.Uint32(data[4*3:]))
+ e := math.Float32frombits(bo.Uint32(data[4*4:]))
+ f := math.Float32frombits(bo.Uint32(data[4*5:]))
+ return f32.NewAffine2D(a, b, c, d, e, f)
+}
+
+// DecodeSave decodes the state id of a save op.
+func DecodeSave(data []byte) int {
+ if opconst.OpType(data[0]) != opconst.TypeSave {
+ panic("invalid op")
+ }
+ bo := binary.LittleEndian
+ return int(bo.Uint32(data[1:]))
+}
+
+// DecodeLoad decodes the state id and mask of a load op.
+func DecodeLoad(data []byte) (int, opconst.StateMask) {
+ if opconst.OpType(data[0]) != opconst.TypeLoad {
+ panic("invalid op")
+ }
+ bo := binary.LittleEndian
+ return int(bo.Uint32(data[2:])), opconst.StateMask(data[1])
+}
diff --git a/gio/giold/internal/ops/reader.go b/gio/giold/internal/ops/reader.go
new file mode 100644
index 0000000..8465446
--- /dev/null
+++ b/gio/giold/internal/ops/reader.go
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package ops
+
+import (
+ "encoding/binary"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/op"
+)
+
+// Reader parses an ops list.
+type Reader struct {
+ pc PC
+ stack []macro
+ ops *op.Ops
+ deferOps op.Ops
+ deferDone bool
+}
+
+// EncodedOp represents an encoded op returned by
+// Reader.
+type EncodedOp struct {
+ Key Key
+ Data []byte
+ Refs []interface{}
+}
+
+// Key is a unique key for a given op.
+type Key struct {
+ ops *op.Ops
+ pc int
+ version int
+ sx, hx, sy, hy float32
+}
+
+// Shadow of op.MacroOp.
+type macroOp struct {
+ ops *op.Ops
+ pc PC
+}
+
+// PC is an instruction counter for an operation list.
+type PC struct {
+ data int
+ refs int
+}
+
+type macro struct {
+ ops *op.Ops
+ retPC PC
+ endPC PC
+}
+
+type opMacroDef struct {
+ endpc PC
+}
+
+// Reset start reading from the beginning of ops.
+func (r *Reader) Reset(ops *op.Ops) {
+ r.ResetAt(ops, PC{})
+}
+
+// ResetAt is like Reset, except it starts reading from pc.
+func (r *Reader) ResetAt(ops *op.Ops, pc PC) {
+ r.stack = r.stack[:0]
+ r.deferOps.Reset()
+ r.deferDone = false
+ r.pc = pc
+ r.ops = ops
+}
+
+// NewPC returns a PC representing the current instruction counter of
+// ops.
+func NewPC(ops *op.Ops) PC {
+ return PC{
+ data: len(ops.Data()),
+ refs: len(ops.Refs()),
+ }
+}
+
+func (k Key) SetTransform(t f32.Affine2D) Key {
+ sx, hx, _, hy, sy, _ := t.Elems()
+ k.sx = sx
+ k.hx = hx
+ k.hy = hy
+ k.sy = sy
+ return k
+}
+
+func (r *Reader) Decode() (EncodedOp, bool) {
+ if r.ops == nil {
+ return EncodedOp{}, false
+ }
+ deferring := false
+ for {
+ if len(r.stack) > 0 {
+ b := r.stack[len(r.stack)-1]
+ if r.pc == b.endPC {
+ r.ops = b.ops
+ r.pc = b.retPC
+ r.stack = r.stack[:len(r.stack)-1]
+ continue
+ }
+ }
+ data := r.ops.Data()
+ data = data[r.pc.data:]
+ refs := r.ops.Refs()
+ if len(data) == 0 {
+ if r.deferDone {
+ return EncodedOp{}, false
+ }
+ r.deferDone = true
+ // Execute deferred macros.
+ r.ops = &r.deferOps
+ r.pc = PC{}
+ continue
+ }
+ key := Key{ops: r.ops, pc: r.pc.data, version: r.ops.Version()}
+ t := opconst.OpType(data[0])
+ n := t.Size()
+ nrefs := t.NumRefs()
+ data = data[:n]
+ refs = refs[r.pc.refs:]
+ refs = refs[:nrefs]
+ switch t {
+ case opconst.TypeDefer:
+ deferring = true
+ r.pc.data += n
+ r.pc.refs += nrefs
+ continue
+ case opconst.TypeAux:
+ // An Aux operations is always wrapped in a macro, and
+ // its length is the remaining space.
+ block := r.stack[len(r.stack)-1]
+ n += block.endPC.data - r.pc.data - opconst.TypeAuxLen
+ data = data[:n]
+ case opconst.TypeCall:
+ if deferring {
+ deferring = false
+ // Copy macro for deferred execution.
+ if t.NumRefs() != 1 {
+ panic("internal error: unexpected number of macro refs")
+ }
+ deferData := r.deferOps.Write1(t.Size(), refs[0])
+ copy(deferData, data)
+ continue
+ }
+ var op macroOp
+ op.decode(data, refs)
+ macroData := op.ops.Data()[op.pc.data:]
+ if opconst.OpType(macroData[0]) != opconst.TypeMacro {
+ panic("invalid macro reference")
+ }
+ var opDef opMacroDef
+ opDef.decode(macroData[:opconst.TypeMacro.Size()])
+ retPC := r.pc
+ retPC.data += n
+ retPC.refs += nrefs
+ r.stack = append(r.stack, macro{
+ ops: r.ops,
+ retPC: retPC,
+ endPC: opDef.endpc,
+ })
+ r.ops = op.ops
+ r.pc = op.pc
+ r.pc.data += opconst.TypeMacro.Size()
+ r.pc.refs += opconst.TypeMacro.NumRefs()
+ continue
+ case opconst.TypeMacro:
+ var op opMacroDef
+ op.decode(data)
+ r.pc = op.endpc
+ continue
+ }
+ r.pc.data += n
+ r.pc.refs += nrefs
+ return EncodedOp{Key: key, Data: data, Refs: refs}, true
+ }
+}
+
+func (op *opMacroDef) decode(data []byte) {
+ if opconst.OpType(data[0]) != opconst.TypeMacro {
+ panic("invalid op")
+ }
+ bo := binary.LittleEndian
+ data = data[:9]
+ dataIdx := int(int32(bo.Uint32(data[1:])))
+ refsIdx := int(int32(bo.Uint32(data[5:])))
+ *op = opMacroDef{
+ endpc: PC{
+ data: dataIdx,
+ refs: refsIdx,
+ },
+ }
+}
+
+func (m *macroOp) decode(data []byte, refs []interface{}) {
+ if opconst.OpType(data[0]) != opconst.TypeCall {
+ panic("invalid op")
+ }
+ data = data[:9]
+ bo := binary.LittleEndian
+ dataIdx := int(int32(bo.Uint32(data[1:])))
+ refsIdx := int(int32(bo.Uint32(data[5:])))
+ *m = macroOp{
+ ops: refs[0].(*op.Ops),
+ pc: PC{
+ data: dataIdx,
+ refs: refsIdx,
+ },
+ }
+}
diff --git a/gio/giold/internal/scene/scene.go b/gio/giold/internal/scene/scene.go
new file mode 100644
index 0000000..8761a13
--- /dev/null
+++ b/gio/giold/internal/scene/scene.go
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package scene encodes and decodes graphics commands in the format used by the
+// compute renderer.
+package scene
+
+import (
+ "fmt"
+ "image/color"
+ "math"
+ "unsafe"
+
+ "realy.lol/gio/f32"
+)
+
+type Op uint32
+
+type Command [sceneElemSize / 4]uint32
+
+// GPU commands from scene.h
+const (
+ OpNop Op = iota
+ OpLine
+ OpQuad
+ OpCubic
+ OpFillColor
+ OpLineWidth
+ OpTransform
+ OpBeginClip
+ OpEndClip
+ OpFillImage
+ OpSetFillMode
+)
+
+// FillModes, from setup.h.
+type FillMode uint32
+
+const (
+ FillModeNonzero = 0
+ FillModeStroke = 1
+)
+
+const CommandSize = int(unsafe.Sizeof(Command{}))
+
+const sceneElemSize = 36
+
+func (c Command) Op() Op {
+ return Op(c[0])
+}
+
+func (c Command) String() string {
+ switch Op(c[0]) {
+ case OpNop:
+ return "nop"
+ case OpLine:
+ from, to := DecodeLine(c)
+ return fmt.Sprintf("line(%v, %v)", from, to)
+ case OpQuad:
+ from, ctrl, to := DecodeQuad(c)
+ return fmt.Sprintf("quad(%v, %v, %v)", from, ctrl, to)
+ case OpCubic:
+ from, ctrl0, ctrl1, to := DecodeCubic(c)
+ return fmt.Sprintf("cubic(%v, %v, %v, %v)", from, ctrl0, ctrl1, to)
+ case OpFillColor:
+ return "fillcolor"
+ case OpLineWidth:
+ return "linewidth"
+ case OpTransform:
+ t := f32.NewAffine2D(
+ math.Float32frombits(c[1]),
+ math.Float32frombits(c[3]),
+ math.Float32frombits(c[5]),
+ math.Float32frombits(c[2]),
+ math.Float32frombits(c[4]),
+ math.Float32frombits(c[6]),
+ )
+ return fmt.Sprintf("transform (%v)", t)
+ case OpBeginClip:
+ bounds := f32.Rectangle{
+ Min: f32.Pt(math.Float32frombits(c[1]), math.Float32frombits(c[2])),
+ Max: f32.Pt(math.Float32frombits(c[3]), math.Float32frombits(c[4])),
+ }
+ return fmt.Sprintf("beginclip (%v)", bounds)
+ case OpEndClip:
+ bounds := f32.Rectangle{
+ Min: f32.Pt(math.Float32frombits(c[1]), math.Float32frombits(c[2])),
+ Max: f32.Pt(math.Float32frombits(c[3]), math.Float32frombits(c[4])),
+ }
+ return fmt.Sprintf("endclip (%v)", bounds)
+ case OpFillImage:
+ return "fillimage"
+ case OpSetFillMode:
+ return "setfillmode"
+ default:
+ panic("unreachable")
+ }
+}
+
+func Line(start, end f32.Point) Command {
+ return Command{
+ 0: uint32(OpLine),
+ 1: math.Float32bits(start.X),
+ 2: math.Float32bits(start.Y),
+ 3: math.Float32bits(end.X),
+ 4: math.Float32bits(end.Y),
+ }
+}
+
+func Cubic(start, ctrl0, ctrl1, end f32.Point) Command {
+ return Command{
+ 0: uint32(OpCubic),
+ 1: math.Float32bits(start.X),
+ 2: math.Float32bits(start.Y),
+ 3: math.Float32bits(ctrl0.X),
+ 4: math.Float32bits(ctrl0.Y),
+ 5: math.Float32bits(ctrl1.X),
+ 6: math.Float32bits(ctrl1.Y),
+ 7: math.Float32bits(end.X),
+ 8: math.Float32bits(end.Y),
+ }
+}
+
+func Quad(start, ctrl, end f32.Point) Command {
+ return Command{
+ 0: uint32(OpQuad),
+ 1: math.Float32bits(start.X),
+ 2: math.Float32bits(start.Y),
+ 3: math.Float32bits(ctrl.X),
+ 4: math.Float32bits(ctrl.Y),
+ 5: math.Float32bits(end.X),
+ 6: math.Float32bits(end.Y),
+ }
+}
+
+func Transform(m f32.Affine2D) Command {
+ sx, hx, ox, hy, sy, oy := m.Elems()
+ return Command{
+ 0: uint32(OpTransform),
+ 1: math.Float32bits(sx),
+ 2: math.Float32bits(hy),
+ 3: math.Float32bits(hx),
+ 4: math.Float32bits(sy),
+ 5: math.Float32bits(ox),
+ 6: math.Float32bits(oy),
+ }
+}
+
+func SetLineWidth(width float32) Command {
+ return Command{
+ 0: uint32(OpLineWidth),
+ 1: math.Float32bits(width),
+ }
+}
+
+func BeginClip(bbox f32.Rectangle) Command {
+ return Command{
+ 0: uint32(OpBeginClip),
+ 1: math.Float32bits(bbox.Min.X),
+ 2: math.Float32bits(bbox.Min.Y),
+ 3: math.Float32bits(bbox.Max.X),
+ 4: math.Float32bits(bbox.Max.Y),
+ }
+}
+
+func EndClip(bbox f32.Rectangle) Command {
+ return Command{
+ 0: uint32(OpEndClip),
+ 1: math.Float32bits(bbox.Min.X),
+ 2: math.Float32bits(bbox.Min.Y),
+ 3: math.Float32bits(bbox.Max.X),
+ 4: math.Float32bits(bbox.Max.Y),
+ }
+}
+
+func FillColor(col color.RGBA) Command {
+ return Command{
+ 0: uint32(OpFillColor),
+ 1: uint32(col.R)<<24 | uint32(col.G)<<16 | uint32(col.B)<<8 | uint32(col.A),
+ }
+}
+
+func FillImage(index int) Command {
+ return Command{
+ 0: uint32(OpFillImage),
+ 1: uint32(index),
+ }
+}
+
+func SetFillMode(mode FillMode) Command {
+ return Command{
+ 0: uint32(OpSetFillMode),
+ 1: uint32(mode),
+ }
+}
+
+func DecodeLine(cmd Command) (from, to f32.Point) {
+ if cmd[0] != uint32(OpLine) {
+ panic("invalid command")
+ }
+ from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2]))
+ to = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4]))
+ return
+}
+
+func DecodeQuad(cmd Command) (from, ctrl, to f32.Point) {
+ if cmd[0] != uint32(OpQuad) {
+ panic("invalid command")
+ }
+ from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2]))
+ ctrl = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4]))
+ to = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6]))
+ return
+}
+
+func DecodeCubic(cmd Command) (from, ctrl0, ctrl1, to f32.Point) {
+ if cmd[0] != uint32(OpCubic) {
+ panic("invalid command")
+ }
+ from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2]))
+ ctrl0 = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4]))
+ ctrl1 = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6]))
+ to = f32.Pt(math.Float32frombits(cmd[7]), math.Float32frombits(cmd[8]))
+ return
+}
diff --git a/gio/giold/internal/srgb/srgb.go b/gio/giold/internal/srgb/srgb.go
new file mode 100644
index 0000000..1cd67cf
--- /dev/null
+++ b/gio/giold/internal/srgb/srgb.go
@@ -0,0 +1,206 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package srgb
+
+import (
+ "fmt"
+ "runtime"
+ "strings"
+
+ "realy.lol/gio/internal/byteslice"
+ "realy.lol/gio/internal/gl"
+)
+
+// FBO implements an intermediate sRGB FBO
+// for gamma-correct rendering on platforms without
+// sRGB enabled native framebuffers.
+type FBO struct {
+ c *gl.Functions
+ width, height int
+ frameBuffer gl.Framebuffer
+ depthBuffer gl.Renderbuffer
+ colorTex gl.Texture
+ blitted bool
+ quad gl.Buffer
+ prog gl.Program
+ gl3 bool
+}
+
+func New(ctx gl.Context) (*FBO, error) {
+ f, err := gl.NewFunctions(ctx)
+ if err != nil {
+ return nil, err
+ }
+ var gl3 bool
+ glVer := f.GetString(gl.VERSION)
+ ver, _, err := gl.ParseGLVersion(glVer)
+ if err != nil {
+ return nil, err
+ }
+ if ver[0] >= 3 {
+ gl3 = true
+ } else {
+ exts := f.GetString(gl.EXTENSIONS)
+ if !strings.Contains(exts, "EXT_sRGB") {
+ return nil, fmt.Errorf("no support for OpenGL ES 3 nor EXT_sRGB")
+ }
+ }
+ s := &FBO{
+ c: f,
+ gl3: gl3,
+ frameBuffer: f.CreateFramebuffer(),
+ colorTex: f.CreateTexture(),
+ depthBuffer: f.CreateRenderbuffer(),
+ }
+ f.BindTexture(gl.TEXTURE_2D, s.colorTex)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
+ return s, nil
+}
+
+func (s *FBO) Blit() {
+ if !s.blitted {
+ prog, err := gl.CreateProgram(s.c, blitVSrc, blitFSrc,
+ []string{"pos", "uv"})
+ if err != nil {
+ panic(err)
+ }
+ s.prog = prog
+ s.c.UseProgram(prog)
+ s.c.Uniform1i(s.c.GetUniformLocation(prog, "tex"), 0)
+ s.quad = s.c.CreateBuffer()
+ s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad)
+ coords := byteslice.Slice([]float32{
+ -1, +1, 0, 1,
+ +1, +1, 1, 1,
+ -1, -1, 0, 0,
+ +1, -1, 1, 0,
+ })
+ s.c.BufferData(gl.ARRAY_BUFFER, len(coords), gl.STATIC_DRAW)
+ s.c.BufferSubData(gl.ARRAY_BUFFER, 0, coords)
+ s.blitted = true
+ }
+ s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{})
+ s.c.UseProgram(s.prog)
+ s.c.BindTexture(gl.TEXTURE_2D, s.colorTex)
+ s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad)
+ s.c.VertexAttribPointer(0 /* pos */, 2, gl.FLOAT, false, 4*4, 0)
+ s.c.VertexAttribPointer(1 /* uv */, 2, gl.FLOAT, false, 4*4, 4*2)
+ s.c.EnableVertexAttribArray(0)
+ s.c.EnableVertexAttribArray(1)
+ s.c.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
+ s.c.BindTexture(gl.TEXTURE_2D, gl.Texture{})
+ s.c.DisableVertexAttribArray(0)
+ s.c.DisableVertexAttribArray(1)
+ s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer)
+ s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0)
+ s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT)
+ // The Android emulator requires framebuffer 0 bound at eglSwapBuffer time.
+ // Bind the sRGB framebuffer again in afterPresent.
+ s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{})
+}
+
+func (s *FBO) AfterPresent() {
+ s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer)
+}
+
+func (s *FBO) Refresh(w, h int) error {
+ s.width, s.height = w, h
+ if w == 0 || h == 0 {
+ return nil
+ }
+ s.c.BindTexture(gl.TEXTURE_2D, s.colorTex)
+ if s.gl3 {
+ s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB8_ALPHA8, w, h, gl.RGBA,
+ gl.UNSIGNED_BYTE)
+ } else /* EXT_sRGB */ {
+ s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB_ALPHA_EXT, w, h,
+ gl.SRGB_ALPHA_EXT, gl.UNSIGNED_BYTE)
+ }
+ currentRB := gl.Renderbuffer(s.c.GetBinding(gl.RENDERBUFFER_BINDING))
+ s.c.BindRenderbuffer(gl.RENDERBUFFER, s.depthBuffer)
+ s.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h)
+ s.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB)
+ s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer)
+ s.c.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
+ gl.TEXTURE_2D, s.colorTex, 0)
+ s.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT,
+ gl.RENDERBUFFER, s.depthBuffer)
+ if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
+ return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x error: %x",
+ s.width, s.height, st, s.c.GetError())
+ }
+
+ if runtime.GOOS == "js" {
+ // With macOS Safari, rendering to and then reading from a SRGB8_ALPHA8
+ // texture result in twice gamma corrected colors. Using a plain RGBA
+ // texture seems to work.
+ s.c.ClearColor(.5, .5, .5, 1.0)
+ s.c.Clear(gl.COLOR_BUFFER_BIT)
+ var pixel [4]byte
+ s.c.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel[:])
+ if pixel[0] == 128 { // Correct sRGB color value is ~188
+ s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, gl.RGBA,
+ gl.UNSIGNED_BYTE)
+ if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
+ return fmt.Errorf("fallback RGBA framebuffer incomplete (%dx%d), status: %#x error: %x",
+ s.width, s.height, st, s.c.GetError())
+ }
+ }
+ }
+
+ return nil
+}
+
+func (s *FBO) Release() {
+ s.c.DeleteFramebuffer(s.frameBuffer)
+ s.c.DeleteTexture(s.colorTex)
+ s.c.DeleteRenderbuffer(s.depthBuffer)
+ if s.blitted {
+ s.c.DeleteBuffer(s.quad)
+ s.c.DeleteProgram(s.prog)
+ }
+ s.c = nil
+}
+
+const (
+ blitVSrc = `
+#version 100
+
+precision highp float;
+
+attribute vec2 pos;
+attribute vec2 uv;
+
+varying vec2 vUV;
+
+void main() {
+ gl_Position = vec4(pos, 0, 1);
+ vUV = uv;
+}
+`
+ blitFSrc = `
+#version 100
+
+precision mediump float;
+
+uniform sampler2D tex;
+varying vec2 vUV;
+
+vec3 gamma(vec3 rgb) {
+ vec3 exp = vec3(1.055)*pow(rgb, vec3(0.41666)) - vec3(0.055);
+ vec3 lin = rgb * vec3(12.92);
+ bvec3 cut = lessThan(rgb, vec3(0.0031308));
+ return vec3(cut.r ? lin.r : exp.r, cut.g ? lin.g : exp.g, cut.b ? lin.b : exp.b);
+}
+
+void main() {
+ vec4 col = texture2D(tex, vUV);
+ vec3 rgb = col.rgb;
+ rgb = gamma(rgb);
+ gl_FragColor = vec4(rgb, col.a);
+}
+`
+)
diff --git a/gio/giold/internal/stroke/dash.go b/gio/giold/internal/stroke/dash.go
new file mode 100644
index 0000000..c57a032
--- /dev/null
+++ b/gio/giold/internal/stroke/dash.go
@@ -0,0 +1,401 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// The algorithms to compute dashes have been extracted, adapted from
+// (and used as a reference implementation):
+// - github.com/tdewolff/canvas (Licensed under MIT)
+
+package stroke
+
+import (
+ "math"
+ "sort"
+
+ "realy.lol/gio/f32"
+)
+
+type DashOp struct {
+ Phase float32
+ Dashes []float32
+}
+
+func IsSolidLine(sty DashOp) bool {
+ return sty.Phase == 0 && len(sty.Dashes) == 0
+}
+
+func (qs StrokeQuads) dash(sty DashOp) StrokeQuads {
+ sty = dashCanonical(sty)
+
+ switch {
+ case len(sty.Dashes) == 0:
+ return qs
+ case len(sty.Dashes) == 1 && sty.Dashes[0] == 0.0:
+ return StrokeQuads{}
+ }
+
+ if len(sty.Dashes)%2 == 1 {
+ // If the dash pattern is of uneven length, dash and space lengths
+ // alternate. The following duplicates the pattern so that uneven
+ // indices are always spaces.
+ sty.Dashes = append(sty.Dashes, sty.Dashes...)
+ }
+
+ var (
+ i0, pos0 = dashStart(sty)
+ out StrokeQuads
+
+ contour uint32 = 1
+ )
+
+ for _, ps := range qs.split() {
+ var (
+ i = i0
+ pos = pos0
+ t []float64
+ length = ps.len()
+ )
+ for pos+sty.Dashes[i] < length {
+ pos += sty.Dashes[i]
+ if 0.0 < pos {
+ t = append(t, float64(pos))
+ }
+ i++
+ if i == len(sty.Dashes) {
+ i = 0
+ }
+ }
+
+ j0 := 0
+ endsInDash := i%2 == 0
+ if len(t)%2 == 1 && endsInDash || len(t)%2 == 0 && !endsInDash {
+ j0 = 1
+ }
+
+ var (
+ qd StrokeQuads
+ pd = ps.splitAt(&contour, t...)
+ )
+ for j := j0; j < len(pd)-1; j += 2 {
+ qd = qd.append(pd[j])
+ }
+ if endsInDash {
+ if ps.closed() {
+ qd = pd[len(pd)-1].append(qd)
+ } else {
+ qd = qd.append(pd[len(pd)-1])
+ }
+ }
+ out = out.append(qd)
+ contour++
+ }
+ return out
+}
+
+func dashCanonical(sty DashOp) DashOp {
+ var (
+ o = sty
+ ds = o.Dashes
+ )
+
+ if len(sty.Dashes) == 0 {
+ return sty
+ }
+
+ // Remove zeros except first and last.
+ for i := 1; i < len(ds)-1; i++ {
+ if f32Eq(ds[i], 0.0) {
+ ds[i-1] += ds[i+1]
+ ds = append(ds[:i], ds[i+2:]...)
+ i--
+ }
+ }
+
+ // Remove first zero, collapse with second and last.
+ if f32Eq(ds[0], 0.0) {
+ if len(ds) < 3 {
+ return DashOp{
+ Phase: 0.0,
+ Dashes: []float32{0.0},
+ }
+ }
+ o.Phase -= ds[1]
+ ds[len(ds)-1] += ds[1]
+ ds = ds[2:]
+ }
+
+ // Remove last zero, collapse with fist and second to last.
+ if f32Eq(ds[len(ds)-1], 0.0) {
+ if len(ds) < 3 {
+ return DashOp{}
+ }
+ o.Phase += ds[len(ds)-2]
+ ds[0] += ds[len(ds)-2]
+ ds = ds[:len(ds)-2]
+ }
+
+ // If there are zeros or negatives, don't draw dashes.
+ for i := 0; i < len(ds); i++ {
+ if ds[i] < 0.0 || f32Eq(ds[i], 0.0) {
+ return DashOp{
+ Phase: 0.0,
+ Dashes: []float32{0.0},
+ }
+ }
+ }
+
+ // Remove repeated patterns.
+loop:
+ for len(ds)%2 == 0 {
+ mid := len(ds) / 2
+ for i := 0; i < mid; i++ {
+ if !f32Eq(ds[i], ds[mid+i]) {
+ break loop
+ }
+ }
+ ds = ds[:mid]
+ }
+ return o
+}
+
+func dashStart(sty DashOp) (int, float32) {
+ i0 := 0 // i0 is the index into dashes.
+ for sty.Dashes[i0] <= sty.Phase {
+ sty.Phase -= sty.Dashes[i0]
+ i0++
+ if i0 == len(sty.Dashes) {
+ i0 = 0
+ }
+ }
+ // pos0 may be negative if the offset lands halfway into dash.
+ pos0 := -sty.Phase
+ if sty.Phase < 0.0 {
+ var sum float32
+ for _, d := range sty.Dashes {
+ sum += d
+ }
+ pos0 = -(sum + sty.Phase) // handle negative offsets
+ }
+ return i0, pos0
+}
+
+func (qs StrokeQuads) len() float32 {
+ var sum float32
+ for i := range qs {
+ q := qs[i].Quad
+ sum += quadBezierLen(q.From, q.Ctrl, q.To)
+ }
+ return sum
+}
+
+// splitAt splits the path into separate paths at the specified intervals
+// along the path.
+// splitAt updates the provided contour counter as it splits the segments.
+func (qs StrokeQuads) splitAt(contour *uint32, ts ...float64) []StrokeQuads {
+ if len(ts) == 0 {
+ qs.setContour(*contour)
+ return []StrokeQuads{qs}
+ }
+
+ sort.Float64s(ts)
+ if ts[0] == 0 {
+ ts = ts[1:]
+ }
+
+ var (
+ j int // index into ts
+ t float64 // current position along curve
+ )
+
+ var oo []StrokeQuads
+ var oi StrokeQuads
+ push := func() {
+ oo = append(oo, oi)
+ oi = nil
+ }
+
+ for _, ps := range qs.split() {
+ for _, q := range ps {
+ if j == len(ts) {
+ oi = append(oi, q)
+ continue
+ }
+ speed := func(t float64) float64 {
+ return float64(lenPt(quadBezierD1(q.Quad.From, q.Quad.Ctrl,
+ q.Quad.To, float32(t))))
+ }
+ invL, dt := invSpeedPolynomialChebyshevApprox(20, gaussLegendre7,
+ speed, 0, 1)
+
+ var (
+ t0 float64
+ r0 = q.Quad.From
+ r1 = q.Quad.Ctrl
+ r2 = q.Quad.To
+
+ // from keeps track of the start of the 'running' segment.
+ from = r0
+ )
+ for j < len(ts) && t < ts[j] && ts[j] <= t+dt {
+ tj := invL(ts[j] - t)
+ tsub := (tj - t0) / (1.0 - t0)
+ t0 = tj
+
+ var q1 f32.Point
+ _, q1, _, r0, r1, r2 = quadBezierSplit(r0, r1, r2,
+ float32(tsub))
+
+ oi = append(oi, StrokeQuad{
+ Contour: *contour,
+ Quad: QuadSegment{
+ From: from,
+ Ctrl: q1,
+ To: r0,
+ },
+ })
+ push()
+ (*contour)++
+
+ from = r0
+ j++
+ }
+ if !f64Eq(t0, 1) {
+ if len(oi) > 0 {
+ r0 = oi.pen()
+ }
+ oi = append(oi, StrokeQuad{
+ Contour: *contour,
+ Quad: QuadSegment{
+ From: r0,
+ Ctrl: r1,
+ To: r2,
+ },
+ })
+ }
+ t += dt
+ }
+ }
+ if len(oi) > 0 {
+ push()
+ (*contour)++
+ }
+
+ return oo
+}
+
+func f32Eq(a, b float32) bool {
+ const epsilon = 1e-10
+ return math.Abs(float64(a-b)) < epsilon
+}
+
+func f64Eq(a, b float64) bool {
+ const epsilon = 1e-10
+ return math.Abs(a-b) < epsilon
+}
+
+func invSpeedPolynomialChebyshevApprox(N int, gaussLegendre gaussLegendreFunc,
+ fp func(float64) float64, tmin, tmax float64) (func(float64) float64,
+ float64) {
+ // The TODOs below are copied verbatim from tdewolff/canvas:
+ //
+ // TODO: find better way to determine N. For Arc 10 seems fine, for some
+ // Quads 10 is too low, for Cube depending on inflection points is
+ // maybe not the best indicator
+ //
+ // TODO: track efficiency, how many times is fp called?
+ // Does a look-up table make more sense?
+ fLength := func(t float64) float64 {
+ return math.Abs(gaussLegendre(fp, tmin, t))
+ }
+ totalLength := fLength(tmax)
+ t := func(L float64) float64 {
+ return bisectionMethod(fLength, L, tmin, tmax)
+ }
+ return polynomialChebyshevApprox(N, t, 0.0, totalLength, tmin,
+ tmax), totalLength
+}
+
+func polynomialChebyshevApprox(N int, f func(float64) float64,
+ xmin, xmax, ymin, ymax float64) func(float64) float64 {
+ var (
+ invN = 1.0 / float64(N)
+ fs = make([]float64, N)
+ )
+ for k := 0; k < N; k++ {
+ u := math.Cos(math.Pi * (float64(k+1) - 0.5) * invN)
+ fs[k] = f(xmin + 0.5*(xmax-xmin)*(u+1))
+ }
+
+ c := make([]float64, N)
+ for j := 0; j < N; j++ {
+ var a float64
+ for k := 0; k < N; k++ {
+ a += fs[k] * math.Cos(float64(j)*math.Pi*(float64(k+1)-0.5)/float64(N))
+ }
+ c[j] = 2 * invN * a
+ }
+
+ if ymax < ymin {
+ ymin, ymax = ymax, ymin
+ }
+ return func(x float64) float64 {
+ x = math.Min(xmax, math.Max(xmin, x))
+ u := (x-xmin)/(xmax-xmin)*2 - 1
+ var a float64
+ for j := 0; j < N; j++ {
+ a += c[j] * math.Cos(float64(j)*math.Acos(u))
+ }
+ y := -0.5*c[0] + a
+ if !math.IsNaN(ymin) && !math.IsNaN(ymax) {
+ y = math.Min(ymax, math.Max(ymin, y))
+ }
+ return y
+ }
+}
+
+// bisectionMethod finds the value x for which f(x) = y in the interval x
+// in [xmin, xmax] using the bisection method.
+func bisectionMethod(f func(float64) float64, y, xmin, xmax float64) float64 {
+ const (
+ maxIter = 100
+ tolerance = 0.001 // 0.1%
+ )
+
+ var (
+ n = 0
+ x float64
+ tolX = math.Abs(xmax-xmin) * tolerance
+ tolY = math.Abs(f(xmax)-f(xmin)) * tolerance
+ )
+ for {
+ x = 0.5 * (xmin + xmax)
+ if n >= maxIter {
+ return x
+ }
+
+ dy := f(x) - y
+ switch {
+ case math.Abs(dy) < tolY, math.Abs(0.5*(xmax-xmin)) < tolX:
+ return x
+ case dy > 0:
+ xmax = x
+ default:
+ xmin = x
+ }
+ n++
+ }
+}
+
+type gaussLegendreFunc func(func(float64) float64, float64, float64) float64
+
+// Gauss-Legendre quadrature integration from a to b with n=7
+func gaussLegendre7(f func(float64) float64, a, b float64) float64 {
+ c := 0.5 * (b - a)
+ d := 0.5 * (a + b)
+ Qd1 := f(-0.949108*c + d)
+ Qd2 := f(-0.741531*c + d)
+ Qd3 := f(-0.405845*c + d)
+ Qd4 := f(d)
+ Qd5 := f(0.405845*c + d)
+ Qd6 := f(0.741531*c + d)
+ Qd7 := f(0.949108*c + d)
+ return c * (0.129485*(Qd1+Qd7) + 0.279705*(Qd2+Qd6) + 0.381830*(Qd3+Qd5) + 0.417959*Qd4)
+}
diff --git a/gio/giold/internal/stroke/stroke.go b/gio/giold/internal/stroke/stroke.go
new file mode 100644
index 0000000..b88a432
--- /dev/null
+++ b/gio/giold/internal/stroke/stroke.go
@@ -0,0 +1,902 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Most of the algorithms to compute strokes and their offsets have been
+// extracted, adapted from (and used as a reference implementation):
+// - github.com/tdewolff/canvas (Licensed under MIT)
+//
+// These algorithms have been implemented from:
+// Fast, precise flattening of cubic BĆ©zier path and offset curves
+// Thomas F. Hain, et al.
+//
+// An electronic version is available at:
+// https://seant23.files.wordpress.com/2010/11/fastpreciseflatteningofbeziercurve.pdf
+//
+// Possible improvements (in term of speed and/or accuracy) on these
+// algorithms are:
+//
+// - Polar Stroking: New Theory and Methods for Stroking Paths,
+// M. Kilgard
+// https://arxiv.org/pdf/2007.00308.pdf
+//
+// - https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html
+// R. Levien
+
+// Package stroke implements conversion of strokes to filled outlines. It is used as a
+// fallback for stroke configurations not natively supported by the renderer.
+package stroke
+
+import (
+ "encoding/binary"
+ "math"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/internal/scene"
+)
+
+// The following are copies of types from op/clip to avoid a circular import of
+// that package.
+// TODO: when the old renderer is gone, this package can be merged with
+// op/clip, eliminating the duplicate types.
+type StrokeStyle struct {
+ Width float32
+ Miter float32
+ Cap StrokeCap
+ Join StrokeJoin
+}
+
+type StrokeCap uint8
+
+const (
+ RoundCap StrokeCap = iota
+ FlatCap
+ SquareCap
+)
+
+type StrokeJoin uint8
+
+const (
+ RoundJoin StrokeJoin = iota
+ BevelJoin
+)
+
+// strokeTolerance is used to reconcile rounding errors arising
+// when splitting quads into smaller and smaller segments to approximate
+// them into straight lines, and when joining back segments.
+//
+// The magic value of 0.01 was found by striking a compromise between
+// aesthetic looking (curves did look like curves, even after linearization)
+// and speed.
+const strokeTolerance = 0.01
+
+type QuadSegment struct {
+ From, Ctrl, To f32.Point
+}
+
+type StrokeQuad struct {
+ Contour uint32
+ Quad QuadSegment
+}
+
+type strokeState struct {
+ p0, p1 f32.Point // p0 is the start point, p1 the end point.
+ n0, n1 f32.Point // n0 is the normal vector at the start point, n1 at the end point.
+ r0, r1 float32 // r0 is the curvature at the start point, r1 at the end point.
+ ctl f32.Point // ctl is the control point of the quadratic BĆ©zier segment.
+}
+
+type StrokeQuads []StrokeQuad
+
+func (qs *StrokeQuads) setContour(n uint32) {
+ for i := range *qs {
+ (*qs)[i].Contour = n
+ }
+}
+
+func (qs *StrokeQuads) pen() f32.Point {
+ return (*qs)[len(*qs)-1].Quad.To
+}
+
+func (qs *StrokeQuads) closed() bool {
+ beg := (*qs)[0].Quad.From
+ end := (*qs)[len(*qs)-1].Quad.To
+ return f32Eq(beg.X, end.X) && f32Eq(beg.Y, end.Y)
+}
+
+func (qs *StrokeQuads) lineTo(pt f32.Point) {
+ end := qs.pen()
+ *qs = append(*qs, StrokeQuad{
+ Quad: QuadSegment{
+ From: end,
+ Ctrl: end.Add(pt).Mul(0.5),
+ To: pt,
+ },
+ })
+}
+
+func (qs *StrokeQuads) arc(f1, f2 f32.Point, angle float32) {
+ const segments = 16
+ pen := qs.pen()
+ m := ArcTransform(pen, f1.Add(pen), f2.Add(pen), angle, segments)
+ for i := 0; i < segments; i++ {
+ p0 := qs.pen()
+ p1 := m.Transform(p0)
+ p2 := m.Transform(p1)
+ ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
+ *qs = append(*qs, StrokeQuad{
+ Quad: QuadSegment{
+ From: p0, Ctrl: ctl, To: p2,
+ },
+ })
+ }
+}
+
+// split splits a slice of quads into slices of quads grouped
+// by contours (ie: splitted at move-to boundaries).
+func (qs StrokeQuads) split() []StrokeQuads {
+ if len(qs) == 0 {
+ return nil
+ }
+
+ var (
+ c uint32
+ o []StrokeQuads
+ i = len(o)
+ )
+ for _, q := range qs {
+ if q.Contour != c {
+ c = q.Contour
+ i = len(o)
+ o = append(o, StrokeQuads{})
+ }
+ o[i] = append(o[i], q)
+ }
+
+ return o
+}
+
+func (qs StrokeQuads) stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads {
+ if !IsSolidLine(dashes) {
+ qs = qs.dash(dashes)
+ }
+
+ var (
+ o StrokeQuads
+ hw = 0.5 * stroke.Width
+ )
+
+ for _, ps := range qs.split() {
+ rhs, lhs := ps.offset(hw, stroke)
+ switch lhs {
+ case nil:
+ o = o.append(rhs)
+ default:
+ // Closed path.
+ // Inner path should go opposite direction to cancel outer path.
+ switch {
+ case ps.ccw():
+ lhs = lhs.reverse()
+ o = o.append(rhs)
+ o = o.append(lhs)
+ default:
+ rhs = rhs.reverse()
+ o = o.append(lhs)
+ o = o.append(rhs)
+ }
+ }
+ }
+
+ return o
+}
+
+// offset returns the right-hand and left-hand sides of the path, offset by
+// the half-width hw.
+// The stroke handles how segments are joined and ends are capped.
+func (qs StrokeQuads) offset(hw float32,
+ stroke StrokeStyle) (rhs, lhs StrokeQuads) {
+ var (
+ states []strokeState
+ beg = qs[0].Quad.From
+ end = qs[len(qs)-1].Quad.To
+ closed = beg == end
+ )
+ for i := range qs {
+ q := qs[i].Quad
+
+ var (
+ n0 = strokePathNorm(q.From, q.Ctrl, q.To, 0, hw)
+ n1 = strokePathNorm(q.From, q.Ctrl, q.To, 1, hw)
+ r0 = strokePathCurv(q.From, q.Ctrl, q.To, 0)
+ r1 = strokePathCurv(q.From, q.Ctrl, q.To, 1)
+ )
+ states = append(states, strokeState{
+ p0: q.From,
+ p1: q.To,
+ n0: n0,
+ n1: n1,
+ r0: r0,
+ r1: r1,
+ ctl: q.Ctrl,
+ })
+ }
+
+ for i, state := range states {
+ rhs = rhs.append(strokeQuadBezier(state, +hw, strokeTolerance))
+ lhs = lhs.append(strokeQuadBezier(state, -hw, strokeTolerance))
+
+ // join the current and next segments
+ if hasNext := i+1 < len(states); hasNext || closed {
+ var next strokeState
+ switch {
+ case hasNext:
+ next = states[i+1]
+ case closed:
+ next = states[0]
+ }
+ if state.n1 != next.n0 {
+ strokePathJoin(stroke, &rhs, &lhs, hw, state.p1, state.n1,
+ next.n0, state.r1, next.r0)
+ }
+ }
+ }
+
+ if closed {
+ rhs.close()
+ lhs.close()
+ return rhs, lhs
+ }
+
+ qbeg := &states[0]
+ qend := &states[len(states)-1]
+
+ // Default to counter-clockwise direction.
+ lhs = lhs.reverse()
+ strokePathCap(stroke, &rhs, hw, qend.p1, qend.n1)
+
+ rhs = rhs.append(lhs)
+ strokePathCap(stroke, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1))
+
+ rhs.close()
+
+ return rhs, nil
+}
+
+func (qs *StrokeQuads) close() {
+ p0 := (*qs)[len(*qs)-1].Quad.To
+ p1 := (*qs)[0].Quad.From
+
+ if p1 == p0 {
+ return
+ }
+
+ *qs = append(*qs, StrokeQuad{
+ Quad: QuadSegment{
+ From: p0,
+ Ctrl: p0.Add(p1).Mul(0.5),
+ To: p1,
+ },
+ })
+}
+
+// ccw returns whether the path is counter-clockwise.
+func (qs StrokeQuads) ccw() bool {
+ // Use the Shoelace formula:
+ // https://en.wikipedia.org/wiki/Shoelace_formula
+ var area float32
+ for _, ps := range qs.split() {
+ for i := 1; i < len(ps); i++ {
+ pi := ps[i].Quad.To
+ pj := ps[i-1].Quad.To
+ area += (pi.X - pj.X) * (pi.Y + pj.Y)
+ }
+ }
+ return area <= 0.0
+}
+
+func (qs StrokeQuads) reverse() StrokeQuads {
+ if len(qs) == 0 {
+ return nil
+ }
+
+ ps := make(StrokeQuads, 0, len(qs))
+ for i := range qs {
+ q := qs[len(qs)-1-i]
+ q.Quad.To, q.Quad.From = q.Quad.From, q.Quad.To
+ ps = append(ps, q)
+ }
+
+ return ps
+}
+
+func (qs StrokeQuads) append(ps StrokeQuads) StrokeQuads {
+ switch {
+ case len(ps) == 0:
+ return qs
+ case len(qs) == 0:
+ return ps
+ }
+
+ // Consolidate quads and smooth out rounding errors.
+ // We need to also check for the strokeTolerance to correctly handle
+ // join/cap points or on-purpose disjoint quads.
+ p0 := qs[len(qs)-1].Quad.To
+ p1 := ps[0].Quad.From
+ if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance {
+ qs = append(qs, StrokeQuad{
+ Quad: QuadSegment{
+ From: p0,
+ Ctrl: p0.Add(p1).Mul(0.5),
+ To: p1,
+ },
+ })
+ }
+ return append(qs, ps...)
+}
+
+func (q QuadSegment) Transform(t f32.Affine2D) QuadSegment {
+ q.From = t.Transform(q.From)
+ q.Ctrl = t.Transform(q.Ctrl)
+ q.To = t.Transform(q.To)
+ return q
+}
+
+// strokePathNorm returns the normal vector at t.
+func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
+ switch t {
+ case 0:
+ n := p1.Sub(p0)
+ if n.X == 0 && n.Y == 0 {
+ return f32.Point{}
+ }
+ n = rot90CW(n)
+ return normPt(n, d)
+ case 1:
+ n := p2.Sub(p1)
+ if n.X == 0 && n.Y == 0 {
+ return f32.Point{}
+ }
+ n = rot90CW(n)
+ return normPt(n, d)
+ }
+ panic("impossible")
+}
+
+func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) }
+func rot90CCW(p f32.Point) f32.Point { return f32.Pt(-p.Y, +p.X) }
+
+// cosPt returns the cosine of the opening angle between p and q.
+func cosPt(p, q f32.Point) float32 {
+ np := math.Hypot(float64(p.X), float64(p.Y))
+ nq := math.Hypot(float64(q.X), float64(q.Y))
+ return dotPt(p, q) / float32(np*nq)
+}
+
+func normPt(p f32.Point, l float32) f32.Point {
+ d := math.Hypot(float64(p.X), float64(p.Y))
+ l64 := float64(l)
+ if math.Abs(d-l64) < 1e-10 {
+ return f32.Point{}
+ }
+ n := float32(l64 / d)
+ return f32.Point{X: p.X * n, Y: p.Y * n}
+}
+
+func lenPt(p f32.Point) float32 {
+ return float32(math.Hypot(float64(p.X), float64(p.Y)))
+}
+
+func dotPt(p, q f32.Point) float32 {
+ return p.X*q.X + p.Y*q.Y
+}
+
+func perpDot(p, q f32.Point) float32 {
+ return p.X*q.Y - p.Y*q.X
+}
+
+// strokePathCurv returns the curvature at t, along the quadratic BĆ©zier
+// curve defined by the triplet (beg, ctl, end).
+func strokePathCurv(beg, ctl, end f32.Point, t float32) float32 {
+ var (
+ d1p = quadBezierD1(beg, ctl, end, t)
+ d2p = quadBezierD2(beg, ctl, end, t)
+
+ // Negative when bending right, ie: the curve is CW at this point.
+ a = float64(perpDot(d1p, d2p))
+ )
+
+ // We check early that the segment isn't too line-like and
+ // save a costly call to math.Pow that will be discarded by dividing
+ // with a too small 'a'.
+ if math.Abs(a) < 1e-10 {
+ return float32(math.NaN())
+ }
+ return float32(math.Pow(float64(d1p.X*d1p.X+d1p.Y*d1p.Y), 1.5) / a)
+}
+
+// quadBezierSample returns the point on the BĆ©zier curve at t.
+// B(t) = (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2
+func quadBezierSample(p0, p1, p2 f32.Point, t float32) f32.Point {
+ t1 := 1 - t
+ c0 := t1 * t1
+ c1 := 2 * t1 * t
+ c2 := t * t
+
+ o := p0.Mul(c0)
+ o = o.Add(p1.Mul(c1))
+ o = o.Add(p2.Mul(c2))
+ return o
+}
+
+// quadBezierD1 returns the first derivative of the BĆ©zier curve with respect to t.
+// B'(t) = 2(1-t)(P1 - P0) + 2t(P2 - P1)
+func quadBezierD1(p0, p1, p2 f32.Point, t float32) f32.Point {
+ p10 := p1.Sub(p0).Mul(2 * (1 - t))
+ p21 := p2.Sub(p1).Mul(2 * t)
+
+ return p10.Add(p21)
+}
+
+// quadBezierD2 returns the second derivative of the BĆ©zier curve with respect to t:
+// B''(t) = 2(P2 - 2P1 + P0)
+func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point {
+ p := p2.Sub(p1.Mul(2)).Add(p0)
+ return p.Mul(2)
+}
+
+// quadBezierLen returns the length of the BĆ©zier curve.
+// See:
+// https://malczak.linuxpl.com/blog/quadratic-bezier-curve-length/
+func quadBezierLen(p0, p1, p2 f32.Point) float32 {
+ a := p0.Sub(p1.Mul(2)).Add(p2)
+ b := p1.Mul(2).Sub(p0.Mul(2))
+ A := float64(4 * dotPt(a, a))
+ B := float64(4 * dotPt(a, b))
+ C := float64(dotPt(b, b))
+ if f64Eq(A, 0.0) {
+ // p1 is in the middle between p0 and p2,
+ // so it is a straight line from p0 to p2.
+ return lenPt(p2.Sub(p0))
+ }
+
+ Sabc := 2 * math.Sqrt(A+B+C)
+ A2 := math.Sqrt(A)
+ A32 := 2 * A * A2
+ C2 := 2 * math.Sqrt(C)
+ BA := B / A2
+ return float32((A32*Sabc + A2*B*(Sabc-C2) + (4*C*A-B*B)*math.Log((2*A2+BA+Sabc)/(BA+C2))) / (4 * A32))
+}
+
+func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads {
+ // Gio strokes are only quadratic BĆ©zier curves, w/o any inflection point.
+ // So we just have to flatten them.
+ var qs StrokeQuads
+ return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness)
+}
+
+// flattenQuadBezier splits a BĆ©zier quadratic curve into linear sub-segments,
+// themselves also encoded as BĆ©zier (degenerate, flat) quadratic curves.
+func flattenQuadBezier(qs StrokeQuads, p0, p1, p2 f32.Point,
+ d, flatness float32) StrokeQuads {
+ var (
+ t float32
+ flat64 = float64(flatness)
+ )
+ for t < 1 {
+ s2 := float64((p2.X-p0.X)*(p1.Y-p0.Y) - (p2.Y-p0.Y)*(p1.X-p0.X))
+ den := math.Hypot(float64(p1.X-p0.X), float64(p1.Y-p0.Y))
+ if s2*den == 0.0 {
+ break
+ }
+
+ s2 /= den
+ t = 2.0 * float32(math.Sqrt(flat64/3.0/math.Abs(s2)))
+ if t >= 1.0 {
+ break
+ }
+ var q0, q1, q2 f32.Point
+ q0, q1, q2, p0, p1, p2 = quadBezierSplit(p0, p1, p2, t)
+ qs.addLine(q0, q1, q2, 0, d)
+ }
+ qs.addLine(p0, p1, p2, 1, d)
+ return qs
+}
+
+func (qs *StrokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) {
+
+ switch i := len(*qs); i {
+ case 0:
+ p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d))
+ default:
+ // Address possible rounding errors and use previous point.
+ p0 = (*qs)[i-1].Quad.To
+ }
+
+ p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d))
+
+ *qs = append(*qs,
+ StrokeQuad{
+ Quad: QuadSegment{
+ From: p0,
+ Ctrl: p0.Add(p1).Mul(0.5),
+ To: p1,
+ },
+ },
+ )
+}
+
+// quadInterp returns the interpolated point at t.
+func quadInterp(p, q f32.Point, t float32) f32.Point {
+ return f32.Pt(
+ (1-t)*p.X+t*q.X,
+ (1-t)*p.Y+t*q.Y,
+ )
+}
+
+// quadBezierSplit returns the pair of triplets (from,ctrl,to) BĆ©zier curve,
+// split before (resp. after) the provided parametric t value.
+func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point,
+ f32.Point, f32.Point, f32.Point, f32.Point) {
+
+ var (
+ b0 = p0
+ b1 = quadInterp(p0, p1, t)
+ b2 = quadBezierSample(p0, p1, p2, t)
+
+ a0 = b2
+ a1 = quadInterp(p1, p2, t)
+ a2 = p2
+ )
+
+ return b0, b1, b2, a0, a1, a2
+}
+
+// strokePathJoin joins the two paths rhs and lhs, according to the provided
+// stroke operation.
+func strokePathJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32,
+ pivot, n0, n1 f32.Point, r0, r1 float32) {
+ if stroke.Miter > 0 {
+ strokePathMiterJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1)
+ return
+ }
+ switch stroke.Join {
+ case BevelJoin:
+ strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
+ case RoundJoin:
+ strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
+ default:
+ panic("impossible")
+ }
+}
+
+func strokePathBevelJoin(rhs, lhs *StrokeQuads, hw float32,
+ pivot, n0, n1 f32.Point, r0, r1 float32) {
+
+ rp := pivot.Add(n1)
+ lp := pivot.Sub(n1)
+
+ rhs.lineTo(rp)
+ lhs.lineTo(lp)
+}
+
+func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32,
+ pivot, n0, n1 f32.Point, r0, r1 float32) {
+ rp := pivot.Add(n1)
+ lp := pivot.Sub(n1)
+ cw := dotPt(rot90CW(n0), n1) >= 0.0
+ switch {
+ case cw:
+ // Path bends to the right, ie. CW (or 180 degree turn).
+ c := pivot.Sub(lhs.pen())
+ angle := -math.Acos(float64(cosPt(n0, n1)))
+ lhs.arc(c, c, float32(angle))
+ lhs.lineTo(lp) // Add a line to accommodate for rounding errors.
+ rhs.lineTo(rp)
+ default:
+ // Path bends to the left, ie. CCW.
+ angle := math.Acos(float64(cosPt(n0, n1)))
+ c := pivot.Sub(rhs.pen())
+ rhs.arc(c, c, float32(angle))
+ rhs.lineTo(rp) // Add a line to accommodate for rounding errors.
+ lhs.lineTo(lp)
+ }
+}
+
+func strokePathMiterJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32,
+ pivot, n0, n1 f32.Point, r0, r1 float32) {
+ if n0 == n1.Mul(-1) {
+ strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
+ return
+ }
+
+ // This is to handle nearly linear joints that would be clipped otherwise.
+ limit := math.Max(float64(stroke.Miter), 1.001)
+
+ cw := dotPt(rot90CW(n0), n1) >= 0.0
+ if cw {
+ // hw is used to calculate |R|.
+ // When running CW, n0 and n1 point the other way,
+ // so the sign of r0 and r1 is negated.
+ hw = -hw
+ }
+ hw64 := float64(hw)
+
+ cos := math.Sqrt(0.5 * (1 + float64(cosPt(n0, n1))))
+ d := hw64 / cos
+ if math.Abs(limit*hw64) < math.Abs(d) {
+ stroke.Miter = 0 // Set miter to zero to disable the miter joint.
+ strokePathJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1)
+ return
+ }
+ mid := pivot.Add(normPt(n0.Add(n1), float32(d)))
+
+ rp := pivot.Add(n1)
+ lp := pivot.Sub(n1)
+ switch {
+ case cw:
+ // Path bends to the right, ie. CW.
+ lhs.lineTo(mid)
+ default:
+ // Path bends to the left, ie. CCW.
+ rhs.lineTo(mid)
+ }
+ rhs.lineTo(rp)
+ lhs.lineTo(lp)
+}
+
+// strokePathCap caps the provided path qs, according to the provided stroke operation.
+func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32,
+ pivot, n0 f32.Point) {
+ switch stroke.Cap {
+ case FlatCap:
+ strokePathFlatCap(qs, hw, pivot, n0)
+ case SquareCap:
+ strokePathSquareCap(qs, hw, pivot, n0)
+ case RoundCap:
+ strokePathRoundCap(qs, hw, pivot, n0)
+ default:
+ panic("impossible")
+ }
+}
+
+// strokePathFlatCap caps the start or end of a path with a flat cap.
+func strokePathFlatCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
+ end := pivot.Sub(n0)
+ qs.lineTo(end)
+}
+
+// strokePathSquareCap caps the start or end of a path with a square cap.
+func strokePathSquareCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
+ var (
+ e = pivot.Add(rot90CCW(n0))
+ corner1 = e.Add(n0)
+ corner2 = e.Sub(n0)
+ end = pivot.Sub(n0)
+ )
+
+ qs.lineTo(corner1)
+ qs.lineTo(corner2)
+ qs.lineTo(end)
+}
+
+// strokePathRoundCap caps the start or end of a path with a round cap.
+func strokePathRoundCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
+ c := pivot.Sub(qs.pen())
+ qs.arc(c, c, math.Pi)
+}
+
+// ArcTransform computes a transformation that can be used for generating quadratic bƩzier
+// curve approximations for an arc.
+//
+// The math is extracted from the following paper:
+// "Drawing an elliptical arc using polylines, quadratic or
+// cubic Bezier curves", L. Maisonobe
+// An electronic version may be found at:
+// http://spaceroots.org/documents/ellipse/elliptical-arc.pdf
+func ArcTransform(p, f1, f2 f32.Point, angle float32,
+ segments int) f32.Affine2D {
+ c := f32.Point{
+ X: 0.5 * (f1.X + f2.X),
+ Y: 0.5 * (f1.Y + f2.Y),
+ }
+
+ // semi-major axis: 2a = |PF1| + |PF2|
+ a := 0.5 * (dist(f1, p) + dist(f2, p))
+
+ // semi-minor axis: c^2 = a^2+b^2 (c: focal distance)
+ f := dist(f1, c)
+ b := math.Sqrt(a*a - f*f)
+
+ var rx, ry, alpha, start float64
+ switch {
+ case a > b:
+ rx = a
+ ry = b
+ default:
+ rx = b
+ ry = a
+ }
+
+ var x float64
+ switch {
+ case f1 == c || f2 == c:
+ // degenerate case of a circle.
+ alpha = 0
+ default:
+ switch {
+ case f1.X > c.X:
+ x = float64(f1.X - c.X)
+ alpha = math.Acos(x / f)
+ case f1.X < c.X:
+ x = float64(f2.X - c.X)
+ alpha = math.Acos(x / f)
+ case f1.X == c.X:
+ // special case of a "vertical" ellipse.
+ alpha = math.Pi / 2
+ if f1.Y < c.Y {
+ alpha = -alpha
+ }
+ }
+ }
+
+ start = math.Acos(float64(p.X-c.X) / dist(c, p))
+ if c.Y > p.Y {
+ start = -start
+ }
+ start -= alpha
+
+ var (
+ Īø = angle / float32(segments)
+ ref f32.Affine2D // transform from absolute frame to ellipse-based one
+ rot f32.Affine2D // rotation matrix for each segment
+ inv f32.Affine2D // transform from ellipse-based frame to absolute one
+ )
+ ref = ref.Offset(f32.Point{}.Sub(c))
+ ref = ref.Rotate(f32.Point{}, float32(-alpha))
+ ref = ref.Scale(f32.Point{}, f32.Point{
+ X: float32(1 / rx),
+ Y: float32(1 / ry),
+ })
+ inv = ref.Invert()
+ rot = rot.Rotate(f32.Point{}, float32(0.5*Īø))
+
+ // Instead of invoking math.Sincos for every segment, compute a rotation
+ // matrix once and apply for each segment.
+ // Before applying the rotation matrix rot, transform the coordinates
+ // to a frame centered to the ellipse (and warped into a unit circle), then rotate.
+ // Finally, transform back into the original frame.
+ return inv.Mul(rot).Mul(ref)
+}
+
+func dist(p1, p2 f32.Point) float64 {
+ var (
+ x1 = float64(p1.X)
+ y1 = float64(p1.Y)
+ x2 = float64(p2.X)
+ y2 = float64(p2.Y)
+ dx = x2 - x1
+ dy = y2 - y1
+ )
+ return math.Hypot(dx, dy)
+}
+
+func StrokePathCommands(style StrokeStyle, dashes DashOp,
+ scene []byte) StrokeQuads {
+ quads := decodeToStrokeQuads(scene)
+ return quads.stroke(style, dashes)
+}
+
+// decodeToStrokeQuads decodes scene commands to quads ready to stroke.
+func decodeToStrokeQuads(pathData []byte) StrokeQuads {
+ quads := make(StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4))
+ for len(pathData) >= scene.CommandSize+4 {
+ contour := binary.LittleEndian.Uint32(pathData)
+ cmd := ops.DecodeCommand(pathData[4:])
+ switch cmd.Op() {
+ case scene.OpLine:
+ var q QuadSegment
+ q.From, q.To = scene.DecodeLine(cmd)
+ q.Ctrl = q.From.Add(q.To).Mul(.5)
+ quad := StrokeQuad{
+ Contour: contour,
+ Quad: q,
+ }
+ quads = append(quads, quad)
+ case scene.OpQuad:
+ var q QuadSegment
+ q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
+ quad := StrokeQuad{
+ Contour: contour,
+ Quad: q,
+ }
+ quads = append(quads, quad)
+ case scene.OpCubic:
+ for _, q := range SplitCubic(scene.DecodeCubic(cmd)) {
+ quad := StrokeQuad{
+ Contour: contour,
+ Quad: q,
+ }
+ quads = append(quads, quad)
+ }
+ default:
+ panic("unsupported scene command")
+ }
+ pathData = pathData[scene.CommandSize+4:]
+ }
+ return quads
+}
+
+func SplitCubic(from, ctrl0, ctrl1, to f32.Point) []QuadSegment {
+ quads := make([]QuadSegment, 0, 10)
+ // Set the maximum distance proportionally to the longest side
+ // of the bounding rectangle.
+ hull := f32.Rectangle{
+ Min: from,
+ Max: ctrl0,
+ }.Canon().Add(ctrl1).Add(to)
+ l := hull.Dx()
+ if h := hull.Dy(); h > l {
+ l = h
+ }
+ approxCubeTo(&quads, 0, l*0.001, from, ctrl0, ctrl1, to)
+ return quads
+}
+
+// approxCubeTo approximates a cubic BĆ©zier by a series of quadratic
+// curves.
+func approxCubeTo(quads *[]QuadSegment, splits int, maxDist float32,
+ from, ctrl0, ctrl1, to f32.Point) int {
+ // The idea is from
+ // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html
+ // where a quadratic approximates a cubic by eliminating its tĀ³ term
+ // from its polynomial expression anchored at the starting point:
+ //
+ // P(t) = pen + 3t(ctrl0 - pen) + 3tĀ²(ctrl1 - 2ctrl0 + pen) + tĀ³(to - 3ctrl1 + 3ctrl0 - pen)
+ //
+ // The control point for the new quadratic Q1 that shares starting point, pen, with P is
+ //
+ // C1 = (3ctrl0 - pen)/2
+ //
+ // The reverse cubic anchored at the end point has the polynomial
+ //
+ // P'(t) = to + 3t(ctrl1 - to) + 3tĀ²(ctrl0 - 2ctrl1 + to) + tĀ³(pen - 3ctrl0 + 3ctrl1 - to)
+ //
+ // The corresponding quadratic Q2 that shares the end point, to, with P has control
+ // point
+ //
+ // C2 = (3ctrl1 - to)/2
+ //
+ // The combined quadratic BĆ©zier, Q, shares both start and end points with its cubic
+ // and use the midpoint between the two curves Q1 and Q2 as control point:
+ //
+ // C = (3ctrl0 - pen + 3ctrl1 - to)/4
+ c := ctrl0.Mul(3).Sub(from).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0)
+ const maxSplits = 32
+ if splits >= maxSplits {
+ *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to})
+ return splits
+ }
+ // The maximum distance between the cubic P and its approximation Q given t
+ // can be shown to be
+ //
+ // d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen|
+ //
+ // To save a square root, compare dĀ² with the squared tolerance.
+ v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(from)
+ d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36)
+ if d2 <= maxDist*maxDist {
+ *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to})
+ return splits
+ }
+ // De Casteljau split the curve and approximate the halves.
+ t := float32(0.5)
+ c0 := from.Add(ctrl0.Sub(from).Mul(t))
+ c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t))
+ c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t))
+ c01 := c0.Add(c1.Sub(c0).Mul(t))
+ c12 := c1.Add(c2.Sub(c1).Mul(t))
+ c0112 := c01.Add(c12.Sub(c01).Mul(t))
+ splits++
+ splits = approxCubeTo(quads, splits, maxDist, from, c0, c01, c0112)
+ splits = approxCubeTo(quads, splits, maxDist, c0112, c12, c2, to)
+ return splits
+}
diff --git a/gio/giold/io/clipboard/clipboard.go b/gio/giold/io/clipboard/clipboard.go
new file mode 100644
index 0000000..3d1e64c
--- /dev/null
+++ b/gio/giold/io/clipboard/clipboard.go
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package clipboard
+
+import (
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/op"
+)
+
+// Event is generated when the clipboard content is requested.
+type Event struct {
+ Text string
+}
+
+// ReadOp requests the text of the clipboard, delivered to
+// the current handler through an Event.
+type ReadOp struct {
+ Tag event.Tag
+}
+
+// WriteOp copies Text to the clipboard.
+type WriteOp struct {
+ Text string
+}
+
+func (h ReadOp) Add(o *op.Ops) {
+ data := o.Write1(opconst.TypeClipboardReadLen, h.Tag)
+ data[0] = byte(opconst.TypeClipboardRead)
+}
+
+func (h WriteOp) Add(o *op.Ops) {
+ data := o.Write1(opconst.TypeClipboardWriteLen, &h.Text)
+ data[0] = byte(opconst.TypeClipboardWrite)
+}
+
+func (Event) ImplementsEvent() {}
diff --git a/gio/giold/io/event/event.go b/gio/giold/io/event/event.go
new file mode 100644
index 0000000..998dccb
--- /dev/null
+++ b/gio/giold/io/event/event.go
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package event contains the types for event handling.
+
+The Queue interface is the protocol for receiving external events.
+
+For example:
+
+ var queue event.Queue = ...
+
+ for _, e := range queue.Events(h) {
+ switch e.(type) {
+ ...
+ }
+ }
+
+In general, handlers must be declared before events become
+available. Other packages such as pointer and key provide
+the means for declaring handlers for specific event types.
+
+The following example declares a handler ready for key input:
+
+ import realy.lol/gio/io/key
+
+ ops := new(op.Ops)
+ var h *Handler = ...
+ key.InputOp{Tag: h}.Add(ops)
+
+*/
+package event
+
+// Queue maps an event handler key to the events
+// available to the handler.
+type Queue interface {
+ // Events returns the available events for an
+ // event handler tag.
+ Events(t Tag) []Event
+}
+
+// Tag is the stable identifier for an event handler.
+// For a handler h, the tag is typically &h.
+type Tag interface{}
+
+// Event is the marker interface for events.
+type Event interface {
+ ImplementsEvent()
+}
diff --git a/gio/giold/io/key/key.go b/gio/giold/io/key/key.go
new file mode 100644
index 0000000..913dbb2
--- /dev/null
+++ b/gio/giold/io/key/key.go
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package key implements key and text events and operations.
+
+The InputOp operations is used for declaring key input handlers. Use
+an implementation of the Queue interface from package ui to receive
+events.
+*/
+package key
+
+import (
+ "fmt"
+ "strings"
+
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/op"
+)
+
+// InputOp declares a handler ready for key events.
+// Key events are in general only delivered to the
+// focused key handler.
+type InputOp struct {
+ Tag event.Tag
+}
+
+// SoftKeyboardOp shows or hide the on-screen keyboard, if available.
+// It replaces any previous SoftKeyboardOp.
+type SoftKeyboardOp struct {
+ Show bool
+}
+
+// FocusOp sets or clears the keyboard focus. It replaces any previous
+// FocusOp in the same frame.
+type FocusOp struct {
+ // Tag is the new focus. The focus is cleared if Tag is nil, or if Tag
+ // has no InputOp in the same frame.
+ Tag event.Tag
+}
+
+// A FocusEvent is generated when a handler gains or loses
+// focus.
+type FocusEvent struct {
+ Focus bool
+}
+
+// An Event is generated when a key is pressed. For text input
+// use EditEvent.
+type Event struct {
+ // Name of the key. For letters, the upper case form is used, via
+ // unicode.ToUpper. The shift modifier is taken into account, all other
+ // modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1"
+ // combinations both give the Name "!" with the US keyboard layout.
+ Name string
+ // Modifiers is the set of active modifiers when the key was pressed.
+ Modifiers Modifiers
+ // State is the state of the key when the event was fired.
+ State State
+}
+
+// An EditEvent is generated when text is input.
+type EditEvent struct {
+ Text string
+}
+
+// State is the state of a key during an event.
+type State uint8
+
+const (
+ // Press is the state of a pressed key.
+ Press State = iota
+ // Release is the state of a key that has been released.
+ //
+ // Note: release events are only implemented on the following platforms:
+ // macOS, Linux, Windows, WebAssembly.
+ Release
+)
+
+// Modifiers
+type Modifiers uint32
+
+const (
+ // ModCtrl is the ctrl modifier key.
+ ModCtrl Modifiers = 1 << iota
+ // ModCommand is the command modifier key
+ // found on Apple keyboards.
+ ModCommand
+ // ModShift is the shift modifier key.
+ ModShift
+ // ModAlt is the alt modifier key, or the option
+ // key on Apple keyboards.
+ ModAlt
+ // ModSuper is the "logo" modifier key, often
+ // represented by a Windows logo.
+ ModSuper
+)
+
+const (
+ // Names for special keys.
+ NameLeftArrow = "ā"
+ NameRightArrow = "ā"
+ NameUpArrow = "ā"
+ NameDownArrow = "ā"
+ NameReturn = "ā"
+ NameEnter = "ā¤"
+ NameEscape = "ā"
+ NameHome = "ā±"
+ NameEnd = "ā²"
+ NameDeleteBackward = "ā«"
+ NameDeleteForward = "ā¦"
+ NamePageUp = "ā"
+ NamePageDown = "ā"
+ NameTab = "ā„"
+ NameSpace = "Space"
+)
+
+// Contain reports whether m contains all modifiers
+// in m2.
+func (m Modifiers) Contain(m2 Modifiers) bool {
+ return m&m2 == m2
+}
+
+func (h InputOp) Add(o *op.Ops) {
+ if h.Tag == nil {
+ panic("Tag must be non-nil")
+ }
+ data := o.Write1(opconst.TypeKeyInputLen, h.Tag)
+ data[0] = byte(opconst.TypeKeyInput)
+}
+
+func (h SoftKeyboardOp) Add(o *op.Ops) {
+ data := o.Write(opconst.TypeKeySoftKeyboardLen)
+ data[0] = byte(opconst.TypeKeySoftKeyboard)
+ if h.Show {
+ data[1] = 1
+ }
+}
+
+func (h FocusOp) Add(o *op.Ops) {
+ data := o.Write1(opconst.TypeKeyFocusLen, h.Tag)
+ data[0] = byte(opconst.TypeKeyFocus)
+}
+
+func (EditEvent) ImplementsEvent() {}
+func (Event) ImplementsEvent() {}
+func (FocusEvent) ImplementsEvent() {}
+
+func (e Event) String() string {
+ return fmt.Sprintf("%v %v %v}", e.Name, e.Modifiers, e.State)
+}
+
+func (m Modifiers) String() string {
+ var strs []string
+ if m.Contain(ModCtrl) {
+ strs = append(strs, "ModCtrl")
+ }
+ if m.Contain(ModCommand) {
+ strs = append(strs, "ModCommand")
+ }
+ if m.Contain(ModShift) {
+ strs = append(strs, "ModShift")
+ }
+ if m.Contain(ModAlt) {
+ strs = append(strs, "ModAlt")
+ }
+ if m.Contain(ModSuper) {
+ strs = append(strs, "ModSuper")
+ }
+ return strings.Join(strs, "|")
+}
+
+func (s State) String() string {
+ switch s {
+ case Press:
+ return "Press"
+ case Release:
+ return "Release"
+ default:
+ panic("invalid State")
+ }
+}
diff --git a/gio/giold/io/key/mod.go b/gio/giold/io/key/mod.go
new file mode 100644
index 0000000..c5db56c
--- /dev/null
+++ b/gio/giold/io/key/mod.go
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build !darwin
+
+package key
+
+// ModShortcut is the platform's shortcut modifier, usually the Ctrl
+// key. On Apple platforms it is the Cmd key.
+const ModShortcut = ModCtrl
diff --git a/gio/giold/io/key/mod_darwin.go b/gio/giold/io/key/mod_darwin.go
new file mode 100644
index 0000000..c0f1437
--- /dev/null
+++ b/gio/giold/io/key/mod_darwin.go
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package key
+
+// ModShortcut is the platform's shortcut modifier, usually the Ctrl
+// key. On Apple platforms it is the Cmd key.
+const ModShortcut = ModCommand
diff --git a/gio/giold/io/pointer/doc.go b/gio/giold/io/pointer/doc.go
new file mode 100644
index 0000000..7243b94
--- /dev/null
+++ b/gio/giold/io/pointer/doc.go
@@ -0,0 +1,131 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package pointer implements pointer events and operations.
+A pointer is either a mouse controlled cursor or a touch
+object such as a finger.
+
+The InputOp operation is used to declare a handler ready for pointer
+events. Use an event.Queue to receive events.
+
+Types
+
+Only events that match a specified list of types are delivered to a handler.
+
+For example, to receive Press, Drag, and Release events (but not Move, Enter,
+Leave, or Scroll):
+
+ var ops op.Ops
+ var h *Handler = ...
+
+ pointer.InputOp{
+ Tag: h,
+ Types: pointer.Press | pointer.Drag | pointer.Release,
+ }.Add(ops)
+
+Cancel events are always delivered.
+
+Areas
+
+The area operations are used for specifying the area where
+subsequent InputOp are active.
+
+For example, to set up a rectangular hit area:
+
+ r := image.Rectangle{...}
+ pointer.Rect(r).Add(ops)
+ pointer.InputOp{Tag: h}.Add(ops)
+
+Note that areas compound: the effective area of multiple area
+operations is the intersection of the areas.
+
+Matching events
+
+StackOp operations and input handlers form an implicit tree.
+Each stack operation is a node, and each input handler is associated
+with the most recent node.
+
+For example:
+
+ ops := new(op.Ops)
+ var stack op.StackOp
+ var h1, h2 *Handler
+
+ state := op.Save(ops)
+ pointer.InputOp{Tag: h1}.Add(Ops)
+ state.Load()
+
+ state = op.Save(ops)
+ pointer.InputOp{Tag: h2}.Add(ops)
+ state.Load()
+
+implies a tree of two inner nodes, each with one pointer handler.
+
+When determining which handlers match an Event, only handlers whose
+areas contain the event position are considered. The matching
+proceeds as follows.
+
+First, the foremost matching handler is included. If the handler
+has pass-through enabled, this step is repeated.
+
+Then, all matching handlers from the current node and all parent
+nodes are included.
+
+In the example above, all events will go to h2 only even though both
+handlers have the same area (the entire screen).
+
+Pass-through
+
+The PassOp operations controls the pass-through setting. A handler's
+pass-through setting is recorded along with the InputOp.
+
+Pass-through handlers are useful for overlay widgets such as a hidden
+side drawer. When the user touches the side, both the (transparent)
+drawer handle and the interface below should receive pointer events.
+
+Disambiguation
+
+When more than one handler matches a pointer event, the event queue
+follows a set of rules for distributing the event.
+
+As long as the pointer has not received a Press event, all
+matching handlers receive all events.
+
+When a pointer is pressed, the set of matching handlers is
+recorded. The set is not updated according to the pointer position
+and hit areas. Rather, handlers stay in the matching set until they
+no longer appear in a InputOp or when another handler in the set
+grabs the pointer.
+
+A handler can exclude all other handler from its matching sets
+by setting the Grab flag in its InputOp. The Grab flag is sticky
+and stays in effect until the handler no longer appears in any
+matching sets.
+
+The losing handlers are notified by a Cancel event.
+
+For multiple grabbing handlers, the foremost handler wins.
+
+Priorities
+
+Handlers know their position in a matching set of a pointer through
+event priorities. The Shared priority is for matching sets with
+multiple handlers; the Grabbed priority indicate exclusive access.
+
+Priorities are useful for deferred gesture matching.
+
+Consider a scrollable list of clickable elements. When the user touches an
+element, it is unknown whether the gesture is a click on the element
+or a drag (scroll) of the list. While the click handler might light up
+the element in anticipation of a click, the scrolling handler does not
+scroll on finger movements with lower than Grabbed priority.
+
+Should the user release the finger, the click handler registers a click.
+
+However, if the finger moves beyond a threshold, the scrolling handler
+determines that the gesture is a drag and sets its Grab flag. The
+click handler receives a Cancel (removing the highlight) and further
+movements for the scroll handler has priority Grabbed, scrolling the
+list.
+*/
+package pointer
diff --git a/gio/giold/io/pointer/pointer.go b/gio/giold/io/pointer/pointer.go
new file mode 100644
index 0000000..f3aafae
--- /dev/null
+++ b/gio/giold/io/pointer/pointer.go
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package pointer
+
+import (
+ "encoding/binary"
+ "fmt"
+ "image"
+ "strings"
+ "time"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/op"
+)
+
+// Event is a pointer event.
+type Event struct {
+ Type Type
+ Source Source
+ // PointerID is the id for the pointer and can be used
+ // to track a particular pointer from Press to
+ // Release or Cancel.
+ PointerID ID
+ // Priority is the priority of the receiving handler
+ // for this event.
+ Priority Priority
+ // Time is when the event was received. The
+ // timestamp is relative to an undefined base.
+ Time time.Duration
+ // Buttons are the set of pressed mouse buttons for this event.
+ Buttons Buttons
+ // Position is the position of the event, relative to
+ // the current transformation, as set by op.TransformOp.
+ Position f32.Point
+ // Scroll is the scroll amount, if any.
+ Scroll f32.Point
+ // Modifiers is the set of active modifiers when
+ // the mouse button was pressed.
+ Modifiers key.Modifiers
+}
+
+// AreaOp updates the hit area to the intersection of the current
+// hit area and the area. The area is transformed before applying
+// it.
+type AreaOp struct {
+ kind areaKind
+ rect image.Rectangle
+}
+
+// CursorNameOp sets the cursor for the current area.
+type CursorNameOp struct {
+ Name CursorName
+}
+
+// InputOp declares an input handler ready for pointer
+// events.
+type InputOp struct {
+ Tag event.Tag
+ // Grab, if set, request that the handler get
+ // Grabbed priority.
+ Grab bool
+ // Types is a bitwise-or of event types to receive.
+ Types Type
+ // ScrollBounds describe the maximum scrollable distances in both
+ // axes. Specifically, any Event e delivered to Tag will satisfy
+ //
+ // ScrollBounds.Min.X <= e.Scroll.X <= ScrollBounds.Max.X (horizontal axis)
+ // ScrollBounds.Min.Y <= e.Scroll.Y <= ScrollBounds.Max.Y (vertical axis)
+ ScrollBounds image.Rectangle
+}
+
+// PassOp sets the pass-through mode.
+type PassOp struct {
+ Pass bool
+}
+
+type ID uint16
+
+// Type of an Event.
+type Type uint8
+
+// Priority of an Event.
+type Priority uint8
+
+// Source of an Event.
+type Source uint8
+
+// Buttons is a set of mouse buttons
+type Buttons uint8
+
+// CursorName is the name of a cursor.
+type CursorName string
+
+// Must match app/internal/input.areaKind
+type areaKind uint8
+
+const (
+ // CursorDefault is the default cursor.
+ CursorDefault CursorName = ""
+ // CursorText is the cursor for text.
+ CursorText CursorName = "text"
+ // CursorPointer is the cursor for a link.
+ CursorPointer CursorName = "pointer"
+ // CursorCrossHair is the cursor for precise location.
+ CursorCrossHair CursorName = "crosshair"
+ // CursorColResize is the cursor for vertical resize.
+ CursorColResize CursorName = "col-resize"
+ // CursorRowResize is the cursor for horizontal resize.
+ CursorRowResize CursorName = "row-resize"
+ // CursorGrab is the cursor for moving object in any direction.
+ CursorGrab CursorName = "grab"
+ // CursorNone hides the cursor. To show it again, use any other cursor.
+ CursorNone CursorName = "none"
+)
+
+const (
+ // A Cancel event is generated when the current gesture is
+ // interrupted by other handlers or the system.
+ Cancel Type = (1 << iota) >> 1
+ // Press of a pointer.
+ Press
+ // Release of a pointer.
+ Release
+ // Move of a pointer.
+ Move
+ // Drag of a pointer.
+ Drag
+ // Pointer enters an area watching for pointer input
+ Enter
+ // Pointer leaves an area watching for pointer input
+ Leave
+ // Scroll of a pointer.
+ Scroll
+)
+
+const (
+ // Mouse generated event.
+ Mouse Source = iota
+ // Touch generated event.
+ Touch
+)
+
+const (
+ // Shared priority is for handlers that
+ // are part of a matching set larger than 1.
+ Shared Priority = iota
+ // Foremost priority is like Shared, but the
+ // handler is the foremost of the matching set.
+ Foremost
+ // Grabbed is used for matching sets of size 1.
+ Grabbed
+)
+
+const (
+ // ButtonPrimary is the primary button, usually the left button for a
+ // right-handed user.
+ ButtonPrimary Buttons = 1 << iota
+ // ButtonSecondary is the secondary button, usually the right button for a
+ // right-handed user.
+ ButtonSecondary
+ // ButtonTertiary is the tertiary button, usually the middle button.
+ ButtonTertiary
+)
+
+const (
+ areaRect areaKind = iota
+ areaEllipse
+)
+
+// Rect constructs a rectangular hit area.
+func Rect(size image.Rectangle) AreaOp {
+ return AreaOp{
+ kind: areaRect,
+ rect: size,
+ }
+}
+
+// Ellipse constructs an ellipsoid hit area.
+func Ellipse(size image.Rectangle) AreaOp {
+ return AreaOp{
+ kind: areaEllipse,
+ rect: size,
+ }
+}
+
+func (op AreaOp) Add(o *op.Ops) {
+ data := o.Write(opconst.TypeAreaLen)
+ data[0] = byte(opconst.TypeArea)
+ data[1] = byte(op.kind)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[2:], uint32(op.rect.Min.X))
+ bo.PutUint32(data[6:], uint32(op.rect.Min.Y))
+ bo.PutUint32(data[10:], uint32(op.rect.Max.X))
+ bo.PutUint32(data[14:], uint32(op.rect.Max.Y))
+}
+
+func (op CursorNameOp) Add(o *op.Ops) {
+ data := o.Write1(opconst.TypeCursorLen, op.Name)
+ data[0] = byte(opconst.TypeCursor)
+}
+
+// Add panics if the scroll range does not contain zero.
+func (op InputOp) Add(o *op.Ops) {
+ if op.Tag == nil {
+ panic("Tag must be non-nil")
+ }
+ if b := op.ScrollBounds; b.Min.X > 0 || b.Max.X < 0 || b.Min.Y > 0 || b.Max.Y < 0 {
+ panic(fmt.Errorf("invalid scroll range value %v", b))
+ }
+ data := o.Write1(opconst.TypePointerInputLen, op.Tag)
+ data[0] = byte(opconst.TypePointerInput)
+ if op.Grab {
+ data[1] = 1
+ }
+ data[2] = byte(op.Types)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[3:], uint32(op.ScrollBounds.Min.X))
+ bo.PutUint32(data[7:], uint32(op.ScrollBounds.Min.Y))
+ bo.PutUint32(data[11:], uint32(op.ScrollBounds.Max.X))
+ bo.PutUint32(data[15:], uint32(op.ScrollBounds.Max.Y))
+}
+
+func (op PassOp) Add(o *op.Ops) {
+ data := o.Write(opconst.TypePassLen)
+ data[0] = byte(opconst.TypePass)
+ if op.Pass {
+ data[1] = 1
+ }
+}
+
+func (t Type) String() string {
+ switch t {
+ case Press:
+ return "Press"
+ case Release:
+ return "Release"
+ case Cancel:
+ return "Cancel"
+ case Move:
+ return "Move"
+ case Drag:
+ return "Drag"
+ case Enter:
+ return "Enter"
+ case Leave:
+ return "Leave"
+ case Scroll:
+ return "Scroll"
+ default:
+ panic("unknown Type")
+ }
+}
+
+func (p Priority) String() string {
+ switch p {
+ case Shared:
+ return "Shared"
+ case Foremost:
+ return "Foremost"
+ case Grabbed:
+ return "Grabbed"
+ default:
+ panic("unknown priority")
+ }
+}
+
+func (s Source) String() string {
+ switch s {
+ case Mouse:
+ return "Mouse"
+ case Touch:
+ return "Touch"
+ default:
+ panic("unknown source")
+ }
+}
+
+// Contain reports whether the set b contains
+// all of the buttons.
+func (b Buttons) Contain(buttons Buttons) bool {
+ return b&buttons == buttons
+}
+
+func (b Buttons) String() string {
+ var strs []string
+ if b.Contain(ButtonPrimary) {
+ strs = append(strs, "ButtonPrimary")
+ }
+ if b.Contain(ButtonSecondary) {
+ strs = append(strs, "ButtonSecondary")
+ }
+ if b.Contain(ButtonTertiary) {
+ strs = append(strs, "ButtonTertiary")
+ }
+ return strings.Join(strs, "|")
+}
+
+func (c CursorName) String() string {
+ if c == CursorDefault {
+ return "default"
+ }
+ return string(c)
+}
+
+func (Event) ImplementsEvent() {}
diff --git a/gio/giold/io/profile/profile.go b/gio/giold/io/profile/profile.go
new file mode 100644
index 0000000..58be154
--- /dev/null
+++ b/gio/giold/io/profile/profile.go
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package profiles provides access to rendering
+// profiles.
+package profile
+
+import (
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/op"
+)
+
+// Op registers a handler for receiving
+// Events.
+type Op struct {
+ Tag event.Tag
+}
+
+// Event contains profile data from a single
+// rendered frame.
+type Event struct {
+ // Timings. Very likely to change.
+ Timings string
+}
+
+func (p Op) Add(o *op.Ops) {
+ data := o.Write1(opconst.TypeProfileLen, p.Tag)
+ data[0] = byte(opconst.TypeProfile)
+}
+
+func (p Event) ImplementsEvent() {}
diff --git a/gio/giold/io/router/clipboard.go b/gio/giold/io/router/clipboard.go
new file mode 100644
index 0000000..122c9bc
--- /dev/null
+++ b/gio/giold/io/router/clipboard.go
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package router
+
+import (
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/io/event"
+)
+
+type clipboardQueue struct {
+ receivers map[event.Tag]struct{}
+ // request avoid read clipboard every frame while waiting.
+ requested bool
+ text *string
+ reader ops.Reader
+}
+
+// WriteClipboard returns the most recent text to be copied
+// to the clipboard, if any.
+func (q *clipboardQueue) WriteClipboard() (string, bool) {
+ if q.text == nil {
+ return "", false
+ }
+ text := *q.text
+ q.text = nil
+ return text, true
+}
+
+// ReadClipboard reports if any new handler is waiting
+// to read the clipboard.
+func (q *clipboardQueue) ReadClipboard() bool {
+ if len(q.receivers) <= 0 || q.requested {
+ return false
+ }
+ q.requested = true
+ return true
+}
+
+func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) {
+ for r := range q.receivers {
+ events.Add(r, e)
+ delete(q.receivers, r)
+ }
+}
+
+func (q *clipboardQueue) ProcessWriteClipboard(d []byte, refs []interface{}) {
+ if opconst.OpType(d[0]) != opconst.TypeClipboardWrite {
+ panic("invalid op")
+ }
+ q.text = refs[0].(*string)
+}
+
+func (q *clipboardQueue) ProcessReadClipboard(d []byte, refs []interface{}) {
+ if opconst.OpType(d[0]) != opconst.TypeClipboardRead {
+ panic("invalid op")
+ }
+ if q.receivers == nil {
+ q.receivers = make(map[event.Tag]struct{})
+ }
+ tag := refs[0].(event.Tag)
+ if _, ok := q.receivers[tag]; !ok {
+ q.receivers[tag] = struct{}{}
+ q.requested = false
+ }
+}
diff --git a/gio/giold/io/router/clipboard_test.go b/gio/giold/io/router/clipboard_test.go
new file mode 100644
index 0000000..ac5ebe7
--- /dev/null
+++ b/gio/giold/io/router/clipboard_test.go
@@ -0,0 +1,155 @@
+package router
+
+import (
+ "testing"
+
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/op"
+)
+
+func TestClipboardDuplicateEvent(t *testing.T) {
+ ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
+
+ // Both must receive the event once
+ clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
+ clipboard.ReadOp{Tag: &handler[1]}.Add(ops)
+
+ router.Frame(ops)
+ event := clipboard.Event{Text: "Test"}
+ router.Queue(event)
+ assertClipboardReadOp(t, router, 0)
+ assertClipboardEvent(t, router.Events(&handler[0]), true)
+ assertClipboardEvent(t, router.Events(&handler[1]), true)
+ ops.Reset()
+
+ // No ReadOp
+
+ router.Frame(ops)
+ assertClipboardReadOp(t, router, 0)
+ assertClipboardEvent(t, router.Events(&handler[0]), false)
+ assertClipboardEvent(t, router.Events(&handler[1]), false)
+ ops.Reset()
+
+ clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
+
+ router.Frame(ops)
+ // No ClipboardEvent sent
+ assertClipboardReadOp(t, router, 1)
+ assertClipboardEvent(t, router.Events(&handler[0]), false)
+ assertClipboardEvent(t, router.Events(&handler[1]), false)
+ ops.Reset()
+}
+
+func TestQueueProcessReadClipboard(t *testing.T) {
+ ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
+ ops.Reset()
+
+ // Request read
+ clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
+
+ router.Frame(ops)
+ assertClipboardReadOp(t, router, 1)
+ ops.Reset()
+
+ for i := 0; i < 3; i++ {
+ // No ReadOp
+ // One receiver must still wait for response
+
+ router.Frame(ops)
+ assertClipboardReadOpDuplicated(t, router, 1)
+ ops.Reset()
+ }
+
+ router.Frame(ops)
+ // Send the clipboard event
+ event := clipboard.Event{Text: "Text 2"}
+ router.Queue(event)
+ assertClipboardReadOp(t, router, 0)
+ assertClipboardEvent(t, router.Events(&handler[0]), true)
+ ops.Reset()
+
+ // No ReadOp
+ // There's no receiver waiting
+
+ router.Frame(ops)
+ assertClipboardReadOp(t, router, 0)
+ assertClipboardEvent(t, router.Events(&handler[0]), false)
+ ops.Reset()
+}
+
+func TestQueueProcessWriteClipboard(t *testing.T) {
+ ops, router := new(op.Ops), new(Router)
+ ops.Reset()
+
+ clipboard.WriteOp{Text: "Write 1"}.Add(ops)
+
+ router.Frame(ops)
+ assertClipboardWriteOp(t, router, "Write 1")
+ ops.Reset()
+
+ // No WriteOp
+
+ router.Frame(ops)
+ assertClipboardWriteOp(t, router, "")
+ ops.Reset()
+
+ clipboard.WriteOp{Text: "Write 2"}.Add(ops)
+
+ router.Frame(ops)
+ assertClipboardReadOp(t, router, 0)
+ assertClipboardWriteOp(t, router, "Write 2")
+ ops.Reset()
+}
+
+func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) {
+ t.Helper()
+ var evtClipboard int
+ for _, e := range events {
+ switch e.(type) {
+ case clipboard.Event:
+ evtClipboard++
+ }
+ }
+ if evtClipboard <= 0 && expected {
+ t.Error("expected to receive some event")
+ }
+ if evtClipboard > 0 && !expected {
+ t.Error("unexpected event received")
+ }
+}
+
+func assertClipboardReadOp(t *testing.T, router *Router, expected int) {
+ t.Helper()
+ if len(router.cqueue.receivers) != expected {
+ t.Error("unexpected number of receivers")
+ }
+ if router.cqueue.ReadClipboard() != (expected > 0) {
+ t.Error("missing requests")
+ }
+}
+
+func assertClipboardReadOpDuplicated(t *testing.T, router *Router,
+ expected int) {
+ t.Helper()
+ if len(router.cqueue.receivers) != expected {
+ t.Error("receivers removed")
+ }
+ if router.cqueue.ReadClipboard() != false {
+ t.Error("duplicated requests")
+ }
+}
+
+func assertClipboardWriteOp(t *testing.T, router *Router, expected string) {
+ t.Helper()
+ if (router.cqueue.text != nil) != (expected != "") {
+ t.Error("text not defined")
+ }
+ text, ok := router.cqueue.WriteClipboard()
+ if ok != (expected != "") {
+ t.Error("duplicated requests")
+ }
+ if text != expected {
+ t.Errorf("got text %s, expected %s", text, expected)
+ }
+}
diff --git a/gio/giold/io/router/key.go b/gio/giold/io/router/key.go
new file mode 100644
index 0000000..0fe946e
--- /dev/null
+++ b/gio/giold/io/router/key.go
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package router
+
+import (
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/op"
+)
+
+type TextInputState uint8
+
+type keyQueue struct {
+ focus event.Tag
+ handlers map[event.Tag]*keyHandler
+ reader ops.Reader
+ state TextInputState
+}
+
+type keyHandler struct {
+ // visible will be true if the InputOp is present
+ // in the current frame.
+ visible bool
+ new bool
+}
+
+const (
+ TextInputKeep TextInputState = iota
+ TextInputClose
+ TextInputOpen
+)
+
+// InputState returns the last text input state as
+// determined in Frame.
+func (q *keyQueue) InputState() TextInputState {
+ return q.state
+}
+
+func (q *keyQueue) Frame(root *op.Ops, events *handlerEvents) {
+ if q.handlers == nil {
+ q.handlers = make(map[event.Tag]*keyHandler)
+ }
+ for _, h := range q.handlers {
+ h.visible, h.new = false, false
+ }
+ q.reader.Reset(root)
+
+ focus, changed, state := q.resolveFocus(events)
+ for k, h := range q.handlers {
+ if !h.visible {
+ delete(q.handlers, k)
+ if q.focus == k {
+ // Remove the focus from the handler that is no longer visible.
+ q.focus = nil
+ state = TextInputClose
+ }
+ } else if h.new && k != focus {
+ // Reset the handler on (each) first appearance, but don't trigger redraw.
+ events.AddNoRedraw(k, key.FocusEvent{Focus: false})
+ }
+ }
+ if changed && focus != nil {
+ if _, exists := q.handlers[focus]; !exists {
+ focus = nil
+ }
+ }
+ if changed && focus != q.focus {
+ if q.focus != nil {
+ events.Add(q.focus, key.FocusEvent{Focus: false})
+ }
+ q.focus = focus
+ if q.focus != nil {
+ events.Add(q.focus, key.FocusEvent{Focus: true})
+ } else {
+ state = TextInputClose
+ }
+ }
+ q.state = state
+}
+
+func (q *keyQueue) Push(e event.Event, events *handlerEvents) {
+ if q.focus != nil {
+ events.Add(q.focus, e)
+ }
+}
+
+func (q *keyQueue) resolveFocus(events *handlerEvents) (focus event.Tag,
+ changed bool, state TextInputState) {
+ for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
+ switch opconst.OpType(encOp.Data[0]) {
+ case opconst.TypeKeyFocus:
+ op := decodeFocusOp(encOp.Data, encOp.Refs)
+ changed = true
+ focus = op.Tag
+ case opconst.TypeKeySoftKeyboard:
+ op := decodeSoftKeyboardOp(encOp.Data, encOp.Refs)
+ if op.Show {
+ state = TextInputOpen
+ } else {
+ state = TextInputClose
+ }
+ case opconst.TypeKeyInput:
+ op := decodeKeyInputOp(encOp.Data, encOp.Refs)
+ h, ok := q.handlers[op.Tag]
+ if !ok {
+ h = &keyHandler{new: true}
+ q.handlers[op.Tag] = h
+ }
+ h.visible = true
+ }
+ }
+ return
+}
+
+func decodeKeyInputOp(d []byte, refs []interface{}) key.InputOp {
+ if opconst.OpType(d[0]) != opconst.TypeKeyInput {
+ panic("invalid op")
+ }
+ return key.InputOp{
+ Tag: refs[0].(event.Tag),
+ }
+}
+
+func decodeSoftKeyboardOp(d []byte, refs []interface{}) key.SoftKeyboardOp {
+ if opconst.OpType(d[0]) != opconst.TypeKeySoftKeyboard {
+ panic("invalid op")
+ }
+ return key.SoftKeyboardOp{
+ Show: d[1] != 0,
+ }
+}
+
+func decodeFocusOp(d []byte, refs []interface{}) key.FocusOp {
+ if opconst.OpType(d[0]) != opconst.TypeKeyFocus {
+ panic("invalid op")
+ }
+ return key.FocusOp{
+ Tag: refs[0],
+ }
+}
diff --git a/gio/giold/io/router/key_test.go b/gio/giold/io/router/key_test.go
new file mode 100644
index 0000000..59176df
--- /dev/null
+++ b/gio/giold/io/router/key_test.go
@@ -0,0 +1,322 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package router
+
+import (
+ "reflect"
+ "testing"
+
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/op"
+)
+
+func TestKeyWakeup(t *testing.T) {
+ handler := new(int)
+ var ops op.Ops
+ key.InputOp{Tag: handler}.Add(&ops)
+
+ var r Router
+ // Test that merely adding a handler doesn't trigger redraw.
+ r.Frame(&ops)
+ if _, wake := r.WakeupTime(); wake {
+ t.Errorf("adding key.InputOp triggered a redraw")
+ }
+ // However, adding a handler queues a Focus(false) event.
+ if evts := r.Events(handler); len(evts) != 1 {
+ t.Errorf("no Focus event for newly registered key.InputOp")
+ }
+ // Verify that r.Events does trigger a redraw.
+ r.Frame(&ops)
+ if _, wake := r.WakeupTime(); !wake {
+ t.Errorf("key.FocusEvent event didn't trigger a redraw")
+ }
+}
+
+func TestKeyMultiples(t *testing.T) {
+ handlers := make([]int, 3)
+ ops := new(op.Ops)
+ r := new(Router)
+
+ key.SoftKeyboardOp{Show: true}.Add(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ key.FocusOp{Tag: &handlers[2]}.Add(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+
+ // The last one must be focused:
+ key.InputOp{Tag: &handlers[2]}.Add(ops)
+
+ r.Frame(ops)
+
+ assertKeyEvent(t, r.Events(&handlers[0]), false)
+ assertKeyEvent(t, r.Events(&handlers[1]), false)
+ assertKeyEvent(t, r.Events(&handlers[2]), true)
+ assertFocus(t, r, &handlers[2])
+ assertKeyboard(t, r, TextInputOpen)
+}
+
+func TestKeyStacked(t *testing.T) {
+ handlers := make([]int, 4)
+ ops := new(op.Ops)
+ r := new(Router)
+
+ s := op.Save(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ key.FocusOp{Tag: nil}.Add(ops)
+ s.Load()
+ s = op.Save(ops)
+ key.SoftKeyboardOp{Show: false}.Add(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ key.FocusOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[2]}.Add(ops)
+ key.SoftKeyboardOp{Show: true}.Add(ops)
+ s.Load()
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[3]}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEvent(t, r.Events(&handlers[0]), false)
+ assertKeyEvent(t, r.Events(&handlers[1]), true)
+ assertKeyEvent(t, r.Events(&handlers[2]), false)
+ assertKeyEvent(t, r.Events(&handlers[3]), false)
+ assertFocus(t, r, &handlers[1])
+ assertKeyboard(t, r, TextInputOpen)
+}
+
+func TestKeySoftKeyboardNoFocus(t *testing.T) {
+ ops := new(op.Ops)
+ r := new(Router)
+
+ // It's possible to open the keyboard
+ // without any active focus:
+ key.SoftKeyboardOp{Show: true}.Add(ops)
+
+ r.Frame(ops)
+
+ assertFocus(t, r, nil)
+ assertKeyboard(t, r, TextInputOpen)
+}
+
+func TestKeyRemoveFocus(t *testing.T) {
+ handlers := make([]int, 2)
+ ops := new(op.Ops)
+ r := new(Router)
+
+ // New InputOp with Focus and Keyboard:
+ s := op.Save(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ key.FocusOp{Tag: &handlers[0]}.Add(ops)
+ key.SoftKeyboardOp{Show: true}.Add(ops)
+ s.Load()
+
+ // New InputOp without any focus:
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ // Add some key events:
+ event := event.Event(key.Event{Name: key.NameTab,
+ Modifiers: key.ModShortcut, State: key.Press})
+ r.Queue(event)
+
+ assertKeyEvent(t, r.Events(&handlers[0]), true, event)
+ assertKeyEvent(t, r.Events(&handlers[1]), false)
+ assertFocus(t, r, &handlers[0])
+ assertKeyboard(t, r, TextInputOpen)
+
+ ops.Reset()
+
+ // Will get the focus removed:
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ s.Load()
+
+ // Unchanged:
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+
+ // Remove focus by focusing on a tag that don't exist.
+ s = op.Save(ops)
+ key.FocusOp{Tag: new(int)}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEventUnexpected(t, r.Events(&handlers[1]))
+ assertFocus(t, r, nil)
+ assertKeyboard(t, r, TextInputClose)
+
+ ops.Reset()
+
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ s.Load()
+
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEventUnexpected(t, r.Events(&handlers[0]))
+ assertKeyEventUnexpected(t, r.Events(&handlers[1]))
+ assertFocus(t, r, nil)
+ assertKeyboard(t, r, TextInputKeep)
+
+ ops.Reset()
+
+ // Set focus to InputOp which already
+ // exists in the previous frame:
+ s = op.Save(ops)
+ key.FocusOp{Tag: &handlers[0]}.Add(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ key.SoftKeyboardOp{Show: true}.Add(ops)
+ s.Load()
+
+ // Remove focus.
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ key.FocusOp{Tag: nil}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEventUnexpected(t, r.Events(&handlers[1]))
+ assertFocus(t, r, nil)
+ assertKeyboard(t, r, TextInputOpen)
+}
+
+func TestKeyFocusedInvisible(t *testing.T) {
+ handlers := make([]int, 2)
+ ops := new(op.Ops)
+ r := new(Router)
+
+ // Set new InputOp with focus:
+ s := op.Save(ops)
+ key.FocusOp{Tag: &handlers[0]}.Add(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ key.SoftKeyboardOp{Show: true}.Add(ops)
+ s.Load()
+
+ // Set new InputOp without focus:
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEvent(t, r.Events(&handlers[0]), true)
+ assertKeyEvent(t, r.Events(&handlers[1]), false)
+ assertFocus(t, r, &handlers[0])
+ assertKeyboard(t, r, TextInputOpen)
+
+ ops.Reset()
+
+ //
+ // Removed first (focused) element!
+ //
+
+ // Unchanged:
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEventUnexpected(t, r.Events(&handlers[0]))
+ assertKeyEventUnexpected(t, r.Events(&handlers[1]))
+ assertFocus(t, r, nil)
+ assertKeyboard(t, r, TextInputClose)
+
+ ops.Reset()
+
+ // Respawn the first element:
+ // It must receive one `Event{Focus: false}`.
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ s.Load()
+
+ // Unchanged
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEvent(t, r.Events(&handlers[0]), false)
+ assertKeyEventUnexpected(t, r.Events(&handlers[1]))
+ assertFocus(t, r, nil)
+ assertKeyboard(t, r, TextInputKeep)
+
+}
+
+func assertKeyEvent(t *testing.T, events []event.Event, expected bool,
+ expectedInputs ...event.Event) {
+ t.Helper()
+ var evtFocus int
+ var evtKeyPress int
+ for _, e := range events {
+ switch ev := e.(type) {
+ case key.FocusEvent:
+ if ev.Focus != expected {
+ t.Errorf("focus is expected to be %v, got %v", expected,
+ ev.Focus)
+ }
+ evtFocus++
+ case key.Event, key.EditEvent:
+ if len(expectedInputs) <= evtKeyPress {
+ t.Errorf("unexpected key events")
+ }
+ if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) {
+ t.Errorf("expected %v events, got %v",
+ expectedInputs[evtKeyPress], ev)
+ }
+ evtKeyPress++
+ }
+ }
+ if evtFocus <= 0 {
+ t.Errorf("expected focus event")
+ }
+ if evtFocus > 1 {
+ t.Errorf("expected single focus event")
+ }
+ if evtKeyPress != len(expectedInputs) {
+ t.Errorf("expected key events")
+ }
+}
+
+func assertKeyEventUnexpected(t *testing.T, events []event.Event) {
+ t.Helper()
+ var evtFocus int
+ for _, e := range events {
+ switch e.(type) {
+ case key.FocusEvent:
+ evtFocus++
+ }
+ }
+ if evtFocus > 1 {
+ t.Errorf("unexpected focus event")
+ }
+}
+
+func assertFocus(t *testing.T, router *Router, expected event.Tag) {
+ t.Helper()
+ if router.kqueue.focus != expected {
+ t.Errorf("expected %v to be focused, got %v", expected,
+ router.kqueue.focus)
+ }
+}
+
+func assertKeyboard(t *testing.T, router *Router, expected TextInputState) {
+ t.Helper()
+ if router.kqueue.state != expected {
+ t.Errorf("expected %v keyboard, got %v", expected, router.kqueue.state)
+ }
+}
diff --git a/gio/giold/io/router/pointer.go b/gio/giold/io/router/pointer.go
new file mode 100644
index 0000000..588657c
--- /dev/null
+++ b/gio/giold/io/router/pointer.go
@@ -0,0 +1,515 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package router
+
+import (
+ "encoding/binary"
+ "image"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/op"
+)
+
+type pointerQueue struct {
+ hitTree []hitNode
+ areas []areaNode
+ cursors []cursorNode
+ cursor pointer.CursorName
+ handlers map[event.Tag]*pointerHandler
+ pointers []pointerInfo
+ reader ops.Reader
+
+ // states holds the storage for save/restore ops.
+ states []collectState
+ scratch []event.Tag
+}
+
+type hitNode struct {
+ next int
+ area int
+ // Pass tracks the most recent PassOp mode.
+ pass bool
+
+ // For handler nodes.
+ tag event.Tag
+}
+
+type cursorNode struct {
+ name pointer.CursorName
+ area int
+}
+
+type pointerInfo struct {
+ id pointer.ID
+ pressed bool
+ handlers []event.Tag
+ // last tracks the last pointer event received,
+ // used while processing frame events.
+ last pointer.Event
+
+ // entered tracks the tags that contain the pointer.
+ entered []event.Tag
+}
+
+type pointerHandler struct {
+ area int
+ active bool
+ wantsGrab bool
+ types pointer.Type
+ // min and max horizontal/vertical scroll
+ scrollRange image.Rectangle
+}
+
+type areaOp struct {
+ kind areaKind
+ rect f32.Rectangle
+}
+
+type areaNode struct {
+ trans f32.Affine2D
+ next int
+ area areaOp
+}
+
+type areaKind uint8
+
+// collectState represents the state for collectHandlers
+type collectState struct {
+ t f32.Affine2D
+ area int
+ node int
+ pass bool
+}
+
+const (
+ areaRect areaKind = iota
+ areaEllipse
+)
+
+func (q *pointerQueue) save(id int, state collectState) {
+ if extra := id - len(q.states) + 1; extra > 0 {
+ q.states = append(q.states, make([]collectState, extra)...)
+ }
+ q.states[id] = state
+}
+
+func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents) {
+ state := collectState{
+ area: -1,
+ node: -1,
+ }
+ q.save(opconst.InitialStateID, state)
+ for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
+ switch opconst.OpType(encOp.Data[0]) {
+ case opconst.TypeSave:
+ id := ops.DecodeSave(encOp.Data)
+ q.save(id, state)
+ case opconst.TypeLoad:
+ id, mask := ops.DecodeLoad(encOp.Data)
+ s := q.states[id]
+ if mask&opconst.TransformState != 0 {
+ state.t = s.t
+ }
+ if mask&^opconst.TransformState != 0 {
+ state = s
+ }
+ case opconst.TypePass:
+ state.pass = encOp.Data[1] != 0
+ case opconst.TypeArea:
+ var op areaOp
+ op.Decode(encOp.Data)
+ q.areas = append(q.areas,
+ areaNode{trans: state.t, next: state.area, area: op})
+ state.area = len(q.areas) - 1
+ q.hitTree = append(q.hitTree, hitNode{
+ next: state.node,
+ area: state.area,
+ pass: state.pass,
+ })
+ state.node = len(q.hitTree) - 1
+ case opconst.TypeTransform:
+ dop := ops.DecodeTransform(encOp.Data)
+ state.t = state.t.Mul(dop)
+ case opconst.TypePointerInput:
+ op := pointer.InputOp{
+ Tag: encOp.Refs[0].(event.Tag),
+ Grab: encOp.Data[1] != 0,
+ Types: pointer.Type(encOp.Data[2]),
+ }
+ q.hitTree = append(q.hitTree, hitNode{
+ next: state.node,
+ area: state.area,
+ pass: state.pass,
+ tag: op.Tag,
+ })
+ state.node = len(q.hitTree) - 1
+ h, ok := q.handlers[op.Tag]
+ if !ok {
+ h = new(pointerHandler)
+ q.handlers[op.Tag] = h
+ // Cancel handlers on (each) first appearance, but don't
+ // trigger redraw.
+ events.AddNoRedraw(op.Tag, pointer.Event{Type: pointer.Cancel})
+ }
+ h.active = true
+ h.area = state.area
+ h.wantsGrab = h.wantsGrab || op.Grab
+ h.types = h.types | op.Types
+ bo := binary.LittleEndian.Uint32
+ h.scrollRange = image.Rectangle{
+ Min: image.Point{
+ X: int(int32(bo(encOp.Data[3:]))),
+ Y: int(int32(bo(encOp.Data[7:]))),
+ },
+ Max: image.Point{
+ X: int(int32(bo(encOp.Data[11:]))),
+ Y: int(int32(bo(encOp.Data[15:]))),
+ },
+ }
+ case opconst.TypeCursor:
+ q.cursors = append(q.cursors, cursorNode{
+ name: encOp.Refs[0].(pointer.CursorName),
+ area: len(q.areas) - 1,
+ })
+ }
+ }
+}
+
+func (q *pointerQueue) opHit(handlers *[]event.Tag, pos f32.Point) {
+ // Track whether we're passing through hits.
+ pass := true
+ idx := len(q.hitTree) - 1
+ for idx >= 0 {
+ n := &q.hitTree[idx]
+ if !q.hit(n.area, pos) {
+ idx--
+ continue
+ }
+ pass = pass && n.pass
+ if pass {
+ idx--
+ } else {
+ idx = n.next
+ }
+ if n.tag != nil {
+ if _, exists := q.handlers[n.tag]; exists {
+ *handlers = append(*handlers, n.tag)
+ }
+ }
+ }
+}
+
+func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point {
+ if areaIdx == -1 {
+ return p
+ }
+ return q.areas[areaIdx].trans.Invert().Transform(p)
+}
+
+func (q *pointerQueue) hit(areaIdx int, p f32.Point) bool {
+ for areaIdx != -1 {
+ a := &q.areas[areaIdx]
+ p := a.trans.Invert().Transform(p)
+ if !a.area.Hit(p) {
+ return false
+ }
+ areaIdx = a.next
+ }
+ return true
+}
+
+func (q *pointerQueue) reset() {
+ if q.handlers == nil {
+ q.handlers = make(map[event.Tag]*pointerHandler)
+ }
+}
+
+func (q *pointerQueue) Frame(root *op.Ops, events *handlerEvents) {
+ q.reset()
+ for _, h := range q.handlers {
+ // Reset handler.
+ h.active = false
+ h.wantsGrab = false
+ h.types = 0
+ }
+ q.hitTree = q.hitTree[:0]
+ q.areas = q.areas[:0]
+ q.cursors = q.cursors[:0]
+ q.reader.Reset(root)
+ q.collectHandlers(&q.reader, events)
+ for k, h := range q.handlers {
+ if !h.active {
+ q.dropHandlers(events, k)
+ delete(q.handlers, k)
+ }
+ if h.wantsGrab {
+ for _, p := range q.pointers {
+ if !p.pressed {
+ continue
+ }
+ for i, k2 := range p.handlers {
+ if k2 == k {
+ // Drop other handlers that lost their grab.
+ dropped := make([]event.Tag, 0, len(p.handlers)-1)
+ dropped = append(dropped, p.handlers[:i]...)
+ dropped = append(dropped, p.handlers[i+1:]...)
+ cancelHandlers(events, dropped...)
+ q.dropHandlers(events, dropped...)
+ break
+ }
+ }
+ }
+ }
+ }
+ for i := range q.pointers {
+ p := &q.pointers[i]
+ q.deliverEnterLeaveEvents(p, events, p.last)
+ }
+}
+
+func cancelHandlers(events *handlerEvents, tags ...event.Tag) {
+ for _, k := range tags {
+ events.Add(k, pointer.Event{Type: pointer.Cancel})
+ }
+}
+
+func (q *pointerQueue) dropHandlers(events *handlerEvents, tags ...event.Tag) {
+ for _, k := range tags {
+ for i := range q.pointers {
+ p := &q.pointers[i]
+ for i := len(p.handlers) - 1; i >= 0; i-- {
+ if p.handlers[i] == k {
+ p.handlers = append(p.handlers[:i], p.handlers[i+1:]...)
+ }
+ }
+ for i := len(p.entered) - 1; i >= 0; i-- {
+ if p.entered[i] == k {
+ p.entered = append(p.entered[:i], p.entered[i+1:]...)
+ }
+ }
+ }
+ }
+}
+
+func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
+ q.reset()
+ if e.Type == pointer.Cancel {
+ q.pointers = q.pointers[:0]
+ for k := range q.handlers {
+ cancelHandlers(events, k)
+ q.dropHandlers(events, k)
+ }
+ return
+ }
+ pidx := -1
+ for i, p := range q.pointers {
+ if p.id == e.PointerID {
+ pidx = i
+ break
+ }
+ }
+ if pidx == -1 {
+ q.pointers = append(q.pointers, pointerInfo{id: e.PointerID})
+ pidx = len(q.pointers) - 1
+ }
+ p := &q.pointers[pidx]
+ p.last = e
+
+ if e.Type == pointer.Move && p.pressed {
+ e.Type = pointer.Drag
+ }
+
+ if e.Type == pointer.Release {
+ q.deliverEvent(p, events, e)
+ p.pressed = false
+ }
+ q.deliverEnterLeaveEvents(p, events, e)
+
+ if !p.pressed {
+ p.handlers = append(p.handlers[:0], q.scratch...)
+ }
+ if e.Type == pointer.Press {
+ p.pressed = true
+ }
+ switch e.Type {
+ case pointer.Release:
+ case pointer.Scroll:
+ q.deliverScrollEvent(p, events, e)
+ default:
+ q.deliverEvent(p, events, e)
+ }
+ if !p.pressed && len(p.entered) == 0 {
+ // No longer need to track pointer.
+ q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...)
+ }
+}
+
+func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents,
+ e pointer.Event) {
+ foremost := true
+ if p.pressed && len(p.handlers) == 1 {
+ e.Priority = pointer.Grabbed
+ foremost = false
+ }
+ for _, k := range p.handlers {
+ h := q.handlers[k]
+ if e.Type&h.types == 0 {
+ continue
+ }
+ e := e
+ if foremost {
+ foremost = false
+ e.Priority = pointer.Foremost
+ }
+ e.Position = q.invTransform(h.area, e.Position)
+ events.Add(k, e)
+ }
+}
+
+func (q *pointerQueue) deliverScrollEvent(p *pointerInfo, events *handlerEvents,
+ e pointer.Event) {
+ foremost := true
+ if p.pressed && len(p.handlers) == 1 {
+ e.Priority = pointer.Grabbed
+ foremost = false
+ }
+ var sx, sy = e.Scroll.X, e.Scroll.Y
+ for _, k := range p.handlers {
+ if sx == 0 && sy == 0 {
+ return
+ }
+ h := q.handlers[k]
+ // Distribute the scroll to the handler based on its ScrollRange.
+ sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X,
+ h.scrollRange.Max.X)
+ sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y,
+ h.scrollRange.Max.Y)
+ e := e
+ if foremost {
+ foremost = false
+ e.Priority = pointer.Foremost
+ }
+ e.Position = q.invTransform(h.area, e.Position)
+ events.Add(k, e)
+ }
+}
+
+func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo,
+ events *handlerEvents, e pointer.Event) {
+ q.scratch = q.scratch[:0]
+ q.opHit(&q.scratch, e.Position)
+ if p.pressed {
+ // Filter out non-participating handlers.
+ for i := len(q.scratch) - 1; i >= 0; i-- {
+ if _, found := searchTag(p.handlers, q.scratch[i]); !found {
+ q.scratch = append(q.scratch[:i], q.scratch[i+1:]...)
+ }
+ }
+ }
+ hits := q.scratch
+ if e.Source != pointer.Mouse && !p.pressed && e.Type != pointer.Press {
+ // Consider non-mouse pointers leaving when they're released.
+ hits = nil
+ }
+ // Deliver Leave events.
+ for _, k := range p.entered {
+ if _, found := searchTag(hits, k); found {
+ continue
+ }
+ h := q.handlers[k]
+ e.Type = pointer.Leave
+
+ if e.Type&h.types != 0 {
+ e.Position = q.invTransform(h.area, e.Position)
+ events.Add(k, e)
+ }
+ }
+ // Deliver Enter events and update cursor.
+ q.cursor = pointer.CursorDefault
+ for _, k := range hits {
+ h := q.handlers[k]
+ for i := len(q.cursors) - 1; i >= 0; i-- {
+ if c := q.cursors[i]; c.area == h.area {
+ q.cursor = c.name
+ break
+ }
+ }
+ if _, found := searchTag(p.entered, k); found {
+ continue
+ }
+ e.Type = pointer.Enter
+
+ if e.Type&h.types != 0 {
+ e.Position = q.invTransform(h.area, e.Position)
+ events.Add(k, e)
+ }
+ }
+ p.entered = append(p.entered[:0], hits...)
+}
+
+func searchTag(tags []event.Tag, tag event.Tag) (int, bool) {
+ for i, t := range tags {
+ if t == tag {
+ return i, true
+ }
+ }
+ return 0, false
+}
+
+func opDecodeFloat32(d []byte) float32 {
+ return float32(int32(binary.LittleEndian.Uint32(d)))
+}
+
+func (op *areaOp) Decode(d []byte) {
+ if opconst.OpType(d[0]) != opconst.TypeArea {
+ panic("invalid op")
+ }
+ rect := f32.Rectangle{
+ Min: f32.Point{
+ X: opDecodeFloat32(d[2:]),
+ Y: opDecodeFloat32(d[6:]),
+ },
+ Max: f32.Point{
+ X: opDecodeFloat32(d[10:]),
+ Y: opDecodeFloat32(d[14:]),
+ },
+ }
+ *op = areaOp{
+ kind: areaKind(d[1]),
+ rect: rect,
+ }
+}
+
+func (op *areaOp) Hit(pos f32.Point) bool {
+ pos = pos.Sub(op.rect.Min)
+ size := op.rect.Size()
+ switch op.kind {
+ case areaRect:
+ return 0 <= pos.X && pos.X < size.X &&
+ 0 <= pos.Y && pos.Y < size.Y
+ case areaEllipse:
+ rx := size.X / 2
+ ry := size.Y / 2
+ xh := pos.X - rx
+ yk := pos.Y - ry
+ // The ellipse function works in all cases because
+ // 0/0 is not <= 1.
+ return (xh*xh)/(rx*rx)+(yk*yk)/(ry*ry) <= 1
+ default:
+ panic("invalid area kind")
+ }
+}
+
+func setScrollEvent(scroll float32, min, max int) (left, scrolled float32) {
+ if v := float32(max); scroll > v {
+ return scroll - v, v
+ }
+ if v := float32(min); scroll < v {
+ return scroll - v, v
+ }
+ return 0, scroll
+}
diff --git a/gio/giold/io/router/pointer_test.go b/gio/giold/io/router/pointer_test.go
new file mode 100644
index 0000000..5a28d0e
--- /dev/null
+++ b/gio/giold/io/router/pointer_test.go
@@ -0,0 +1,787 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package router
+
+import (
+ "fmt"
+ "image"
+ "reflect"
+ "testing"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/op"
+)
+
+func TestPointerWakeup(t *testing.T) {
+ handler := new(int)
+ var ops op.Ops
+ addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100))
+
+ var r Router
+ // Test that merely adding a handler doesn't trigger redraw.
+ r.Frame(&ops)
+ if _, wake := r.WakeupTime(); wake {
+ t.Errorf("adding pointer.InputOp triggered a redraw")
+ }
+ // However, adding a handler queues a Cancel event.
+ assertEventSequence(t, r.Events(handler), pointer.Cancel)
+ // Verify that r.Events does trigger a redraw.
+ r.Frame(&ops)
+ if _, wake := r.WakeupTime(); !wake {
+ t.Errorf("pointer.Cancel event didn't trigger a redraw")
+ }
+}
+
+func TestPointerDrag(t *testing.T) {
+ handler := new(int)
+ var ops op.Ops
+ addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100))
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ // Press.
+ pointer.Event{
+ Type: pointer.Press,
+ Position: f32.Pt(50, 50),
+ },
+ // Move outside the area.
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(150, 150),
+ },
+ )
+ assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter,
+ pointer.Press, pointer.Leave, pointer.Drag)
+}
+
+func TestPointerDragNegative(t *testing.T) {
+ handler := new(int)
+ var ops op.Ops
+ addPointerHandler(&ops, handler, image.Rect(-100, -100, 0, 0))
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ // Press.
+ pointer.Event{
+ Type: pointer.Press,
+ Position: f32.Pt(-50, -50),
+ },
+ // Move outside the area.
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(-150, -150),
+ },
+ )
+ assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter,
+ pointer.Press, pointer.Leave, pointer.Drag)
+}
+
+func TestPointerGrab(t *testing.T) {
+ handler1 := new(int)
+ handler2 := new(int)
+ handler3 := new(int)
+ var ops op.Ops
+
+ types := pointer.Press | pointer.Release
+
+ pointer.InputOp{Tag: handler1, Types: types, Grab: true}.Add(&ops)
+ pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
+ pointer.InputOp{Tag: handler3, Types: types}.Add(&ops)
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Press,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Press)
+ assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Press)
+ assertEventSequence(t, r.Events(handler3), pointer.Cancel, pointer.Press)
+ r.Frame(&ops)
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Release,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Release)
+ assertEventSequence(t, r.Events(handler2), pointer.Cancel)
+ assertEventSequence(t, r.Events(handler3), pointer.Cancel)
+}
+
+func TestPointerMove(t *testing.T) {
+ handler1 := new(int)
+ handler2 := new(int)
+ var ops op.Ops
+
+ types := pointer.Move | pointer.Enter | pointer.Leave
+
+ // Handler 1 area: (0, 0) - (100, 100)
+ pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops)
+ pointer.InputOp{Tag: handler1, Types: types}.Add(&ops)
+ // Handler 2 area: (50, 50) - (100, 100) (areas intersect).
+ pointer.Rect(image.Rect(50, 50, 200, 200)).Add(&ops)
+ pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ // Hit both handlers.
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(50, 50),
+ },
+ // Hit handler 1.
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(49, 50),
+ },
+ // Hit no handlers.
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(100, 50),
+ },
+ pointer.Event{
+ Type: pointer.Cancel,
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter,
+ pointer.Move, pointer.Move, pointer.Leave, pointer.Cancel)
+ assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter,
+ pointer.Move, pointer.Leave, pointer.Cancel)
+}
+
+func TestPointerTypes(t *testing.T) {
+ handler := new(int)
+ var ops op.Ops
+ pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops)
+ pointer.InputOp{
+ Tag: handler,
+ Types: pointer.Press | pointer.Release,
+ }.Add(&ops)
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Press,
+ Position: f32.Pt(50, 50),
+ },
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(150, 150),
+ },
+ pointer.Event{
+ Type: pointer.Release,
+ Position: f32.Pt(150, 150),
+ },
+ )
+ assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Press,
+ pointer.Release)
+}
+
+func TestPointerPriority(t *testing.T) {
+ handler1 := new(int)
+ handler2 := new(int)
+ handler3 := new(int)
+ var ops op.Ops
+
+ st := op.Save(&ops)
+ pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops)
+ pointer.InputOp{
+ Tag: handler1,
+ Types: pointer.Scroll,
+ ScrollBounds: image.Rectangle{Max: image.Point{X: 100}},
+ }.Add(&ops)
+
+ pointer.Rect(image.Rect(0, 0, 100, 50)).Add(&ops)
+ pointer.InputOp{
+ Tag: handler2,
+ Types: pointer.Scroll,
+ ScrollBounds: image.Rectangle{Max: image.Point{X: 20}},
+ }.Add(&ops)
+ st.Load()
+
+ pointer.Rect(image.Rect(0, 100, 100, 200)).Add(&ops)
+ pointer.InputOp{
+ Tag: handler3,
+ Types: pointer.Scroll,
+ ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}},
+ }.Add(&ops)
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ // Hit handler 1 and 2.
+ pointer.Event{
+ Type: pointer.Scroll,
+ Position: f32.Pt(50, 25),
+ Scroll: f32.Pt(50, 0),
+ },
+ // Hit handler 1.
+ pointer.Event{
+ Type: pointer.Scroll,
+ Position: f32.Pt(50, 75),
+ Scroll: f32.Pt(50, 50),
+ },
+ // Hit handler 3.
+ pointer.Event{
+ Type: pointer.Scroll,
+ Position: f32.Pt(50, 150),
+ Scroll: f32.Pt(-30, -30),
+ },
+ // Hit no handlers.
+ pointer.Event{
+ Type: pointer.Scroll,
+ Position: f32.Pt(50, 225),
+ },
+ )
+
+ hev1 := r.Events(handler1)
+ hev2 := r.Events(handler2)
+ hev3 := r.Events(handler3)
+ assertEventSequence(t, hev1, pointer.Cancel, pointer.Scroll, pointer.Scroll)
+ assertEventSequence(t, hev2, pointer.Cancel, pointer.Scroll)
+ assertEventSequence(t, hev3, pointer.Cancel, pointer.Scroll)
+ assertEventPriorities(t, hev1, pointer.Shared, pointer.Shared,
+ pointer.Foremost)
+ assertEventPriorities(t, hev2, pointer.Shared, pointer.Foremost)
+ assertEventPriorities(t, hev3, pointer.Shared, pointer.Foremost)
+ assertScrollEvent(t, hev1[1], f32.Pt(30, 0))
+ assertScrollEvent(t, hev2[1], f32.Pt(20, 0))
+ assertScrollEvent(t, hev1[2], f32.Pt(50, 0))
+ assertScrollEvent(t, hev3[1], f32.Pt(-20, -30))
+}
+
+func TestPointerEnterLeave(t *testing.T) {
+ handler1 := new(int)
+ handler2 := new(int)
+ var ops op.Ops
+
+ // Handler 1 area: (0, 0) - (100, 100)
+ addPointerHandler(&ops, handler1, image.Rect(0, 0, 100, 100))
+
+ // Handler 2 area: (50, 50) - (200, 200) (areas overlap).
+ addPointerHandler(&ops, handler2, image.Rect(50, 50, 200, 200))
+
+ var r Router
+ r.Frame(&ops)
+ // Hit both handlers.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ // First event for a handler is always a Cancel.
+ // Only handler2 should receive the enter/move events because it is on top
+ // and handler1 is not an ancestor in the hit tree.
+ assertEventSequence(t, r.Events(handler1), pointer.Cancel)
+ assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter,
+ pointer.Move)
+
+ // Leave the second area by moving into the first.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(45, 45),
+ },
+ )
+ // The cursor leaves handler2 and enters handler1.
+ assertEventSequence(t, r.Events(handler1), pointer.Enter, pointer.Move)
+ assertEventSequence(t, r.Events(handler2), pointer.Leave)
+
+ // Move, but stay within the same hit area.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(40, 40),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Move)
+ assertEventSequence(t, r.Events(handler2))
+
+ // Move outside of both inputs.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(300, 300),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Leave)
+ assertEventSequence(t, r.Events(handler2))
+
+ // Check that a Press event generates Enter Events.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Press,
+ Position: f32.Pt(125, 125),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1))
+ assertEventSequence(t, r.Events(handler2), pointer.Enter, pointer.Press)
+
+ // Check that a drag only affects the participating handlers.
+ r.Queue(
+ // Leave
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(25, 25),
+ },
+ // Enter
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1))
+ assertEventSequence(t, r.Events(handler2), pointer.Leave, pointer.Drag,
+ pointer.Enter, pointer.Drag)
+
+ // Check that a Release event generates Enter/Leave Events.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Release,
+ Position: f32.Pt(25,
+ 25),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Enter)
+ // The second handler gets the release event because the press started inside it.
+ assertEventSequence(t, r.Events(handler2), pointer.Release, pointer.Leave)
+
+}
+
+func TestMultipleAreas(t *testing.T) {
+ handler := new(int)
+
+ var ops op.Ops
+
+ addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100))
+ st := op.Save(&ops)
+ pointer.Rect(image.Rect(50, 50, 200, 200)).Add(&ops)
+ // Second area has no Types set, yet should receive events because
+ // Types for the same handles are or-ed together.
+ pointer.InputOp{Tag: handler}.Add(&ops)
+ st.Load()
+
+ var r Router
+ r.Frame(&ops)
+ // Hit first area, then second area, then both.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(25, 25),
+ },
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(150, 150),
+ },
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter,
+ pointer.Move, pointer.Move, pointer.Move)
+}
+
+func TestPointerEnterLeaveNested(t *testing.T) {
+ handler1 := new(int)
+ handler2 := new(int)
+ var ops op.Ops
+
+ types := pointer.Press | pointer.Move | pointer.Release | pointer.Enter | pointer.Leave
+
+ // Handler 1 area: (0, 0) - (100, 100)
+ pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops)
+ pointer.InputOp{Tag: handler1, Types: types}.Add(&ops)
+
+ // Handler 2 area: (25, 25) - (75, 75) (nested within first).
+ pointer.Rect(image.Rect(25, 25, 75, 75)).Add(&ops)
+ pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
+
+ var r Router
+ r.Frame(&ops)
+ // Hit both handlers.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ // First event for a handler is always a Cancel.
+ // Both handlers should receive the Enter and Move events because handler2 is a child of handler1.
+ assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter,
+ pointer.Move)
+ assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter,
+ pointer.Move)
+
+ // Leave the second area by moving into the first.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(20, 20),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Move)
+ assertEventSequence(t, r.Events(handler2), pointer.Leave)
+
+ // Move, but stay within the same hit area.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(10, 10),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Move)
+ assertEventSequence(t, r.Events(handler2))
+
+ // Move outside of both inputs.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(200, 200),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Leave)
+ assertEventSequence(t, r.Events(handler2))
+
+ // Check that a Press event generates Enter Events.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Press,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Enter, pointer.Press)
+ assertEventSequence(t, r.Events(handler2), pointer.Enter, pointer.Press)
+
+ // Check that a Release event generates Enter/Leave Events.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Release,
+ Position: f32.Pt(20, 20),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Release)
+ assertEventSequence(t, r.Events(handler2), pointer.Release, pointer.Leave)
+}
+
+func TestPointerActiveInputDisappears(t *testing.T) {
+ handler1 := new(int)
+ var ops op.Ops
+ var r Router
+
+ // Draw handler.
+ ops.Reset()
+ addPointerHandler(&ops, handler1, image.Rect(0, 0, 100, 100))
+ r.Frame(&ops)
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(25, 25),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter,
+ pointer.Move)
+
+ // Re-render with handler missing.
+ ops.Reset()
+ r.Frame(&ops)
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(25, 25),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1))
+}
+
+func TestMultitouch(t *testing.T) {
+ var ops op.Ops
+
+ // Add two separate handlers.
+ h1, h2 := new(int), new(int)
+ addPointerHandler(&ops, h1, image.Rect(0, 0, 100, 100))
+ addPointerHandler(&ops, h2, image.Rect(0, 100, 100, 200))
+
+ h1pt, h2pt := f32.Pt(0, 0), f32.Pt(0, 100)
+ var p1, p2 pointer.ID = 0, 1
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Press,
+ Position: h1pt,
+ PointerID: p1,
+ },
+ )
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Press,
+ Position: h2pt,
+ PointerID: p2,
+ },
+ )
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Release,
+ Position: h2pt,
+ PointerID: p2,
+ },
+ )
+ assertEventSequence(t, r.Events(h1), pointer.Cancel, pointer.Enter,
+ pointer.Press)
+ assertEventSequence(t, r.Events(h2), pointer.Cancel, pointer.Enter,
+ pointer.Press, pointer.Release)
+}
+
+func TestCursorNameOp(t *testing.T) {
+ ops := new(op.Ops)
+ var r Router
+ var h, h2 int
+ var widget2 func()
+ widget := func() {
+ // This is the area where the cursor is changed to CursorPointer.
+ pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops)
+ // The cursor is checked and changed upon cursor movement.
+ pointer.InputOp{Tag: &h}.Add(ops)
+ pointer.CursorNameOp{Name: pointer.CursorPointer}.Add(ops)
+ if widget2 != nil {
+ widget2()
+ }
+ }
+ // Register the handlers.
+ widget()
+ // No cursor change as the mouse has not moved yet.
+ if got, want := r.Cursor(), pointer.CursorDefault; got != want {
+ t.Errorf("got %q; want %q", got, want)
+ }
+
+ _at := func(x, y float32) pointer.Event {
+ return pointer.Event{
+ Type: pointer.Move,
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Position: f32.Pt(x, y),
+ }
+ }
+ for _, tc := range []struct {
+ label string
+ event interface{}
+ want pointer.CursorName
+ }{
+ {label: "move inside",
+ event: _at(50, 50),
+ want: pointer.CursorPointer,
+ },
+ {label: "move outside",
+ event: _at(200, 200),
+ want: pointer.CursorDefault,
+ },
+ {label: "move back inside",
+ event: _at(50, 50),
+ want: pointer.CursorPointer,
+ },
+ {label: "send key events while inside",
+ event: []event.Event{
+ key.Event{Name: "A", State: key.Press},
+ key.Event{Name: "A", State: key.Release},
+ },
+ want: pointer.CursorPointer,
+ },
+ {label: "send key events while outside",
+ event: []event.Event{
+ _at(200, 200),
+ key.Event{Name: "A", State: key.Press},
+ key.Event{Name: "A", State: key.Release},
+ },
+ want: pointer.CursorDefault,
+ },
+ {label: "add new input on top while inside",
+ event: func() []event.Event {
+ widget2 = func() {
+ pointer.InputOp{Tag: &h2}.Add(ops)
+ pointer.CursorNameOp{Name: pointer.CursorCrossHair}.Add(ops)
+ }
+ return []event.Event{
+ _at(50, 50),
+ key.Event{
+ Name: "A",
+ State: key.Press,
+ },
+ }
+ },
+ want: pointer.CursorCrossHair,
+ },
+ {label: "remove input on top while inside",
+ event: func() []event.Event {
+ widget2 = nil
+ return []event.Event{
+ _at(50, 50),
+ key.Event{
+ Name: "A",
+ State: key.Press,
+ },
+ }
+ },
+ want: pointer.CursorPointer,
+ },
+ } {
+ t.Run(tc.label, func(t *testing.T) {
+ ops.Reset()
+ widget()
+ r.Frame(ops)
+ switch ev := tc.event.(type) {
+ case event.Event:
+ r.Queue(ev)
+ case []event.Event:
+ r.Queue(ev...)
+ case func() event.Event:
+ r.Queue(ev())
+ case func() []event.Event:
+ r.Queue(ev()...)
+ default:
+ panic(fmt.Sprintf("unkown event %T", ev))
+ }
+ widget()
+ r.Frame(ops)
+ // The cursor should now have been changed if the mouse moved over the declared area.
+ if got, want := r.Cursor(), tc.want; got != want {
+ t.Errorf("got %q; want %q", got, want)
+ }
+ })
+ }
+}
+
+// addPointerHandler adds a pointer.InputOp for the tag in a
+// rectangular area.
+func addPointerHandler(ops *op.Ops, tag event.Tag, area image.Rectangle) {
+ defer op.Save(ops).Load()
+ pointer.Rect(area).Add(ops)
+ pointer.InputOp{
+ Tag: tag,
+ Types: pointer.Press | pointer.Release | pointer.Move | pointer.Drag | pointer.Enter | pointer.Leave,
+ }.Add(ops)
+}
+
+// pointerTypes converts a sequence of event.Event to their pointer.Types. It assumes
+// that all input events are of underlying type pointer.Event, and thus will
+// panic if some are not.
+func pointerTypes(events []event.Event) []pointer.Type {
+ var types []pointer.Type
+ for _, e := range events {
+ if e, ok := e.(pointer.Event); ok {
+ types = append(types, e.Type)
+ }
+ }
+ return types
+}
+
+// assertEventSequence checks that the provided events match the expected pointer event types
+// in the provided order.
+func assertEventSequence(t *testing.T, events []event.Event,
+ expected ...pointer.Type) {
+ t.Helper()
+ got := pointerTypes(events)
+ if !reflect.DeepEqual(got, expected) {
+ t.Errorf("expected %v events, got %v", expected, got)
+ }
+}
+
+// assertEventPriorities checks that the pointer.Event priorities of events match prios.
+func assertEventPriorities(t *testing.T, events []event.Event,
+ prios ...pointer.Priority) {
+ t.Helper()
+ var got []pointer.Priority
+ for _, e := range events {
+ if e, ok := e.(pointer.Event); ok {
+ got = append(got, e.Priority)
+ }
+ }
+ if !reflect.DeepEqual(got, prios) {
+ t.Errorf("expected priorities %v, got %v", prios, got)
+ }
+}
+
+// assertScrollEvent checks that the event scrolling amount matches the supplied value.
+func assertScrollEvent(t *testing.T, ev event.Event, scroll f32.Point) {
+ t.Helper()
+ if got, want := ev.(pointer.Event).Scroll, scroll; got != want {
+ t.Errorf("got %v; want %v", got, want)
+ }
+}
+
+func BenchmarkRouterAdd(b *testing.B) {
+ // Set this to the number of overlapping handlers that you want to
+ // evaluate performance for. Typical values for the example applications
+ // are 1-3, though checking highers values helps evaluate performance for
+ // more complex applications.
+ const startingHandlerCount = 3
+ const maxHandlerCount = 100
+ for i := startingHandlerCount; i < maxHandlerCount; i *= 3 {
+ handlerCount := i
+ b.Run(fmt.Sprintf("%d-handlers", i), func(b *testing.B) {
+ handlers := make([]event.Tag, handlerCount)
+ for i := 0; i < handlerCount; i++ {
+ h := new(int)
+ *h = i
+ handlers[i] = h
+ }
+ var ops op.Ops
+
+ for i := range handlers {
+ pointer.Rect(image.Rectangle{
+ Max: image.Point{
+ X: 100,
+ Y: 100,
+ },
+ }).Add(&ops)
+ pointer.InputOp{
+ Tag: handlers[i],
+ Types: pointer.Move,
+ }.Add(&ops)
+ }
+ var r Router
+ r.Frame(&ops)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ }
+ })
+ }
+}
+
+var benchAreaOp areaOp
+
+func BenchmarkAreaOp_Decode(b *testing.B) {
+ ops := new(op.Ops)
+ pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops)
+ for i := 0; i < b.N; i++ {
+ benchAreaOp.Decode(ops.Data())
+ }
+}
+
+func BenchmarkAreaOp_Hit(b *testing.B) {
+ ops := new(op.Ops)
+ pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops)
+ benchAreaOp.Decode(ops.Data())
+ for i := 0; i < b.N; i++ {
+ benchAreaOp.Hit(f32.Pt(50, 50))
+ }
+}
diff --git a/gio/giold/io/router/router.go b/gio/giold/io/router/router.go
new file mode 100644
index 0000000..f7e251b
--- /dev/null
+++ b/gio/giold/io/router/router.go
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package router implements Router, a event.Queue implementation
+that that disambiguates and routes events to handlers declared
+in operation lists.
+
+Router is used by app.Window and is otherwise only useful for
+using Gio with external window implementations.
+*/
+package router
+
+import (
+ "encoding/binary"
+ "time"
+
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/profile"
+ "realy.lol/gio/op"
+)
+
+// Router is a Queue implementation that routes events
+// to handlers declared in operation lists.
+type Router struct {
+ pqueue pointerQueue
+ kqueue keyQueue
+ cqueue clipboardQueue
+
+ handlers handlerEvents
+
+ reader ops.Reader
+
+ // InvalidateOp summary.
+ wakeup bool
+ wakeupTime time.Time
+
+ // ProfileOp summary.
+ profHandlers map[event.Tag]struct{}
+ profile profile.Event
+}
+
+type handlerEvents struct {
+ handlers map[event.Tag][]event.Event
+ hadEvents bool
+}
+
+// Events returns the available events for the handler key.
+func (q *Router) Events(k event.Tag) []event.Event {
+ events := q.handlers.Events(k)
+ if _, isprof := q.profHandlers[k]; isprof {
+ delete(q.profHandlers, k)
+ events = append(events, q.profile)
+ }
+ return events
+}
+
+// Frame replaces the declared handlers from the supplied
+// operation list. The text input state, wakeup time and whether
+// there are active profile handlers is also saved.
+func (q *Router) Frame(ops *op.Ops) {
+ q.handlers.Clear()
+ q.wakeup = false
+ for k := range q.profHandlers {
+ delete(q.profHandlers, k)
+ }
+ q.reader.Reset(ops)
+ q.collect()
+
+ q.pqueue.Frame(ops, &q.handlers)
+ q.kqueue.Frame(ops, &q.handlers)
+ if q.handlers.HadEvents() {
+ q.wakeup = true
+ q.wakeupTime = time.Time{}
+ }
+}
+
+// Queue an event and report whether at least one handler had an event queued.
+func (q *Router) Queue(events ...event.Event) bool {
+ for _, e := range events {
+ switch e := e.(type) {
+ case profile.Event:
+ q.profile = e
+ case pointer.Event:
+ q.pqueue.Push(e, &q.handlers)
+ case key.EditEvent, key.Event, key.FocusEvent:
+ q.kqueue.Push(e, &q.handlers)
+ case clipboard.Event:
+ q.cqueue.Push(e, &q.handlers)
+ }
+ }
+ return q.handlers.HadEvents()
+}
+
+// TextInputState returns the input state from the most recent
+// call to Frame.
+func (q *Router) TextInputState() TextInputState {
+ return q.kqueue.InputState()
+}
+
+// WriteClipboard returns the most recent text to be copied
+// to the clipboard, if any.
+func (q *Router) WriteClipboard() (string, bool) {
+ return q.cqueue.WriteClipboard()
+}
+
+// ReadClipboard reports if any new handler is waiting
+// to read the clipboard.
+func (q *Router) ReadClipboard() bool {
+ return q.cqueue.ReadClipboard()
+}
+
+// Cursor returns the last cursor set.
+func (q *Router) Cursor() pointer.CursorName {
+ return q.pqueue.cursor
+}
+
+func (q *Router) collect() {
+ for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
+ switch opconst.OpType(encOp.Data[0]) {
+ case opconst.TypeInvalidate:
+ op := decodeInvalidateOp(encOp.Data)
+ if !q.wakeup || op.At.Before(q.wakeupTime) {
+ q.wakeup = true
+ q.wakeupTime = op.At
+ }
+ case opconst.TypeProfile:
+ op := decodeProfileOp(encOp.Data, encOp.Refs)
+ if q.profHandlers == nil {
+ q.profHandlers = make(map[event.Tag]struct{})
+ }
+ q.profHandlers[op.Tag] = struct{}{}
+ case opconst.TypeClipboardRead:
+ q.cqueue.ProcessReadClipboard(encOp.Data, encOp.Refs)
+ case opconst.TypeClipboardWrite:
+ q.cqueue.ProcessWriteClipboard(encOp.Data, encOp.Refs)
+ }
+ }
+}
+
+// Profiling reports whether there was profile handlers in the
+// most recent Frame call.
+func (q *Router) Profiling() bool {
+ return len(q.profHandlers) > 0
+}
+
+// WakeupTime returns the most recent time for doing another frame,
+// as determined from the last call to Frame.
+func (q *Router) WakeupTime() (time.Time, bool) {
+ return q.wakeupTime, q.wakeup
+}
+
+func (h *handlerEvents) init() {
+ if h.handlers == nil {
+ h.handlers = make(map[event.Tag][]event.Event)
+ }
+}
+
+func (h *handlerEvents) AddNoRedraw(k event.Tag, e event.Event) {
+ h.init()
+ h.handlers[k] = append(h.handlers[k], e)
+}
+
+func (h *handlerEvents) Add(k event.Tag, e event.Event) {
+ h.AddNoRedraw(k, e)
+ h.hadEvents = true
+}
+
+func (h *handlerEvents) HadEvents() bool {
+ u := h.hadEvents
+ h.hadEvents = false
+ return u
+}
+
+func (h *handlerEvents) Events(k event.Tag) []event.Event {
+ if events, ok := h.handlers[k]; ok {
+ h.handlers[k] = h.handlers[k][:0]
+ // Schedule another frame if we delivered events to the user
+ // to flush half-updated state. This is important when an
+ // event changes UI state that has already been laid out. In
+ // the worst case, we waste a frame, increasing power usage.
+ //
+ // Gio is expected to grow the ability to construct
+ // frame-to-frame differences and only render to changed
+ // areas. In that case, the waste of a spurious frame should
+ // be minimal.
+ h.hadEvents = h.hadEvents || len(events) > 0
+ return events
+ }
+ return nil
+}
+
+func (h *handlerEvents) Clear() {
+ for k := range h.handlers {
+ delete(h.handlers, k)
+ }
+}
+
+func decodeProfileOp(d []byte, refs []interface{}) profile.Op {
+ if opconst.OpType(d[0]) != opconst.TypeProfile {
+ panic("invalid op")
+ }
+ return profile.Op{
+ Tag: refs[0].(event.Tag),
+ }
+}
+
+func decodeInvalidateOp(d []byte) op.InvalidateOp {
+ bo := binary.LittleEndian
+ if opconst.OpType(d[0]) != opconst.TypeInvalidate {
+ panic("invalid op")
+ }
+ var o op.InvalidateOp
+ if nanos := bo.Uint64(d[1:]); nanos > 0 {
+ o.At = time.Unix(0, int64(nanos))
+ }
+ return o
+}
diff --git a/gio/giold/io/system/system.go b/gio/giold/io/system/system.go
new file mode 100644
index 0000000..14e4dd7
--- /dev/null
+++ b/gio/giold/io/system/system.go
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package system contains events usually handled at the top-level
+// program level.
+package system
+
+import (
+ "image"
+ "time"
+
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+)
+
+// A FrameEvent requests a new frame in the form of a list of
+// operations that describes what to display and how to handle
+// input.
+type FrameEvent struct {
+ // Now is the current animation. Use Now instead of time.Now to
+ // synchronize animation and to avoid the time.Now call overhead.
+ Now time.Time
+ // Metric converts device independent dp and sp to device pixels.
+ Metric unit.Metric
+ // Size is the dimensions of the window.
+ Size image.Point
+ // Insets is the insets to apply.
+ Insets Insets
+ // Frame is the callback to supply the list of
+ // operations to complete the FrameEvent.
+ //
+ // Note that the operation list and the operations themselves
+ // may not be mutated until another FrameEvent is received from
+ // the same event source.
+ // That means that calls to frame.Reset and changes to referenced
+ // data such as ImageOp backing images should happen between
+ // receiving a FrameEvent and calling Frame.
+ //
+ // Example:
+ //
+ // var w *app.Window
+ // var frame *op.Ops
+ // for e := range w.Events() {
+ // if e, ok := e.(system.FrameEvent); ok {
+ // // Call frame.Reset and manipulate images for ImageOps
+ // // here.
+ // e.Frame(frame)
+ // }
+ // }
+ Frame func(frame *op.Ops)
+ // Queue supplies the events for event handlers.
+ Queue event.Queue
+}
+
+// DestroyEvent is the last event sent through
+// a window event channel.
+type DestroyEvent struct {
+ // Err is nil for normal window closures. If a
+ // window is prematurely closed, Err is the cause.
+ Err error
+}
+
+// Insets is the space taken up by
+// system decoration such as translucent
+// system bars and software keyboards.
+type Insets struct {
+ Top, Bottom, Left, Right unit.Value
+}
+
+// A StageEvent is generated whenever the stage of a
+// Window changes.
+type StageEvent struct {
+ Stage Stage
+}
+
+// CommandEvent is a system event. Unlike most other events, CommandEvent is
+// delivered as a pointer to allow Cancel to suppress it.
+type CommandEvent struct {
+ Type CommandType
+ // Cancel suppress the default action of the command.
+ Cancel bool
+}
+
+// Stage of a Window.
+type Stage uint8
+
+// CommandType is the type of a CommandEvent.
+type CommandType uint8
+
+const (
+ // StagePaused is the Stage for inactive Windows.
+ // Inactive Windows don't receive FrameEvents.
+ StagePaused Stage = iota
+ // StateRunning is for active Windows.
+ StageRunning
+)
+
+const (
+ // CommandBack is the command for a back action
+ // such as the Android back button.
+ CommandBack CommandType = iota
+)
+
+func (l Stage) String() string {
+ switch l {
+ case StagePaused:
+ return "StagePaused"
+ case StageRunning:
+ return "StageRunning"
+ default:
+ panic("unexpected Stage value")
+ }
+}
+
+func (FrameEvent) ImplementsEvent() {}
+func (StageEvent) ImplementsEvent() {}
+func (*CommandEvent) ImplementsEvent() {}
+func (DestroyEvent) ImplementsEvent() {}
diff --git a/gio/giold/layout/alloc_test.go b/gio/giold/layout/alloc_test.go
new file mode 100644
index 0000000..1df19e9
--- /dev/null
+++ b/gio/giold/layout/alloc_test.go
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build !race
+// +build !race
+
+package layout
+
+import (
+ "image"
+ "testing"
+
+ "realy.lol/gio/op"
+)
+
+func TestStackAllocs(t *testing.T) {
+ var ops op.Ops
+ allocs := testing.AllocsPerRun(1, func() {
+ ops.Reset()
+ gtx := Context{
+ Ops: &ops,
+ }
+ Stack{}.Layout(gtx,
+ Stacked(func(gtx Context) Dimensions {
+ return Dimensions{Size: image.Point{X: 50, Y: 50}}
+ }),
+ )
+ })
+ if allocs != 0 {
+ t.Errorf("expected no allocs, got %f", allocs)
+ }
+}
+
+func TestFlexAllocs(t *testing.T) {
+ var ops op.Ops
+ allocs := testing.AllocsPerRun(1, func() {
+ ops.Reset()
+ gtx := Context{
+ Ops: &ops,
+ }
+ Flex{}.Layout(gtx,
+ Rigid(func(gtx Context) Dimensions {
+ return Dimensions{Size: image.Point{X: 50, Y: 50}}
+ }),
+ )
+ })
+ if allocs != 0 {
+ t.Errorf("expected no allocs, got %f", allocs)
+ }
+}
diff --git a/gio/giold/layout/context.go b/gio/giold/layout/context.go
new file mode 100644
index 0000000..4f8d2c8
--- /dev/null
+++ b/gio/giold/layout/context.go
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "time"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+)
+
+// Context carries the state needed by almost all layouts and widgets.
+// A zero value Context never returns events, map units to pixels
+// with a scale of 1.0, and returns the zero time from Now.
+type Context struct {
+ // Constraints track the constraints for the active widget or
+ // layout.
+ Constraints Constraints
+
+ Metric unit.Metric
+ // By convention, a nil Queue is a signal to widgets to draw themselves
+ // in a disabled state.
+ Queue event.Queue
+ // Now is the animation time.
+ Now time.Time
+
+ *op.Ops
+}
+
+// NewContext is a shorthand for
+//
+// Context{
+// Ops: ops,
+// Now: e.Now,
+// Queue: e.Queue,
+// Config: e.Config,
+// Constraints: Exact(e.Size),
+// }
+//
+// NewContext calls ops.Reset and adjusts ops for e.Insets.
+func NewContext(ops *op.Ops, e system.FrameEvent) Context {
+ ops.Reset()
+
+ size := e.Size
+
+ if e.Insets != (system.Insets{}) {
+ left := e.Metric.Px(e.Insets.Left)
+ top := e.Metric.Px(e.Insets.Top)
+ op.Offset(f32.Point{
+ X: float32(left),
+ Y: float32(top),
+ }).Add(ops)
+
+ size.X -= left + e.Metric.Px(e.Insets.Right)
+ size.Y -= top + e.Metric.Px(e.Insets.Bottom)
+ }
+
+ return Context{
+ Ops: ops,
+ Now: e.Now,
+ Queue: e.Queue,
+ Metric: e.Metric,
+ Constraints: Exact(size),
+ }
+}
+
+// Px maps the value to pixels.
+func (c Context) Px(v unit.Value) int {
+ return c.Metric.Px(v)
+}
+
+// Events returns the events available for the key. If no
+// queue is configured, Events returns nil.
+func (c Context) Events(k event.Tag) []event.Event {
+ if c.Queue == nil {
+ return nil
+ }
+ return c.Queue.Events(k)
+}
+
+// Disabled returns a copy of this context with a nil Queue,
+// blocking events to widgets using it.
+//
+// By convention, a nil Queue is a signal to widgets to draw themselves
+// in a disabled state.
+func (c Context) Disabled() Context {
+ c.Queue = nil
+ return c
+}
diff --git a/gio/giold/layout/doc.go b/gio/giold/layout/doc.go
new file mode 100644
index 0000000..3824084
--- /dev/null
+++ b/gio/giold/layout/doc.go
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package layout implements layouts common to GUI programs.
+
+Constraints and dimensions
+
+Constraints and dimensions form the interface between layouts and
+interface child elements. This package operates on Widgets, functions
+that compute Dimensions from a a set of constraints for acceptable
+widths and heights. Both the constraints and dimensions are maintained
+in an implicit Context to keep the Widget declaration short.
+
+For example, to add space above a widget:
+
+ var gtx layout.Context
+
+ // Configure a top inset.
+ inset := layout.Inset{Top: unit.Dp(8), ...}
+ // Use the inset to lay out a widget.
+ inset.Layout(gtx, func() {
+ // Lay out widget and determine its size given the constraints
+ // in gtx.Constraints.
+ ...
+ return layout.Dimensions{...}
+ })
+
+Note that the example does not generate any garbage even though the
+Inset is transient. Layouts that don't accept user input are designed
+to not escape to the heap during their use.
+
+Layout operations are recursive: a child in a layout operation can
+itself be another layout. That way, complex user interfaces can
+be created from a few generic layouts.
+
+This example both aligns and insets a child:
+
+ inset := layout.Inset{...}
+ inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ align := layout.Alignment(...)
+ return align.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return widget.Layout(gtx, ...)
+ })
+ })
+
+More complex layouts such as Stack and Flex lay out multiple children,
+and stateful layouts such as List accept user input.
+
+*/
+package layout
diff --git a/gio/giold/layout/example_test.go b/gio/giold/layout/example_test.go
new file mode 100644
index 0000000..9636c8d
--- /dev/null
+++ b/gio/giold/layout/example_test.go
@@ -0,0 +1,137 @@
+package layout_test
+
+import (
+ "fmt"
+ "image"
+
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+)
+
+func ExampleInset() {
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ // Loose constraints with no minimal size.
+ Constraints: layout.Constraints{
+ Max: image.Point{X: 100, Y: 100},
+ },
+ }
+
+ // Inset all edges by 10.
+ inset := layout.UniformInset(unit.Dp(10))
+ dims := inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ // Lay out a 50x50 sized widget.
+ dims := layoutWidget(gtx, 50, 50)
+ fmt.Println(dims.Size)
+ return dims
+ })
+
+ fmt.Println(dims.Size)
+
+ // Output:
+ // (50,50)
+ // (70,70)
+}
+
+func ExampleDirection() {
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ // Rigid constraints with both minimum and maximum set.
+ Constraints: layout.Exact(image.Point{X: 100, Y: 100}),
+ }
+
+ dims := layout.Center.Layout(gtx,
+ func(gtx layout.Context) layout.Dimensions {
+ // Lay out a 50x50 sized widget.
+ dims := layoutWidget(gtx, 50, 50)
+ fmt.Println(dims.Size)
+ return dims
+ })
+
+ fmt.Println(dims.Size)
+
+ // Output:
+ // (50,50)
+ // (100,100)
+}
+
+func ExampleFlex() {
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ // Rigid constraints with both minimum and maximum set.
+ Constraints: layout.Exact(image.Point{X: 100, Y: 100}),
+ }
+
+ layout.Flex{WeightSum: 2}.Layout(gtx,
+ // Rigid 10x10 widget.
+ layout.Rigid(func(gtx layout.Context) layout.Dimensions {
+ fmt.Printf("Rigid: %v\n", gtx.Constraints)
+ return layoutWidget(gtx, 10, 10)
+ }),
+ // Child with 50% space allowance.
+ layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
+ fmt.Printf("50%%: %v\n", gtx.Constraints)
+ return layoutWidget(gtx, 10, 10)
+ }),
+ )
+
+ // Output:
+ // Rigid: {(0,100) (100,100)}
+ // 50%: {(45,100) (45,100)}
+}
+
+func ExampleStack() {
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Constraints{
+ Max: image.Point{X: 100, Y: 100},
+ },
+ }
+
+ layout.Stack{}.Layout(gtx,
+ // Force widget to the same size as the second.
+ layout.Expanded(func(gtx layout.Context) layout.Dimensions {
+ fmt.Printf("Expand: %v\n", gtx.Constraints)
+ return layoutWidget(gtx, 10, 10)
+ }),
+ // Rigid 50x50 widget.
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ return layoutWidget(gtx, 50, 50)
+ }),
+ )
+
+ // Output:
+ // Expand: {(50,50) (100,100)}
+}
+
+func ExampleList() {
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ // Rigid constraints with both minimum and maximum set.
+ Constraints: layout.Exact(image.Point{X: 100, Y: 100}),
+ }
+
+ // The list is 1e6 elements, but only 5 fit the constraints.
+ const listLen = 1e6
+
+ var list layout.List
+ list.Layout(gtx, listLen,
+ func(gtx layout.Context, i int) layout.Dimensions {
+ return layoutWidget(gtx, 20, 20)
+ })
+
+ fmt.Println(list.Position.Count)
+
+ // Output:
+ // 5
+}
+
+func layoutWidget(ctx layout.Context, width, height int) layout.Dimensions {
+ return layout.Dimensions{
+ Size: image.Point{
+ X: width,
+ Y: height,
+ },
+ }
+}
diff --git a/gio/giold/layout/flex.go b/gio/giold/layout/flex.go
new file mode 100644
index 0000000..50d936d
--- /dev/null
+++ b/gio/giold/layout/flex.go
@@ -0,0 +1,241 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "image"
+
+ "realy.lol/gio/op"
+)
+
+// Flex lays out child elements along an axis,
+// according to alignment and weights.
+type Flex struct {
+ // Axis is the main axis, either Horizontal or Vertical.
+ Axis Axis
+ // Spacing controls the distribution of space left after
+ // layout.
+ Spacing Spacing
+ // Alignment is the alignment in the cross axis.
+ Alignment Alignment
+ // WeightSum is the sum of weights used for the weighted
+ // size of Flexed children. If WeightSum is zero, the sum
+ // of all Flexed weights is used.
+ WeightSum float32
+}
+
+// FlexChild is the descriptor for a Flex child.
+type FlexChild struct {
+ flex bool
+ weight float32
+
+ widget Widget
+
+ // Scratch space.
+ call op.CallOp
+ dims Dimensions
+}
+
+// Spacing determine the spacing mode for a Flex.
+type Spacing uint8
+
+const (
+ // SpaceEnd leaves space at the end.
+ SpaceEnd Spacing = iota
+ // SpaceStart leaves space at the start.
+ SpaceStart
+ // SpaceSides shares space between the start and end.
+ SpaceSides
+ // SpaceAround distributes space evenly between children,
+ // with half as much space at the start and end.
+ SpaceAround
+ // SpaceBetween distributes space evenly between children,
+ // leaving no space at the start and end.
+ SpaceBetween
+ // SpaceEvenly distributes space evenly between children and
+ // at the start and end.
+ SpaceEvenly
+)
+
+// Rigid returns a Flex child with a maximal constraint of the
+// remaining space.
+func Rigid(widget Widget) FlexChild {
+ return FlexChild{
+ widget: widget,
+ }
+}
+
+// Flexed returns a Flex child forced to take up weight fraction of the
+// space left over from Rigid children. The fraction is weight
+// divided by either the weight sum of all Flexed children or the Flex
+// WeightSum if non zero.
+func Flexed(weight float32, widget Widget) FlexChild {
+ return FlexChild{
+ flex: true,
+ weight: weight,
+ widget: widget,
+ }
+}
+
+// Layout a list of children. The position of the children are
+// determined by the specified order, but Rigid children are laid out
+// before Flexed children.
+func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions {
+ size := 0
+ cs := gtx.Constraints
+ mainMin, mainMax := f.Axis.mainConstraint(cs)
+ crossMin, crossMax := f.Axis.crossConstraint(cs)
+ remaining := mainMax
+ var totalWeight float32
+ cgtx := gtx
+ // Lay out Rigid children.
+ for i, child := range children {
+ if child.flex {
+ totalWeight += child.weight
+ continue
+ }
+ macro := op.Record(gtx.Ops)
+ cgtx.Constraints = f.Axis.constraints(0, remaining, crossMin, crossMax)
+ dims := child.widget(cgtx)
+ c := macro.Stop()
+ sz := f.Axis.Convert(dims.Size).X
+ size += sz
+ remaining -= sz
+ if remaining < 0 {
+ remaining = 0
+ }
+ children[i].call = c
+ children[i].dims = dims
+ }
+ if w := f.WeightSum; w != 0 {
+ totalWeight = w
+ }
+ // fraction is the rounding error from a Flex weighting.
+ var fraction float32
+ flexTotal := remaining
+ // Lay out Flexed children.
+ for i, child := range children {
+ if !child.flex {
+ continue
+ }
+ var flexSize int
+ if remaining > 0 && totalWeight > 0 {
+ // Apply weight and add any leftover fraction from a
+ // previous Flexed.
+ childSize := float32(flexTotal) * child.weight / totalWeight
+ flexSize = int(childSize + fraction + .5)
+ fraction = childSize - float32(flexSize)
+ if flexSize > remaining {
+ flexSize = remaining
+ }
+ }
+ macro := op.Record(gtx.Ops)
+ cgtx.Constraints = f.Axis.constraints(flexSize, flexSize, crossMin,
+ crossMax)
+ dims := child.widget(cgtx)
+ c := macro.Stop()
+ sz := f.Axis.Convert(dims.Size).X
+ size += sz
+ remaining -= sz
+ if remaining < 0 {
+ remaining = 0
+ }
+ children[i].call = c
+ children[i].dims = dims
+ }
+ var maxCross int
+ var maxBaseline int
+ for _, child := range children {
+ if c := f.Axis.Convert(child.dims.Size).Y; c > maxCross {
+ maxCross = c
+ }
+ if b := child.dims.Size.Y - child.dims.Baseline; b > maxBaseline {
+ maxBaseline = b
+ }
+ }
+ var space int
+ if mainMin > size {
+ space = mainMin - size
+ }
+ var mainSize int
+ switch f.Spacing {
+ case SpaceSides:
+ mainSize += space / 2
+ case SpaceStart:
+ mainSize += space
+ case SpaceEvenly:
+ mainSize += space / (1 + len(children))
+ case SpaceAround:
+ if len(children) > 0 {
+ mainSize += space / (len(children) * 2)
+ }
+ }
+ for i, child := range children {
+ dims := child.dims
+ b := dims.Size.Y - dims.Baseline
+ var cross int
+ switch f.Alignment {
+ case End:
+ cross = maxCross - f.Axis.Convert(dims.Size).Y
+ case Middle:
+ cross = (maxCross - f.Axis.Convert(dims.Size).Y) / 2
+ case Baseline:
+ if f.Axis == Horizontal {
+ cross = maxBaseline - b
+ }
+ }
+ stack := op.Save(gtx.Ops)
+ pt := f.Axis.Convert(image.Pt(mainSize, cross))
+ op.Offset(FPt(pt)).Add(gtx.Ops)
+ child.call.Add(gtx.Ops)
+ stack.Load()
+ mainSize += f.Axis.Convert(dims.Size).X
+ if i < len(children)-1 {
+ switch f.Spacing {
+ case SpaceEvenly:
+ mainSize += space / (1 + len(children))
+ case SpaceAround:
+ if len(children) > 0 {
+ mainSize += space / len(children)
+ }
+ case SpaceBetween:
+ if len(children) > 1 {
+ mainSize += space / (len(children) - 1)
+ }
+ }
+ }
+ }
+ switch f.Spacing {
+ case SpaceSides:
+ mainSize += space / 2
+ case SpaceEnd:
+ mainSize += space
+ case SpaceEvenly:
+ mainSize += space / (1 + len(children))
+ case SpaceAround:
+ if len(children) > 0 {
+ mainSize += space / (len(children) * 2)
+ }
+ }
+ sz := f.Axis.Convert(image.Pt(mainSize, maxCross))
+ return Dimensions{Size: sz, Baseline: sz.Y - maxBaseline}
+}
+
+func (s Spacing) String() string {
+ switch s {
+ case SpaceEnd:
+ return "SpaceEnd"
+ case SpaceStart:
+ return "SpaceStart"
+ case SpaceSides:
+ return "SpaceSides"
+ case SpaceAround:
+ return "SpaceAround"
+ case SpaceBetween:
+ return "SpaceAround"
+ case SpaceEvenly:
+ return "SpaceEvenly"
+ default:
+ panic("unreachable")
+ }
+}
diff --git a/gio/giold/layout/layout.go b/gio/giold/layout/layout.go
new file mode 100644
index 0000000..6a4bdd2
--- /dev/null
+++ b/gio/giold/layout/layout.go
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "image"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+)
+
+// Constraints represent the minimum and maximum size of a widget.
+//
+// A widget does not have to treat its constraints as "hard". For
+// example, if it's passed a constraint with a minimum size that's
+// smaller than its actual minimum size, it should return its minimum
+// size dimensions instead. Parent widgets should deal appropriately
+// with child widgets that return dimensions that do not fit their
+// constraints (for example, by clipping).
+type Constraints struct {
+ Min, Max image.Point
+}
+
+// Dimensions are the resolved size and baseline for a widget.
+//
+// Baseline is the distance from the bottom of a widget to the baseline of
+// any text it contains (or 0). The purpose is to be able to align text
+// that span multiple widgets.
+type Dimensions struct {
+ Size image.Point
+ Baseline int
+}
+
+// Axis is the Horizontal or Vertical direction.
+type Axis uint8
+
+// Alignment is the mutual alignment of a list of widgets.
+type Alignment uint8
+
+// Direction is the alignment of widgets relative to a containing
+// space.
+type Direction uint8
+
+// Widget is a function scope for drawing, processing events and
+// computing dimensions for a user interface element.
+type Widget func(gtx Context) Dimensions
+
+const (
+ Start Alignment = iota
+ End
+ Middle
+ Baseline
+)
+
+const (
+ NW Direction = iota
+ N
+ NE
+ E
+ SE
+ S
+ SW
+ W
+ Center
+)
+
+const (
+ Horizontal Axis = iota
+ Vertical
+)
+
+// Exact returns the Constraints with the minimum and maximum size
+// set to size.
+func Exact(size image.Point) Constraints {
+ return Constraints{
+ Min: size, Max: size,
+ }
+}
+
+// FPt converts an point to a f32.Point.
+func FPt(p image.Point) f32.Point {
+ return f32.Point{
+ X: float32(p.X), Y: float32(p.Y),
+ }
+}
+
+// FRect converts a rectangle to a f32.Rectangle.
+func FRect(r image.Rectangle) f32.Rectangle {
+ return f32.Rectangle{
+ Min: FPt(r.Min), Max: FPt(r.Max),
+ }
+}
+
+// Constrain a size so each dimension is in the range [min;max].
+func (c Constraints) Constrain(size image.Point) image.Point {
+ if min := c.Min.X; size.X < min {
+ size.X = min
+ }
+ if min := c.Min.Y; size.Y < min {
+ size.Y = min
+ }
+ if max := c.Max.X; size.X > max {
+ size.X = max
+ }
+ if max := c.Max.Y; size.Y > max {
+ size.Y = max
+ }
+ return size
+}
+
+// Inset adds space around a widget by decreasing its maximum
+// constraints. The minimum constraints will be adjusted to ensure
+// they do not exceed the maximum.
+type Inset struct {
+ Top, Right, Bottom, Left unit.Value
+}
+
+// Layout a widget.
+func (in Inset) Layout(gtx Context, w Widget) Dimensions {
+ top := gtx.Px(in.Top)
+ right := gtx.Px(in.Right)
+ bottom := gtx.Px(in.Bottom)
+ left := gtx.Px(in.Left)
+ mcs := gtx.Constraints
+ mcs.Max.X -= left + right
+ if mcs.Max.X < 0 {
+ left = 0
+ right = 0
+ mcs.Max.X = 0
+ }
+ if mcs.Min.X > mcs.Max.X {
+ mcs.Min.X = mcs.Max.X
+ }
+ mcs.Max.Y -= top + bottom
+ if mcs.Max.Y < 0 {
+ bottom = 0
+ top = 0
+ mcs.Max.Y = 0
+ }
+ if mcs.Min.Y > mcs.Max.Y {
+ mcs.Min.Y = mcs.Max.Y
+ }
+ stack := op.Save(gtx.Ops)
+ op.Offset(FPt(image.Point{X: left, Y: top})).Add(gtx.Ops)
+ gtx.Constraints = mcs
+ dims := w(gtx)
+ stack.Load()
+ return Dimensions{
+ Size: dims.Size.Add(image.Point{X: right + left, Y: top + bottom}),
+ Baseline: dims.Baseline + bottom,
+ }
+}
+
+// UniformInset returns an Inset with a single inset applied to all
+// edges.
+func UniformInset(v unit.Value) Inset {
+ return Inset{Top: v, Right: v, Bottom: v, Left: v}
+}
+
+// Layout a widget according to the direction.
+// The widget is called with the context constraints minimum cleared.
+func (d Direction) Layout(gtx Context, w Widget) Dimensions {
+ macro := op.Record(gtx.Ops)
+ cs := gtx.Constraints
+ gtx.Constraints.Min = image.Point{}
+ dims := w(gtx)
+ call := macro.Stop()
+ sz := dims.Size
+ if sz.X < cs.Min.X {
+ sz.X = cs.Min.X
+ }
+ if sz.Y < cs.Min.Y {
+ sz.Y = cs.Min.Y
+ }
+
+ defer op.Save(gtx.Ops).Load()
+ p := d.Position(dims.Size, sz)
+ op.Offset(FPt(p)).Add(gtx.Ops)
+ call.Add(gtx.Ops)
+
+ return Dimensions{
+ Size: sz,
+ Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y,
+ }
+}
+
+// Position calculates widget position according to the direction.
+func (d Direction) Position(widget, bounds image.Point) image.Point {
+ var p image.Point
+
+ switch d {
+ case N, S, Center:
+ p.X = (bounds.X - widget.X) / 2
+ case NE, SE, E:
+ p.X = bounds.X - widget.X
+ }
+
+ switch d {
+ case W, Center, E:
+ p.Y = (bounds.Y - widget.Y) / 2
+ case SW, S, SE:
+ p.Y = bounds.Y - widget.Y
+ }
+
+ return p
+}
+
+// Spacer adds space between widgets.
+type Spacer struct {
+ Width, Height unit.Value
+}
+
+func (s Spacer) Layout(gtx Context) Dimensions {
+ return Dimensions{
+ Size: image.Point{
+ X: gtx.Px(s.Width),
+ Y: gtx.Px(s.Height),
+ },
+ }
+}
+
+func (a Alignment) String() string {
+ switch a {
+ case Start:
+ return "Start"
+ case End:
+ return "End"
+ case Middle:
+ return "Middle"
+ case Baseline:
+ return "Baseline"
+ default:
+ panic("unreachable")
+ }
+}
+
+// Convert a point in (x, y) coordinates to (main, cross) coordinates,
+// or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged
+// for the horizontal axis, or (y, x) for the vertical axis.
+func (a Axis) Convert(pt image.Point) image.Point {
+ if a == Horizontal {
+ return pt
+ }
+ return image.Pt(pt.Y, pt.X)
+}
+
+// mainConstraint returns the min and max main constraints for axis a.
+func (a Axis) mainConstraint(cs Constraints) (int, int) {
+ if a == Horizontal {
+ return cs.Min.X, cs.Max.X
+ }
+ return cs.Min.Y, cs.Max.Y
+}
+
+// crossConstraint returns the min and max cross constraints for axis a.
+func (a Axis) crossConstraint(cs Constraints) (int, int) {
+ if a == Horizontal {
+ return cs.Min.Y, cs.Max.Y
+ }
+ return cs.Min.X, cs.Max.X
+}
+
+// constraints returns the constraints for axis a.
+func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints {
+ if a == Horizontal {
+ return Constraints{Min: image.Pt(mainMin, crossMin),
+ Max: image.Pt(mainMax, crossMax)}
+ }
+ return Constraints{Min: image.Pt(crossMin, mainMin),
+ Max: image.Pt(crossMax, mainMax)}
+}
+
+func (a Axis) String() string {
+ switch a {
+ case Horizontal:
+ return "Horizontal"
+ case Vertical:
+ return "Vertical"
+ default:
+ panic("unreachable")
+ }
+}
+
+func (d Direction) String() string {
+ switch d {
+ case NW:
+ return "NW"
+ case N:
+ return "N"
+ case NE:
+ return "NE"
+ case E:
+ return "E"
+ case SE:
+ return "SE"
+ case S:
+ return "S"
+ case SW:
+ return "SW"
+ case W:
+ return "W"
+ case Center:
+ return "Center"
+ default:
+ panic("unreachable")
+ }
+}
diff --git a/gio/giold/layout/layout_test.go b/gio/giold/layout/layout_test.go
new file mode 100644
index 0000000..b04863c
--- /dev/null
+++ b/gio/giold/layout/layout_test.go
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "image"
+ "testing"
+
+ "realy.lol/gio/op"
+)
+
+func TestStack(t *testing.T) {
+ gtx := Context{
+ Ops: new(op.Ops),
+ Constraints: Constraints{
+ Max: image.Pt(100, 100),
+ },
+ }
+ exp := image.Point{X: 60, Y: 70}
+ dims := Stack{Alignment: Center}.Layout(gtx,
+ Expanded(func(gtx Context) Dimensions {
+ return Dimensions{Size: exp}
+ }),
+ Stacked(func(gtx Context) Dimensions {
+ return Dimensions{Size: image.Point{X: 50, Y: 50}}
+ }),
+ )
+ if got := dims.Size; got != exp {
+ t.Errorf("Stack ignored Expanded size, got %v expected %v", got, exp)
+ }
+}
diff --git a/gio/giold/layout/list.go b/gio/giold/layout/list.go
new file mode 100644
index 0000000..45c884e
--- /dev/null
+++ b/gio/giold/layout/list.go
@@ -0,0 +1,309 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "image"
+
+ "realy.lol/gio/gesture"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+)
+
+type scrollChild struct {
+ size image.Point
+ call op.CallOp
+}
+
+// List displays a subsection of a potentially infinitely
+// large underlying list. List accepts user input to scroll
+// the subsection.
+type List struct {
+ Axis Axis
+ // ScrollToEnd instructs the list to stay scrolled to the far end position
+ // once reached. A List with ScrollToEnd == true and Position.BeforeEnd ==
+ // false draws its content with the last item at the bottom of the list
+ // area.
+ ScrollToEnd bool
+ // Alignment is the cross axis alignment of list elements.
+ Alignment Alignment
+
+ cs Constraints
+ scroll gesture.Scroll
+ scrollDelta int
+
+ // Position is updated during Layout. To save the list scroll position,
+ // just save Position after Layout finishes. To scroll the list
+ // programmatically, update Position (e.g. restore it from a saved value)
+ // before calling Layout.
+ Position Position
+
+ len int
+
+ // maxSize is the total size of visible children.
+ maxSize int
+ children []scrollChild
+ dir iterationDir
+}
+
+// ListElement is a function that computes the dimensions of
+// a list element.
+type ListElement func(gtx Context, index int) Dimensions
+
+type iterationDir uint8
+
+// Position is a List scroll offset represented as an offset from the top edge
+// of a child element.
+type Position struct {
+ // BeforeEnd tracks whether the List position is before the very end. We
+ // use "before end" instead of "at end" so that the zero value of a
+ // Position struct is useful.
+ //
+ // When laying out a list, if ScrollToEnd is true and BeforeEnd is false,
+ // then First and Offset are ignored, and the list is drawn with the last
+ // item at the bottom. If ScrollToEnd is false then BeforeEnd is ignored.
+ BeforeEnd bool
+ // First is the index of the first visible child.
+ First int
+ // Offset is the distance in pixels from the top edge to the child at index
+ // First.
+ Offset int
+ // OffsetLast is the signed distance in pixels from the bottom edge to the
+ // bottom edge of the child at index First+Count.
+ OffsetLast int
+ // Count is the number of visible children.
+ Count int
+}
+
+const (
+ iterateNone iterationDir = iota
+ iterateForward
+ iterateBackward
+)
+
+const inf = 1e6
+
+// init prepares the list for iterating through its children with next.
+func (l *List) init(gtx Context, len int) {
+ if l.more() {
+ panic("unfinished child")
+ }
+ l.cs = gtx.Constraints
+ l.maxSize = 0
+ l.children = l.children[:0]
+ l.len = len
+ l.update(gtx)
+ if l.scrollToEnd() || l.Position.First > len {
+ l.Position.Offset = 0
+ l.Position.First = len
+ }
+}
+
+// Layout the List.
+func (l *List) Layout(gtx Context, len int, w ListElement) Dimensions {
+ l.init(gtx, len)
+ crossMin, crossMax := l.Axis.crossConstraint(gtx.Constraints)
+ gtx.Constraints = l.Axis.constraints(0, inf, crossMin, crossMax)
+ macro := op.Record(gtx.Ops)
+ for l.next(); l.more(); l.next() {
+ child := op.Record(gtx.Ops)
+ dims := w(gtx, l.index())
+ call := child.Stop()
+ l.end(dims, call)
+ }
+ return l.layout(gtx.Ops, macro)
+}
+
+func (l *List) scrollToEnd() bool {
+ return l.ScrollToEnd && !l.Position.BeforeEnd
+}
+
+// Dragging reports whether the List is being dragged.
+func (l *List) Dragging() bool {
+ return l.scroll.State() == gesture.StateDragging
+}
+
+func (l *List) update(gtx Context) {
+ d := l.scroll.Scroll(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis))
+ l.scrollDelta = d
+ l.Position.Offset += d
+}
+
+// next advances to the next child.
+func (l *List) next() {
+ l.dir = l.nextDir()
+ // The user scroll offset is applied after scrolling to
+ // list end.
+ if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 {
+ l.Position.BeforeEnd = true
+ l.Position.Offset += l.scrollDelta
+ l.dir = l.nextDir()
+ }
+}
+
+// index is current child's position in the underlying list.
+func (l *List) index() int {
+ switch l.dir {
+ case iterateBackward:
+ return l.Position.First - 1
+ case iterateForward:
+ return l.Position.First + len(l.children)
+ default:
+ panic("Index called before Next")
+ }
+}
+
+// more reports whether more children are needed.
+func (l *List) more() bool {
+ return l.dir != iterateNone
+}
+
+func (l *List) nextDir() iterationDir {
+ _, vsize := l.Axis.mainConstraint(l.cs)
+ last := l.Position.First + len(l.children)
+ // Clamp offset.
+ if l.maxSize-l.Position.Offset < vsize && last == l.len {
+ l.Position.Offset = l.maxSize - vsize
+ }
+ if l.Position.Offset < 0 && l.Position.First == 0 {
+ l.Position.Offset = 0
+ }
+ switch {
+ case len(l.children) == l.len:
+ return iterateNone
+ case l.maxSize-l.Position.Offset < vsize:
+ return iterateForward
+ case l.Position.Offset < 0:
+ return iterateBackward
+ }
+ return iterateNone
+}
+
+// End the current child by specifying its dimensions.
+func (l *List) end(dims Dimensions, call op.CallOp) {
+ child := scrollChild{dims.Size, call}
+ mainSize := l.Axis.Convert(child.size).X
+ l.maxSize += mainSize
+ switch l.dir {
+ case iterateForward:
+ l.children = append(l.children, child)
+ case iterateBackward:
+ l.children = append(l.children, scrollChild{})
+ copy(l.children[1:], l.children)
+ l.children[0] = child
+ l.Position.First--
+ l.Position.Offset += mainSize
+ default:
+ panic("call Next before End")
+ }
+ l.dir = iterateNone
+}
+
+// Layout the List and return its dimensions.
+func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
+ if l.more() {
+ panic("unfinished child")
+ }
+ mainMin, mainMax := l.Axis.mainConstraint(l.cs)
+ children := l.children
+ // Skip invisible children
+ for len(children) > 0 {
+ sz := children[0].size
+ mainSize := l.Axis.Convert(sz).X
+ if l.Position.Offset < mainSize {
+ // First child is partially visible.
+ break
+ }
+ l.Position.First++
+ l.Position.Offset -= mainSize
+ children = children[1:]
+ }
+ size := -l.Position.Offset
+ var maxCross int
+ for i, child := range children {
+ sz := l.Axis.Convert(child.size)
+ if c := sz.Y; c > maxCross {
+ maxCross = c
+ }
+ size += sz.X
+ if size >= mainMax {
+ children = children[:i+1]
+ break
+ }
+ }
+ l.Position.Count = len(children)
+ l.Position.OffsetLast = mainMax - size
+ pos := -l.Position.Offset
+ // ScrollToEnd lists are end aligned.
+ if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 {
+ pos += space
+ }
+ for _, child := range children {
+ sz := l.Axis.Convert(child.size)
+ var cross int
+ switch l.Alignment {
+ case End:
+ cross = maxCross - sz.Y
+ case Middle:
+ cross = (maxCross - sz.Y) / 2
+ }
+ childSize := sz.X
+ max := childSize + pos
+ if max > mainMax {
+ max = mainMax
+ }
+ min := pos
+ if min < 0 {
+ min = 0
+ }
+ r := image.Rectangle{
+ Min: l.Axis.Convert(image.Pt(min, -inf)),
+ Max: l.Axis.Convert(image.Pt(max, inf)),
+ }
+ stack := op.Save(ops)
+ clip.Rect(r).Add(ops)
+ pt := l.Axis.Convert(image.Pt(pos, cross))
+ op.Offset(FPt(pt)).Add(ops)
+ child.call.Add(ops)
+ stack.Load()
+ pos += childSize
+ }
+ atStart := l.Position.First == 0 && l.Position.Offset <= 0
+ atEnd := l.Position.First+len(children) == l.len && mainMax >= pos
+ if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 {
+ l.scroll.Stop()
+ }
+ l.Position.BeforeEnd = !atEnd
+ if pos < mainMin {
+ pos = mainMin
+ }
+ if pos > mainMax {
+ pos = mainMax
+ }
+ dims := l.Axis.Convert(image.Pt(pos, maxCross))
+ call := macro.Stop()
+ defer op.Save(ops).Load()
+ pointer.Rect(image.Rectangle{Max: dims}).Add(ops)
+
+ var min, max int
+ if o := l.Position.Offset; o > 0 {
+ // Use the size of the invisible part as scroll boundary.
+ min = -o
+ } else if l.Position.First > 0 {
+ min = -inf
+ }
+ if o := l.Position.OffsetLast; o < 0 {
+ max = -o
+ } else if l.Position.First+l.Position.Count < l.len {
+ max = inf
+ }
+ scrollRange := image.Rectangle{
+ Min: l.Axis.Convert(image.Pt(min, 0)),
+ Max: l.Axis.Convert(image.Pt(max, 0)),
+ }
+ l.scroll.Add(ops, scrollRange)
+
+ call.Add(ops)
+ return Dimensions{Size: dims}
+}
diff --git a/gio/giold/layout/list_test.go b/gio/giold/layout/list_test.go
new file mode 100644
index 0000000..6a026b3
--- /dev/null
+++ b/gio/giold/layout/list_test.go
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "image"
+ "testing"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/router"
+ "realy.lol/gio/op"
+)
+
+func TestListPosition(t *testing.T) {
+ _s := func(e ...event.Event) []event.Event { return e }
+ r := new(router.Router)
+ gtx := Context{
+ Ops: new(op.Ops),
+ Constraints: Constraints{
+ Max: image.Pt(20, 10),
+ },
+ Queue: r,
+ }
+ el := func(gtx Context, idx int) Dimensions {
+ return Dimensions{Size: image.Pt(10, 10)}
+ }
+ for _, tc := range []struct {
+ label string
+ num int
+ scroll []event.Event
+ first int
+ count int
+ offset int
+ last int
+ }{
+ {label: "no item", last: 20},
+ {label: "1 visible 0 hidden", num: 1, count: 1, last: 10},
+ {label: "2 visible 0 hidden", num: 2, count: 2},
+ {label: "2 visible 1 hidden", num: 3, count: 2},
+ {label: "3 visible 0 hidden small scroll", num: 3, count: 3, offset: 5,
+ last: -5,
+ scroll: _s(
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Press,
+ Position: f32.Pt(0, 0),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Type: pointer.Scroll,
+ Scroll: f32.Pt(5, 0),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Release,
+ Position: f32.Pt(5, 0),
+ },
+ )},
+ {label: "3 visible 0 hidden small scroll 2", num: 3, count: 3,
+ offset: 3, last: -7,
+ scroll: _s(
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Press,
+ Position: f32.Pt(0, 0),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Type: pointer.Scroll,
+ Scroll: f32.Pt(3, 0),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Release,
+ Position: f32.Pt(5, 0),
+ },
+ )},
+ {label: "2 visible 1 hidden large scroll", num: 3, count: 2, first: 1,
+ scroll: _s(
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Press,
+ Position: f32.Pt(0, 0),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Type: pointer.Scroll,
+ Scroll: f32.Pt(10, 0),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Release,
+ Position: f32.Pt(15, 0),
+ },
+ )},
+ } {
+ t.Run(tc.label, func(t *testing.T) {
+ gtx.Ops.Reset()
+
+ var list List
+ // Initialize the list.
+ list.Layout(gtx, tc.num, el)
+ // Generate the scroll events.
+ r.Frame(gtx.Ops)
+ r.Queue(tc.scroll...)
+ // Let the list process the events.
+ list.Layout(gtx, tc.num, el)
+
+ pos := list.Position
+ if got, want := pos.First, tc.first; got != want {
+ t.Errorf("List: invalid first position: got %v; want %v", got,
+ want)
+ }
+ if got, want := pos.Count, tc.count; got != want {
+ t.Errorf("List: invalid number of visible children: got %v; want %v",
+ got, want)
+ }
+ if got, want := pos.Offset, tc.offset; got != want {
+ t.Errorf("List: invalid first visible offset: got %v; want %v",
+ got, want)
+ }
+ if got, want := pos.OffsetLast, tc.last; got != want {
+ t.Errorf("List: invalid last visible offset: got %v; want %v",
+ got, want)
+ }
+ })
+ }
+}
diff --git a/gio/giold/layout/stack.go b/gio/giold/layout/stack.go
new file mode 100644
index 0000000..f46a091
--- /dev/null
+++ b/gio/giold/layout/stack.go
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "image"
+
+ "realy.lol/gio/op"
+)
+
+// Stack lays out child elements on top of each other,
+// according to an alignment direction.
+type Stack struct {
+ // Alignment is the direction to align children
+ // smaller than the available space.
+ Alignment Direction
+}
+
+// StackChild represents a child for a Stack layout.
+type StackChild struct {
+ expanded bool
+ widget Widget
+
+ // Scratch space.
+ call op.CallOp
+ dims Dimensions
+}
+
+// Stacked returns a Stack child that is laid out with no minimum
+// constraints and the maximum constraints passed to Stack.Layout.
+func Stacked(w Widget) StackChild {
+ return StackChild{
+ widget: w,
+ }
+}
+
+// Expanded returns a Stack child with the minimum constraints set
+// to the largest Stacked child. The maximum constraints are set to
+// the same as passed to Stack.Layout.
+func Expanded(w Widget) StackChild {
+ return StackChild{
+ expanded: true,
+ widget: w,
+ }
+}
+
+// Layout a stack of children. The position of the children are
+// determined by the specified order, but Stacked children are laid out
+// before Expanded children.
+func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions {
+ var maxSZ image.Point
+ // First lay out Stacked children.
+ cgtx := gtx
+ cgtx.Constraints.Min = image.Point{}
+ for i, w := range children {
+ if w.expanded {
+ continue
+ }
+ macro := op.Record(gtx.Ops)
+ dims := w.widget(cgtx)
+ call := macro.Stop()
+ if w := dims.Size.X; w > maxSZ.X {
+ maxSZ.X = w
+ }
+ if h := dims.Size.Y; h > maxSZ.Y {
+ maxSZ.Y = h
+ }
+ children[i].call = call
+ children[i].dims = dims
+ }
+ // Then lay out Expanded children.
+ for i, w := range children {
+ if !w.expanded {
+ continue
+ }
+ macro := op.Record(gtx.Ops)
+ cgtx.Constraints.Min = maxSZ
+ dims := w.widget(cgtx)
+ call := macro.Stop()
+ if w := dims.Size.X; w > maxSZ.X {
+ maxSZ.X = w
+ }
+ if h := dims.Size.Y; h > maxSZ.Y {
+ maxSZ.Y = h
+ }
+ children[i].call = call
+ children[i].dims = dims
+ }
+
+ maxSZ = gtx.Constraints.Constrain(maxSZ)
+ var baseline int
+ for _, ch := range children {
+ sz := ch.dims.Size
+ var p image.Point
+ switch s.Alignment {
+ case N, S, Center:
+ p.X = (maxSZ.X - sz.X) / 2
+ case NE, SE, E:
+ p.X = maxSZ.X - sz.X
+ }
+ switch s.Alignment {
+ case W, Center, E:
+ p.Y = (maxSZ.Y - sz.Y) / 2
+ case SW, S, SE:
+ p.Y = maxSZ.Y - sz.Y
+ }
+ stack := op.Save(gtx.Ops)
+ op.Offset(FPt(p)).Add(gtx.Ops)
+ ch.call.Add(gtx.Ops)
+ stack.Load()
+ if baseline == 0 {
+ if b := ch.dims.Baseline; b != 0 {
+ baseline = b + maxSZ.Y - sz.Y - p.Y
+ }
+ }
+ }
+ return Dimensions{
+ Size: maxSZ,
+ Baseline: baseline,
+ }
+}
diff --git a/gio/giold/op/clip/clip.go b/gio/giold/op/clip/clip.go
new file mode 100644
index 0000000..360d89f
--- /dev/null
+++ b/gio/giold/op/clip/clip.go
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package clip
+
+import (
+ "encoding/binary"
+ "image"
+ "math"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/internal/scene"
+ "realy.lol/gio/internal/stroke"
+ "realy.lol/gio/op"
+)
+
+// Op represents a clip area. Op intersects the current clip area with
+// itself.
+type Op struct {
+ bounds image.Rectangle
+ path PathSpec
+
+ outline bool
+ stroke StrokeStyle
+ dashes DashSpec
+}
+
+func (p Op) Add(o *op.Ops) {
+ str := p.stroke
+ dashes := p.dashes
+ path := p.path
+ outline := p.outline
+ approx := str.Width > 0 && !(dashes == DashSpec{} && str.Miter == 0 && str.Join == RoundJoin && str.Cap == RoundCap)
+ if approx {
+ // If the stroke is not natively supported by the compute renderer, construct a filled path
+ // that approximates it.
+ path = p.approximateStroke(o)
+ dashes = DashSpec{}
+ str = StrokeStyle{}
+ outline = true
+ }
+
+ if path.hasSegments {
+ data := o.Write(opconst.TypePathLen)
+ data[0] = byte(opconst.TypePath)
+ path.spec.Add(o)
+ }
+
+ if str.Width > 0 {
+ data := o.Write(opconst.TypeStrokeLen)
+ data[0] = byte(opconst.TypeStroke)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[1:], math.Float32bits(str.Width))
+ }
+
+ data := o.Write(opconst.TypeClipLen)
+ data[0] = byte(opconst.TypeClip)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[1:], uint32(p.bounds.Min.X))
+ bo.PutUint32(data[5:], uint32(p.bounds.Min.Y))
+ bo.PutUint32(data[9:], uint32(p.bounds.Max.X))
+ bo.PutUint32(data[13:], uint32(p.bounds.Max.Y))
+ if outline {
+ data[17] = byte(1)
+ }
+}
+
+func (p Op) approximateStroke(o *op.Ops) PathSpec {
+ if !p.path.hasSegments {
+ return PathSpec{}
+ }
+
+ var r ops.Reader
+ // Add path op for us to decode. Use a macro to omit it from later decodes.
+ ignore := op.Record(o)
+ r.ResetAt(o, ops.NewPC(o))
+ p.path.spec.Add(o)
+ ignore.Stop()
+ encOp, ok := r.Decode()
+ if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux {
+ panic("corrupt path data")
+ }
+ pathData := encOp.Data[opconst.TypeAuxLen:]
+
+ // Decode dashes in a similar way.
+ var dashes stroke.DashOp
+ if p.dashes.phase != 0 || p.dashes.size > 0 {
+ ignore := op.Record(o)
+ r.ResetAt(o, ops.NewPC(o))
+ p.dashes.spec.Add(o)
+ ignore.Stop()
+ encOp, ok := r.Decode()
+ if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux {
+ panic("corrupt dash data")
+ }
+ dashes.Dashes = make([]float32, p.dashes.size)
+ dashData := encOp.Data[opconst.TypeAuxLen:]
+ bo := binary.LittleEndian
+ for i := range dashes.Dashes {
+ dashes.Dashes[i] = math.Float32frombits(bo.Uint32(dashData[i*4:]))
+ }
+ dashes.Phase = p.dashes.phase
+ }
+
+ // Approximate and output path data.
+ var outline Path
+ outline.Begin(o)
+ ss := stroke.StrokeStyle{
+ Width: p.stroke.Width,
+ Miter: p.stroke.Miter,
+ Cap: stroke.StrokeCap(p.stroke.Cap),
+ Join: stroke.StrokeJoin(p.stroke.Join),
+ }
+ quads := stroke.StrokePathCommands(ss, dashes, pathData)
+ pen := f32.Pt(0, 0)
+ for _, quad := range quads {
+ q := quad.Quad
+ if q.From != pen {
+ pen = q.From
+ outline.MoveTo(pen)
+ }
+ outline.contour = int(quad.Contour)
+ outline.QuadTo(q.Ctrl, q.To)
+ }
+ return outline.End()
+}
+
+type PathSpec struct {
+ spec op.CallOp
+ // open is true if any path contour is not closed. A closed contour starts
+ // and ends in the same point.
+ open bool
+ // hasSegments tracks whether there are any segments in the path.
+ hasSegments bool
+}
+
+// Path constructs a Op clip path described by lines and
+// BĆ©zier curves, where drawing outside the Path is discarded.
+// The inside-ness of a pixel is determines by the non-zero winding rule,
+// similar to the SVG rule of the same name.
+//
+// Path generates no garbage and can be used for dynamic paths; path
+// data is stored directly in the Ops list supplied to Begin.
+type Path struct {
+ ops *op.Ops
+ open bool
+ contour int
+ pen f32.Point
+ macro op.MacroOp
+ start f32.Point
+ hasSegments bool
+}
+
+// Pos returns the current pen position.
+func (p *Path) Pos() f32.Point { return p.pen }
+
+// Begin the path, storing the path data and final Op into ops.
+func (p *Path) Begin(ops *op.Ops) {
+ p.ops = ops
+ p.macro = op.Record(ops)
+ // Write the TypeAux opcode
+ data := ops.Write(opconst.TypeAuxLen)
+ data[0] = byte(opconst.TypeAux)
+}
+
+// End returns a PathSpec ready to use in clipping operations.
+func (p *Path) End() PathSpec {
+ c := p.macro.Stop()
+ return PathSpec{
+ spec: c,
+ open: p.open || p.pen != p.start,
+ hasSegments: p.hasSegments,
+ }
+}
+
+// Move moves the pen by the amount specified by delta.
+func (p *Path) Move(delta f32.Point) {
+ to := delta.Add(p.pen)
+ p.MoveTo(to)
+}
+
+// MoveTo moves the pen to the specified absolute coordinate.
+func (p *Path) MoveTo(to f32.Point) {
+ p.open = p.open || p.pen != p.start
+ p.end()
+ p.pen = to
+ p.start = to
+}
+
+// end completes the current contour.
+func (p *Path) end() {
+ p.contour++
+}
+
+// Line moves the pen by the amount specified by delta, recording a line.
+func (p *Path) Line(delta f32.Point) {
+ to := delta.Add(p.pen)
+ p.LineTo(to)
+}
+
+// LineTo moves the pen to the absolute point specified, recording a line.
+func (p *Path) LineTo(to f32.Point) {
+ data := p.ops.Write(scene.CommandSize + 4)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[0:], uint32(p.contour))
+ ops.EncodeCommand(data[4:], scene.Line(p.pen, to))
+ p.pen = to
+ p.hasSegments = true
+}
+
+// Quad records a quadratic BĆ©zier from the pen to end
+// with the control point ctrl.
+func (p *Path) Quad(ctrl, to f32.Point) {
+ ctrl = ctrl.Add(p.pen)
+ to = to.Add(p.pen)
+ p.QuadTo(ctrl, to)
+}
+
+// QuadTo records a quadratic BĆ©zier from the pen to end
+// with the control point ctrl, with absolute coordinates.
+func (p *Path) QuadTo(ctrl, to f32.Point) {
+ data := p.ops.Write(scene.CommandSize + 4)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[0:], uint32(p.contour))
+ ops.EncodeCommand(data[4:], scene.Quad(p.pen, ctrl, to))
+ p.pen = to
+ p.hasSegments = true
+}
+
+// Arc adds an elliptical arc to the path. The implied ellipse is defined
+// by its focus points f1 and f2.
+// The arc starts in the current point and ends angle radians along the ellipse boundary.
+// The sign of angle determines the direction; positive being counter-clockwise,
+// negative clockwise.
+func (p *Path) Arc(f1, f2 f32.Point, angle float32) {
+ f1 = f1.Add(p.pen)
+ f2 = f2.Add(p.pen)
+ const segments = 16
+ m := stroke.ArcTransform(p.pen, f1, f2, angle, segments)
+
+ for i := 0; i < segments; i++ {
+ p0 := p.pen
+ p1 := m.Transform(p0)
+ p2 := m.Transform(p1)
+ ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
+ p.QuadTo(ctl, p2)
+ }
+}
+
+// Cube records a cubic BĆ©zier from the pen through
+// two control points ending in to.
+func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) {
+ p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to))
+}
+
+// CubeTo records a cubic BĆ©zier from the pen through
+// two control points ending in to, with absolute coordinates.
+func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) {
+ if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen {
+ return
+ }
+ data := p.ops.Write(scene.CommandSize + 4)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[0:], uint32(p.contour))
+ ops.EncodeCommand(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to))
+ p.pen = to
+ p.hasSegments = true
+}
+
+// Close closes the path contour.
+func (p *Path) Close() {
+ if p.pen != p.start {
+ p.LineTo(p.start)
+ }
+ p.end()
+}
+
+// Outline represents the area inside of a path, according to the
+// non-zero winding rule.
+type Outline struct {
+ Path PathSpec
+}
+
+// Op returns a clip operation representing the outline.
+func (o Outline) Op() Op {
+ if o.Path.open {
+ panic("not all path contours are closed")
+ }
+ return Op{
+ path: o.Path,
+ outline: true,
+ }
+}
diff --git a/gio/giold/op/clip/clip_test.go b/gio/giold/op/clip/clip_test.go
new file mode 100644
index 0000000..7962c6d
--- /dev/null
+++ b/gio/giold/op/clip/clip_test.go
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package clip
+
+import (
+ "testing"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/op"
+)
+
+func TestOpenPathOutlinePanic(t *testing.T) {
+ defer func() {
+ if err := recover(); err == nil {
+ t.Error("Outline of an open path didn't panic")
+ }
+ }()
+ var p Path
+ p.Begin(new(op.Ops))
+ p.Line(f32.Pt(10, 10))
+ Outline{Path: p.End()}.Op()
+}
diff --git a/gio/giold/op/clip/doc.go b/gio/giold/op/clip/doc.go
new file mode 100644
index 0000000..6ba5546
--- /dev/null
+++ b/gio/giold/op/clip/doc.go
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package clip provides operations for clipping paint operations.
+Drawing outside the current clip area is ignored.
+
+The current clip is initially the infinite set. An Op sets the clip
+to the intersection of the current clip and the clip area it
+represents. If you need to reset the current clip to its value
+before applying an Op, use op.StackOp.
+
+General clipping areas are constructed with Path. Simpler special
+cases such as rectangular clip areas also exist as convenient
+constructors.
+*/
+package clip
diff --git a/gio/giold/op/clip/shapes.go b/gio/giold/op/clip/shapes.go
new file mode 100644
index 0000000..9ea84e3
--- /dev/null
+++ b/gio/giold/op/clip/shapes.go
@@ -0,0 +1,167 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package clip
+
+import (
+ "image"
+ "math"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/op"
+)
+
+// Rect represents the clip area of a pixel-aligned rectangle.
+type Rect image.Rectangle
+
+// Op returns the op for the rectangle.
+func (r Rect) Op() Op {
+ return Op{
+ bounds: image.Rectangle(r),
+ outline: true,
+ }
+}
+
+// Add the clip operation.
+func (r Rect) Add(ops *op.Ops) {
+ r.Op().Add(ops)
+}
+
+// UniformRRect returns an RRect with all corner radii set to the
+// provided radius.
+func UniformRRect(rect f32.Rectangle, radius float32) RRect {
+ return RRect{
+ Rect: rect,
+ SE: radius,
+ SW: radius,
+ NE: radius,
+ NW: radius,
+ }
+}
+
+// RRect represents the clip area of a rectangle with rounded
+// corners.
+//
+// Specify a square with corner radii equal to half the square size to
+// construct a circular clip area.
+type RRect struct {
+ Rect f32.Rectangle
+ // The corner radii.
+ SE, SW, NW, NE float32
+}
+
+// Op returns the op for the rounded rectangle.
+func (rr RRect) Op(ops *op.Ops) Op {
+ if rr.SE == 0 && rr.SW == 0 && rr.NW == 0 && rr.NE == 0 {
+ r := image.Rectangle{
+ Min: image.Point{X: int(rr.Rect.Min.X), Y: int(rr.Rect.Min.Y)},
+ Max: image.Point{X: int(rr.Rect.Max.X), Y: int(rr.Rect.Max.Y)},
+ }
+ // Only use Rect if rr is pixel-aligned, as Rect is guaranteed to be.
+ if fPt(r.Min) == rr.Rect.Min && fPt(r.Max) == rr.Rect.Max {
+ return Rect(r).Op()
+ }
+ }
+ return Outline{Path: rr.Path(ops)}.Op()
+}
+
+// Add the rectangle clip.
+func (rr RRect) Add(ops *op.Ops) {
+ rr.Op(ops).Add(ops)
+}
+
+// Path returns the PathSpec for the rounded rectangle.
+func (rr RRect) Path(ops *op.Ops) PathSpec {
+ var p Path
+ p.Begin(ops)
+
+ // https://pomax.github.io/bezierinfo/#circles_cubic.
+ const q = 4 * (math.Sqrt2 - 1) / 3
+ const iq = 1 - q
+
+ se, sw, nw, ne := rr.SE, rr.SW, rr.NW, rr.NE
+ w, n, e, s := rr.Rect.Min.X, rr.Rect.Min.Y, rr.Rect.Max.X, rr.Rect.Max.Y
+
+ p.MoveTo(f32.Point{X: w + nw, Y: n})
+ p.LineTo(f32.Point{X: e - ne, Y: n}) // N
+ p.CubeTo( // NE
+ f32.Point{X: e - ne*iq, Y: n},
+ f32.Point{X: e, Y: n + ne*iq},
+ f32.Point{X: e, Y: n + ne})
+ p.LineTo(f32.Point{X: e, Y: s - se}) // E
+ p.CubeTo( // SE
+ f32.Point{X: e, Y: s - se*iq},
+ f32.Point{X: e - se*iq, Y: s},
+ f32.Point{X: e - se, Y: s})
+ p.LineTo(f32.Point{X: w + sw, Y: s}) // S
+ p.CubeTo( // SW
+ f32.Point{X: w + sw*iq, Y: s},
+ f32.Point{X: w, Y: s - sw*iq},
+ f32.Point{X: w, Y: s - sw})
+ p.LineTo(f32.Point{X: w, Y: n + nw}) // W
+ p.CubeTo( // NW
+ f32.Point{X: w, Y: n + nw*iq},
+ f32.Point{X: w + nw*iq, Y: n},
+ f32.Point{X: w + nw, Y: n})
+
+ return p.End()
+}
+
+// Circle represents the clip area of a circle.
+type Circle struct {
+ Center f32.Point
+ Radius float32
+}
+
+// Op returns the op for the circle.
+func (c Circle) Op(ops *op.Ops) Op {
+ return Outline{Path: c.Path(ops)}.Op()
+}
+
+// Add the circle clip.
+func (c Circle) Add(ops *op.Ops) {
+ c.Op(ops).Add(ops)
+}
+
+// Path returns the PathSpec for the circle.
+func (c Circle) Path(ops *op.Ops) PathSpec {
+ var p Path
+ p.Begin(ops)
+
+ center := c.Center
+ r := c.Radius
+
+ // https://pomax.github.io/bezierinfo/#circles_cubic.
+ const q = 4 * (math.Sqrt2 - 1) / 3
+
+ curve := r * q
+ top := f32.Point{X: center.X, Y: center.Y - r}
+
+ p.MoveTo(top)
+ p.CubeTo(
+ f32.Point{X: center.X + curve, Y: center.Y - r},
+ f32.Point{X: center.X + r, Y: center.Y - curve},
+ f32.Point{X: center.X + r, Y: center.Y},
+ )
+ p.CubeTo(
+ f32.Point{X: center.X + r, Y: center.Y + curve},
+ f32.Point{X: center.X + curve, Y: center.Y + r},
+ f32.Point{X: center.X, Y: center.Y + r},
+ )
+ p.CubeTo(
+ f32.Point{X: center.X - curve, Y: center.Y + r},
+ f32.Point{X: center.X - r, Y: center.Y + curve},
+ f32.Point{X: center.X - r, Y: center.Y},
+ )
+ p.CubeTo(
+ f32.Point{X: center.X - r, Y: center.Y - curve},
+ f32.Point{X: center.X - curve, Y: center.Y - r},
+ top,
+ )
+ return p.End()
+}
+
+func fPt(p image.Point) f32.Point {
+ return f32.Point{
+ X: float32(p.X), Y: float32(p.Y),
+ }
+}
diff --git a/gio/giold/op/clip/stroke.go b/gio/giold/op/clip/stroke.go
new file mode 100644
index 0000000..8610eab
--- /dev/null
+++ b/gio/giold/op/clip/stroke.go
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package clip
+
+import (
+ "encoding/binary"
+ "math"
+
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/op"
+)
+
+// Stroke represents a stroked path.
+type Stroke struct {
+ Path PathSpec
+ Style StrokeStyle
+
+ // Dashes specify the dashes of the stroke.
+ // The empty value denotes no dashes.
+ Dashes DashSpec
+}
+
+// Op returns a clip operation representing the stroke.
+func (s Stroke) Op() Op {
+ return Op{
+ path: s.Path,
+ stroke: s.Style,
+ dashes: s.Dashes,
+ }
+}
+
+// StrokeStyle describes how a path should be stroked.
+type StrokeStyle struct {
+ Width float32 // Width of the stroked path.
+
+ // Miter is the limit to apply to a miter joint.
+ // The zero Miter disables the miter joint; setting Miter to +ā
+ // unconditionally enables the miter joint.
+ Miter float32
+ Cap StrokeCap // Cap describes the head or tail of a stroked path.
+ Join StrokeJoin // Join describes how stroked paths are collated.
+}
+
+// StrokeCap describes the head or tail of a stroked path.
+type StrokeCap uint8
+
+const (
+ // RoundCap caps stroked paths with a round cap, joining the right-hand and
+ // left-hand sides of a stroked path with a half disc of diameter the
+ // stroked path's width.
+ RoundCap StrokeCap = iota
+
+ // FlatCap caps stroked paths with a flat cap, joining the right-hand
+ // and left-hand sides of a stroked path with a straight line.
+ FlatCap
+
+ // SquareCap caps stroked paths with a square cap, joining the right-hand
+ // and left-hand sides of a stroked path with a half square of length
+ // the stroked path's width.
+ SquareCap
+)
+
+// StrokeJoin describes how stroked paths are collated.
+type StrokeJoin uint8
+
+const (
+ // RoundJoin joins path segments with a round segment.
+ RoundJoin StrokeJoin = iota
+
+ // BevelJoin joins path segments with sharp bevels.
+ BevelJoin
+)
+
+// Dash records dashes' lengths and phase for a stroked path.
+type Dash struct {
+ ops *op.Ops
+ macro op.MacroOp
+ phase float32
+ size uint8 // size of the pattern
+}
+
+func (d *Dash) Begin(ops *op.Ops) {
+ d.ops = ops
+ d.macro = op.Record(ops)
+ // Write the TypeAux opcode
+ data := ops.Write(opconst.TypeAuxLen)
+ data[0] = byte(opconst.TypeAux)
+}
+
+func (d *Dash) Phase(v float32) {
+ d.phase = v
+}
+
+func (d *Dash) Dash(length float32) {
+ if d.size == math.MaxUint8 {
+ panic("clip: dash pattern too large")
+ }
+ data := d.ops.Write(4)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[0:], math.Float32bits(length))
+ d.size++
+}
+
+func (d *Dash) End() DashSpec {
+ c := d.macro.Stop()
+ return DashSpec{
+ spec: c,
+ phase: d.phase,
+ size: d.size,
+ }
+}
+
+// DashSpec describes a dashed pattern.
+type DashSpec struct {
+ spec op.CallOp
+ phase float32
+ size uint8 // size of the pattern
+}
diff --git a/gio/giold/op/op.go b/gio/giold/op/op.go
new file mode 100644
index 0000000..f29aa0b
--- /dev/null
+++ b/gio/giold/op/op.go
@@ -0,0 +1,369 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+
+Package op implements operations for updating a user interface.
+
+Gio programs use operations, or ops, for describing their user
+interfaces. There are operations for drawing, defining input
+handlers, changing window properties as well as operations for
+controlling the execution of other operations.
+
+Ops represents a list of operations. The most important use
+for an Ops list is to describe a complete user interface update
+to a ui/app.Window's Update method.
+
+Drawing a colored square:
+
+ import "realy.lol/gio/unit"
+ import "realy.lol/gio/app"
+ import "realy.lol/gio/op/paint"
+
+ var w app.Window
+ var e system.FrameEvent
+ ops := new(op.Ops)
+ ...
+ ops.Reset()
+ paint.ColorOp{Color: ...}.Add(ops)
+ paint.PaintOp{Rect: ...}.Add(ops)
+ e.Frame(ops)
+
+State
+
+An Ops list can be viewed as a very simple virtual machine: it has an implicit
+mutable state stack and execution flow can be controlled with macros.
+
+The Save function saves the current state for later restoring:
+
+ ops := new(op.Ops)
+ // Save the current state, in particular the transform.
+ state := op.Save(ops)
+ // Apply a transform to subsequent operations.
+ op.Offset(...).Add(ops)
+ ...
+ // Restore the previous transform.
+ state.Load()
+
+You can also use this one-line to save the current state and restore it at the
+end of a function :
+
+ defer op.Save(ops).Load()
+
+The MacroOp records a list of operations to be executed later:
+
+ ops := new(op.Ops)
+ macro := op.Record(ops)
+ // Record operations by adding them.
+ op.InvalidateOp{}.Add(ops)
+ ...
+ // End recording.
+ call := macro.Stop()
+
+ // replay the recorded operations:
+ call.Add(ops)
+
+*/
+package op
+
+import (
+ "encoding/binary"
+ "math"
+ "time"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/opconst"
+)
+
+// Ops holds a list of operations. Operations are stored in
+// serialized form to avoid garbage during construction of
+// the ops list.
+type Ops struct {
+ // version is incremented at each Reset.
+ version int
+ // data contains the serialized operations.
+ data []byte
+ // refs hold external references for operations.
+ refs []interface{}
+ // nextStateID is the id allocated for the next
+ // StateOp.
+ nextStateID int
+
+ macroStack stack
+}
+
+// StateOp represents a saved operation snapshop to be restored
+// later.
+type StateOp struct {
+ id int
+ macroID int
+ ops *Ops
+}
+
+// MacroOp records a list of operations for later use.
+type MacroOp struct {
+ ops *Ops
+ id stackID
+ pc pc
+}
+
+// CallOp invokes the operations recorded by Record.
+type CallOp struct {
+ // Ops is the list of operations to invoke.
+ ops *Ops
+ pc pc
+}
+
+// InvalidateOp requests a redraw at the given time. Use
+// the zero value to request an immediate redraw.
+type InvalidateOp struct {
+ At time.Time
+}
+
+// TransformOp applies a transform to the current transform. The zero value
+// for TransformOp represents the identity transform.
+type TransformOp struct {
+ t f32.Affine2D
+}
+
+// stack tracks the integer identities of MacroOp
+// operations to ensure correct pairing of Record/End.
+type stack struct {
+ currentID int
+ nextID int
+}
+
+type stackID struct {
+ id int
+ prev int
+}
+
+type pc struct {
+ data int
+ refs int
+}
+
+// Defer executes c after all other operations have completed,
+// including previously deferred operations.
+// Defer saves the current transformation and restores it prior
+// to execution. All other operation state is reset.
+//
+// Note that deferred operations are executed in first-in-first-out
+// order, unlike the Go facility of the same name.
+func Defer(o *Ops, c CallOp) {
+ if c.ops == nil {
+ return
+ }
+ state := Save(o)
+ // Wrap c in a macro that loads the saved state before execution.
+ m := Record(o)
+ load(o, opconst.InitialStateID, opconst.AllState)
+ load(o, state.id, opconst.TransformState)
+ c.Add(o)
+ c = m.Stop()
+ // A Defer is recorded as a TypeDefer followed by the
+ // wrapped macro.
+ data := o.Write(opconst.TypeDeferLen)
+ data[0] = byte(opconst.TypeDefer)
+ c.Add(o)
+}
+
+// Save the current operations state.
+func Save(o *Ops) StateOp {
+ o.nextStateID++
+ s := StateOp{
+ ops: o,
+ id: o.nextStateID,
+ macroID: o.macroStack.currentID,
+ }
+ save(o, s.id)
+ return s
+}
+
+// save records a save of the operations state to
+// id.
+func save(o *Ops, id int) {
+ bo := binary.LittleEndian
+ data := o.Write(opconst.TypeSaveLen)
+ data[0] = byte(opconst.TypeSave)
+ bo.PutUint32(data[1:], uint32(id))
+}
+
+// Load a previously saved operations state.
+func (s StateOp) Load() {
+ if s.ops.macroStack.currentID != s.macroID {
+ panic("load in a different macro than save")
+ }
+ if s.id == 0 {
+ panic("zero-value op")
+ }
+ load(s.ops, s.id, opconst.AllState)
+}
+
+// load a previously saved operations state given
+// its ID. Only state included in mask is affected.
+func load(o *Ops, id int, m opconst.StateMask) {
+ bo := binary.LittleEndian
+ data := o.Write(opconst.TypeLoadLen)
+ data[0] = byte(opconst.TypeLoad)
+ data[1] = byte(m)
+ bo.PutUint32(data[2:], uint32(id))
+}
+
+// Reset the Ops, preparing it for re-use. Reset invalidates
+// any recorded macros.
+func (o *Ops) Reset() {
+ o.macroStack = stack{}
+ // Leave references to the GC.
+ for i := range o.refs {
+ o.refs[i] = nil
+ }
+ o.data = o.data[:0]
+ o.refs = o.refs[:0]
+ o.nextStateID = 0
+ o.version++
+}
+
+// Data is for internal use only.
+func (o *Ops) Data() []byte {
+ return o.data
+}
+
+// Refs is for internal use only.
+func (o *Ops) Refs() []interface{} {
+ return o.refs
+}
+
+// Version is for internal use only.
+func (o *Ops) Version() int {
+ return o.version
+}
+
+// Write is for internal use only.
+func (o *Ops) Write(n int) []byte {
+ o.data = append(o.data, make([]byte, n)...)
+ return o.data[len(o.data)-n:]
+}
+
+// Write1 is for internal use only.
+func (o *Ops) Write1(n int, ref1 interface{}) []byte {
+ o.data = append(o.data, make([]byte, n)...)
+ o.refs = append(o.refs, ref1)
+ return o.data[len(o.data)-n:]
+}
+
+// Write2 is for internal use only.
+func (o *Ops) Write2(n int, ref1, ref2 interface{}) []byte {
+ o.data = append(o.data, make([]byte, n)...)
+ o.refs = append(o.refs, ref1, ref2)
+ return o.data[len(o.data)-n:]
+}
+
+func (o *Ops) pc() pc {
+ return pc{data: len(o.data), refs: len(o.refs)}
+}
+
+// Record a macro of operations.
+func Record(o *Ops) MacroOp {
+ m := MacroOp{
+ ops: o,
+ id: o.macroStack.push(),
+ pc: o.pc(),
+ }
+ // Reserve room for a macro definition. Updated in Stop.
+ m.ops.Write(opconst.TypeMacroLen)
+ m.fill()
+ return m
+}
+
+// Stop ends a previously started recording and returns an
+// operation for replaying it.
+func (m MacroOp) Stop() CallOp {
+ m.ops.macroStack.pop(m.id)
+ m.fill()
+ return CallOp{
+ ops: m.ops,
+ pc: m.pc,
+ }
+}
+
+func (m MacroOp) fill() {
+ pc := m.ops.pc()
+ // Fill out the macro definition reserved in Record.
+ data := m.ops.data[m.pc.data:]
+ data = data[:opconst.TypeMacroLen]
+ data[0] = byte(opconst.TypeMacro)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[1:], uint32(pc.data))
+ bo.PutUint32(data[5:], uint32(pc.refs))
+}
+
+// Add the recorded list of operations. Add
+// panics if the Ops containing the recording
+// has been reset.
+func (c CallOp) Add(o *Ops) {
+ if c.ops == nil {
+ return
+ }
+ data := o.Write1(opconst.TypeCallLen, c.ops)
+ data[0] = byte(opconst.TypeCall)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[1:], uint32(c.pc.data))
+ bo.PutUint32(data[5:], uint32(c.pc.refs))
+}
+
+func (r InvalidateOp) Add(o *Ops) {
+ data := o.Write(opconst.TypeRedrawLen)
+ data[0] = byte(opconst.TypeInvalidate)
+ bo := binary.LittleEndian
+ // UnixNano cannot represent the zero time.
+ if t := r.At; !t.IsZero() {
+ nanos := t.UnixNano()
+ if nanos > 0 {
+ bo.PutUint64(data[1:], uint64(nanos))
+ }
+ }
+}
+
+// Offset creates a TransformOp with the offset o.
+func Offset(o f32.Point) TransformOp {
+ return TransformOp{t: f32.Affine2D{}.Offset(o)}
+}
+
+// Affine creates a TransformOp representing the transformation a.
+func Affine(a f32.Affine2D) TransformOp {
+ return TransformOp{t: a}
+}
+
+func (t TransformOp) Add(o *Ops) {
+ data := o.Write(opconst.TypeTransformLen)
+ data[0] = byte(opconst.TypeTransform)
+ bo := binary.LittleEndian
+ a, b, c, d, e, f := t.t.Elems()
+ bo.PutUint32(data[1:], math.Float32bits(a))
+ bo.PutUint32(data[1+4*1:], math.Float32bits(b))
+ bo.PutUint32(data[1+4*2:], math.Float32bits(c))
+ bo.PutUint32(data[1+4*3:], math.Float32bits(d))
+ bo.PutUint32(data[1+4*4:], math.Float32bits(e))
+ bo.PutUint32(data[1+4*5:], math.Float32bits(f))
+}
+
+func (s *stack) push() stackID {
+ s.nextID++
+ sid := stackID{
+ id: s.nextID,
+ prev: s.currentID,
+ }
+ s.currentID = s.nextID
+ return sid
+}
+
+func (s *stack) check(sid stackID) {
+ if s.currentID != sid.id {
+ panic("unbalanced operation")
+ }
+}
+
+func (s *stack) pop(sid stackID) {
+ s.check(sid)
+ s.currentID = sid.prev
+}
diff --git a/gio/giold/op/paint/doc.go b/gio/giold/op/paint/doc.go
new file mode 100644
index 0000000..79054ab
--- /dev/null
+++ b/gio/giold/op/paint/doc.go
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package paint provides drawing operations for 2D graphics.
+
+The PaintOp operation fills the current clip with the current brush,
+taking the current transformation into account.
+
+The current brush is set by either a ColorOp for a constant color, or
+ImageOp for an image, or LinearGradientOp for gradients.
+
+All color.NRGBA values are in the sRGB color space.
+*/
+package paint
diff --git a/gio/giold/op/paint/paint.go b/gio/giold/op/paint/paint.go
new file mode 100644
index 0000000..e53a763
--- /dev/null
+++ b/gio/giold/op/paint/paint.go
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package paint
+
+import (
+ "encoding/binary"
+ "image"
+ "image/color"
+ "image/draw"
+ "math"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+)
+
+// ImageOp sets the brush to an image.
+//
+// Note: the ImageOp may keep a reference to the backing image.
+// See NewImageOp for details.
+type ImageOp struct {
+ uniform bool
+ color color.NRGBA
+ src *image.RGBA
+
+ // handle is a key to uniquely identify this ImageOp
+ // in a map of cached textures.
+ handle interface{}
+}
+
+// ColorOp sets the brush to a constant color.
+type ColorOp struct {
+ Color color.NRGBA
+}
+
+// LinearGradientOp sets the brush to a gradient starting at stop1 with color1 and
+// ending at stop2 with color2.
+type LinearGradientOp struct {
+ Stop1 f32.Point
+ Color1 color.NRGBA
+ Stop2 f32.Point
+ Color2 color.NRGBA
+}
+
+// PaintOp fills fills the current clip area with the current brush.
+type PaintOp struct {
+}
+
+// NewImageOp creates an ImageOp backed by src. See
+// realy.lol/gio/io/system.FrameEvent for a description of when data
+// referenced by operations is safe to re-use.
+//
+// NewImageOp assumes the backing image is immutable, and may cache a
+// copy of its contents in a GPU-friendly way. Create new ImageOps to
+// ensure that changes to an image is reflected in the display of
+// it.
+func NewImageOp(src image.Image) ImageOp {
+ switch src := src.(type) {
+ case *image.Uniform:
+ col := color.NRGBAModel.Convert(src.C).(color.NRGBA)
+ return ImageOp{
+ uniform: true,
+ color: col,
+ }
+ case *image.RGBA:
+ bounds := src.Bounds()
+ if bounds.Min == (image.Point{}) && src.Stride == bounds.Dx()*4 {
+ return ImageOp{
+ src: src,
+ handle: new(int),
+ }
+ }
+ }
+
+ sz := src.Bounds().Size()
+ // Copy the image into a GPU friendly format.
+ dst := image.NewRGBA(image.Rectangle{
+ Max: sz,
+ })
+ draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src)
+ return ImageOp{
+ src: dst,
+ handle: new(int),
+ }
+}
+
+func (i ImageOp) Size() image.Point {
+ if i.src == nil {
+ return image.Point{}
+ }
+ return i.src.Bounds().Size()
+}
+
+func (i ImageOp) Add(o *op.Ops) {
+ if i.uniform {
+ ColorOp{
+ Color: i.color,
+ }.Add(o)
+ return
+ }
+ data := o.Write2(opconst.TypeImageLen, i.src, i.handle)
+ data[0] = byte(opconst.TypeImage)
+}
+
+func (c ColorOp) Add(o *op.Ops) {
+ data := o.Write(opconst.TypeColorLen)
+ data[0] = byte(opconst.TypeColor)
+ data[1] = c.Color.R
+ data[2] = c.Color.G
+ data[3] = c.Color.B
+ data[4] = c.Color.A
+}
+
+func (c LinearGradientOp) Add(o *op.Ops) {
+ data := o.Write(opconst.TypeLinearGradientLen)
+ data[0] = byte(opconst.TypeLinearGradient)
+
+ bo := binary.LittleEndian
+ bo.PutUint32(data[1:], math.Float32bits(c.Stop1.X))
+ bo.PutUint32(data[5:], math.Float32bits(c.Stop1.Y))
+ bo.PutUint32(data[9:], math.Float32bits(c.Stop2.X))
+ bo.PutUint32(data[13:], math.Float32bits(c.Stop2.Y))
+
+ data[17+0] = c.Color1.R
+ data[17+1] = c.Color1.G
+ data[17+2] = c.Color1.B
+ data[17+3] = c.Color1.A
+ data[21+0] = c.Color2.R
+ data[21+1] = c.Color2.G
+ data[21+2] = c.Color2.B
+ data[21+3] = c.Color2.A
+}
+
+func (d PaintOp) Add(o *op.Ops) {
+ data := o.Write(opconst.TypePaintLen)
+ data[0] = byte(opconst.TypePaint)
+}
+
+// FillShape fills the clip shape with a color.
+func FillShape(ops *op.Ops, c color.NRGBA, shape clip.Op) {
+ defer op.Save(ops).Load()
+ shape.Add(ops)
+ Fill(ops, c)
+}
+
+// Fill paints an infinitely large plane with the provided color. It
+// is intended to be used with a clip.Op already in place to limit
+// the painted area. Use FillShape unless you need to paint several
+// times within the same clip.Op.
+func Fill(ops *op.Ops, c color.NRGBA) {
+ defer op.Save(ops).Load()
+ ColorOp{Color: c}.Add(ops)
+ PaintOp{}.Add(ops)
+}
diff --git a/gio/giold/text/lru.go b/gio/giold/text/lru.go
new file mode 100644
index 0000000..4f1c033
--- /dev/null
+++ b/gio/giold/text/lru.go
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package text
+
+import (
+ "golang.org/x/image/math/fixed"
+
+ "realy.lol/gio/op"
+)
+
+type layoutCache struct {
+ m map[layoutKey]*layoutElem
+ head, tail *layoutElem
+}
+
+type pathCache struct {
+ m map[pathKey]*path
+ head, tail *path
+}
+
+type layoutElem struct {
+ next, prev *layoutElem
+ key layoutKey
+ layout []Line
+}
+
+type path struct {
+ next, prev *path
+ key pathKey
+ val op.CallOp
+}
+
+type layoutKey struct {
+ ppem fixed.Int26_6
+ maxWidth int
+ str string
+}
+
+type pathKey struct {
+ ppem fixed.Int26_6
+ str string
+}
+
+const maxSize = 1000
+
+func (l *layoutCache) Get(k layoutKey) ([]Line, bool) {
+ if lt, ok := l.m[k]; ok {
+ l.remove(lt)
+ l.insert(lt)
+ return lt.layout, true
+ }
+ return nil, false
+}
+
+func (l *layoutCache) Put(k layoutKey, lt []Line) {
+ if l.m == nil {
+ l.m = make(map[layoutKey]*layoutElem)
+ l.head = new(layoutElem)
+ l.tail = new(layoutElem)
+ l.head.prev = l.tail
+ l.tail.next = l.head
+ }
+ val := &layoutElem{key: k, layout: lt}
+ l.m[k] = val
+ l.insert(val)
+ if len(l.m) > maxSize {
+ oldest := l.tail.next
+ l.remove(oldest)
+ delete(l.m, oldest.key)
+ }
+}
+
+func (l *layoutCache) remove(lt *layoutElem) {
+ lt.next.prev = lt.prev
+ lt.prev.next = lt.next
+}
+
+func (l *layoutCache) insert(lt *layoutElem) {
+ lt.next = l.head
+ lt.prev = l.head.prev
+ lt.prev.next = lt
+ lt.next.prev = lt
+}
+
+func (c *pathCache) Get(k pathKey) (op.CallOp, bool) {
+ if v, ok := c.m[k]; ok {
+ c.remove(v)
+ c.insert(v)
+ return v.val, true
+ }
+ return op.CallOp{}, false
+}
+
+func (c *pathCache) Put(k pathKey, v op.CallOp) {
+ if c.m == nil {
+ c.m = make(map[pathKey]*path)
+ c.head = new(path)
+ c.tail = new(path)
+ c.head.prev = c.tail
+ c.tail.next = c.head
+ }
+ val := &path{key: k, val: v}
+ c.m[k] = val
+ c.insert(val)
+ if len(c.m) > maxSize {
+ oldest := c.tail.next
+ c.remove(oldest)
+ delete(c.m, oldest.key)
+ }
+}
+
+func (c *pathCache) remove(v *path) {
+ v.next.prev = v.prev
+ v.prev.next = v.next
+}
+
+func (c *pathCache) insert(v *path) {
+ v.next = c.head
+ v.prev = c.head.prev
+ v.prev.next = v
+ v.next.prev = v
+}
diff --git a/gio/giold/text/lru_test.go b/gio/giold/text/lru_test.go
new file mode 100644
index 0000000..fb8d8d1
--- /dev/null
+++ b/gio/giold/text/lru_test.go
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package text
+
+import (
+ "strconv"
+ "testing"
+
+ "realy.lol/gio/op"
+)
+
+func TestLayoutLRU(t *testing.T) {
+ c := new(layoutCache)
+ put := func(i int) {
+ c.Put(layoutKey{str: strconv.Itoa(i)}, nil)
+ }
+ get := func(i int) bool {
+ _, ok := c.Get(layoutKey{str: strconv.Itoa(i)})
+ return ok
+ }
+ testLRU(t, put, get)
+}
+
+func TestPathLRU(t *testing.T) {
+ c := new(pathCache)
+ put := func(i int) {
+ c.Put(pathKey{str: strconv.Itoa(i)}, op.CallOp{})
+ }
+ get := func(i int) bool {
+ _, ok := c.Get(pathKey{str: strconv.Itoa(i)})
+ return ok
+ }
+ testLRU(t, put, get)
+}
+
+func testLRU(t *testing.T, put func(i int), get func(i int) bool) {
+ for i := 0; i < maxSize; i++ {
+ put(i)
+ }
+ for i := 0; i < maxSize; i++ {
+ if !get(i) {
+ t.Fatalf("key %d was evicted", i)
+ }
+ }
+ put(maxSize)
+ for i := 1; i < maxSize+1; i++ {
+ if !get(i) {
+ t.Fatalf("key %d was evicted", i)
+ }
+ }
+ if i := 0; get(i) {
+ t.Fatalf("key %d was not evicted", i)
+ }
+}
diff --git a/gio/giold/text/shaper.go b/gio/giold/text/shaper.go
new file mode 100644
index 0000000..88f1fbf
--- /dev/null
+++ b/gio/giold/text/shaper.go
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package text
+
+import (
+ "io"
+ "strings"
+
+ "golang.org/x/image/math/fixed"
+
+ "realy.lol/gio/op"
+)
+
+// Shaper implements layout and shaping of text.
+type Shaper interface {
+ // Layout a text according to a set of options.
+ Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line,
+ error)
+ // LayoutString is Layout for strings.
+ LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line
+ // Shape a line of text and return a clipping operation for its outline.
+ Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp
+}
+
+// A FontFace is a Font and a matching Face.
+type FontFace struct {
+ Font Font
+ Face Face
+}
+
+// Cache implements cached layout and shaping of text from a set of
+// registered fonts.
+//
+// If a font matches no registered shape, Cache falls back to the
+// first registered face.
+//
+// The LayoutString and ShapeString results are cached and re-used if
+// possible.
+type Cache struct {
+ def Typeface
+ faces map[Font]*faceCache
+}
+
+type faceCache struct {
+ face Face
+ layoutCache layoutCache
+ pathCache pathCache
+}
+
+func (c *Cache) lookup(font Font) *faceCache {
+ f := c.faceForStyle(font)
+ if f == nil {
+ font.Typeface = c.def
+ f = c.faceForStyle(font)
+ }
+ return f
+}
+
+func (c *Cache) faceForStyle(font Font) *faceCache {
+ tf := c.faces[font]
+ if tf == nil {
+ font := font
+ font.Weight = Normal
+ tf = c.faces[font]
+ }
+ if tf == nil {
+ font := font
+ font.Style = Regular
+ tf = c.faces[font]
+ }
+ if tf == nil {
+ font := font
+ font.Style = Regular
+ font.Weight = Normal
+ tf = c.faces[font]
+ }
+ return tf
+}
+
+func NewCache(collection []FontFace) *Cache {
+ c := &Cache{
+ faces: make(map[Font]*faceCache),
+ }
+ for i, ff := range collection {
+ if i == 0 {
+ c.def = ff.Font.Typeface
+ }
+ c.faces[ff.Font] = &faceCache{face: ff.Face}
+ }
+ return c
+}
+
+// Layout implements the Shaper interface.
+func (s *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int,
+ txt io.Reader) ([]Line, error) {
+ cache := s.lookup(font)
+ return cache.face.Layout(size, maxWidth, txt)
+}
+
+// LayoutString is a caching implementation of the Shaper interface.
+func (s *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int,
+ str string) []Line {
+ cache := s.lookup(font)
+ return cache.layout(size, maxWidth, str)
+}
+
+// Shape is a caching implementation of the Shaper interface. Shape assumes that the layout
+// argument is unchanged from a call to Layout or LayoutString.
+func (s *Cache) Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp {
+ cache := s.lookup(font)
+ return cache.shape(size, layout)
+}
+
+func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int,
+ str string) []Line {
+ if f == nil {
+ return nil
+ }
+ lk := layoutKey{
+ ppem: ppem,
+ maxWidth: maxWidth,
+ str: str,
+ }
+ if l, ok := f.layoutCache.Get(lk); ok {
+ return l
+ }
+ l, _ := f.face.Layout(ppem, maxWidth, strings.NewReader(str))
+ f.layoutCache.Put(lk, l)
+ return l
+}
+
+func (f *faceCache) shape(ppem fixed.Int26_6, layout Layout) op.CallOp {
+ if f == nil {
+ return op.CallOp{}
+ }
+ pk := pathKey{
+ ppem: ppem,
+ str: layout.Text,
+ }
+ if clip, ok := f.pathCache.Get(pk); ok {
+ return clip
+ }
+ clip := f.face.Shape(ppem, layout)
+ f.pathCache.Put(pk, clip)
+ return clip
+}
diff --git a/gio/giold/text/text.go b/gio/giold/text/text.go
new file mode 100644
index 0000000..b50cc8a
--- /dev/null
+++ b/gio/giold/text/text.go
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package text
+
+import (
+ "io"
+
+ "golang.org/x/image/math/fixed"
+
+ "realy.lol/gio/op"
+)
+
+// A Line contains the measurements of a line of text.
+type Line struct {
+ Layout Layout
+ // Width is the width of the line.
+ Width fixed.Int26_6
+ // Ascent is the height above the baseline.
+ Ascent fixed.Int26_6
+ // Descent is the height below the baseline, including
+ // the line gap.
+ Descent fixed.Int26_6
+ // Bounds is the visible bounds of the line.
+ Bounds fixed.Rectangle26_6
+}
+
+type Layout struct {
+ Text string
+ Advances []fixed.Int26_6
+}
+
+// Style is the font style.
+type Style int
+
+// Weight is a font weight, in CSS units subtracted 400 so the zero value
+// is normal text weight.
+type Weight int
+
+// Font specify a particular typeface variant, style and weight.
+type Font struct {
+ Typeface Typeface
+ Variant Variant
+ Style Style
+ // Weight is the text weight. If zero, Normal is used instead.
+ Weight Weight
+}
+
+// Face implements text layout and shaping for a particular font. All
+// methods must be safe for concurrent use.
+type Face interface {
+ Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error)
+ Shape(ppem fixed.Int26_6, str Layout) op.CallOp
+}
+
+// Typeface identifies a particular typeface design. The empty
+// string denotes the default typeface.
+type Typeface string
+
+// Variant denotes a typeface variant such as "Mono" or "Smallcaps".
+type Variant string
+
+type Alignment uint8
+
+const (
+ Start Alignment = iota
+ End
+ Middle
+)
+
+const (
+ Regular Style = iota
+ Italic
+)
+
+const (
+ Normal Weight = 400 - 400
+ Medium Weight = 500 - 400
+ Bold Weight = 600 - 400
+)
+
+func (a Alignment) String() string {
+ switch a {
+ case Start:
+ return "Start"
+ case End:
+ return "End"
+ case Middle:
+ return "Middle"
+ default:
+ panic("unreachable")
+ }
+}
diff --git a/gio/giold/unit/unit.go b/gio/giold/unit/unit.go
new file mode 100644
index 0000000..fd2245c
--- /dev/null
+++ b/gio/giold/unit/unit.go
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+
+Package unit implements device independent units and values.
+
+A Value is a value with a Unit attached.
+
+Device independent pixel, or dp, is the unit for sizes independent of
+the underlying display device.
+
+Scaled pixels, or sp, is the unit for text sizes. An sp is like dp with
+text scaling applied.
+
+Finally, pixels, or px, is the unit for display dependent pixels. Their
+size vary between platforms and displays.
+
+To maintain a constant visual size across platforms and displays, always
+use dps or sps to define user interfaces. Only use pixels for derived
+values.
+
+*/
+package unit
+
+import (
+ "fmt"
+ "math"
+)
+
+// Value is a value with a unit.
+type Value struct {
+ V float32
+ U Unit
+}
+
+// Unit represents a unit for a Value.
+type Unit uint8
+
+// Metric converts Values to device-dependent pixels, px. The zero
+// value represents a 1-to-1 scale from dp, sp to pixels.
+type Metric struct {
+ // PxPerDp is the device-dependent pixels per dp.
+ PxPerDp float32
+ // PxPerSp is the device-dependent pixels per sp.
+ PxPerSp float32
+}
+
+const (
+ // UnitPx represent device pixels in the resolution of
+ // the underlying display.
+ UnitPx Unit = iota
+ // UnitDp represents device independent pixels. 1 dp will
+ // have the same apparent size across platforms and
+ // display resolutions.
+ UnitDp
+ // UnitSp is like UnitDp but for font sizes.
+ UnitSp
+)
+
+// Px returns the Value for v device pixels.
+func Px(v float32) Value {
+ return Value{V: v, U: UnitPx}
+}
+
+// Dp returns the Value for v device independent
+// pixels.
+func Dp(v float32) Value {
+ return Value{V: v, U: UnitDp}
+}
+
+// Sp returns the Value for v scaled dps.
+func Sp(v float32) Value {
+ return Value{V: v, U: UnitSp}
+}
+
+// Scale returns the value scaled by s.
+func (v Value) Scale(s float32) Value {
+ v.V *= s
+ return v
+}
+
+func (v Value) String() string {
+ return fmt.Sprintf("%g%s", v.V, v.U)
+}
+
+func (u Unit) String() string {
+ switch u {
+ case UnitPx:
+ return "px"
+ case UnitDp:
+ return "dp"
+ case UnitSp:
+ return "sp"
+ default:
+ panic("unknown unit")
+ }
+}
+
+// Add a list of Values.
+func Add(c Metric, values ...Value) Value {
+ var sum Value
+ for _, v := range values {
+ sum, v = compatible(c, sum, v)
+ sum.V += v.V
+ }
+ return sum
+}
+
+// Max returns the maximum of a list of Values.
+func Max(c Metric, values ...Value) Value {
+ var max Value
+ for _, v := range values {
+ max, v = compatible(c, max, v)
+ if v.V > max.V {
+ max.V = v.V
+ }
+ }
+ return max
+}
+
+func (c Metric) Px(v Value) int {
+ var r float32
+ switch v.U {
+ case UnitPx:
+ r = v.V
+ case UnitDp:
+ s := c.PxPerDp
+ if s == 0 {
+ s = 1
+ }
+ r = s * v.V
+ case UnitSp:
+ s := c.PxPerSp
+ if s == 0 {
+ s = 1
+ }
+ r = s * v.V
+ default:
+ panic("unknown unit")
+ }
+ return int(math.Round(float64(r)))
+}
+
+func compatible(c Metric, v1, v2 Value) (Value, Value) {
+ if v1.U == v2.U {
+ return v1, v2
+ }
+ if v1.V == 0 {
+ v1.U = v2.U
+ return v1, v2
+ }
+ if v2.V == 0 {
+ v2.U = v1.U
+ return v1, v2
+ }
+ return Px(float32(c.Px(v1))), Px(float32(c.Px(v2)))
+}
diff --git a/gio/giold/widget/bool.go b/gio/giold/widget/bool.go
new file mode 100644
index 0000000..feb4a8a
--- /dev/null
+++ b/gio/giold/widget/bool.go
@@ -0,0 +1,44 @@
+package widget
+
+import (
+ "realy.lol/gio/layout"
+)
+
+type Bool struct {
+ Value bool
+
+ clk Clickable
+
+ changed bool
+}
+
+// Changed reports whether Value has changed since the last
+// call to Changed.
+func (b *Bool) Changed() bool {
+ changed := b.changed
+ b.changed = false
+ return changed
+}
+
+// Hovered returns whether pointer is over the element.
+func (b *Bool) Hovered() bool {
+ return b.clk.Hovered()
+}
+
+// Pressed returns whether pointer is pressing the element.
+func (b *Bool) Pressed() bool {
+ return b.clk.Pressed()
+}
+
+func (b *Bool) History() []Press {
+ return b.clk.History()
+}
+
+func (b *Bool) Layout(gtx layout.Context) layout.Dimensions {
+ dims := b.clk.Layout(gtx)
+ for b.clk.Clicked() {
+ b.Value = !b.Value
+ b.changed = true
+ }
+ return dims
+}
diff --git a/gio/giold/widget/border.go b/gio/giold/widget/border.go
new file mode 100644
index 0000000..e4bed6a
--- /dev/null
+++ b/gio/giold/widget/border.go
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image/color"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+)
+
+// Border lays out a widget and draws a border inside it.
+type Border struct {
+ Color color.NRGBA
+ CornerRadius unit.Value
+ Width unit.Value
+}
+
+func (b Border) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
+ dims := w(gtx)
+ sz := layout.FPt(dims.Size)
+
+ rr := float32(gtx.Px(b.CornerRadius))
+ width := float32(gtx.Px(b.Width))
+ sz.X -= width
+ sz.Y -= width
+
+ r := f32.Rectangle{Max: sz}
+ r = r.Add(f32.Point{X: width * 0.5, Y: width * 0.5})
+
+ paint.FillShape(gtx.Ops,
+ b.Color,
+ clip.Stroke{
+ Path: clip.UniformRRect(r, rr).Path(gtx.Ops),
+ Style: clip.StrokeStyle{Width: width},
+ }.Op(),
+ )
+
+ return dims
+}
diff --git a/gio/giold/widget/buffer.go b/gio/giold/widget/buffer.go
new file mode 100644
index 0000000..e658d56
--- /dev/null
+++ b/gio/giold/widget/buffer.go
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "io"
+ "strings"
+ "unicode/utf8"
+)
+
+// editBuffer implements a gap buffer for text editing.
+type editBuffer struct {
+ // pos is the byte position for Read and ReadRune.
+ pos int
+
+ // The gap start and end in bytes.
+ gapstart, gapend int
+ text []byte
+
+ // changed tracks whether the buffer content
+ // has changed since the last call to Changed.
+ changed bool
+}
+
+const minSpace = 5
+
+func (e *editBuffer) Changed() bool {
+ c := e.changed
+ e.changed = false
+ return c
+}
+
+func (e *editBuffer) deleteRunes(caret, runes int) int {
+ e.moveGap(caret, 0)
+ for ; runes < 0 && e.gapstart > 0; runes++ {
+ _, s := utf8.DecodeLastRune(e.text[:e.gapstart])
+ e.gapstart -= s
+ caret -= s
+ e.changed = e.changed || s > 0
+ }
+ for ; runes > 0 && e.gapend < len(e.text); runes-- {
+ _, s := utf8.DecodeRune(e.text[e.gapend:])
+ e.gapend += s
+ e.changed = e.changed || s > 0
+ }
+ return caret
+}
+
+// moveGap moves the gap to the caret position. After returning,
+// the gap is guaranteed to be at least space bytes long.
+func (e *editBuffer) moveGap(caret, space int) {
+ if e.gapLen() < space {
+ if space < minSpace {
+ space = minSpace
+ }
+ txt := make([]byte, e.len()+space)
+ // Expand to capacity.
+ txt = txt[:cap(txt)]
+ gaplen := len(txt) - e.len()
+ if caret > e.gapstart {
+ copy(txt, e.text[:e.gapstart])
+ copy(txt[caret+gaplen:], e.text[caret:])
+ copy(txt[e.gapstart:], e.text[e.gapend:caret+e.gapLen()])
+ } else {
+ copy(txt, e.text[:caret])
+ copy(txt[e.gapstart+gaplen:], e.text[e.gapend:])
+ copy(txt[caret+gaplen:], e.text[caret:e.gapstart])
+ }
+ e.text = txt
+ e.gapstart = caret
+ e.gapend = e.gapstart + gaplen
+ } else {
+ if caret > e.gapstart {
+ copy(e.text[e.gapstart:], e.text[e.gapend:caret+e.gapLen()])
+ } else {
+ copy(e.text[caret+e.gapLen():], e.text[caret:e.gapstart])
+ }
+ l := e.gapLen()
+ e.gapstart = caret
+ e.gapend = e.gapstart + l
+ }
+}
+
+func (e *editBuffer) len() int {
+ return len(e.text) - e.gapLen()
+}
+
+func (e *editBuffer) gapLen() int {
+ return e.gapend - e.gapstart
+}
+
+func (e *editBuffer) Reset() {
+ e.Seek(0, io.SeekStart)
+}
+
+// Seek implements io.Seeker
+func (e *editBuffer) Seek(offset int64, whence int) (ret int64, err error) {
+ switch whence {
+ case io.SeekStart:
+ e.pos = int(offset)
+ case io.SeekCurrent:
+ e.pos += int(offset)
+ case io.SeekEnd:
+ e.pos = e.len() - int(offset)
+ }
+ if e.pos < 0 {
+ e.pos = 0
+ } else if e.pos > e.len() {
+ e.pos = e.len()
+ }
+ return int64(e.pos), nil
+}
+
+func (e *editBuffer) Read(p []byte) (int, error) {
+ if e.pos == e.len() {
+ return 0, io.EOF
+ }
+ var total int
+ if e.pos < e.gapstart {
+ n := copy(p, e.text[e.pos:e.gapstart])
+ p = p[n:]
+ total += n
+ e.pos += n
+ }
+ if e.pos >= e.gapstart {
+ n := copy(p, e.text[e.pos+e.gapLen():])
+ total += n
+ e.pos += n
+ }
+ if e.pos > e.len() {
+ panic("hey!")
+ }
+ return total, nil
+}
+
+func (e *editBuffer) ReadRune() (rune, int, error) {
+ if e.pos == e.len() {
+ return 0, 0, io.EOF
+ }
+ r, s := e.runeAt(e.pos)
+ e.pos += s
+ return r, s, nil
+}
+
+func (e *editBuffer) String() string {
+ var b strings.Builder
+ b.Grow(e.len())
+ b.Write(e.text[:e.gapstart])
+ b.Write(e.text[e.gapend:])
+ return b.String()
+}
+
+func (e *editBuffer) prepend(caret int, s string) {
+ e.moveGap(caret, len(s))
+ copy(e.text[caret:], s)
+ e.gapstart += len(s)
+ e.changed = e.changed || len(s) > 0
+}
+
+func (e *editBuffer) runeBefore(idx int) (rune, int) {
+ if idx > e.gapstart {
+ idx += e.gapLen()
+ }
+ return utf8.DecodeLastRune(e.text[:idx])
+}
+
+func (e *editBuffer) runeAt(idx int) (rune, int) {
+ if idx >= e.gapstart {
+ idx += e.gapLen()
+ }
+ return utf8.DecodeRune(e.text[idx:])
+}
diff --git a/gio/giold/widget/button.go b/gio/giold/widget/button.go
new file mode 100644
index 0000000..2c23c5d
--- /dev/null
+++ b/gio/giold/widget/button.go
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+ "time"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/gesture"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+)
+
+// Clickable represents a clickable area.
+type Clickable struct {
+ click gesture.Click
+ clicks []Click
+ // prevClicks is the index into clicks that marks the clicks
+ // from the most recent Layout call. prevClicks is used to keep
+ // clicks bounded.
+ prevClicks int
+ history []Press
+}
+
+// Click represents a click.
+type Click struct {
+ Modifiers key.Modifiers
+ NumClicks int
+}
+
+// Press represents a past pointer press.
+type Press struct {
+ // Position of the press.
+ Position f32.Point
+ // Start is when the press began.
+ Start time.Time
+ // End is when the press was ended by a release or cancel.
+ // A zero End means it hasn't ended yet.
+ End time.Time
+ // Cancelled is true for cancelled presses.
+ Cancelled bool
+}
+
+// Click executes a simple programmatic click
+func (b *Clickable) Click() {
+ b.clicks = append(b.clicks, Click{
+ Modifiers: 0,
+ NumClicks: 1,
+ })
+}
+
+// Clicked reports whether there are pending clicks as would be
+// reported by Clicks. If so, Clicked removes the earliest click.
+func (b *Clickable) Clicked() bool {
+ if len(b.clicks) == 0 {
+ return false
+ }
+ n := copy(b.clicks, b.clicks[1:])
+ b.clicks = b.clicks[:n]
+ if b.prevClicks > 0 {
+ b.prevClicks--
+ }
+ return true
+}
+
+// Hovered returns whether pointer is over the element.
+func (b *Clickable) Hovered() bool {
+ return b.click.Hovered()
+}
+
+// Pressed returns whether pointer is pressing the element.
+func (b *Clickable) Pressed() bool {
+ return b.click.Pressed()
+}
+
+// Clicks returns and clear the clicks since the last call to Clicks.
+func (b *Clickable) Clicks() []Click {
+ clicks := b.clicks
+ b.clicks = nil
+ b.prevClicks = 0
+ return clicks
+}
+
+// History is the past pointer presses useful for drawing markers.
+// History is retained for a short duration (about a second).
+func (b *Clickable) History() []Press {
+ return b.history
+}
+
+// Layout and update the button state
+func (b *Clickable) Layout(gtx layout.Context) layout.Dimensions {
+ b.update(gtx)
+ stack := op.Save(gtx.Ops)
+ pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
+ b.click.Add(gtx.Ops)
+ stack.Load()
+ for len(b.history) > 0 {
+ c := b.history[0]
+ if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second {
+ break
+ }
+ n := copy(b.history, b.history[1:])
+ b.history = b.history[:n]
+ }
+ return layout.Dimensions{Size: gtx.Constraints.Min}
+}
+
+// update the button state by processing events.
+func (b *Clickable) update(gtx layout.Context) {
+ // Flush clicks from before the last update.
+ n := copy(b.clicks, b.clicks[b.prevClicks:])
+ b.clicks = b.clicks[:n]
+ b.prevClicks = n
+
+ for _, e := range b.click.Events(gtx) {
+ switch e.Type {
+ case gesture.TypeClick:
+ b.clicks = append(b.clicks, Click{
+ Modifiers: e.Modifiers,
+ NumClicks: e.NumClicks,
+ })
+ if l := len(b.history); l > 0 {
+ b.history[l-1].End = gtx.Now
+ }
+ case gesture.TypeCancel:
+ for i := range b.history {
+ b.history[i].Cancelled = true
+ if b.history[i].End.IsZero() {
+ b.history[i].End = gtx.Now
+ }
+ }
+ case gesture.TypePress:
+ b.history = append(b.history, Press{
+ Position: e.Position,
+ Start: gtx.Now,
+ })
+ }
+ }
+}
diff --git a/gio/giold/widget/doc.go b/gio/giold/widget/doc.go
new file mode 100644
index 0000000..df4e55f
--- /dev/null
+++ b/gio/giold/widget/doc.go
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package widget implements state tracking and event handling of
+// common user interface controls. To draw widgets, use a theme
+// packages such as package realy.lol/gio/widget/material.
+package widget
diff --git a/gio/giold/widget/editor.go b/gio/giold/widget/editor.go
new file mode 100644
index 0000000..e44f54f
--- /dev/null
+++ b/gio/giold/widget/editor.go
@@ -0,0 +1,1328 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "bufio"
+ "bytes"
+ "image"
+ "io"
+ "math"
+ "runtime"
+ "sort"
+ "strings"
+ "time"
+ "unicode"
+ "unicode/utf8"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/gesture"
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+
+ "golang.org/x/image/math/fixed"
+)
+
+// Editor implements an editable and scrollable text area.
+type Editor struct {
+ Alignment text.Alignment
+ // SingleLine force the text to stay on a single line.
+ // SingleLine also sets the scrolling direction to
+ // horizontal.
+ SingleLine bool
+ // Submit enabled translation of carriage return keys to SubmitEvents.
+ // If not enabled, carriage returns are inserted as newlines in the text.
+ Submit bool
+ // Mask replaces the visual display of each rune in the contents with the given rune.
+ // Newline characters are not masked. When non-zero, the unmasked contents
+ // are accessed by Len, Text, and SetText.
+ Mask rune
+
+ eventKey int
+ font text.Font
+ shaper text.Shaper
+ textSize fixed.Int26_6
+ blinkStart time.Time
+ focused bool
+ rr editBuffer
+ maskReader maskReader
+ lastMask rune
+ maxWidth int
+ viewSize image.Point
+ valid bool
+ lines []text.Line
+ shapes []line
+ dims layout.Dimensions
+ requestFocus bool
+
+ caret struct {
+ on bool
+ scroll bool
+ // start is the current caret position, and also the start position of
+ // selected text. end is the end positon of selected text. If start.ofs
+ // == end.ofs, then there's no selection. Note that it's possible (and
+ // common) that the caret (start) is after the end, e.g. after
+ // Shift-DownArrow.
+ start combinedPos
+ end combinedPos
+ }
+
+ dragging bool
+ dragger gesture.Drag
+ scroller gesture.Scroll
+ scrollOff image.Point
+
+ clicker gesture.Click
+
+ // events is the list of events not yet processed.
+ events []EditorEvent
+ // prevEvents is the number of events from the previous frame.
+ prevEvents int
+}
+
+type maskReader struct {
+ // rr is the underlying reader.
+ rr io.RuneReader
+ maskBuf [utf8.UTFMax]byte
+ // mask is the utf-8 encoded mask rune.
+ mask []byte
+ // overflow contains excess mask bytes left over after the last Read call.
+ overflow []byte
+}
+
+// combinedPos is a point in the editor.
+type combinedPos struct {
+ // editorBuffer offset. The other three fields are based off of this one.
+ ofs int
+
+ // lineCol.Y = line (offset into Editor.lines), and X = col (offset into
+ // Editor.lines[Y])
+ lineCol screenPos
+
+ // Pixel coordinates
+ x fixed.Int26_6
+ y int
+
+ // xoff is the offset to the current position when moving between lines.
+ xoff fixed.Int26_6
+}
+
+type selectionAction int
+
+const (
+ selectionExtend selectionAction = iota
+ selectionClear
+)
+
+func (m *maskReader) Reset(r io.RuneReader, mr rune) {
+ m.rr = r
+ n := utf8.EncodeRune(m.maskBuf[:], mr)
+ m.mask = m.maskBuf[:n]
+}
+
+// Read reads from the underlying reader and replaces every
+// rune with the mask rune.
+func (m *maskReader) Read(b []byte) (n int, err error) {
+ for len(b) > 0 {
+ var replacement []byte
+ if len(m.overflow) > 0 {
+ replacement = m.overflow
+ } else {
+ var r rune
+ r, _, err = m.rr.ReadRune()
+ if err != nil {
+ break
+ }
+ if r == '\n' {
+ replacement = []byte{'\n'}
+ } else {
+ replacement = m.mask
+ }
+ }
+ nn := copy(b, replacement)
+ m.overflow = replacement[nn:]
+ n += nn
+ b = b[nn:]
+ }
+ return n, err
+}
+
+type EditorEvent interface {
+ isEditorEvent()
+}
+
+// A ChangeEvent is generated for every user change to the text.
+type ChangeEvent struct{}
+
+// A SubmitEvent is generated when Submit is set
+// and a carriage return key is pressed.
+type SubmitEvent struct {
+ Text string
+}
+
+// A SelectEvent is generated when the user selects some text, or changes the
+// selection (e.g. with a shift-click), including if they remove the
+// selection. The selected text is not part of the event, on the theory that
+// it could be a relatively expensive operation (for a large editor), most
+// applications won't actually care about it, and those that do can call
+// Editor.SelectedText() (which can be empty).
+type SelectEvent struct{}
+
+type line struct {
+ offset image.Point
+ clip op.CallOp
+ selected bool
+ selectionYOffs int
+ selectionSize image.Point
+}
+
+const (
+ blinksPerSecond = 1
+ maxBlinkDuration = 10 * time.Second
+)
+
+// Events returns available editor events.
+func (e *Editor) Events() []EditorEvent {
+ events := e.events
+ e.events = nil
+ e.prevEvents = 0
+ return events
+}
+
+func (e *Editor) processEvents(gtx layout.Context) {
+ // Flush events from before the previous Layout.
+ n := copy(e.events, e.events[e.prevEvents:])
+ e.events = e.events[:n]
+ e.prevEvents = n
+
+ if e.shaper == nil {
+ // Can't process events without a shaper.
+ return
+ }
+ oldStart, oldLen := min(e.caret.start.ofs,
+ e.caret.end.ofs), e.SelectionLen()
+ e.processPointer(gtx)
+ e.processKey(gtx)
+ // Queue a SelectEvent if the selection changed, including if it went away.
+ if newStart, newLen := min(e.caret.start.ofs,
+ e.caret.end.ofs), e.SelectionLen(); oldStart != newStart || oldLen != newLen {
+ e.events = append(e.events, SelectEvent{})
+ }
+}
+
+func (e *Editor) makeValid(positions ...*combinedPos) {
+ if e.valid {
+ return
+ }
+ e.lines, e.dims = e.layoutText(e.shaper)
+ e.makeValidCaret(positions...)
+ e.valid = true
+}
+
+func (e *Editor) processPointer(gtx layout.Context) {
+ sbounds := e.scrollBounds()
+ var smin, smax int
+ var axis gesture.Axis
+ if e.SingleLine {
+ axis = gesture.Horizontal
+ smin, smax = sbounds.Min.X, sbounds.Max.X
+ } else {
+ axis = gesture.Vertical
+ smin, smax = sbounds.Min.Y, sbounds.Max.Y
+ }
+ sdist := e.scroller.Scroll(gtx.Metric, gtx, gtx.Now, axis)
+ var soff int
+ if e.SingleLine {
+ e.scrollRel(sdist, 0)
+ soff = e.scrollOff.X
+ } else {
+ e.scrollRel(0, sdist)
+ soff = e.scrollOff.Y
+ }
+ for _, evt := range e.clickDragEvents(gtx) {
+ switch evt := evt.(type) {
+ case gesture.ClickEvent:
+ switch {
+ case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse,
+ evt.Type == gesture.TypeClick:
+ prevCaretPos := e.caret.start
+ e.blinkStart = gtx.Now
+ e.moveCoord(image.Point{
+ X: int(math.Round(float64(evt.Position.X))),
+ Y: int(math.Round(float64(evt.Position.Y))),
+ })
+ e.requestFocus = true
+ if e.scroller.State() != gesture.StateFlinging {
+ e.caret.scroll = true
+ }
+
+ if evt.Modifiers == key.ModShift {
+ // If they clicked closer to the end, then change the end to
+ // where the caret used to be (effectively swapping start & end).
+ if abs(e.caret.end.ofs-e.caret.start.ofs) < abs(e.caret.start.ofs-prevCaretPos.ofs) {
+ e.caret.end = prevCaretPos
+ }
+ } else {
+ e.ClearSelection()
+ }
+ e.dragging = true
+
+ // Process a double-click.
+ if evt.NumClicks == 2 {
+ e.moveWord(-1, selectionClear)
+ e.moveWord(1, selectionExtend)
+ e.dragging = false
+ }
+ }
+ case pointer.Event:
+ release := false
+ switch {
+ case evt.Type == pointer.Release && evt.Source == pointer.Mouse:
+ release = true
+ fallthrough
+ case evt.Type == pointer.Drag && evt.Source == pointer.Mouse:
+ if e.dragging {
+ e.blinkStart = gtx.Now
+ e.moveCoord(image.Point{
+ X: int(math.Round(float64(evt.Position.X))),
+ Y: int(math.Round(float64(evt.Position.Y))),
+ })
+ e.caret.scroll = true
+
+ if release {
+ e.dragging = false
+ }
+ }
+ }
+ }
+ }
+
+ if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) {
+ e.scroller.Stop()
+ }
+}
+
+func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event {
+ var combinedEvents []event.Event
+ for _, evt := range e.clicker.Events(gtx) {
+ combinedEvents = append(combinedEvents, evt)
+ }
+ for _, evt := range e.dragger.Events(gtx.Metric, gtx, gesture.Both) {
+ combinedEvents = append(combinedEvents, evt)
+ }
+ return combinedEvents
+}
+
+func (e *Editor) processKey(gtx layout.Context) {
+ if e.rr.Changed() {
+ e.events = append(e.events, ChangeEvent{})
+ }
+ for _, ke := range gtx.Events(&e.eventKey) {
+ e.blinkStart = gtx.Now
+ switch ke := ke.(type) {
+ case key.FocusEvent:
+ e.focused = ke.Focus
+ case key.Event:
+ if !e.focused || ke.State != key.Press {
+ break
+ }
+ if e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
+ if !ke.Modifiers.Contain(key.ModShift) {
+ e.events = append(e.events, SubmitEvent{
+ Text: e.Text(),
+ })
+ continue
+ }
+ }
+ if e.command(gtx, ke) {
+ e.caret.scroll = true
+ e.scroller.Stop()
+ }
+ case key.EditEvent:
+ e.caret.scroll = true
+ e.scroller.Stop()
+ e.append(ke.Text)
+ // Complete a paste event, initiated by Shortcut-V in Editor.command().
+ case clipboard.Event:
+ e.caret.scroll = true
+ e.scroller.Stop()
+ e.append(ke.Text)
+ }
+ if e.rr.Changed() {
+ e.events = append(e.events, ChangeEvent{})
+ }
+ }
+}
+
+func (e *Editor) moveLines(distance int, selAct selectionAction) {
+ e.caret.start = e.movePosToLine(e.caret.start,
+ e.caret.start.x+e.caret.start.xoff, e.caret.start.lineCol.Y+distance)
+ e.updateSelection(selAct)
+}
+
+func (e *Editor) command(gtx layout.Context, k key.Event) bool {
+ modSkip := key.ModCtrl
+ if runtime.GOOS == "darwin" {
+ modSkip = key.ModAlt
+ }
+ moveByWord := k.Modifiers.Contain(modSkip)
+ selAct := selectionClear
+ if k.Modifiers.Contain(key.ModShift) {
+ selAct = selectionExtend
+ }
+ switch k.Name {
+ case key.NameReturn, key.NameEnter:
+ e.append("\n")
+ case key.NameDeleteBackward:
+ if moveByWord {
+ e.deleteWord(-1)
+ } else {
+ e.Delete(-1)
+ }
+ case key.NameDeleteForward:
+ if moveByWord {
+ e.deleteWord(1)
+ } else {
+ e.Delete(1)
+ }
+ case key.NameUpArrow:
+ e.moveLines(-1, selAct)
+ case key.NameDownArrow:
+ e.moveLines(+1, selAct)
+ case key.NameLeftArrow:
+ if moveByWord {
+ e.moveWord(-1, selAct)
+ } else {
+ if selAct == selectionClear {
+ e.ClearSelection()
+ }
+ e.MoveCaret(-1, -1*int(selAct))
+ }
+ case key.NameRightArrow:
+ if moveByWord {
+ e.moveWord(1, selAct)
+ } else {
+ if selAct == selectionClear {
+ e.ClearSelection()
+ }
+ e.MoveCaret(1, int(selAct))
+ }
+ case key.NamePageUp:
+ e.movePages(-1, selAct)
+ case key.NamePageDown:
+ e.movePages(+1, selAct)
+ case key.NameHome:
+ e.moveStart(selAct)
+ case key.NameEnd:
+ e.moveEnd(selAct)
+ // Initiate a paste operation, by requesting the clipboard contents; other
+ // half is in Editor.processKey() under clipboard.Event.
+ case "V":
+ if k.Modifiers != key.ModShortcut {
+ return false
+ }
+ clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops)
+ // Copy or Cut selection -- ignored if nothing selected.
+ case "C", "X":
+ if k.Modifiers != key.ModShortcut {
+ return false
+ }
+ if text := e.SelectedText(); text != "" {
+ clipboard.WriteOp{Text: text}.Add(gtx.Ops)
+ if k.Name == "X" {
+ e.Delete(1)
+ }
+ }
+ // Select all
+ case "A":
+ if k.Modifiers != key.ModShortcut {
+ return false
+ }
+ e.caret.end, e.caret.start = e.offsetToScreenPos2(0, e.Len())
+ default:
+ return false
+ }
+ return true
+}
+
+// Focus requests the input focus for the Editor.
+func (e *Editor) Focus() {
+ e.requestFocus = true
+}
+
+// Focused returns whether the editor is focused or not.
+func (e *Editor) Focused() bool {
+ return e.focused
+}
+
+// Layout lays out the editor.
+func (e *Editor) Layout(gtx layout.Context, sh text.Shaper, font text.Font,
+ size unit.Value) layout.Dimensions {
+ textSize := fixed.I(gtx.Px(size))
+ if e.font != font || e.textSize != textSize {
+ e.invalidate()
+ e.font = font
+ e.textSize = textSize
+ }
+ maxWidth := gtx.Constraints.Max.X
+ if e.SingleLine {
+ maxWidth = inf
+ }
+ if maxWidth != e.maxWidth {
+ e.maxWidth = maxWidth
+ e.invalidate()
+ }
+ if sh != e.shaper {
+ e.shaper = sh
+ e.invalidate()
+ }
+ if e.Mask != e.lastMask {
+ e.lastMask = e.Mask
+ e.invalidate()
+ }
+
+ e.makeValid()
+ e.processEvents(gtx)
+ e.makeValid()
+
+ if viewSize := gtx.Constraints.Constrain(e.dims.Size); viewSize != e.viewSize {
+ e.viewSize = viewSize
+ e.invalidate()
+ }
+ e.makeValid()
+
+ return e.layout(gtx)
+}
+
+func (e *Editor) layout(gtx layout.Context) layout.Dimensions {
+ // Adjust scrolling for new viewport and layout.
+ e.scrollRel(0, 0)
+
+ if e.caret.scroll {
+ e.caret.scroll = false
+ e.scrollToCaret()
+ }
+
+ off := image.Point{
+ X: -e.scrollOff.X,
+ Y: -e.scrollOff.Y,
+ }
+ clip := textPadding(e.lines)
+ clip.Max = clip.Max.Add(e.viewSize)
+ startSel, endSel := sortPoints(e.caret.start.lineCol, e.caret.end.lineCol)
+ it := segmentIterator{
+ startSel: startSel,
+ endSel: endSel,
+ Lines: e.lines,
+ Clip: clip,
+ Alignment: e.Alignment,
+ Width: e.viewSize.X,
+ Offset: off,
+ }
+ e.shapes = e.shapes[:0]
+ for {
+ layout, off, selected, yOffs, size, ok := it.Next()
+ if !ok {
+ break
+ }
+ path := e.shaper.Shape(e.font, e.textSize, layout)
+ e.shapes = append(e.shapes, line{off, path, selected, yOffs, size})
+ }
+
+ key.InputOp{Tag: &e.eventKey}.Add(gtx.Ops)
+ if e.requestFocus {
+ key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops)
+ key.SoftKeyboardOp{Show: true}.Add(gtx.Ops)
+ }
+ e.requestFocus = false
+ pointerPadding := gtx.Px(unit.Dp(4))
+ r := image.Rectangle{Max: e.viewSize}
+ r.Min.X -= pointerPadding
+ r.Min.Y -= pointerPadding
+ r.Max.X += pointerPadding
+ r.Max.X += pointerPadding
+ pointer.Rect(r).Add(gtx.Ops)
+ pointer.CursorNameOp{Name: pointer.CursorText}.Add(gtx.Ops)
+
+ var scrollRange image.Rectangle
+ if e.SingleLine {
+ scrollRange.Min.X = -e.scrollOff.X
+ scrollRange.Max.X = max(0, e.dims.Size.X-(e.scrollOff.X+e.viewSize.X))
+ } else {
+ scrollRange.Min.Y = -e.scrollOff.Y
+ scrollRange.Max.Y = max(0, e.dims.Size.Y-(e.scrollOff.Y+e.viewSize.Y))
+ }
+ e.scroller.Add(gtx.Ops, scrollRange)
+
+ e.clicker.Add(gtx.Ops)
+ e.dragger.Add(gtx.Ops)
+ e.caret.on = false
+ if e.focused {
+ now := gtx.Now
+ dt := now.Sub(e.blinkStart)
+ blinking := dt < maxBlinkDuration
+ const timePerBlink = time.Second / blinksPerSecond
+ nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2))
+ if blinking {
+ redraw := op.InvalidateOp{At: nextBlink}
+ redraw.Add(gtx.Ops)
+ }
+ e.caret.on = e.focused && (!blinking || dt%timePerBlink < timePerBlink/2)
+ }
+
+ return layout.Dimensions{Size: e.viewSize, Baseline: e.dims.Baseline}
+}
+
+// PaintSelection paints the contrasting background for selected text.
+func (e *Editor) PaintSelection(gtx layout.Context) {
+ cl := textPadding(e.lines)
+ cl.Max = cl.Max.Add(e.viewSize)
+ clip.Rect(cl).Add(gtx.Ops)
+ for _, shape := range e.shapes {
+ if !shape.selected {
+ continue
+ }
+ stack := op.Save(gtx.Ops)
+ offset := shape.offset
+ offset.Y += shape.selectionYOffs
+ op.Offset(layout.FPt(offset)).Add(gtx.Ops)
+ clip.Rect(image.Rectangle{Max: shape.selectionSize}).Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ stack.Load()
+ }
+}
+
+func (e *Editor) PaintText(gtx layout.Context) {
+ cl := textPadding(e.lines)
+ cl.Max = cl.Max.Add(e.viewSize)
+ clip.Rect(cl).Add(gtx.Ops)
+ for _, shape := range e.shapes {
+ stack := op.Save(gtx.Ops)
+ op.Offset(layout.FPt(shape.offset)).Add(gtx.Ops)
+ shape.clip.Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ stack.Load()
+ }
+}
+
+func (e *Editor) PaintCaret(gtx layout.Context) {
+ if !e.caret.on {
+ return
+ }
+ e.makeValid()
+ carWidth := fixed.I(gtx.Px(unit.Dp(1)))
+ carX := e.caret.start.x
+ carY := e.caret.start.y
+
+ defer op.Save(gtx.Ops).Load()
+ carX -= carWidth / 2
+ carAsc, carDesc := -e.lines[e.caret.start.lineCol.Y].Bounds.Min.Y, e.lines[e.caret.start.lineCol.Y].Bounds.Max.Y
+ carRect := image.Rectangle{
+ Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()},
+ Max: image.Point{X: carX.Ceil() + carWidth.Ceil(),
+ Y: carY + carDesc.Ceil()},
+ }
+ carRect = carRect.Add(image.Point{
+ X: -e.scrollOff.X,
+ Y: -e.scrollOff.Y,
+ })
+ cl := textPadding(e.lines)
+ // Account for caret width to each side.
+ whalf := (carWidth / 2).Ceil()
+ if cl.Max.X < whalf {
+ cl.Max.X = whalf
+ }
+ if cl.Min.X > -whalf {
+ cl.Min.X = -whalf
+ }
+ cl.Max = cl.Max.Add(e.viewSize)
+ carRect = cl.Intersect(carRect)
+ if !carRect.Empty() {
+ st := op.Save(gtx.Ops)
+ clip.Rect(carRect).Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ st.Load()
+ }
+}
+
+// Len is the length of the editor contents.
+func (e *Editor) Len() int {
+ return e.rr.len()
+}
+
+// Text returns the contents of the editor.
+func (e *Editor) Text() string {
+ return e.rr.String()
+}
+
+// SetText replaces the contents of the editor, clearing any selection first.
+func (e *Editor) SetText(s string) {
+ e.rr = editBuffer{}
+ e.caret.start = combinedPos{}
+ e.caret.end = combinedPos{}
+ e.prepend(s)
+}
+
+func (e *Editor) scrollBounds() image.Rectangle {
+ var b image.Rectangle
+ if e.SingleLine {
+ if len(e.lines) > 0 {
+ b.Min.X = align(e.Alignment, e.lines[0].Width, e.viewSize.X).Floor()
+ if b.Min.X > 0 {
+ b.Min.X = 0
+ }
+ }
+ b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X
+ } else {
+ b.Max.Y = e.dims.Size.Y - e.viewSize.Y
+ }
+ return b
+}
+
+func (e *Editor) scrollRel(dx, dy int) {
+ e.scrollAbs(e.scrollOff.X+dx, e.scrollOff.Y+dy)
+}
+
+func (e *Editor) scrollAbs(x, y int) {
+ e.scrollOff.X = x
+ e.scrollOff.Y = y
+ b := e.scrollBounds()
+ if e.scrollOff.X > b.Max.X {
+ e.scrollOff.X = b.Max.X
+ }
+ if e.scrollOff.X < b.Min.X {
+ e.scrollOff.X = b.Min.X
+ }
+ if e.scrollOff.Y > b.Max.Y {
+ e.scrollOff.Y = b.Max.Y
+ }
+ if e.scrollOff.Y < b.Min.Y {
+ e.scrollOff.Y = b.Min.Y
+ }
+}
+
+func (e *Editor) moveCoord(pos image.Point) {
+ var (
+ prevDesc fixed.Int26_6
+ carLine int
+ y int
+ )
+ for _, l := range e.lines {
+ y += (prevDesc + l.Ascent).Ceil()
+ prevDesc = l.Descent
+ if y+prevDesc.Ceil() >= pos.Y+e.scrollOff.Y {
+ break
+ }
+ carLine++
+ }
+ x := fixed.I(pos.X + e.scrollOff.X)
+ e.caret.start = e.movePosToLine(e.caret.start, x, carLine)
+ e.caret.start.xoff = 0
+}
+
+func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) {
+ e.rr.Reset()
+ var r io.Reader = &e.rr
+ if e.Mask != 0 {
+ e.maskReader.Reset(&e.rr, e.Mask)
+ r = &e.maskReader
+ }
+ var lines []text.Line
+ if s != nil {
+ lines, _ = s.Layout(e.font, e.textSize, e.maxWidth, r)
+ } else {
+ lines, _ = nullLayout(r)
+ }
+ dims := linesDimens(lines)
+ for i := 0; i < len(lines)-1; i++ {
+ // To avoid layout flickering while editing, assume a soft newline takes
+ // up all available space.
+ if layout := lines[i].Layout; len(layout.Text) > 0 {
+ r := layout.Text[len(layout.Text)-1]
+ if r != '\n' {
+ dims.Size.X = e.maxWidth
+ break
+ }
+ }
+ }
+ return lines, dims
+}
+
+// CaretPos returns the line & column numbers of the caret.
+func (e *Editor) CaretPos() (line, col int) {
+ e.makeValid()
+ return e.caret.start.lineCol.Y, e.caret.start.lineCol.X
+}
+
+// CaretCoords returns the coordinates of the caret, relative to the
+// editor itself.
+func (e *Editor) CaretCoords() f32.Point {
+ e.makeValid()
+ return f32.Pt(float32(e.caret.start.x)/64, float32(e.caret.start.y))
+}
+
+// offsetToScreenPos2 is a utility function to shortcut the common case of
+// wanting the positions of exactly two offsets.
+func (e *Editor) offsetToScreenPos2(o1, o2 int) (combinedPos, combinedPos) {
+ cp1, iter := e.offsetToScreenPos(o1)
+ return cp1, iter(o2)
+}
+
+// offsetToScreenPos takes an offset into the editor text (e.g.
+// e.caret.end.ofs) and returns a combinedPos that corresponds to its current
+// screen position, as well as an iterator that lets you get the combinedPos
+// of a later offset. The offsets given to offsetToScreenPos and to the
+// returned iterator must be sorted, lowest first, and they must be valid (0
+// <= offset <= e.Len()).
+//
+// This function is written this way to take advantage of previous work done
+// for offsets after the first. Otherwise you have to start from the top each
+// time.
+func (e *Editor) offsetToScreenPos(offset int) (combinedPos,
+ func(int) combinedPos) {
+ var col, line, idx int
+ var x fixed.Int26_6
+
+ l := e.lines[line]
+ y := l.Ascent.Ceil()
+ prevDesc := l.Descent
+
+ iter := func(offset int) combinedPos {
+ LOOP:
+ for {
+ for ; col < len(l.Layout.Advances); col++ {
+ if idx >= offset {
+ break LOOP
+ }
+
+ x += l.Layout.Advances[col]
+ _, s := e.rr.runeAt(idx)
+ idx += s
+ }
+ if lastLine := line == len(e.lines)-1; lastLine || idx > offset {
+ break LOOP
+ }
+
+ line++
+ x = 0
+ col = 0
+ l = e.lines[line]
+ y += (prevDesc + l.Ascent).Ceil()
+ prevDesc = l.Descent
+ }
+ return combinedPos{
+ lineCol: screenPos{Y: line, X: col},
+ x: x + align(e.Alignment, e.lines[line].Width, e.viewSize.X),
+ y: y,
+ ofs: offset,
+ }
+ }
+ return iter(offset), iter
+}
+
+func (e *Editor) invalidate() {
+ e.valid = false
+}
+
+// Delete runes from the caret position. The sign of runes specifies the
+// direction to delete: positive is forward, negative is backward.
+//
+// If there is a selection, it is deleted and counts as a single rune.
+func (e *Editor) Delete(runes int) {
+ if runes == 0 {
+ return
+ }
+
+ if l := e.caret.end.ofs - e.caret.start.ofs; l != 0 {
+ e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, l)
+ runes -= sign(runes)
+ }
+
+ e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, runes)
+ e.caret.start.xoff = 0
+ e.ClearSelection()
+ e.invalidate()
+}
+
+// Insert inserts text at the caret, moving the caret forward. If there is a
+// selection, Insert overwrites it.
+func (e *Editor) Insert(s string) {
+ e.append(s)
+ e.caret.scroll = true
+}
+
+// append inserts s at the cursor, leaving the caret is at the end of s. If
+// there is a selection, append overwrites it.
+// xxx|yyy + append zzz => xxxzzz|yyy
+func (e *Editor) append(s string) {
+ e.prepend(s)
+ e.caret.start.ofs += len(s)
+ e.caret.end.ofs = e.caret.start.ofs
+}
+
+// prepend inserts s after the cursor; the caret does not change. If there is
+// a selection, prepend overwrites it.
+// xxx|yyy + prepend zzz => xxx|zzzyyy
+func (e *Editor) prepend(s string) {
+ if e.SingleLine {
+ s = strings.ReplaceAll(s, "\n", " ")
+ }
+ e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs,
+ e.caret.end.ofs-e.caret.start.ofs) // Delete any selection first.
+ e.rr.prepend(e.caret.start.ofs, s)
+ e.caret.start.xoff = 0
+ e.invalidate()
+}
+
+func (e *Editor) movePages(pages int, selAct selectionAction) {
+ e.makeValid()
+ y := e.caret.start.y + pages*e.viewSize.Y
+ var (
+ prevDesc fixed.Int26_6
+ carLine2 int
+ )
+ y2 := e.lines[0].Ascent.Ceil()
+ for i := 1; i < len(e.lines); i++ {
+ if y2 >= y {
+ break
+ }
+ l := e.lines[i]
+ h := (prevDesc + l.Ascent).Ceil()
+ prevDesc = l.Descent
+ if y2+h-y >= y-y2 {
+ break
+ }
+ y2 += h
+ carLine2++
+ }
+ e.caret.start = e.movePosToLine(e.caret.start,
+ e.caret.start.x+e.caret.start.xoff, carLine2)
+ e.updateSelection(selAct)
+}
+
+func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6,
+ line int) combinedPos {
+ e.makeValid(&pos)
+ if line < 0 {
+ line = 0
+ }
+ if line >= len(e.lines) {
+ line = len(e.lines) - 1
+ }
+
+ prevDesc := e.lines[line].Descent
+ for pos.lineCol.Y < line {
+ pos = e.movePosToEnd(pos)
+ l := e.lines[pos.lineCol.Y]
+ _, s := e.rr.runeAt(pos.ofs)
+ pos.ofs += s
+ pos.y += (prevDesc + l.Ascent).Ceil()
+ pos.lineCol.X = 0
+ prevDesc = l.Descent
+ pos.lineCol.Y++
+ }
+ for pos.lineCol.Y > line {
+ pos = e.movePosToStart(pos)
+ l := e.lines[pos.lineCol.Y]
+ _, s := e.rr.runeBefore(pos.ofs)
+ pos.ofs -= s
+ pos.y -= (prevDesc + l.Ascent).Ceil()
+ prevDesc = l.Descent
+ pos.lineCol.Y--
+ l = e.lines[pos.lineCol.Y]
+ pos.lineCol.X = len(l.Layout.Advances) - 1
+ }
+
+ pos = e.movePosToStart(pos)
+ l := e.lines[line]
+ pos.x = align(e.Alignment, l.Width, e.viewSize.X)
+ // Only move past the end of the last line
+ end := 0
+ if line < len(e.lines)-1 {
+ end = 1
+ }
+ // Move to rune closest to x.
+ for i := 0; i < len(l.Layout.Advances)-end; i++ {
+ adv := l.Layout.Advances[i]
+ if pos.x >= x {
+ break
+ }
+ if pos.x+adv-x >= x-pos.x {
+ break
+ }
+ pos.x += adv
+ _, s := e.rr.runeAt(pos.ofs)
+ pos.ofs += s
+ pos.lineCol.X++
+ }
+ pos.xoff = x - pos.x
+ return pos
+}
+
+// MoveCaret moves the caret (aka selection start) and the selection end
+// relative to their current positions. Positive distances moves forward,
+// negative distances moves backward. Distances are in runes.
+func (e *Editor) MoveCaret(startDelta, endDelta int) {
+ e.makeValid()
+ keepSame := e.caret.start.ofs == e.caret.end.ofs && startDelta == endDelta
+ e.caret.start = e.movePos(e.caret.start, startDelta)
+ e.caret.start.xoff = 0
+ // If they were in the same place, and we're moving them the same distance,
+ // just assign the new position, instead of recalculating it.
+ if keepSame {
+ e.caret.end = e.caret.start
+ } else {
+ e.caret.end = e.movePos(e.caret.end, endDelta)
+ e.caret.end.xoff = 0
+ }
+}
+
+func (e *Editor) movePos(pos combinedPos, distance int) combinedPos {
+ for ; distance < 0 && pos.ofs > 0; distance++ {
+ if pos.lineCol.X == 0 {
+ // Move to end of previous line.
+ pos = e.movePosToLine(pos, fixed.I(e.maxWidth), pos.lineCol.Y-1)
+ continue
+ }
+ l := e.lines[pos.lineCol.Y].Layout
+ _, s := e.rr.runeBefore(pos.ofs)
+ pos.ofs -= s
+ pos.lineCol.X--
+ pos.x -= l.Advances[pos.lineCol.X]
+ }
+ for ; distance > 0 && pos.ofs < e.rr.len(); distance-- {
+ l := e.lines[pos.lineCol.Y].Layout
+ // Only move past the end of the last line
+ end := 0
+ if pos.lineCol.Y < len(e.lines)-1 {
+ end = 1
+ }
+ if pos.lineCol.X >= len(l.Advances)-end {
+ // Move to start of next line.
+ pos = e.movePosToLine(pos, 0, pos.lineCol.Y+1)
+ continue
+ }
+ pos.x += l.Advances[pos.lineCol.X]
+ _, s := e.rr.runeAt(pos.ofs)
+ pos.ofs += s
+ pos.lineCol.X++
+ }
+ return pos
+}
+
+func (e *Editor) moveStart(selAct selectionAction) {
+ e.caret.start = e.movePosToStart(e.caret.start)
+ e.updateSelection(selAct)
+}
+
+func (e *Editor) movePosToStart(pos combinedPos) combinedPos {
+ e.makeValid(&pos)
+ layout := e.lines[pos.lineCol.Y].Layout
+ for i := pos.lineCol.X - 1; i >= 0; i-- {
+ _, s := e.rr.runeBefore(pos.ofs)
+ pos.ofs -= s
+ pos.x -= layout.Advances[i]
+ }
+ pos.lineCol.X = 0
+ pos.xoff = -pos.x
+ return pos
+}
+
+func (e *Editor) moveEnd(selAct selectionAction) {
+ e.caret.start = e.movePosToEnd(e.caret.start)
+ e.updateSelection(selAct)
+}
+
+func (e *Editor) movePosToEnd(pos combinedPos) combinedPos {
+ e.makeValid(&pos)
+ l := e.lines[pos.lineCol.Y]
+ // Only move past the end of the last line
+ end := 0
+ if pos.lineCol.Y < len(e.lines)-1 {
+ end = 1
+ }
+ layout := l.Layout
+ for i := pos.lineCol.X; i < len(layout.Advances)-end; i++ {
+ adv := layout.Advances[i]
+ _, s := e.rr.runeAt(pos.ofs)
+ pos.ofs += s
+ pos.x += adv
+ pos.lineCol.X++
+ }
+ a := align(e.Alignment, l.Width, e.viewSize.X)
+ pos.xoff = l.Width + a - pos.x
+ return pos
+}
+
+// moveWord moves the caret to the next word in the specified direction.
+// Positive is forward, negative is backward.
+// Absolute values greater than one will skip that many words.
+func (e *Editor) moveWord(distance int, selAct selectionAction) {
+ e.makeValid()
+ // split the distance information into constituent parts to be
+ // used independently.
+ words, direction := distance, 1
+ if distance < 0 {
+ words, direction = distance*-1, -1
+ }
+ // atEnd if caret is at either side of the buffer.
+ atEnd := func() bool {
+ return e.caret.start.ofs == 0 || e.caret.start.ofs == e.rr.len()
+ }
+ // next returns the appropriate rune given the direction.
+ next := func() (r rune) {
+ if direction < 0 {
+ r, _ = e.rr.runeBefore(e.caret.start.ofs)
+ } else {
+ r, _ = e.rr.runeAt(e.caret.start.ofs)
+ }
+ return r
+ }
+ for ii := 0; ii < words; ii++ {
+ for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() {
+ e.MoveCaret(direction, 0)
+ }
+ e.MoveCaret(direction, 0)
+ for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() {
+ e.MoveCaret(direction, 0)
+ }
+ }
+ e.updateSelection(selAct)
+}
+
+// deleteWord deletes the next word(s) in the specified direction.
+// Unlike moveWord, deleteWord treats whitespace as a word itself.
+// Positive is forward, negative is backward.
+// Absolute values greater than one will delete that many words.
+// The selection counts as a single word.
+func (e *Editor) deleteWord(distance int) {
+ if distance == 0 {
+ return
+ }
+
+ e.makeValid()
+
+ if e.caret.start.ofs != e.caret.end.ofs {
+ e.Delete(1)
+ distance -= sign(distance)
+ }
+ if distance == 0 {
+ return
+ }
+
+ // split the distance information into constituent parts to be
+ // used independently.
+ words, direction := distance, 1
+ if distance < 0 {
+ words, direction = distance*-1, -1
+ }
+ // atEnd if offset is at or beyond either side of the buffer.
+ atEnd := func(offset int) bool {
+ idx := e.caret.start.ofs + offset*direction
+ return idx <= 0 || idx >= e.rr.len()
+ }
+ // next returns the appropriate rune given the direction and offset.
+ next := func(offset int) (r rune) {
+ idx := e.caret.start.ofs + offset*direction
+ if idx < 0 {
+ idx = 0
+ } else if idx > e.rr.len() {
+ idx = e.rr.len()
+ }
+ if direction < 0 {
+ r, _ = e.rr.runeBefore(idx)
+ } else {
+ r, _ = e.rr.runeAt(idx)
+ }
+ return r
+ }
+ var runes = 1
+ for ii := 0; ii < words; ii++ {
+ if r := next(runes); unicode.IsSpace(r) {
+ for r := next(runes); unicode.IsSpace(r) && !atEnd(runes); r = next(runes) {
+ runes += 1
+ }
+ } else {
+ for r := next(runes); !unicode.IsSpace(r) && !atEnd(runes); r = next(runes) {
+ runes += 1
+ }
+ }
+ }
+ e.Delete(runes * direction)
+}
+
+func (e *Editor) scrollToCaret() {
+ e.makeValid()
+ l := e.lines[e.caret.start.lineCol.Y]
+ if e.SingleLine {
+ var dist int
+ if d := e.caret.start.x.Floor() - e.scrollOff.X; d < 0 {
+ dist = d
+ } else if d := e.caret.start.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 {
+ dist = d
+ }
+ e.scrollRel(dist, 0)
+ } else {
+ miny := e.caret.start.y - l.Ascent.Ceil()
+ maxy := e.caret.start.y + l.Descent.Ceil()
+ var dist int
+ if d := miny - e.scrollOff.Y; d < 0 {
+ dist = d
+ } else if d := maxy - (e.scrollOff.Y + e.viewSize.Y); d > 0 {
+ dist = d
+ }
+ e.scrollRel(0, dist)
+ }
+}
+
+// NumLines returns the number of lines in the editor.
+func (e *Editor) NumLines() int {
+ e.makeValid()
+ return len(e.lines)
+}
+
+// SelectionLen returns the length of the selection, in bytes; it is
+// equivalent to len(e.SelectedText()).
+func (e *Editor) SelectionLen() int {
+ return abs(e.caret.start.ofs - e.caret.end.ofs)
+}
+
+// Selection returns the start and end of the selection, as offsets into the
+// editor text. start can be > end.
+func (e *Editor) Selection() (start, end int) {
+ return e.caret.start.ofs, e.caret.end.ofs
+}
+
+// SetCaret moves the caret to start, and sets the selection end to end. start
+// and end are in bytes, and represent offsets into the editor text. start and
+// end must be at a rune boundary.
+func (e *Editor) SetCaret(start, end int) {
+ e.makeValid()
+ // Constrain start and end to [0, e.Len()].
+ l := e.Len()
+ start = max(min(start, l), 0)
+ end = max(min(end, l), 0)
+ e.caret.start.ofs, e.caret.end.ofs = start, end
+ e.makeValidCaret()
+ e.caret.scroll = true
+ e.scroller.Stop()
+}
+
+func (e *Editor) makeValidCaret(positions ...*combinedPos) {
+ // Jump through some hoops to order the offsets given to offsetToScreenPos,
+ // but still be able to update them correctly with the results thereof.
+ positions = append(positions, &e.caret.start, &e.caret.end)
+ sort.Slice(positions, func(i, j int) bool {
+ return positions[i].ofs < positions[j].ofs
+ })
+ var iter func(offset int) combinedPos
+ *positions[0], iter = e.offsetToScreenPos(positions[0].ofs)
+ for _, cp := range positions[1:] {
+ *cp = iter(cp.ofs)
+ }
+}
+
+// SelectedText returns the currently selected text (if any) from the editor.
+func (e *Editor) SelectedText() string {
+ l := e.SelectionLen()
+ if l == 0 {
+ return ""
+ }
+ buf := make([]byte, l)
+ e.rr.Seek(int64(min(e.caret.start.ofs, e.caret.end.ofs)), io.SeekStart)
+ _, err := e.rr.Read(buf)
+ if err != nil {
+ // The only error that rr.Read can return is EOF, which just means no
+ // selection, but we've already made sure that shouldn't happen.
+ panic("impossible error because end is before e.rr.Len()")
+ }
+ return string(buf)
+}
+
+func (e *Editor) updateSelection(selAct selectionAction) {
+ if selAct == selectionClear {
+ e.ClearSelection()
+ }
+}
+
+// ClearSelection clears the selection, by setting the selection end equal to
+// the selection start.
+func (e *Editor) ClearSelection() {
+ e.caret.end = e.caret.start
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+func abs(n int) int {
+ if n < 0 {
+ return -n
+ }
+ return n
+}
+
+func sign(n int) int {
+ switch {
+ case n < 0:
+ return -1
+ case n > 0:
+ return 1
+ default:
+ return 0
+ }
+}
+
+// sortPoints returns a and b sorted such that a2 <= b2.
+func sortPoints(a, b screenPos) (a2, b2 screenPos) {
+ if b.Less(a) {
+ return b, a
+ }
+ return a, b
+}
+
+func nullLayout(r io.Reader) ([]text.Line, error) {
+ rr := bufio.NewReader(r)
+ var rerr error
+ var n int
+ var buf bytes.Buffer
+ for {
+ r, s, err := rr.ReadRune()
+ n += s
+ buf.WriteRune(r)
+ if err != nil {
+ rerr = err
+ break
+ }
+ }
+ return []text.Line{
+ {
+ Layout: text.Layout{
+ Text: buf.String(),
+ Advances: make([]fixed.Int26_6, n),
+ },
+ },
+ }, rerr
+}
+
+func (s ChangeEvent) isEditorEvent() {}
+func (s SubmitEvent) isEditorEvent() {}
+func (s SelectEvent) isEditorEvent() {}
diff --git a/gio/giold/widget/editor_test.go b/gio/giold/widget/editor_test.go
new file mode 100644
index 0000000..6b37b50
--- /dev/null
+++ b/gio/giold/widget/editor_test.go
@@ -0,0 +1,540 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "fmt"
+ "image"
+ "math/rand"
+ "reflect"
+ "strings"
+ "testing"
+ "testing/quick"
+ "unicode"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/font/gofont"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+ "golang.org/x/image/math/fixed"
+)
+
+func TestEditor(t *testing.T) {
+ e := new(Editor)
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Exact(image.Pt(100, 100)),
+ }
+ cache := text.NewCache(gofont.Collection())
+ fontSize := unit.Px(10)
+ font := text.Font{}
+
+ e.SetCaret(0, 0) // shouldn't panic
+ assertCaret(t, e, 0, 0, 0)
+ e.SetText("Ʀbc\naĆøĆ„ā¢")
+ e.Layout(gtx, cache, font, fontSize)
+ assertCaret(t, e, 0, 0, 0)
+ e.moveEnd(selectionClear)
+ assertCaret(t, e, 0, 3, len("Ʀbc"))
+ e.MoveCaret(+1, +1)
+ assertCaret(t, e, 1, 0, len("Ʀbc\n"))
+ e.MoveCaret(-1, -1)
+ assertCaret(t, e, 0, 3, len("Ʀbc"))
+ e.moveLines(+1, +1)
+ assertCaret(t, e, 1, 3, len("Ʀbc\naĆøĆ„"))
+ e.moveEnd(selectionClear)
+ assertCaret(t, e, 1, 4, len("Ʀbc\naĆøĆ„ā¢"))
+ e.MoveCaret(+1, +1)
+ assertCaret(t, e, 1, 4, len("Ʀbc\naĆøĆ„ā¢"))
+
+ e.SetCaret(0, 0)
+ assertCaret(t, e, 0, 0, 0)
+ e.SetCaret(len("Ʀ"), len("Ʀ"))
+ assertCaret(t, e, 0, 1, 2)
+ e.SetCaret(len("Ʀbc\naĆøĆ„ā¢"), len("Ʀbc\naĆøĆ„ā¢"))
+ assertCaret(t, e, 1, 4, len("Ʀbc\naĆøĆ„ā¢"))
+
+ // Ensure that password masking does not affect caret behavior
+ e.MoveCaret(-3, -3)
+ assertCaret(t, e, 1, 1, len("Ʀbc\na"))
+ e.Mask = '*'
+ e.Layout(gtx, cache, font, fontSize)
+ assertCaret(t, e, 1, 1, len("Ʀbc\na"))
+ e.MoveCaret(-3, -3)
+ assertCaret(t, e, 0, 2, len("Ʀb"))
+ e.Mask = '\U0001F92B'
+ e.Layout(gtx, cache, font, fontSize)
+ e.moveEnd(selectionClear)
+ assertCaret(t, e, 0, 3, len("Ʀbc"))
+
+ // When a password mask is applied, it should replace all visible glyphs
+ for i, line := range e.lines {
+ for j, r := range line.Layout.Text {
+ if r != e.Mask && !unicode.IsSpace(r) {
+ t.Errorf("glyph at (%d, %d) is unmasked rune %d", i, j, r)
+ }
+ }
+ }
+}
+
+func TestEditorDimensions(t *testing.T) {
+ e := new(Editor)
+ tq := &testQueue{
+ events: []event.Event{
+ key.EditEvent{Text: "A"},
+ },
+ }
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Constraints{Max: image.Pt(100, 100)},
+ Queue: tq,
+ }
+ cache := text.NewCache(gofont.Collection())
+ fontSize := unit.Px(10)
+ font := text.Font{}
+ dims := e.Layout(gtx, cache, font, fontSize)
+ if dims.Size.X == 0 {
+ t.Errorf("EditEvent was not reflected in Editor width")
+ }
+}
+
+// assertCaret asserts that the editor caret is at a particular line
+// and column, and that the byte position matches as well.
+func assertCaret(t *testing.T, e *Editor, line, col, bytes int) {
+ t.Helper()
+ gotLine, gotCol := e.CaretPos()
+ if gotLine != line || gotCol != col {
+ t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line,
+ col)
+ }
+ if bytes != e.caret.start.ofs {
+ t.Errorf("caret at buffer position %d, expected %d", e.caret.start.ofs,
+ bytes)
+ }
+}
+
+type editMutation int
+
+const (
+ setText editMutation = iota
+ moveRune
+ moveLine
+ movePage
+ moveStart
+ moveEnd
+ moveCoord
+ moveWord
+ deleteWord
+ moveLast // Mark end; never generated.
+)
+
+func TestEditorCaretConsistency(t *testing.T) {
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Exact(image.Pt(100, 100)),
+ }
+ cache := text.NewCache(gofont.Collection())
+ fontSize := unit.Px(10)
+ font := text.Font{}
+ for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
+ e := &Editor{
+ Alignment: a,
+ }
+ e.Layout(gtx, cache, font, fontSize)
+
+ consistent := func() error {
+ t.Helper()
+ gotLine, gotCol := e.CaretPos()
+ gotCoords := e.CaretCoords()
+ want, _ := e.offsetToScreenPos(e.caret.start.ofs)
+ wantCoords := f32.Pt(float32(want.x)/64, float32(want.y))
+ if want.lineCol.Y == gotLine && want.lineCol.X == gotCol && gotCoords == wantCoords {
+ return nil
+ }
+ return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s",
+ gotLine, gotCol, gotCoords, want.lineCol.Y, want.lineCol.X,
+ wantCoords)
+ }
+ if err := consistent(); err != nil {
+ t.Errorf("initial editor inconsistency (alignment %s): %v", a, err)
+ }
+
+ move := func(mutation editMutation, str string, distance int8,
+ x, y uint16) bool {
+ switch mutation {
+ case setText:
+ e.SetText(str)
+ e.Layout(gtx, cache, font, fontSize)
+ case moveRune:
+ e.MoveCaret(int(distance), int(distance))
+ case moveLine:
+ e.moveLines(int(distance), selectionClear)
+ case movePage:
+ e.movePages(int(distance), selectionClear)
+ case moveStart:
+ e.moveStart(selectionClear)
+ case moveEnd:
+ e.moveEnd(selectionClear)
+ case moveCoord:
+ e.moveCoord(image.Pt(int(x), int(y)))
+ case moveWord:
+ e.moveWord(int(distance), selectionClear)
+ case deleteWord:
+ e.deleteWord(int(distance))
+ default:
+ return false
+ }
+ if err := consistent(); err != nil {
+ t.Error(err)
+ return false
+ }
+ return true
+ }
+ if err := quick.Check(move, nil); err != nil {
+ t.Errorf("editor inconsistency (alignment %s): %v", a, err)
+ }
+ }
+}
+
+func TestEditorMoveWord(t *testing.T) {
+ type Test struct {
+ Text string
+ Start int
+ Skip int
+ Want int
+ }
+ tests := []Test{
+ {"", 0, 0, 0},
+ {"", 0, -1, 0},
+ {"", 0, 1, 0},
+ {"hello", 0, -1, 0},
+ {"hello", 0, 1, 5},
+ {"hello world", 3, 1, 5},
+ {"hello world", 3, -1, 0},
+ {"hello world", 8, -1, 6},
+ {"hello world", 8, 1, 11},
+ {"hello world", 3, 1, 5},
+ {"hello world", 3, 2, 14},
+ {"hello world", 8, 1, 14},
+ {"hello world", 8, -1, 0},
+ {"hello brave new world", 0, 3, 15},
+ }
+ setup := func(t string) *Editor {
+ e := new(Editor)
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Exact(image.Pt(100, 100)),
+ }
+ cache := text.NewCache(gofont.Collection())
+ fontSize := unit.Px(10)
+ font := text.Font{}
+ e.SetText(t)
+ e.Layout(gtx, cache, font, fontSize)
+ return e
+ }
+ for ii, tt := range tests {
+ e := setup(tt.Text)
+ e.MoveCaret(tt.Start, tt.Start)
+ e.moveWord(tt.Skip, selectionClear)
+ if e.caret.start.ofs != tt.Want {
+ t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii,
+ e.caret.start.ofs, tt.Want)
+ }
+ }
+}
+
+func TestEditorDeleteWord(t *testing.T) {
+ type Test struct {
+ Text string
+ Start int
+ Selection int
+ Delete int
+
+ Want int
+ Result string
+ }
+ tests := []Test{
+ // No text selected
+ {"", 0, 0, 0, 0, ""},
+ {"", 0, 0, -1, 0, ""},
+ {"", 0, 0, 1, 0, ""},
+ {"", 0, 0, -2, 0, ""},
+ {"", 0, 0, 2, 0, ""},
+ {"hello", 0, 0, -1, 0, "hello"},
+ {"hello", 0, 0, 1, 0, ""},
+
+ // Document (imho) incorrect behavior w.r.t. deleting spaces following
+ // words.
+ {"hello world", 0, 0, 1, 0,
+ " world"}, // Should be "world", if you ask me.
+ {"hello world", 0, 0, 2, 0, "world"}, // Should be "".
+ {"hello ", 0, 0, 1, 0, " "}, // Should be "".
+ {"hello world", 11, 0, -1, 6, "hello "}, // Should be "hello".
+ {"hello world", 11, 0, -2, 5, "hello"}, // Should be "".
+ {"hello ", 6, 0, -1, 0, ""}, // Correct result.
+
+ {"hello world", 3, 0, 1, 3, "hel world"},
+ {"hello world", 3, 0, -1, 0, "lo world"},
+ {"hello world", 8, 0, -1, 6, "hello rld"},
+ {"hello world", 8, 0, 1, 8, "hello wo"},
+ {"hello world", 3, 0, 1, 3, "hel world"},
+ {"hello world", 3, 0, 2, 3, "helworld"},
+ {"hello world", 8, 0, 1, 8, "hello "},
+ {"hello world", 8, 0, -1, 5, "hello world"},
+ {"hello brave new world", 0, 0, 3, 0, " new world"},
+ // Add selected text.
+ //
+ // Several permutations must be tested:
+ // - select from the left or right
+ // - Delete + or -
+ // - abs(Delete) == 1 or > 1
+ //
+ // "brave |" selected; caret at |
+ {"hello there brave new world", 12, 6, 1, 12,
+ "hello there new world"}, // #16
+ {"hello there brave new world", 12, 6, 2, 12,
+ "hello there world"}, // The two spaces after "there" are actually suboptimal, if you ask me. See also above cases.
+ {"hello there brave new world", 12, 6, -1, 12, "hello there new world"},
+ {"hello there brave new world", 12, 6, -2, 6, "hello new world"},
+ // "|brave " selected
+ {"hello there brave new world", 18, -6, 1, 12,
+ "hello there new world"}, // #20
+ {"hello there brave new world", 18, -6, 2, 12,
+ "hello there world"}, // ditto
+ {"hello there brave new world", 18, -6, -1, 12,
+ "hello there new world"},
+ {"hello there brave new world", 18, -6, -2, 6, "hello new world"},
+ // Random edge cases
+ {"hello there brave new world", 12, 6, 99, 12, "hello there "},
+ {"hello there brave new world", 18, -6, -99, 0, "new world"},
+ }
+ setup := func(t string) *Editor {
+ e := new(Editor)
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Exact(image.Pt(100, 100)),
+ }
+ cache := text.NewCache(gofont.Collection())
+ fontSize := unit.Px(10)
+ font := text.Font{}
+ e.SetText(t)
+ e.Layout(gtx, cache, font, fontSize)
+ return e
+ }
+ for ii, tt := range tests {
+ e := setup(tt.Text)
+ e.MoveCaret(tt.Start, tt.Start)
+ e.MoveCaret(0, tt.Selection)
+ e.deleteWord(tt.Delete)
+ if e.caret.start.ofs != tt.Want {
+ t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii,
+ e.caret.start.ofs, tt.Want)
+ }
+ if e.Text() != tt.Result {
+ t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii,
+ e.Text(), tt.Result)
+ }
+ }
+}
+
+func TestEditorNoLayout(t *testing.T) {
+ var e Editor
+ e.SetText("hi!\n")
+ e.MoveCaret(1, 1)
+}
+
+// Generate generates a value of itself, for testing/quick.
+func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
+ t := editMutation(rand.Intn(int(moveLast)))
+ return reflect.ValueOf(t)
+}
+
+// TestSelect tests the selection code. It lays out an editor with several
+// lines in it, selects some text, verifies the selection, resizes the editor
+// to make it much narrower (which makes the lines in the editor reflow), and
+// then verifies that the updated (col, line) positions of the selected text
+// are where we expect.
+func TestSelect(t *testing.T) {
+ e := new(Editor)
+ e.SetText(`a123456789a
+b123456789b
+c123456789c
+d123456789d
+e123456789e
+f123456789f
+g123456789g
+`)
+
+ gtx := layout.Context{Ops: new(op.Ops)}
+ cache := text.NewCache(gofont.Collection())
+ font := text.Font{}
+ fontSize := unit.Px(10)
+
+ selected := func(start, end int) string {
+ // Layout once with no events; populate e.lines.
+ gtx.Queue = nil
+ e.Layout(gtx, cache, font, fontSize)
+ _ = e.Events() // throw away any events from this layout
+
+ // Build the selection events
+ startPos, endPos := e.offsetToScreenPos2(sortInts(start, end))
+ tq := &testQueue{
+ events: []event.Event{
+ pointer.Event{
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Press,
+ Source: pointer.Mouse,
+ Position: f32.Pt(textWidth(e, startPos.lineCol.Y, 0,
+ startPos.lineCol.X), textHeight(e, startPos.lineCol.Y)),
+ },
+ pointer.Event{
+ Type: pointer.Release,
+ Source: pointer.Mouse,
+ Position: f32.Pt(textWidth(e, endPos.lineCol.Y, 0,
+ endPos.lineCol.X), textHeight(e, endPos.lineCol.Y)),
+ },
+ },
+ }
+ gtx.Queue = tq
+
+ e.Layout(gtx, cache, font, fontSize)
+ for _, evt := range e.Events() {
+ switch evt.(type) {
+ case SelectEvent:
+ return e.SelectedText()
+ }
+ }
+ return ""
+ }
+
+ type testCase struct {
+ // input text offsets
+ start, end int
+
+ // expected selected text
+ selection string
+ // expected line/col positions of selection after resize
+ startPos, endPos screenPos
+ }
+
+ for n, tst := range []testCase{
+ {0, 1, "a", screenPos{}, screenPos{Y: 0, X: 1}},
+ {0, 4, "a123", screenPos{}, screenPos{Y: 0, X: 4}},
+ {0, 11, "a123456789a", screenPos{}, screenPos{Y: 1, X: 5}},
+ {2, 6, "2345", screenPos{Y: 0, X: 2}, screenPos{Y: 1, X: 0}},
+ {41, 66, "56789d\ne123456789e\nf12345", screenPos{Y: 6, X: 5},
+ screenPos{Y: 11, X: 0}},
+ } {
+ // printLines(e)
+
+ gtx.Constraints = layout.Exact(image.Pt(100, 100))
+ if got := selected(tst.start, tst.end); got != tst.selection {
+ t.Errorf("Test %d pt1: Expected %q, got %q", n, tst.selection, got)
+ continue
+ }
+
+ // Constrain the editor to roughly 6 columns wide and redraw
+ gtx.Constraints = layout.Exact(image.Pt(36, 36))
+ // Keep existing selection
+ gtx.Queue = nil
+ e.Layout(gtx, cache, font, fontSize)
+
+ if e.caret.end.lineCol != tst.startPos || e.caret.start.lineCol != tst.endPos {
+ t.Errorf("Test %d pt2: Expected %#v, %#v; got %#v, %#v",
+ n,
+ e.caret.end.lineCol, e.caret.start.lineCol,
+ tst.startPos, tst.endPos)
+ continue
+ }
+
+ // printLines(e)
+ }
+}
+
+// Verify that an existing selection is dismissed when you press arrow keys.
+func TestSelectMove(t *testing.T) {
+ e := new(Editor)
+ e.SetText(`0123456789`)
+
+ gtx := layout.Context{Ops: new(op.Ops)}
+ cache := text.NewCache(gofont.Collection())
+ font := text.Font{}
+ fontSize := unit.Px(10)
+
+ // Layout once to populate e.lines and get focus.
+ gtx.Queue = newQueue(key.FocusEvent{Focus: true})
+ e.Layout(gtx, cache, font, fontSize)
+
+ testKey := func(keyName string) {
+ // Select 345
+ e.SetCaret(3, 6)
+ if expected, got := "345", e.SelectedText(); expected != got {
+ t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got)
+ }
+
+ // Press the key
+ gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName})
+ e.Layout(gtx, cache, font, fontSize)
+
+ if expected, got := "", e.SelectedText(); expected != got {
+ t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got)
+ }
+ }
+
+ testKey(key.NameLeftArrow)
+ testKey(key.NameRightArrow)
+ testKey(key.NameUpArrow)
+ testKey(key.NameDownArrow)
+}
+
+func textWidth(e *Editor, lineNum, colStart, colEnd int) float32 {
+ var w fixed.Int26_6
+ advances := e.lines[lineNum].Layout.Advances
+ if colEnd > len(advances) {
+ colEnd = len(advances)
+ }
+ for _, adv := range advances[colStart:colEnd] {
+ w += adv
+ }
+ return float32(w.Floor())
+}
+
+func textHeight(e *Editor, lineNum int) float32 {
+ var h fixed.Int26_6
+ for _, line := range e.lines[0:lineNum] {
+ h += line.Ascent + line.Descent
+ }
+ return float32(h.Floor() + 1)
+}
+
+type testQueue struct {
+ events []event.Event
+}
+
+func newQueue(e ...event.Event) *testQueue {
+ return &testQueue{events: e}
+}
+
+func (q *testQueue) Events(_ event.Tag) []event.Event {
+ return q.events
+}
+
+func printLines(e *Editor) {
+ for n, line := range e.lines {
+ text := strings.TrimSuffix(line.Layout.Text, "\n")
+ fmt.Printf("%d: %s\n", n, text)
+ }
+}
+
+// sortInts returns a and b sorted such that a2 <= b2.
+func sortInts(a, b int) (a2, b2 int) {
+ if b < a {
+ return b, a
+ }
+ return a, b
+}
diff --git a/gio/giold/widget/enum.go b/gio/giold/widget/enum.go
new file mode 100644
index 0000000..1ef721a
--- /dev/null
+++ b/gio/giold/widget/enum.go
@@ -0,0 +1,77 @@
+package widget
+
+import (
+ "image"
+
+ "realy.lol/gio/gesture"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+)
+
+type Enum struct {
+ Value string
+ hovered string
+ hovering bool
+
+ changed bool
+
+ clicks []gesture.Click
+ values []string
+}
+
+func index(vs []string, t string) int {
+ for i, v := range vs {
+ if v == t {
+ return i
+ }
+ }
+ return -1
+}
+
+// Changed reports whether Value has changed by user interaction since the last
+// call to Changed.
+func (e *Enum) Changed() bool {
+ changed := e.changed
+ e.changed = false
+ return changed
+}
+
+// Hovered returns the key that is highlighted, or false if none are.
+func (e *Enum) Hovered() (string, bool) {
+ return e.hovered, e.hovering
+}
+
+// Layout adds the event handler for key.
+func (e *Enum) Layout(gtx layout.Context, key string) layout.Dimensions {
+ defer op.Save(gtx.Ops).Load()
+ pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
+
+ if index(e.values, key) == -1 {
+ e.values = append(e.values, key)
+ e.clicks = append(e.clicks, gesture.Click{})
+ e.clicks[len(e.clicks)-1].Add(gtx.Ops)
+ } else {
+ idx := index(e.values, key)
+ clk := &e.clicks[idx]
+ for _, ev := range clk.Events(gtx) {
+ switch ev.Type {
+ case gesture.TypeClick:
+ if new := e.values[idx]; new != e.Value {
+ e.Value = new
+ e.changed = true
+ }
+ }
+ }
+ if e.hovering && e.hovered == key {
+ e.hovering = false
+ }
+ if clk.Hovered() {
+ e.hovered = key
+ e.hovering = true
+ }
+ clk.Add(gtx.Ops)
+ }
+
+ return layout.Dimensions{Size: gtx.Constraints.Min}
+}
diff --git a/gio/giold/widget/example_test.go b/gio/giold/widget/example_test.go
new file mode 100644
index 0000000..f5e9cf5
--- /dev/null
+++ b/gio/giold/widget/example_test.go
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget_test
+
+import (
+ "fmt"
+ "image"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/router"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/widget"
+)
+
+func ExampleClickable_passthrough() {
+ // When laying out clickable widgets on top of each other,
+ // pointer events can be passed down for the underlying
+ // widgets to pick them up.
+ var button1, button2 widget.Clickable
+ var r router.Router
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Exact(image.Pt(100, 100)),
+ Queue: &r,
+ }
+
+ // widget lays out two buttons on top of each other.
+ widget := func() {
+ // button2 completely covers button1, but PassOp allows pointer
+ // events to pass through to button1.
+ button1.Layout(gtx)
+ // PassOp is applied to the area defined by button1.
+ pointer.PassOp{Pass: true}.Add(gtx.Ops)
+ button2.Layout(gtx)
+ }
+
+ // The first layout and call to Frame declare the Clickable handlers
+ // to the input router, so the following pointer events are propagated.
+ widget()
+ r.Frame(gtx.Ops)
+ // Simulate one click on the buttons by sending a Press and Release event.
+ r.Queue(
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Press,
+ Position: f32.Pt(50, 50),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Release,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ // The second layout ensures that the click event is registered by the buttons.
+ widget()
+
+ if button1.Clicked() {
+ fmt.Println("button1 clicked!")
+ }
+ if button2.Clicked() {
+ fmt.Println("button2 clicked!")
+ }
+
+ // Output:
+ // button1 clicked!
+ // button2 clicked!
+}
diff --git a/gio/giold/widget/fit.go b/gio/giold/widget/fit.go
new file mode 100644
index 0000000..08adb74
--- /dev/null
+++ b/gio/giold/widget/fit.go
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+)
+
+// Fit scales a widget to fit and clip to the constraints.
+type Fit uint8
+
+const (
+ // Unscaled does not alter the scale of a widget.
+ Unscaled Fit = iota
+ // Contain scales widget as large as possible without cropping
+ // and it preserves aspect-ratio.
+ Contain
+ // Cover scales the widget to cover the constraint area and
+ // preserves aspect-ratio.
+ Cover
+ // ScaleDown scales the widget smaller without cropping,
+ // when it exceeds the constraint area.
+ // It preserves aspect-ratio.
+ ScaleDown
+ // Fill stretches the widget to the constraints and does not
+ // preserve aspect-ratio.
+ Fill
+)
+
+// scale adds clip and scale operations to fit dims to the constraints.
+// It positions the widget to the appropriate position.
+// It returns dimensions modified accordingly.
+func (fit Fit) scale(gtx layout.Context, pos layout.Direction,
+ dims layout.Dimensions) layout.Dimensions {
+ widgetSize := dims.Size
+
+ if fit == Unscaled || dims.Size.X == 0 || dims.Size.Y == 0 {
+ dims.Size = gtx.Constraints.Constrain(dims.Size)
+ clip.Rect{Max: dims.Size}.Add(gtx.Ops)
+
+ offset := pos.Position(widgetSize, dims.Size)
+ op.Offset(layout.FPt(offset)).Add(gtx.Ops)
+ dims.Baseline += offset.Y
+ return dims
+ }
+
+ scale := f32.Point{
+ X: float32(gtx.Constraints.Max.X) / float32(dims.Size.X),
+ Y: float32(gtx.Constraints.Max.Y) / float32(dims.Size.Y),
+ }
+
+ switch fit {
+ case Contain:
+ if scale.Y < scale.X {
+ scale.X = scale.Y
+ } else {
+ scale.Y = scale.X
+ }
+ case Cover:
+ if scale.Y > scale.X {
+ scale.X = scale.Y
+ } else {
+ scale.Y = scale.X
+ }
+ case ScaleDown:
+ if scale.Y < scale.X {
+ scale.X = scale.Y
+ } else {
+ scale.Y = scale.X
+ }
+
+ // The widget would need to be scaled up, no change needed.
+ if scale.X >= 1 {
+ dims.Size = gtx.Constraints.Constrain(dims.Size)
+ clip.Rect{Max: dims.Size}.Add(gtx.Ops)
+
+ offset := pos.Position(widgetSize, dims.Size)
+ op.Offset(layout.FPt(offset)).Add(gtx.Ops)
+ dims.Baseline += offset.Y
+ return dims
+ }
+ case Fill:
+ }
+
+ var scaledSize image.Point
+ scaledSize.X = int(float32(widgetSize.X) * scale.X)
+ scaledSize.Y = int(float32(widgetSize.Y) * scale.Y)
+ dims.Size = gtx.Constraints.Constrain(scaledSize)
+ dims.Baseline = int(float32(dims.Baseline) * scale.Y)
+
+ clip.Rect{Max: dims.Size}.Add(gtx.Ops)
+
+ offset := pos.Position(scaledSize, dims.Size)
+ op.Affine(f32.Affine2D{}.
+ Scale(f32.Point{}, scale).
+ Offset(layout.FPt(offset)),
+ ).Add(gtx.Ops)
+
+ dims.Baseline += offset.Y
+
+ return dims
+}
diff --git a/gio/giold/widget/fit_test.go b/gio/giold/widget/fit_test.go
new file mode 100644
index 0000000..925ad34
--- /dev/null
+++ b/gio/giold/widget/fit_test.go
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "bytes"
+ "encoding/binary"
+ "image"
+ "math"
+ "testing"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+)
+
+func TestFit(t *testing.T) {
+ type test struct {
+ Dims image.Point
+ Scale f32.Point
+ Result image.Point
+ }
+
+ fittests := [...][]test{
+ Unscaled: {
+ {
+ Dims: image.Point{0, 0},
+ Scale: f32.Point{X: 1, Y: 1},
+ Result: image.Point{X: 0, Y: 0},
+ }, {
+ Dims: image.Point{50, 25},
+ Scale: f32.Point{X: 1, Y: 1},
+ Result: image.Point{X: 50, Y: 25},
+ }, {
+ Dims: image.Point{50, 200},
+ Scale: f32.Point{X: 1, Y: 1},
+ Result: image.Point{X: 50, Y: 100},
+ }},
+ Contain: {
+ {
+ Dims: image.Point{50, 25},
+ Scale: f32.Point{X: 2, Y: 2},
+ Result: image.Point{X: 100, Y: 50},
+ }, {
+ Dims: image.Point{50, 200},
+ Scale: f32.Point{X: 0.5, Y: 0.5},
+ Result: image.Point{X: 25, Y: 100},
+ }},
+ Cover: {
+ {
+ Dims: image.Point{50, 25},
+ Scale: f32.Point{X: 4, Y: 4},
+ Result: image.Point{X: 100, Y: 100},
+ }, {
+ Dims: image.Point{50, 200},
+ Scale: f32.Point{X: 2, Y: 2},
+ Result: image.Point{X: 100, Y: 100},
+ }},
+ ScaleDown: {
+ {
+ Dims: image.Point{50, 25},
+ Scale: f32.Point{X: 1, Y: 1},
+ Result: image.Point{X: 50, Y: 25},
+ }, {
+ Dims: image.Point{50, 200},
+ Scale: f32.Point{X: 0.5, Y: 0.5},
+ Result: image.Point{X: 25, Y: 100},
+ }},
+ Fill: {
+ {
+ Dims: image.Point{50, 25},
+ Scale: f32.Point{X: 2, Y: 4},
+ Result: image.Point{X: 100, Y: 100},
+ }, {
+ Dims: image.Point{50, 200},
+ Scale: f32.Point{X: 2, Y: 0.5},
+ Result: image.Point{X: 100, Y: 100},
+ }},
+ }
+
+ for fit, tests := range fittests {
+ fit := Fit(fit)
+ for i, test := range tests {
+ ops := new(op.Ops)
+ gtx := layout.Context{
+ Ops: ops,
+ Constraints: layout.Constraints{
+ Max: image.Point{X: 100, Y: 100},
+ },
+ }
+
+ result := fit.scale(gtx, layout.NW,
+ layout.Dimensions{Size: test.Dims})
+
+ if test.Scale.X != 1 || test.Scale.Y != 1 {
+ opsdata := gtx.Ops.Data()
+ scaleX := float32Bytes(test.Scale.X)
+ scaleY := float32Bytes(test.Scale.Y)
+ if !bytes.Contains(opsdata, scaleX) {
+ t.Errorf("did not find scale.X:%v (%x) in ops: %x",
+ test.Scale.X, scaleX, opsdata)
+ }
+ if !bytes.Contains(opsdata, scaleY) {
+ t.Errorf("did not find scale.Y:%v (%x) in ops: %x",
+ test.Scale.Y, scaleY, opsdata)
+ }
+ }
+
+ if result.Size != test.Result {
+ t.Errorf("fit %v, #%v: expected %#v, got %#v", fit, i,
+ test.Result, result.Size)
+ }
+ }
+ }
+}
+
+func float32Bytes(v float32) []byte {
+ var dst [4]byte
+ binary.LittleEndian.PutUint32(dst[:], math.Float32bits(v))
+ return dst[:]
+}
diff --git a/gio/giold/widget/float.go b/gio/giold/widget/float.go
new file mode 100644
index 0000000..e26e296
--- /dev/null
+++ b/gio/giold/widget/float.go
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+
+ "realy.lol/gio/gesture"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+)
+
+// Float is for selecting a value in a range.
+type Float struct {
+ Value float32
+ Axis layout.Axis
+
+ drag gesture.Drag
+ pos float32 // position normalized to [0, 1]
+ length float32
+ changed bool
+}
+
+// Dragging returns whether the value is being interacted with.
+func (f *Float) Dragging() bool { return f.drag.Dragging() }
+
+// Layout updates the value according to drag events along the f's main axis.
+//
+// The range of f is set by the minimum constraints main axis value.
+func (f *Float) Layout(gtx layout.Context, pointerMargin int,
+ min, max float32) layout.Dimensions {
+ size := gtx.Constraints.Min
+ f.length = float32(f.Axis.Convert(size).X)
+
+ var de *pointer.Event
+ for _, e := range f.drag.Events(gtx.Metric, gtx, gesture.Axis(f.Axis)) {
+ if e.Type == pointer.Press || e.Type == pointer.Drag {
+ de = &e
+ }
+ }
+
+ value := f.Value
+ if de != nil {
+ xy := de.Position.X
+ if f.Axis == layout.Vertical {
+ xy = de.Position.Y
+ }
+ f.pos = xy / f.length
+ value = min + (max-min)*f.pos
+ } else if min != max {
+ f.pos = (value - min) / (max - min)
+ }
+ // Unconditionally call setValue in case min, max, or value changed.
+ f.setValue(value, min, max)
+
+ if f.pos < 0 {
+ f.pos = 0
+ } else if f.pos > 1 {
+ f.pos = 1
+ }
+
+ defer op.Save(gtx.Ops).Load()
+ margin := f.Axis.Convert(image.Pt(pointerMargin, 0))
+ rect := image.Rectangle{
+ Min: margin.Mul(-1),
+ Max: size.Add(margin),
+ }
+ pointer.Rect(rect).Add(gtx.Ops)
+ f.drag.Add(gtx.Ops)
+
+ return layout.Dimensions{Size: size}
+}
+
+func (f *Float) setValue(value, min, max float32) {
+ if min > max {
+ min, max = max, min
+ }
+ if value < min {
+ value = min
+ } else if value > max {
+ value = max
+ }
+ if f.Value != value {
+ f.Value = value
+ f.changed = true
+ }
+}
+
+// Pos reports the selected position.
+func (f *Float) Pos() float32 {
+ return f.pos * f.length
+}
+
+// Changed reports whether the value has changed since
+// the last call to Changed.
+func (f *Float) Changed() bool {
+ changed := f.changed
+ f.changed = false
+ return changed
+}
diff --git a/gio/giold/widget/icon.go b/gio/giold/widget/icon.go
new file mode 100644
index 0000000..6f37d48
--- /dev/null
+++ b/gio/giold/widget/icon.go
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+ "image/color"
+ "image/draw"
+
+ "golang.org/x/exp/shiny/iconvg"
+
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+)
+
+type Icon struct {
+ Color color.NRGBA
+ src []byte
+ // Cached values.
+ op paint.ImageOp
+ imgSize int
+ imgColor color.NRGBA
+}
+
+// NewIcon returns a new Icon from IconVG data.
+func NewIcon(data []byte) (*Icon, error) {
+ _, err := iconvg.DecodeMetadata(data)
+ if err != nil {
+ return nil, err
+ }
+ return &Icon{src: data, Color: color.NRGBA{A: 0xff}}, nil
+}
+
+func (ic *Icon) Layout(gtx layout.Context, sz unit.Value) layout.Dimensions {
+ ico := ic.image(gtx.Px(sz))
+ ico.Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ return layout.Dimensions{
+ Size: ico.Size(),
+ }
+}
+
+func (ic *Icon) image(sz int) paint.ImageOp {
+ if sz == ic.imgSize && ic.Color == ic.imgColor {
+ return ic.op
+ }
+ m, _ := iconvg.DecodeMetadata(ic.src)
+ dx, dy := m.ViewBox.AspectRatio()
+ img := image.NewRGBA(image.Rectangle{Max: image.Point{X: sz,
+ Y: int(float32(sz) * dy / dx)}})
+ var ico iconvg.Rasterizer
+ ico.SetDstImage(img, img.Bounds(), draw.Src)
+ m.Palette[0] = f32color.NRGBAToLinearRGBA(ic.Color)
+ iconvg.Decode(&ico, ic.src, &iconvg.DecodeOptions{
+ Palette: &m.Palette,
+ })
+ ic.op = paint.NewImageOp(img)
+ ic.imgSize = sz
+ ic.imgColor = ic.Color
+ return ic.op
+}
diff --git a/gio/giold/widget/icon_test.go b/gio/giold/widget/icon_test.go
new file mode 100644
index 0000000..1a3e8d9
--- /dev/null
+++ b/gio/giold/widget/icon_test.go
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+ "image/color"
+ "testing"
+
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+ "golang.org/x/exp/shiny/materialdesign/icons"
+)
+
+func TestIcon_Alpha(t *testing.T) {
+ icon, err := NewIcon(icons.ToggleCheckBox)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ icon.Color = color.NRGBA{B: 0xff, A: 0x40}
+
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Exact(image.Pt(100, 100)),
+ }
+
+ _ = icon.Layout(gtx, unit.Sp(18))
+}
diff --git a/gio/giold/widget/image.go b/gio/giold/widget/image.go
new file mode 100644
index 0000000..0e0351f
--- /dev/null
+++ b/gio/giold/widget/image.go
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+)
+
+// Image is a widget that displays an image.
+type Image struct {
+ // Src is the image to display.
+ Src paint.ImageOp
+ // Fit specifies how to scale the image to the constraints.
+ // By default it does not do any scaling.
+ Fit Fit
+ // Position specifies where to position the image within
+ // the constraints.
+ Position layout.Direction
+ // Scale is the ratio of image pixels to
+ // dps. If Scale is zero Image falls back to
+ // a scale that match a standard 72 DPI.
+ Scale float32
+}
+
+const defaultScale = float32(160.0 / 72.0)
+
+func (im Image) Layout(gtx layout.Context) layout.Dimensions {
+ defer op.Save(gtx.Ops).Load()
+
+ scale := im.Scale
+ if scale == 0 {
+ scale = defaultScale
+ }
+
+ size := im.Src.Size()
+ wf, hf := float32(size.X), float32(size.Y)
+ w, h := gtx.Px(unit.Dp(wf*scale)), gtx.Px(unit.Dp(hf*scale))
+
+ dims := im.Fit.scale(gtx, im.Position,
+ layout.Dimensions{Size: image.Pt(w, h)})
+
+ pixelScale := scale * gtx.Metric.PxPerDp
+ op.Affine(f32.Affine2D{}.Scale(f32.Point{},
+ f32.Pt(pixelScale, pixelScale))).Add(gtx.Ops)
+
+ im.Src.Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+
+ return dims
+}
diff --git a/gio/giold/widget/image_test.go b/gio/giold/widget/image_test.go
new file mode 100644
index 0000000..774dfc7
--- /dev/null
+++ b/gio/giold/widget/image_test.go
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+ "testing"
+
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/paint"
+)
+
+func TestImageScale(t *testing.T) {
+ var ops op.Ops
+ gtx := layout.Context{
+ Ops: &ops,
+ Constraints: layout.Constraints{
+ Max: image.Pt(50, 50),
+ },
+ }
+ imgSize := image.Pt(10, 10)
+ img := image.NewNRGBA(image.Rectangle{Max: imgSize})
+ imgOp := paint.NewImageOp(img)
+
+ // Ensure the default scales correctly.
+ dims := Image{Src: imgOp}.Layout(gtx)
+ expectedSize := imgSize
+ expectedSize.X = int(float32(expectedSize.X) * defaultScale)
+ expectedSize.Y = int(float32(expectedSize.Y) * defaultScale)
+ if dims.Size != expectedSize {
+ t.Fatalf("non-scaled image is wrong size, expected %v, got %v",
+ expectedSize, dims.Size)
+ }
+
+ // Ensure scaling the image via the Scale field works.
+ currentScale := float32(0.5)
+ dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx)
+ expectedSize = imgSize
+ expectedSize.X = int(float32(expectedSize.X) * currentScale)
+ expectedSize.Y = int(float32(expectedSize.Y) * currentScale)
+ if dims.Size != expectedSize {
+ t.Fatalf(".5 scale image is wrong size, expected %v, got %v",
+ expectedSize, dims.Size)
+ }
+
+ // Ensure the image responds to changes in DPI.
+ currentScale = float32(1)
+ gtx.Metric.PxPerDp = 2
+ dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx)
+ expectedSize = imgSize
+ expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp)
+ expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp)
+ if dims.Size != expectedSize {
+ t.Fatalf("HiDPI non-scaled image is wrong size, expected %v, got %v",
+ expectedSize, dims.Size)
+ }
+
+ // Ensure scaling the image responds to changes in DPI.
+ currentScale = float32(.5)
+ gtx.Metric.PxPerDp = 2
+ dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx)
+ expectedSize = imgSize
+ expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp)
+ expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp)
+ if dims.Size != expectedSize {
+ t.Fatalf("HiDPI .5 scale image is wrong size, expected %v, got %v",
+ expectedSize, dims.Size)
+ }
+}
diff --git a/gio/giold/widget/label.go b/gio/giold/widget/label.go
new file mode 100644
index 0000000..acf6b50
--- /dev/null
+++ b/gio/giold/widget/label.go
@@ -0,0 +1,252 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "fmt"
+ "image"
+ "unicode/utf8"
+
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+
+ "golang.org/x/image/math/fixed"
+)
+
+// Label is a widget for laying out and drawing text.
+type Label struct {
+ // Alignment specify the text alignment.
+ Alignment text.Alignment
+ // MaxLines limits the number of lines. Zero means no limit.
+ MaxLines int
+}
+
+// screenPos describes a character position (in text line and column numbers,
+// not pixels): Y = line number, X = rune column.
+type screenPos image.Point
+
+type segmentIterator struct {
+ Lines []text.Line
+ Clip image.Rectangle
+ Alignment text.Alignment
+ Width int
+ Offset image.Point
+ startSel screenPos
+ endSel screenPos
+
+ pos screenPos // current position
+ line text.Line // current line
+ layout text.Layout // current line's Layout
+
+ // pixel positions
+ off fixed.Point26_6
+ y, prevDesc fixed.Int26_6
+}
+
+const inf = 1e6
+
+func (l *segmentIterator) Next() (text.Layout, image.Point, bool, int,
+ image.Point, bool) {
+ for l.pos.Y < len(l.Lines) {
+ if l.pos.X == 0 {
+ l.line = l.Lines[l.pos.Y]
+
+ // Calculate X & Y pixel coordinates of left edge of line. We need y
+ // for the next line, so it's in l, but we only need x here, so it's
+ // not.
+ x := align(l.Alignment, l.line.Width, l.Width) + fixed.I(l.Offset.X)
+ l.y += l.prevDesc + l.line.Ascent
+ l.prevDesc = l.line.Descent
+ // Align baseline and line start to the pixel grid.
+ l.off = fixed.Point26_6{X: fixed.I(x.Floor()),
+ Y: fixed.I(l.y.Ceil())}
+ l.y = l.off.Y
+ l.off.Y += fixed.I(l.Offset.Y)
+ if (l.off.Y + l.line.Bounds.Min.Y).Floor() > l.Clip.Max.Y {
+ break
+ }
+
+ if (l.off.Y + l.line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
+ // This line is outside/before the clip area; go on to the next line.
+ l.pos.Y++
+ continue
+ }
+
+ // Copy the line's Layout, since we slice it up later.
+ l.layout = l.line.Layout
+
+ // Find the left edge of the text visible in the l.Clip clipping
+ // area.
+ for len(l.layout.Advances) > 0 {
+ _, n := utf8.DecodeRuneInString(l.layout.Text)
+ adv := l.layout.Advances[0]
+ if (l.off.X + adv + l.line.Bounds.Max.X - l.line.Width).Ceil() >= l.Clip.Min.X {
+ break
+ }
+ l.off.X += adv
+ l.layout.Text = l.layout.Text[n:]
+ l.layout.Advances = l.layout.Advances[1:]
+ l.pos.X++
+ }
+ }
+
+ selected := l.inSelection()
+ endx := l.off.X
+ rune := 0
+ nextLine := true
+ retLayout := l.layout
+ for n := range l.layout.Text {
+ selChanged := selected != l.inSelection()
+ beyondClipEdge := (endx + l.line.Bounds.Min.X).Floor() > l.Clip.Max.X
+ if selChanged || beyondClipEdge {
+ retLayout.Advances = l.layout.Advances[:rune]
+ retLayout.Text = l.layout.Text[:n]
+ if selChanged {
+ // Save the rest of the line
+ l.layout.Advances = l.layout.Advances[rune:]
+ l.layout.Text = l.layout.Text[n:]
+ nextLine = false
+ }
+ break
+ }
+ endx += l.layout.Advances[rune]
+ rune++
+ l.pos.X++
+ }
+ offFloor := image.Point{X: l.off.X.Floor(), Y: l.off.Y.Floor()}
+
+ // Calculate the width & height if the returned text.
+ //
+ // If there's a better way to do this, I'm all ears.
+ var d fixed.Int26_6
+ for _, adv := range retLayout.Advances {
+ d += adv
+ }
+ size := image.Point{
+ X: d.Ceil(),
+ Y: (l.line.Ascent + l.line.Descent).Ceil(),
+ }
+
+ if nextLine {
+ l.pos.Y++
+ l.pos.X = 0
+ } else {
+ l.off.X = endx
+ }
+
+ return retLayout, offFloor, selected, l.prevDesc.Ceil() - size.Y, size, true
+ }
+ return text.Layout{}, image.Point{}, false, 0, image.Point{}, false
+}
+
+func (l *segmentIterator) inSelection() bool {
+ return l.startSel.LessOrEqual(l.pos) &&
+ l.pos.Less(l.endSel)
+}
+
+func (p1 screenPos) LessOrEqual(p2 screenPos) bool {
+ return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X <= p2.X)
+}
+
+func (p1 screenPos) Less(p2 screenPos) bool {
+ return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X < p2.X)
+}
+
+func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font,
+ size unit.Value, txt string) layout.Dimensions {
+ cs := gtx.Constraints
+ textSize := fixed.I(gtx.Px(size))
+ lines := s.LayoutString(font, textSize, cs.Max.X, txt)
+ if max := l.MaxLines; max > 0 && len(lines) > max {
+ lines = lines[:max]
+ }
+ dims := linesDimens(lines)
+ dims.Size = cs.Constrain(dims.Size)
+ cl := textPadding(lines)
+ cl.Max = cl.Max.Add(dims.Size)
+ it := segmentIterator{
+ Lines: lines,
+ Clip: cl,
+ Alignment: l.Alignment,
+ Width: dims.Size.X,
+ }
+ for {
+ l, off, _, _, _, ok := it.Next()
+ if !ok {
+ break
+ }
+ stack := op.Save(gtx.Ops)
+ op.Offset(layout.FPt(off)).Add(gtx.Ops)
+ s.Shape(font, textSize, l).Add(gtx.Ops)
+ clip.Rect(cl.Sub(off)).Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ stack.Load()
+ }
+ return dims
+}
+
+func textPadding(lines []text.Line) (padding image.Rectangle) {
+ if len(lines) == 0 {
+ return
+ }
+ first := lines[0]
+ if d := first.Ascent + first.Bounds.Min.Y; d < 0 {
+ padding.Min.Y = d.Ceil()
+ }
+ last := lines[len(lines)-1]
+ if d := last.Bounds.Max.Y - last.Descent; d > 0 {
+ padding.Max.Y = d.Ceil()
+ }
+ if d := first.Bounds.Min.X; d < 0 {
+ padding.Min.X = d.Ceil()
+ }
+ if d := first.Bounds.Max.X - first.Width; d > 0 {
+ padding.Max.X = d.Ceil()
+ }
+ return
+}
+
+func linesDimens(lines []text.Line) layout.Dimensions {
+ var width fixed.Int26_6
+ var h int
+ var baseline int
+ if len(lines) > 0 {
+ baseline = lines[0].Ascent.Ceil()
+ var prevDesc fixed.Int26_6
+ for _, l := range lines {
+ h += (prevDesc + l.Ascent).Ceil()
+ prevDesc = l.Descent
+ if l.Width > width {
+ width = l.Width
+ }
+ }
+ h += lines[len(lines)-1].Descent.Ceil()
+ }
+ w := width.Ceil()
+ return layout.Dimensions{
+ Size: image.Point{
+ X: w,
+ Y: h,
+ },
+ Baseline: h - baseline,
+ }
+}
+
+func align(align text.Alignment, width fixed.Int26_6,
+ maxWidth int) fixed.Int26_6 {
+ mw := fixed.I(maxWidth)
+ switch align {
+ case text.Middle:
+ return fixed.I(((mw - width) / 2).Floor())
+ case text.End:
+ return fixed.I((mw - width).Floor())
+ case text.Start:
+ return 0
+ default:
+ panic(fmt.Errorf("unknown alignment %v", align))
+ }
+}
diff --git a/gio/giold/widget/material/button.go b/gio/giold/widget/material/button.go
new file mode 100644
index 0000000..78bfcf2
--- /dev/null
+++ b/gio/giold/widget/material/button.go
@@ -0,0 +1,291 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image"
+ "image/color"
+ "math"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type ButtonStyle struct {
+ Text string
+ // Color is the text color.
+ Color color.NRGBA
+ Font text.Font
+ TextSize unit.Value
+ Background color.NRGBA
+ CornerRadius unit.Value
+ Inset layout.Inset
+ Button *widget.Clickable
+ shaper text.Shaper
+}
+
+type ButtonLayoutStyle struct {
+ Background color.NRGBA
+ CornerRadius unit.Value
+ Button *widget.Clickable
+}
+
+type IconButtonStyle struct {
+ Background color.NRGBA
+ // Color is the icon color.
+ Color color.NRGBA
+ Icon *widget.Icon
+ // Size is the icon size.
+ Size unit.Value
+ Inset layout.Inset
+ Button *widget.Clickable
+}
+
+func Button(th *Theme, button *widget.Clickable, txt string) ButtonStyle {
+ return ButtonStyle{
+ Text: txt,
+ Color: th.Palette.ContrastFg,
+ CornerRadius: unit.Dp(4),
+ Background: th.Palette.ContrastBg,
+ TextSize: th.TextSize.Scale(14.0 / 16.0),
+ Inset: layout.Inset{
+ Top: unit.Dp(10), Bottom: unit.Dp(10),
+ Left: unit.Dp(12), Right: unit.Dp(12),
+ },
+ Button: button,
+ shaper: th.Shaper,
+ }
+}
+
+func ButtonLayout(th *Theme, button *widget.Clickable) ButtonLayoutStyle {
+ return ButtonLayoutStyle{
+ Button: button,
+ Background: th.Palette.ContrastBg,
+ CornerRadius: unit.Dp(4),
+ }
+}
+
+func IconButton(th *Theme, button *widget.Clickable,
+ icon *widget.Icon) IconButtonStyle {
+ return IconButtonStyle{
+ Background: th.Palette.ContrastBg,
+ Color: th.Palette.ContrastFg,
+ Icon: icon,
+ Size: unit.Dp(24),
+ Inset: layout.UniformInset(unit.Dp(12)),
+ Button: button,
+ }
+}
+
+// Clickable lays out a rectangular clickable widget without further
+// decoration.
+func Clickable(gtx layout.Context, button *widget.Clickable,
+ w layout.Widget) layout.Dimensions {
+ return layout.Stack{}.Layout(gtx,
+ layout.Expanded(button.Layout),
+ layout.Expanded(func(gtx layout.Context) layout.Dimensions {
+ clip.Rect{Max: gtx.Constraints.Min}.Add(gtx.Ops)
+ for _, c := range button.History() {
+ drawInk(gtx, c)
+ }
+ return layout.Dimensions{Size: gtx.Constraints.Min}
+ }),
+ layout.Stacked(w),
+ )
+}
+
+func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
+ return ButtonLayoutStyle{
+ Background: b.Background,
+ CornerRadius: b.CornerRadius,
+ Button: b.Button,
+ }.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ paint.ColorOp{Color: b.Color}.Add(gtx.Ops)
+ return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper,
+ b.Font, b.TextSize, b.Text)
+ })
+ })
+}
+
+func (b ButtonLayoutStyle) Layout(gtx layout.Context,
+ w layout.Widget) layout.Dimensions {
+ min := gtx.Constraints.Min
+ return layout.Stack{Alignment: layout.Center}.Layout(gtx,
+ layout.Expanded(func(gtx layout.Context) layout.Dimensions {
+ rr := float32(gtx.Px(b.CornerRadius))
+ clip.UniformRRect(f32.Rectangle{Max: f32.Point{
+ X: float32(gtx.Constraints.Min.X),
+ Y: float32(gtx.Constraints.Min.Y),
+ }}, rr).Add(gtx.Ops)
+ background := b.Background
+ switch {
+ case gtx.Queue == nil:
+ background = f32color.Disabled(b.Background)
+ case b.Button.Hovered():
+ background = f32color.Hovered(b.Background)
+ }
+ paint.Fill(gtx.Ops, background)
+ for _, c := range b.Button.History() {
+ drawInk(gtx, c)
+ }
+ return layout.Dimensions{Size: gtx.Constraints.Min}
+ }),
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ gtx.Constraints.Min = min
+ return layout.Center.Layout(gtx, w)
+ }),
+ layout.Expanded(b.Button.Layout),
+ )
+}
+
+func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
+ return layout.Stack{Alignment: layout.Center}.Layout(gtx,
+ layout.Expanded(func(gtx layout.Context) layout.Dimensions {
+ sizex, sizey := gtx.Constraints.Min.X, gtx.Constraints.Min.Y
+ sizexf, sizeyf := float32(sizex), float32(sizey)
+ rr := (sizexf + sizeyf) * .25
+ clip.UniformRRect(f32.Rectangle{
+ Max: f32.Point{X: sizexf, Y: sizeyf},
+ }, rr).Add(gtx.Ops)
+ background := b.Background
+ switch {
+ case gtx.Queue == nil:
+ background = f32color.Disabled(b.Background)
+ case b.Button.Hovered():
+ background = f32color.Hovered(b.Background)
+ }
+ paint.Fill(gtx.Ops, background)
+ for _, c := range b.Button.History() {
+ drawInk(gtx, c)
+ }
+ return layout.Dimensions{Size: gtx.Constraints.Min}
+ }),
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ return b.Inset.Layout(gtx,
+ func(gtx layout.Context) layout.Dimensions {
+ size := gtx.Px(b.Size)
+ if b.Icon != nil {
+ b.Icon.Color = b.Color
+ b.Icon.Layout(gtx, unit.Px(float32(size)))
+ }
+ return layout.Dimensions{
+ Size: image.Point{X: size, Y: size},
+ }
+ })
+ }),
+ layout.Expanded(func(gtx layout.Context) layout.Dimensions {
+ pointer.Ellipse(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
+ return b.Button.Layout(gtx)
+ }),
+ )
+}
+
+func drawInk(gtx layout.Context, c widget.Press) {
+ // duration is the number of seconds for the
+ // completed animation: expand while fading in, then
+ // out.
+ const (
+ expandDuration = float32(0.5)
+ fadeDuration = float32(0.9)
+ )
+
+ now := gtx.Now
+
+ t := float32(now.Sub(c.Start).Seconds())
+
+ end := c.End
+ if end.IsZero() {
+ // If the press hasn't ended, don't fade-out.
+ end = now
+ }
+
+ endt := float32(end.Sub(c.Start).Seconds())
+
+ // Compute the fade-in/out position in [0;1].
+ var alphat float32
+ {
+ var haste float32
+ if c.Cancelled {
+ // If the press was cancelled before the inkwell
+ // was fully faded in, fast forward the animation
+ // to match the fade-out.
+ if h := 0.5 - endt/fadeDuration; h > 0 {
+ haste = h
+ }
+ }
+ // Fade in.
+ half1 := t/fadeDuration + haste
+ if half1 > 0.5 {
+ half1 = 0.5
+ }
+
+ // Fade out.
+ half2 := float32(now.Sub(end).Seconds())
+ half2 /= fadeDuration
+ half2 += haste
+ if half2 > 0.5 {
+ // Too old.
+ return
+ }
+
+ alphat = half1 + half2
+ }
+
+ // Compute the expand position in [0;1].
+ sizet := t
+ if c.Cancelled {
+ // Freeze expansion of cancelled presses.
+ sizet = endt
+ }
+ sizet /= expandDuration
+
+ // Animate only ended presses, and presses that are fading in.
+ if !c.End.IsZero() || sizet <= 1.0 {
+ op.InvalidateOp{}.Add(gtx.Ops)
+ }
+
+ if sizet > 1.0 {
+ sizet = 1.0
+ }
+
+ if alphat > .5 {
+ // Start fadeout after half the animation.
+ alphat = 1.0 - alphat
+ }
+ // Twice the speed to attain fully faded in at 0.5.
+ t2 := alphat * 2
+ // BeziƩr ease-in curve.
+ alphaBezier := t2 * t2 * (3.0 - 2.0*t2)
+ sizeBezier := sizet * sizet * (3.0 - 2.0*sizet)
+ size := float32(gtx.Constraints.Min.X)
+ if h := float32(gtx.Constraints.Min.Y); h > size {
+ size = h
+ }
+ // Cover the entire constraints min rectangle.
+ size *= 2 * float32(math.Sqrt(2))
+ // Apply curve values to size and color.
+ size *= sizeBezier
+ alpha := 0.7 * alphaBezier
+ const col = 0.8
+ ba, bc := byte(alpha*0xff), byte(col*0xff)
+ defer op.Save(gtx.Ops).Load()
+ rgba := f32color.MulAlpha(color.NRGBA{A: 0xff, R: bc, G: bc, B: bc}, ba)
+ ink := paint.ColorOp{Color: rgba}
+ ink.Add(gtx.Ops)
+ rr := size * .5
+ op.Offset(c.Position.Add(f32.Point{
+ X: -rr,
+ Y: -rr,
+ })).Add(gtx.Ops)
+ clip.UniformRRect(f32.Rectangle{Max: f32.Pt(size, size)}, rr).Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+}
diff --git a/gio/giold/widget/material/checkable.go b/gio/giold/widget/material/checkable.go
new file mode 100644
index 0000000..e895b81
--- /dev/null
+++ b/gio/giold/widget/material/checkable.go
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image"
+ "image/color"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type checkable struct {
+ Label string
+ Color color.NRGBA
+ Font text.Font
+ TextSize unit.Value
+ IconColor color.NRGBA
+ Size unit.Value
+ shaper text.Shaper
+ checkedStateIcon *widget.Icon
+ uncheckedStateIcon *widget.Icon
+}
+
+func (c *checkable) layout(gtx layout.Context,
+ checked, hovered bool) layout.Dimensions {
+ var icon *widget.Icon
+ if checked {
+ icon = c.checkedStateIcon
+ } else {
+ icon = c.uncheckedStateIcon
+ }
+
+ dims := layout.Flex{Alignment: layout.Middle}.Layout(gtx,
+ layout.Rigid(func(gtx layout.Context) layout.Dimensions {
+ return layout.Stack{Alignment: layout.Center}.Layout(gtx,
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ size := gtx.Px(c.Size) * 4 / 3
+ dims := layout.Dimensions{
+ Size: image.Point{X: size, Y: size},
+ }
+ if !hovered {
+ return dims
+ }
+
+ background := f32color.MulAlpha(c.IconColor, 70)
+
+ radius := float32(size) / 2
+ paint.FillShape(gtx.Ops, background,
+ clip.Circle{
+ Center: f32.Point{X: radius, Y: radius},
+ Radius: radius,
+ }.Op(gtx.Ops))
+
+ return dims
+ }),
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ return layout.UniformInset(unit.Dp(2)).Layout(gtx,
+ func(gtx layout.Context) layout.Dimensions {
+ size := gtx.Px(c.Size)
+ icon.Color = c.IconColor
+ if gtx.Queue == nil {
+ icon.Color = f32color.Disabled(icon.Color)
+ }
+ icon.Layout(gtx, unit.Px(float32(size)))
+ return layout.Dimensions{
+ Size: image.Point{X: size, Y: size},
+ }
+ })
+ }),
+ )
+ }),
+
+ layout.Rigid(func(gtx layout.Context) layout.Dimensions {
+ return layout.UniformInset(unit.Dp(2)).Layout(gtx,
+ func(gtx layout.Context) layout.Dimensions {
+ paint.ColorOp{Color: c.Color}.Add(gtx.Ops)
+ return widget.Label{}.Layout(gtx, c.shaper, c.Font,
+ c.TextSize, c.Label)
+ })
+ }),
+ )
+ pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
+ return dims
+}
diff --git a/gio/giold/widget/material/checkbox.go b/gio/giold/widget/material/checkbox.go
new file mode 100644
index 0000000..2483cfe
--- /dev/null
+++ b/gio/giold/widget/material/checkbox.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "realy.lol/gio/layout"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type CheckBoxStyle struct {
+ checkable
+ CheckBox *widget.Bool
+}
+
+func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle {
+ return CheckBoxStyle{
+ CheckBox: checkBox,
+ checkable: checkable{
+ Label: label,
+ Color: th.Palette.Fg,
+ IconColor: th.Palette.ContrastBg,
+ TextSize: th.TextSize.Scale(14.0 / 16.0),
+ Size: unit.Dp(26),
+ shaper: th.Shaper,
+ checkedStateIcon: th.Icon.CheckBoxChecked,
+ uncheckedStateIcon: th.Icon.CheckBoxUnchecked,
+ },
+ }
+}
+
+// Layout updates the checkBox and displays it.
+func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions {
+ dims := c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered())
+ gtx.Constraints.Min = dims.Size
+ c.CheckBox.Layout(gtx)
+ return dims
+}
diff --git a/gio/giold/widget/material/doc.go b/gio/giold/widget/material/doc.go
new file mode 100644
index 0000000..715f5a0
--- /dev/null
+++ b/gio/giold/widget/material/doc.go
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package material implements the Material design.
+//
+// To maximize reusability and visual flexibility, user interface controls are
+// split into two parts: the stateful widget and the stateless drawing of it.
+//
+// For example, widget.Clickable encapsulates the state and event
+// handling of all clickable areas, while the Theme is responsible to
+// draw a specific area, for example a button.
+//
+// This snippet defines a button that prints a message when clicked:
+//
+// var gtx layout.Context
+// button := new(widget.Clickable)
+//
+// for button.Clicked(gtx) {
+// fmt.Println("Clicked!")
+// }
+//
+// Use a Theme to draw the button:
+//
+// theme := material.NewTheme(...)
+//
+// material.Button(theme, "Click me!").Layout(gtx, button)
+//
+// Customization
+//
+// Quite often, a program needs to customize the theme-provided defaults. Several
+// options are available, depending on the nature of the change.
+//
+// Mandatory parameters: Some parameters are not part of the widget state but
+// have no obvious default. In the program above, the button text is a
+// parameter to the Theme.Button method.
+//
+// Theme-global parameters: For changing the look of all widgets drawn with a
+// particular theme, adjust the `Theme` fields:
+//
+// theme.Color.Primary = color.NRGBA{...}
+//
+// Widget-local parameters: For changing the look of a particular widget,
+// adjust the widget specific theme object:
+//
+// btn := material.Button(theme, "Click me!")
+// btn.Font.Style = text.Italic
+// btn.Layout(gtx, button)
+//
+// Widget variants: A widget can have several distinct representations even
+// though the underlying state is the same. A widget.Clickable can be drawn as a
+// round icon button:
+//
+// icon := material.NewIcon(...)
+//
+// material.IconButton(theme, icon).Layout(gtx, button)
+//
+// Specialized widgets: Theme both define a generic Label method
+// that takes a text size, and specialized methods for standard text
+// sizes such as Theme.H1 and Theme.Body2.
+package material
diff --git a/gio/giold/widget/material/editor.go b/gio/giold/widget/material/editor.go
new file mode 100644
index 0000000..93d02cf
--- /dev/null
+++ b/gio/giold/widget/material/editor.go
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image/color"
+
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type EditorStyle struct {
+ Font text.Font
+ TextSize unit.Value
+ // Color is the text color.
+ Color color.NRGBA
+ // Hint contains the text displayed when the editor is empty.
+ Hint string
+ // HintColor is the color of hint text.
+ HintColor color.NRGBA
+ // SelectionColor is the color of the background for selected text.
+ SelectionColor color.NRGBA
+ Editor *widget.Editor
+
+ shaper text.Shaper
+}
+
+func Editor(th *Theme, editor *widget.Editor, hint string) EditorStyle {
+ return EditorStyle{
+ Editor: editor,
+ TextSize: th.TextSize,
+ Color: th.Palette.Fg,
+ shaper: th.Shaper,
+ Hint: hint,
+ HintColor: f32color.MulAlpha(th.Palette.Fg, 0xbb),
+ SelectionColor: f32color.MulAlpha(th.Palette.ContrastBg, 0x60),
+ }
+}
+
+func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions {
+ defer op.Save(gtx.Ops).Load()
+ macro := op.Record(gtx.Ops)
+ paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops)
+ var maxlines int
+ if e.Editor.SingleLine {
+ maxlines = 1
+ }
+ tl := widget.Label{Alignment: e.Editor.Alignment, MaxLines: maxlines}
+ dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint)
+ call := macro.Stop()
+ if w := dims.Size.X; gtx.Constraints.Min.X < w {
+ gtx.Constraints.Min.X = w
+ }
+ if h := dims.Size.Y; gtx.Constraints.Min.Y < h {
+ gtx.Constraints.Min.Y = h
+ }
+ dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize)
+ disabled := gtx.Queue == nil
+ if e.Editor.Len() > 0 {
+ paint.ColorOp{Color: blendDisabledColor(disabled,
+ e.SelectionColor)}.Add(gtx.Ops)
+ e.Editor.PaintSelection(gtx)
+ paint.ColorOp{Color: blendDisabledColor(disabled, e.Color)}.Add(gtx.Ops)
+ e.Editor.PaintText(gtx)
+ } else {
+ call.Add(gtx.Ops)
+ }
+ if !disabled {
+ paint.ColorOp{Color: e.Color}.Add(gtx.Ops)
+ e.Editor.PaintCaret(gtx)
+ }
+ return dims
+}
+
+func blendDisabledColor(disabled bool, c color.NRGBA) color.NRGBA {
+ if disabled {
+ return f32color.Disabled(c)
+ }
+ return c
+}
diff --git a/gio/giold/widget/material/label.go b/gio/giold/widget/material/label.go
new file mode 100644
index 0000000..80c4b02
--- /dev/null
+++ b/gio/giold/widget/material/label.go
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image/color"
+
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type LabelStyle struct {
+ // Face defines the text style.
+ Font text.Font
+ // Color is the text color.
+ Color color.NRGBA
+ // Alignment specify the text alignment.
+ Alignment text.Alignment
+ // MaxLines limits the number of lines. Zero means no limit.
+ MaxLines int
+ Text string
+ TextSize unit.Value
+
+ shaper text.Shaper
+}
+
+func H1(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(96.0/16.0), txt)
+}
+
+func H2(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(60.0/16.0), txt)
+}
+
+func H3(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(48.0/16.0), txt)
+}
+
+func H4(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(34.0/16.0), txt)
+}
+
+func H5(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(24.0/16.0), txt)
+}
+
+func H6(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(20.0/16.0), txt)
+}
+
+func Body1(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize, txt)
+}
+
+func Body2(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(14.0/16.0), txt)
+}
+
+func Caption(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(12.0/16.0), txt)
+}
+
+func Label(th *Theme, size unit.Value, txt string) LabelStyle {
+ return LabelStyle{
+ Text: txt,
+ Color: th.Palette.Fg,
+ TextSize: size,
+ shaper: th.Shaper,
+ }
+}
+
+func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
+ paint.ColorOp{Color: l.Color}.Add(gtx.Ops)
+ tl := widget.Label{Alignment: l.Alignment, MaxLines: l.MaxLines}
+ return tl.Layout(gtx, l.shaper, l.Font, l.TextSize, l.Text)
+}
diff --git a/gio/giold/widget/material/loader.go b/gio/giold/widget/material/loader.go
new file mode 100644
index 0000000..77afede
--- /dev/null
+++ b/gio/giold/widget/material/loader.go
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image"
+ "image/color"
+ "math"
+ "time"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+)
+
+type LoaderStyle struct {
+ Color color.NRGBA
+}
+
+func Loader(th *Theme) LoaderStyle {
+ return LoaderStyle{
+ Color: th.Palette.ContrastBg,
+ }
+}
+
+func (l LoaderStyle) Layout(gtx layout.Context) layout.Dimensions {
+ diam := gtx.Constraints.Min.X
+ if minY := gtx.Constraints.Min.Y; minY > diam {
+ diam = minY
+ }
+ if diam == 0 {
+ diam = gtx.Px(unit.Dp(24))
+ }
+ sz := gtx.Constraints.Constrain(image.Pt(diam, diam))
+ radius := float64(sz.X) * .5
+ defer op.Save(gtx.Ops).Load()
+ op.Offset(f32.Pt(float32(radius), float32(radius))).Add(gtx.Ops)
+
+ dt := (time.Duration(gtx.Now.UnixNano()) % (time.Second)).Seconds()
+ startAngle := dt * math.Pi * 2
+ endAngle := startAngle + math.Pi*1.5
+
+ clipLoader(gtx.Ops, startAngle, endAngle, radius)
+ paint.ColorOp{
+ Color: l.Color,
+ }.Add(gtx.Ops)
+ op.Offset(f32.Pt(-float32(radius), -float32(radius))).Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ op.InvalidateOp{}.Add(gtx.Ops)
+ return layout.Dimensions{
+ Size: sz,
+ }
+}
+
+func clipLoader(ops *op.Ops, startAngle, endAngle, radius float64) {
+ const thickness = .25
+
+ var (
+ width = float32(radius * thickness)
+ delta = float32(endAngle - startAngle)
+
+ vy, vx = math.Sincos(startAngle)
+
+ pen = f32.Pt(float32(vx), float32(vy)).Mul(float32(radius))
+ center = f32.Pt(0, 0).Sub(pen)
+
+ p clip.Path
+ )
+
+ p.Begin(ops)
+ p.Move(pen)
+ p.Arc(center, center, delta)
+ clip.Stroke{
+ Path: p.End(),
+ Style: clip.StrokeStyle{
+ Width: width,
+ Cap: clip.FlatCap,
+ },
+ }.Op().Add(ops)
+}
diff --git a/gio/giold/widget/material/progressbar.go b/gio/giold/widget/material/progressbar.go
new file mode 100644
index 0000000..98ae4cf
--- /dev/null
+++ b/gio/giold/widget/material/progressbar.go
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image"
+ "image/color"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+)
+
+type ProgressBarStyle struct {
+ Color color.NRGBA
+ TrackColor color.NRGBA
+ Progress float32
+}
+
+func ProgressBar(th *Theme, progress float32) ProgressBarStyle {
+ return ProgressBarStyle{
+ Progress: progress,
+ Color: th.Palette.ContrastBg,
+ TrackColor: f32color.MulAlpha(th.Palette.Fg, 0x88),
+ }
+}
+
+func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions {
+ shader := func(width float32, color color.NRGBA) layout.Dimensions {
+ maxHeight := unit.Dp(4)
+ rr := float32(gtx.Px(unit.Dp(2)))
+
+ d := image.Point{X: int(width), Y: gtx.Px(maxHeight)}
+
+ height := float32(gtx.Px(maxHeight))
+ clip.UniformRRect(f32.Rectangle{Max: f32.Pt(width, height)},
+ rr).Add(gtx.Ops)
+ paint.ColorOp{Color: color}.Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+
+ return layout.Dimensions{Size: d}
+ }
+
+ progressBarWidth := float32(gtx.Constraints.Max.X)
+ return layout.Stack{Alignment: layout.W}.Layout(gtx,
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ return shader(progressBarWidth, p.TrackColor)
+ }),
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ fillWidth := progressBarWidth * clamp1(p.Progress)
+ fillColor := p.Color
+ if gtx.Queue == nil {
+ fillColor = f32color.Disabled(fillColor)
+ }
+ return shader(fillWidth, fillColor)
+ }),
+ )
+}
+
+// clamp1 limits v to range [0..1].
+func clamp1(v float32) float32 {
+ if v >= 1 {
+ return 1
+ } else if v <= 0 {
+ return 0
+ } else {
+ return v
+ }
+}
diff --git a/gio/giold/widget/material/radiobutton.go b/gio/giold/widget/material/radiobutton.go
new file mode 100644
index 0000000..79dd763
--- /dev/null
+++ b/gio/giold/widget/material/radiobutton.go
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "realy.lol/gio/layout"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type RadioButtonStyle struct {
+ checkable
+ Key string
+ Group *widget.Enum
+}
+
+// RadioButton returns a RadioButton with a label. The key specifies
+// the value for the Enum.
+func RadioButton(th *Theme, group *widget.Enum,
+ key, label string) RadioButtonStyle {
+ return RadioButtonStyle{
+ Group: group,
+ checkable: checkable{
+ Label: label,
+
+ Color: th.Palette.Fg,
+ IconColor: th.Palette.ContrastBg,
+ TextSize: th.TextSize.Scale(14.0 / 16.0),
+ Size: unit.Dp(26),
+ shaper: th.Shaper,
+ checkedStateIcon: th.Icon.RadioChecked,
+ uncheckedStateIcon: th.Icon.RadioUnchecked,
+ },
+ Key: key,
+ }
+}
+
+// Layout updates enum and displays the radio button.
+func (r RadioButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
+ hovered, hovering := r.Group.Hovered()
+ dims := r.layout(gtx, r.Group.Value == r.Key, hovering && hovered == r.Key)
+ gtx.Constraints.Min = dims.Size
+ r.Group.Layout(gtx, r.Key)
+ return dims
+}
diff --git a/gio/giold/widget/material/slider.go b/gio/giold/widget/material/slider.go
new file mode 100644
index 0000000..e038d75
--- /dev/null
+++ b/gio/giold/widget/material/slider.go
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image"
+ "image/color"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+// Slider is for selecting a value in a range.
+func Slider(th *Theme, float *widget.Float, min, max float32) SliderStyle {
+ return SliderStyle{
+ Min: min,
+ Max: max,
+ Color: th.Palette.ContrastBg,
+ Float: float,
+ FingerSize: th.FingerSize,
+ }
+}
+
+type SliderStyle struct {
+ Min, Max float32
+ Color color.NRGBA
+ Float *widget.Float
+
+ FingerSize unit.Value
+}
+
+func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions {
+ thumbRadius := gtx.Px(unit.Dp(6))
+ trackWidth := gtx.Px(unit.Dp(2))
+
+ axis := s.Float.Axis
+ // Keep a minimum length so that the track is always visible.
+ minLength := thumbRadius + 3*thumbRadius + thumbRadius
+ // Try to expand to finger size, but only if the constraints
+ // allow for it.
+ touchSizePx := min(gtx.Px(s.FingerSize),
+ axis.Convert(gtx.Constraints.Max).Y)
+ sizeMain := max(axis.Convert(gtx.Constraints.Min).X, minLength)
+ sizeCross := max(2*thumbRadius, touchSizePx)
+ size := axis.Convert(image.Pt(sizeMain, sizeCross))
+
+ st := op.Save(gtx.Ops)
+ o := axis.Convert(image.Pt(thumbRadius, 0))
+ op.Offset(layout.FPt(o)).Add(gtx.Ops)
+ gtx.Constraints.Min = axis.Convert(image.Pt(sizeMain-2*thumbRadius,
+ sizeCross))
+ s.Float.Layout(gtx, thumbRadius, s.Min, s.Max)
+ gtx.Constraints.Min = gtx.Constraints.Min.Add(axis.Convert(image.Pt(0,
+ sizeCross)))
+ thumbPos := thumbRadius + int(s.Float.Pos())
+ st.Load()
+
+ color := s.Color
+ if gtx.Queue == nil {
+ color = f32color.Disabled(color)
+ }
+
+ // Draw track before thumb.
+ st = op.Save(gtx.Ops)
+ track := image.Rectangle{
+ Min: axis.Convert(image.Pt(thumbRadius, sizeCross/2-trackWidth/2)),
+ Max: axis.Convert(image.Pt(thumbPos, sizeCross/2+trackWidth/2)),
+ }
+ clip.Rect(track).Add(gtx.Ops)
+ paint.Fill(gtx.Ops, color)
+ st.Load()
+
+ // Draw track after thumb.
+ st = op.Save(gtx.Ops)
+ track = image.Rectangle{
+ Min: axis.Convert(image.Pt(thumbPos, axis.Convert(track.Min).Y)),
+ Max: axis.Convert(image.Pt(sizeMain-thumbRadius,
+ axis.Convert(track.Max).Y)),
+ }
+ clip.Rect(track).Add(gtx.Ops)
+ paint.Fill(gtx.Ops, f32color.MulAlpha(color, 96))
+ st.Load()
+
+ // Draw thumb.
+ pt := axis.Convert(image.Pt(thumbPos, sizeCross/2))
+ paint.FillShape(gtx.Ops, color,
+ clip.Circle{
+ Center: f32.Point{X: float32(pt.X), Y: float32(pt.Y)},
+ Radius: float32(thumbRadius),
+ }.Op(gtx.Ops))
+
+ return layout.Dimensions{Size: size}
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
diff --git a/gio/giold/widget/material/switch.go b/gio/giold/widget/material/switch.go
new file mode 100644
index 0000000..14a4134
--- /dev/null
+++ b/gio/giold/widget/material/switch.go
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image"
+ "image/color"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type SwitchStyle struct {
+ Color struct {
+ Enabled color.NRGBA
+ Disabled color.NRGBA
+ Track color.NRGBA
+ }
+ Switch *widget.Bool
+}
+
+// Switch is for selecting a boolean value.
+func Switch(th *Theme, swtch *widget.Bool) SwitchStyle {
+ sw := SwitchStyle{
+ Switch: swtch,
+ }
+ sw.Color.Enabled = th.Palette.ContrastBg
+ sw.Color.Disabled = th.Palette.Bg
+ sw.Color.Track = f32color.MulAlpha(th.Palette.Fg, 0x88)
+ return sw
+}
+
+// Layout updates the switch and displays it.
+func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions {
+ trackWidth := gtx.Px(unit.Dp(36))
+ trackHeight := gtx.Px(unit.Dp(16))
+ thumbSize := gtx.Px(unit.Dp(20))
+ trackOff := float32(thumbSize-trackHeight) * .5
+
+ // Draw track.
+ stack := op.Save(gtx.Ops)
+ trackCorner := float32(trackHeight) / 2
+ trackRect := f32.Rectangle{Max: f32.Point{
+ X: float32(trackWidth),
+ Y: float32(trackHeight),
+ }}
+ col := s.Color.Disabled
+ if s.Switch.Value {
+ col = s.Color.Enabled
+ }
+ if gtx.Queue == nil {
+ col = f32color.Disabled(col)
+ }
+ trackColor := s.Color.Track
+ op.Offset(f32.Point{Y: trackOff}).Add(gtx.Ops)
+ clip.UniformRRect(trackRect, trackCorner).Add(gtx.Ops)
+ paint.ColorOp{Color: trackColor}.Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ stack.Load()
+
+ // Draw thumb ink.
+ stack = op.Save(gtx.Ops)
+ inkSize := gtx.Px(unit.Dp(44))
+ rr := float32(inkSize) * .5
+ inkOff := f32.Point{
+ X: float32(trackWidth)*.5 - rr,
+ Y: -rr + float32(trackHeight)*.5 + trackOff,
+ }
+ op.Offset(inkOff).Add(gtx.Ops)
+ gtx.Constraints.Min = image.Pt(inkSize, inkSize)
+ clip.UniformRRect(f32.Rectangle{Max: layout.FPt(gtx.Constraints.Min)},
+ rr).Add(gtx.Ops)
+ for _, p := range s.Switch.History() {
+ drawInk(gtx, p)
+ }
+ stack.Load()
+
+ // Compute thumb offset and color.
+ stack = op.Save(gtx.Ops)
+ if s.Switch.Value {
+ off := trackWidth - thumbSize
+ op.Offset(f32.Point{X: float32(off)}).Add(gtx.Ops)
+ }
+
+ thumbRadius := float32(thumbSize) / 2
+
+ // Draw hover.
+ if s.Switch.Hovered() {
+ r := 1.7 * thumbRadius
+ background := f32color.MulAlpha(s.Color.Enabled, 70)
+ paint.FillShape(gtx.Ops, background,
+ clip.Circle{
+ Center: f32.Point{X: thumbRadius, Y: thumbRadius},
+ Radius: r,
+ }.Op(gtx.Ops))
+ }
+
+ // Draw thumb shadow, a translucent disc slightly larger than the
+ // thumb itself.
+ // Center shadow horizontally and slightly adjust its Y.
+ paint.FillShape(gtx.Ops, argb(0x55000000),
+ clip.Circle{
+ Center: f32.Point{X: thumbRadius, Y: thumbRadius + .25},
+ Radius: thumbRadius + 1,
+ }.Op(gtx.Ops))
+
+ // Draw thumb.
+ paint.FillShape(gtx.Ops, col,
+ clip.Circle{
+ Center: f32.Point{X: thumbRadius, Y: thumbRadius},
+ Radius: thumbRadius,
+ }.Op(gtx.Ops))
+
+ // Set up click area.
+ stack = op.Save(gtx.Ops)
+ clickSize := gtx.Px(unit.Dp(40))
+ clickOff := f32.Point{
+ X: (float32(trackWidth) - float32(clickSize)) * .5,
+ Y: (float32(trackHeight)-float32(clickSize))*.5 + trackOff,
+ }
+ op.Offset(clickOff).Add(gtx.Ops)
+ sz := image.Pt(clickSize, clickSize)
+ pointer.Ellipse(image.Rectangle{Max: sz}).Add(gtx.Ops)
+ gtx.Constraints.Min = sz
+ s.Switch.Layout(gtx)
+ stack.Load()
+
+ dims := image.Point{X: trackWidth, Y: thumbSize}
+ return layout.Dimensions{Size: dims}
+}
diff --git a/gio/giold/widget/material/theme.go b/gio/giold/widget/material/theme.go
new file mode 100644
index 0000000..e19f7df
--- /dev/null
+++ b/gio/giold/widget/material/theme.go
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image/color"
+
+ "golang.org/x/exp/shiny/materialdesign/icons"
+
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+// Palette contains the minimal set of colors that a widget may need to
+// draw itself.
+type Palette struct {
+ // Bg is the background color atop which content is currently being
+ // drawn.
+ Bg color.NRGBA
+
+ // Fg is a color suitable for drawing on top of Bg.
+ Fg color.NRGBA
+
+ // ContrastBg is a color used to draw attention to active,
+ // important, interactive widgets such as buttons.
+ ContrastBg color.NRGBA
+
+ // ContrastFg is a color suitable for content drawn on top of
+ // ContrastBg.
+ ContrastFg color.NRGBA
+}
+
+type Theme struct {
+ Shaper text.Shaper
+ Palette
+ TextSize unit.Value
+ Icon struct {
+ CheckBoxChecked *widget.Icon
+ CheckBoxUnchecked *widget.Icon
+ RadioChecked *widget.Icon
+ RadioUnchecked *widget.Icon
+ }
+
+ // FingerSize is the minimum touch target size.
+ FingerSize unit.Value
+}
+
+func NewTheme(fontCollection []text.FontFace) *Theme {
+ t := &Theme{
+ Shaper: text.NewCache(fontCollection),
+ }
+ t.Palette = Palette{
+ Fg: rgb(0x000000),
+ Bg: rgb(0xffffff),
+ ContrastBg: rgb(0x3f51b5),
+ ContrastFg: rgb(0xffffff),
+ }
+ t.TextSize = unit.Sp(16)
+
+ t.Icon.CheckBoxChecked = mustIcon(widget.NewIcon(icons.ToggleCheckBox))
+ t.Icon.CheckBoxUnchecked = mustIcon(widget.NewIcon(icons.ToggleCheckBoxOutlineBlank))
+ t.Icon.RadioChecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonChecked))
+ t.Icon.RadioUnchecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonUnchecked))
+
+ // 38dp is on the lower end of possible finger size.
+ t.FingerSize = unit.Dp(38)
+
+ return t
+}
+
+func (t Theme) WithPalette(p Palette) Theme {
+ t.Palette = p
+ return t
+}
+
+func mustIcon(ic *widget.Icon, err error) *widget.Icon {
+ if err != nil {
+ panic(err)
+ }
+ return ic
+}
+
+func rgb(c uint32) color.NRGBA {
+ return argb(0xff000000 | c)
+}
+
+func argb(c uint32) color.NRGBA {
+ return color.NRGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8),
+ B: uint8(c)}
+}
diff --git a/gio/gpu/api.go b/gio/gpu/api.go
new file mode 100644
index 0000000..1a87684
--- /dev/null
+++ b/gio/gpu/api.go
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+import "realy.lol/gio/gpu/internal/driver"
+
+// An API carries the necessary GPU API specific resources to create a Device.
+// There is an API type for each supported GPU API such as OpenGL and Direct3D.
+type API = driver.API
+
+// OpenGL denotes the OpenGL or OpenGL ES API.
+type OpenGL = driver.OpenGL
+
+// Direct3D11 denotes the Direct3D API.
+type Direct3D11 = driver.Direct3D11
diff --git a/gio/gpu/caches.go b/gio/gpu/caches.go
new file mode 100644
index 0000000..3dd93cf
--- /dev/null
+++ b/gio/gpu/caches.go
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+import (
+ "fmt"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/ops"
+)
+
+type resourceCache struct {
+ res map[interface{}]resource
+ newRes map[interface{}]resource
+}
+
+// opCache is like a resourceCache but using concrete types and a
+// freelist instead of two maps to avoid runtime.mapaccess2 calls
+// since benchmarking showed them as a bottleneck.
+type opCache struct {
+ // store the index + 1 in cache this key is stored in
+ index map[ops.Key]int
+ // list of indexes in cache that are free and can be used
+ freelist []int
+ cache []opCacheValue
+}
+
+type opCacheValue struct {
+ data pathData
+ // computePath is the encoded path for compute.
+ computePath encoder
+
+ bounds f32.Rectangle
+ // the fields below are handled by opCache
+ key ops.Key
+ keep bool
+}
+
+func newResourceCache() *resourceCache {
+ return &resourceCache{
+ res: make(map[interface{}]resource),
+ newRes: make(map[interface{}]resource),
+ }
+}
+
+func (r *resourceCache) get(key interface{}) (resource, bool) {
+ v, exists := r.res[key]
+ if exists {
+ r.newRes[key] = v
+ }
+ return v, exists
+}
+
+func (r *resourceCache) put(key interface{}, val resource) {
+ if _, exists := r.newRes[key]; exists {
+ panic(fmt.Errorf("key exists, %p", key))
+ }
+ r.res[key] = val
+ r.newRes[key] = val
+}
+
+func (r *resourceCache) frame() {
+ for k, v := range r.res {
+ if _, exists := r.newRes[k]; !exists {
+ delete(r.res, k)
+ v.release()
+ }
+ }
+ for k, v := range r.newRes {
+ delete(r.newRes, k)
+ r.res[k] = v
+ }
+}
+
+func (r *resourceCache) release() {
+ for _, v := range r.newRes {
+ v.release()
+ }
+ r.newRes = nil
+ r.res = nil
+}
+
+func newOpCache() *opCache {
+ return &opCache{
+ index: make(map[ops.Key]int),
+ freelist: make([]int, 0),
+ cache: make([]opCacheValue, 0),
+ }
+}
+
+func (r *opCache) get(key ops.Key) (o opCacheValue, exist bool) {
+ v := r.index[key]
+ if v == 0 {
+ return
+ }
+ r.cache[v-1].keep = true
+ return r.cache[v-1], true
+}
+
+func (r *opCache) put(key ops.Key, val opCacheValue) {
+ v := r.index[key]
+ val.keep = true
+ val.key = key
+ if v == 0 {
+ // not in cache
+ i := len(r.cache)
+ if len(r.freelist) > 0 {
+ i = r.freelist[len(r.freelist)-1]
+ r.freelist = r.freelist[:len(r.freelist)-1]
+ r.cache[i] = val
+ } else {
+ r.cache = append(r.cache, val)
+ }
+ r.index[key] = i + 1
+ } else {
+ r.cache[v-1] = val
+ }
+}
+
+func (r *opCache) frame() {
+ r.freelist = r.freelist[:0]
+ for i, v := range r.cache {
+ r.cache[i].keep = false
+ if v.keep {
+ continue
+ }
+ if v.data.data != nil {
+ v.data.release()
+ r.cache[i].data.data = nil
+ }
+ delete(r.index, v.key)
+ r.freelist = append(r.freelist, i)
+ }
+}
+
+func (r *opCache) release() {
+ for i := range r.cache {
+ r.cache[i].keep = false
+ }
+ r.frame()
+ r.index = nil
+ r.freelist = nil
+ r.cache = nil
+}
diff --git a/gio/gpu/clip.go b/gio/gpu/clip.go
new file mode 100644
index 0000000..7e24449
--- /dev/null
+++ b/gio/gpu/clip.go
@@ -0,0 +1,98 @@
+package gpu
+
+import (
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/stroke"
+)
+
+type quadSplitter struct {
+ bounds f32.Rectangle
+ contour uint32
+ d *drawOps
+}
+
+func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) {
+ // NW.
+ encodeVertex(data, meta, -1, 1, from, ctrl, to)
+ // NE.
+ encodeVertex(data[vertStride:], meta, 1, 1, from, ctrl, to)
+ // SW.
+ encodeVertex(data[vertStride*2:], meta, -1, -1, from, ctrl, to)
+ // SE.
+ encodeVertex(data[vertStride*3:], meta, 1, -1, from, ctrl, to)
+}
+
+func encodeVertex(data []byte, meta uint32, cornerx, cornery int16,
+ from, ctrl, to f32.Point) {
+ var corner float32
+ if cornerx == 1 {
+ corner += .5
+ }
+ if cornery == 1 {
+ corner += .25
+ }
+ v := vertex{
+ Corner: corner,
+ FromX: from.X,
+ FromY: from.Y,
+ CtrlX: ctrl.X,
+ CtrlY: ctrl.Y,
+ ToX: to.X,
+ ToY: to.Y,
+ }
+ v.encode(data, meta)
+}
+
+func (qs *quadSplitter) encodeQuadTo(from, ctrl, to f32.Point) {
+ data := qs.d.writeVertCache(vertStride * 4)
+ encodeQuadTo(data, qs.contour, from, ctrl, to)
+}
+
+func (qs *quadSplitter) splitAndEncode(quad stroke.QuadSegment) {
+ cbnd := f32.Rectangle{
+ Min: quad.From,
+ Max: quad.To,
+ }.Canon()
+ from, ctrl, to := quad.From, quad.Ctrl, quad.To
+
+ // If the curve contain areas where a vertical line
+ // intersects it twice, split the curve in two x monotone
+ // lower and upper curves. The stencil fragment program
+ // expects only one intersection per curve.
+
+ // Find the t where the derivative in x is 0.
+ v0 := ctrl.Sub(from)
+ v1 := to.Sub(ctrl)
+ d := v0.X - v1.X
+ // t = v0 / d. Split if t is in ]0;1[.
+ if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X {
+ t := v0.X / d
+ ctrl0 := from.Mul(1 - t).Add(ctrl.Mul(t))
+ ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t))
+ mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t))
+ qs.encodeQuadTo(from, ctrl0, mid)
+ qs.encodeQuadTo(mid, ctrl1, to)
+ if mid.X > cbnd.Max.X {
+ cbnd.Max.X = mid.X
+ }
+ if mid.X < cbnd.Min.X {
+ cbnd.Min.X = mid.X
+ }
+ } else {
+ qs.encodeQuadTo(from, ctrl, to)
+ }
+ // Find the y extremum, if any.
+ d = v0.Y - v1.Y
+ if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y {
+ t := v0.Y / d
+ y := (1-t)*(1-t)*from.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y
+ if y > cbnd.Max.Y {
+ cbnd.Max.Y = y
+ }
+ if y < cbnd.Min.Y {
+ cbnd.Min.Y = y
+ }
+ }
+
+ qs.bounds = qs.bounds.Union(cbnd)
+}
diff --git a/gio/gpu/compute.go b/gio/gpu/compute.go
new file mode 100644
index 0000000..e7c7fd6
--- /dev/null
+++ b/gio/gpu/compute.go
@@ -0,0 +1,1093 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "image"
+ "image/color"
+ "math/bits"
+ "time"
+ "unsafe"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/internal/byteslice"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/internal/scene"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+)
+
+type compute struct {
+ ctx driver.Device
+ enc encoder
+
+ drawOps drawOps
+ texOps []textureOp
+ cache *resourceCache
+ maxTextureDim int
+
+ programs struct {
+ elements driver.Program
+ tileAlloc driver.Program
+ pathCoarse driver.Program
+ backdrop driver.Program
+ binning driver.Program
+ coarse driver.Program
+ kernel4 driver.Program
+ }
+ buffers struct {
+ config driver.Buffer
+ scene sizedBuffer
+ state sizedBuffer
+ memory sizedBuffer
+ }
+ output struct {
+ size image.Point
+ // image is the output texture. Note that it is in RGBA format,
+ // but contains data in sRGB. See blitOutput for more detail.
+ image driver.Texture
+ blitProg driver.Program
+ }
+ // images contains ImageOp images packed into a texture atlas.
+ images struct {
+ packer packer
+ // positions maps imageOpData.handles to positions inside tex.
+ positions map[interface{}]image.Point
+ tex driver.Texture
+ }
+ // materials contains the pre-processed materials (transformed images for
+ // now, gradients etc. later) packed in a texture atlas. The atlas is used
+ // as source in kernel4.
+ materials struct {
+ // offsets maps texture ops to the offsets to put in their FillImage commands.
+ offsets map[textureKey]image.Point
+
+ prog driver.Program
+ layout driver.InputLayout
+
+ packer packer
+
+ tex driver.Texture
+ fbo driver.Framebuffer
+ quads []materialVertex
+
+ bufSize int
+ buffer driver.Buffer
+ }
+ timers struct {
+ profile string
+ t *timers
+ elements *timer
+ tileAlloc *timer
+ pathCoarse *timer
+ backdropBinning *timer
+ coarse *timer
+ kernel4 *timer
+ }
+
+ // The following fields hold scratch space to avoid garbage.
+ zeroSlice []byte
+ memHeader *memoryHeader
+ conf *config
+}
+
+// materialVertex describes a vertex of a quad used to render a transformed
+// material.
+type materialVertex struct {
+ posX, posY float32
+ u, v float32
+}
+
+// textureKey identifies textureOp.
+type textureKey struct {
+ handle interface{}
+ transform f32.Affine2D
+}
+
+// textureOp represents an imageOp that requires texture space.
+type textureOp struct {
+ // sceneIdx is the index in the scene that contains the fill image command
+ // that corresponds to the operation.
+ sceneIdx int
+ key textureKey
+ img imageOpData
+
+ // pos is the position of the untransformed image in the images texture.
+ pos image.Point
+}
+
+type encoder struct {
+ scene []scene.Command
+ npath int
+ npathseg int
+ ntrans int
+}
+
+type encodeState struct {
+ trans f32.Affine2D
+ clip f32.Rectangle
+}
+
+type sizedBuffer struct {
+ size int
+ buffer driver.Buffer
+}
+
+// config matches Config in setup.h
+type config struct {
+ n_elements uint32 // paths
+ n_pathseg uint32
+ width_in_tiles uint32
+ height_in_tiles uint32
+ tile_alloc memAlloc
+ bin_alloc memAlloc
+ ptcl_alloc memAlloc
+ pathseg_alloc memAlloc
+ anno_alloc memAlloc
+ trans_alloc memAlloc
+}
+
+// memAlloc matches Alloc in mem.h
+type memAlloc struct {
+ offset uint32
+ // size uint32
+}
+
+// memoryHeader matches the header of Memory in mem.h.
+type memoryHeader struct {
+ mem_offset uint32
+ mem_error uint32
+}
+
+// GPU structure sizes and constants.
+const (
+ tileWidthPx = 32
+ tileHeightPx = 32
+ ptclInitialAlloc = 1024
+ kernel4OutputUnit = 2
+ kernel4AtlasUnit = 3
+
+ pathSize = 12
+ binSize = 8
+ pathsegSize = 52
+ annoSize = 32
+ transSize = 24
+ stateSize = 60
+ stateStride = 4 + 2*stateSize
+)
+
+// mem.h constants.
+const (
+ memNoError = 0 // NO_ERROR
+ memMallocFailed = 1 // ERR_MALLOC_FAILED
+)
+
+func newCompute(ctx driver.Device) (*compute, error) {
+ maxDim := ctx.Caps().MaxTextureSize
+ // Large atlas textures cause artifacts due to precision loss in
+ // shaders.
+ if cap := 8192; maxDim > cap {
+ maxDim = cap
+ }
+ g := &compute{
+ ctx: ctx,
+ cache: newResourceCache(),
+ maxTextureDim: maxDim,
+ conf: new(config),
+ memHeader: new(memoryHeader),
+ }
+
+ blitProg, err := ctx.NewProgram(shader_copy_vert, shader_copy_frag)
+ if err != nil {
+ g.Release()
+ return nil, err
+ }
+ g.output.blitProg = blitProg
+
+ materialProg, err := ctx.NewProgram(shader_material_vert,
+ shader_material_frag)
+ if err != nil {
+ g.Release()
+ return nil, err
+ }
+ g.materials.prog = materialProg
+ progLayout, err := ctx.NewInputLayout(shader_material_vert,
+ []driver.InputDesc{
+ {Type: driver.DataTypeFloat, Size: 2, Offset: 0},
+ {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2},
+ })
+ if err != nil {
+ g.Release()
+ return nil, err
+ }
+ g.materials.layout = progLayout
+
+ g.drawOps.pathCache = newOpCache()
+ g.drawOps.compute = true
+
+ buf, err := ctx.NewBuffer(driver.BufferBindingShaderStorage,
+ int(unsafe.Sizeof(config{})))
+ if err != nil {
+ g.Release()
+ return nil, err
+ }
+ g.buffers.config = buf
+
+ shaders := []struct {
+ prog *driver.Program
+ src driver.ShaderSources
+ }{
+ {&g.programs.elements, shader_elements_comp},
+ {&g.programs.tileAlloc, shader_tile_alloc_comp},
+ {&g.programs.pathCoarse, shader_path_coarse_comp},
+ {&g.programs.backdrop, shader_backdrop_comp},
+ {&g.programs.binning, shader_binning_comp},
+ {&g.programs.coarse, shader_coarse_comp},
+ {&g.programs.kernel4, shader_kernel4_comp},
+ }
+ for _, shader := range shaders {
+ p, err := ctx.NewComputeProgram(shader.src)
+ if err != nil {
+ g.Release()
+ return nil, err
+ }
+ *shader.prog = p
+ }
+ return g, nil
+}
+
+func (g *compute) Collect(viewport image.Point, ops *op.Ops) {
+ g.drawOps.reset(g.cache, viewport)
+ g.drawOps.collect(g.ctx, g.cache, ops, viewport)
+ for _, img := range g.drawOps.allImageOps {
+ expandPathOp(img.path, img.clip)
+ }
+ if g.drawOps.profile && g.timers.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
+ t := &g.timers
+ t.t = newTimers(g.ctx)
+ t.elements = g.timers.t.newTimer()
+ t.tileAlloc = g.timers.t.newTimer()
+ t.pathCoarse = g.timers.t.newTimer()
+ t.backdropBinning = g.timers.t.newTimer()
+ t.coarse = g.timers.t.newTimer()
+ t.kernel4 = g.timers.t.newTimer()
+ }
+}
+
+func (g *compute) Clear(col color.NRGBA) {
+ g.drawOps.clear = true
+ g.drawOps.clearColor = f32color.LinearFromSRGB(col)
+}
+
+func (g *compute) Frame() error {
+ viewport := g.drawOps.viewport
+ tileDims := image.Point{
+ X: (viewport.X + tileWidthPx - 1) / tileWidthPx,
+ Y: (viewport.Y + tileHeightPx - 1) / tileHeightPx,
+ }
+
+ defFBO := g.ctx.BeginFrame()
+ defer g.ctx.EndFrame()
+
+ if err := g.encode(viewport); err != nil {
+ return err
+ }
+ if err := g.uploadImages(); err != nil {
+ return err
+ }
+ if err := g.renderMaterials(); err != nil {
+ return err
+ }
+ if err := g.render(tileDims); err != nil {
+ return err
+ }
+ g.ctx.BindFramebuffer(defFBO)
+ g.blitOutput(viewport)
+ g.cache.frame()
+ g.drawOps.pathCache.frame()
+ t := &g.timers
+ if g.drawOps.profile && t.t.ready() {
+ et, tat, pct, bbt := t.elements.Elapsed, t.tileAlloc.Elapsed, t.pathCoarse.Elapsed, t.backdropBinning.Elapsed
+ ct, k4t := t.coarse.Elapsed, t.kernel4.Elapsed
+ ft := et + tat + pct + bbt + ct + k4t
+ q := 100 * time.Microsecond
+ ft = ft.Round(q)
+ et, tat, pct, bbt = et.Round(q), tat.Round(q), pct.Round(q), bbt.Round(q)
+ ct, k4t = ct.Round(q), k4t.Round(q)
+ t.profile = fmt.Sprintf("ft:%7s et:%7s tat:%7s pct:%7s bbt:%7s ct:%7s k4t:%7s",
+ ft, et, tat, pct, bbt, ct, k4t)
+ }
+ g.drawOps.clear = false
+ return nil
+}
+
+func (g *compute) Profile() string {
+ return g.timers.profile
+}
+
+// blitOutput copies the compute render output to the output FBO. We need to
+// copy because compute shaders can only write to textures, not FBOs. Compute
+// shader can only write to RGBA textures, but since we actually render in sRGB
+// format we can't use glBlitFramebuffer, because it does sRGB conversion.
+func (g *compute) blitOutput(viewport image.Point) {
+ if !g.drawOps.clear {
+ g.ctx.BlendFunc(driver.BlendFactorOne,
+ driver.BlendFactorOneMinusSrcAlpha)
+ g.ctx.SetBlend(true)
+ defer g.ctx.SetBlend(false)
+ }
+ g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
+ g.ctx.BindTexture(0, g.output.image)
+ g.ctx.BindProgram(g.output.blitProg)
+ g.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4)
+}
+
+func (g *compute) encode(viewport image.Point) error {
+ g.texOps = g.texOps[:0]
+ g.enc.reset()
+
+ // Flip Y-axis.
+ flipY := f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(1, -1)).Offset(f32.Pt(0,
+ float32(viewport.Y)))
+ g.enc.transform(flipY)
+ if g.drawOps.clear {
+ g.enc.rect(f32.Rectangle{Max: layout.FPt(viewport)})
+ g.enc.fillColor(f32color.NRGBAToRGBA(g.drawOps.clearColor.SRGB()))
+ }
+ return g.encodeOps(flipY, viewport, g.drawOps.allImageOps)
+}
+
+func (g *compute) renderMaterials() error {
+ m := &g.materials
+ m.quads = m.quads[:0]
+ resize := false
+ reclaimed := false
+restart:
+ for {
+ for _, op := range g.texOps {
+ if off, exists := m.offsets[op.key]; exists {
+ g.enc.setFillImageOffset(op.sceneIdx, off)
+ continue
+ }
+ quad, bounds := g.materialQuad(op.key.transform, op.img, op.pos)
+
+ // A material is clipped to avoid drawing outside its bounds inside the atlas. However,
+ // imprecision in the clipping may cause a single pixel overflow. Be safe.
+ size := bounds.Size().Add(image.Pt(1, 1))
+ place, fits := m.packer.tryAdd(size)
+ if !fits {
+ m.offsets = nil
+ m.quads = m.quads[:0]
+ m.packer.clear()
+ if !reclaimed {
+ // Some images may no longer be in use, try again
+ // after clearing existing maps.
+ reclaimed = true
+ } else {
+ m.packer.maxDim += 256
+ resize = true
+ if m.packer.maxDim > g.maxTextureDim {
+ return errors.New("compute: no space left in material atlas")
+ }
+ }
+ m.packer.newPage()
+ continue restart
+ }
+ // Position quad to match place.
+ offset := place.Pos.Sub(bounds.Min)
+ offsetf := layout.FPt(offset)
+ for i := range quad {
+ quad[i].posX += offsetf.X
+ quad[i].posY += offsetf.Y
+ }
+ // Draw quad as two triangles.
+ m.quads = append(m.quads, quad[0], quad[1], quad[3], quad[3],
+ quad[1], quad[2])
+ if m.offsets == nil {
+ m.offsets = make(map[textureKey]image.Point)
+ }
+ m.offsets[op.key] = offset
+ g.enc.setFillImageOffset(op.sceneIdx, offset)
+ }
+ break
+ }
+ if len(m.quads) == 0 {
+ return nil
+ }
+ texSize := m.packer.maxDim
+ if resize {
+ if m.fbo != nil {
+ m.fbo.Release()
+ m.fbo = nil
+ }
+ if m.tex != nil {
+ m.tex.Release()
+ m.tex = nil
+ }
+ handle, err := g.ctx.NewTexture(driver.TextureFormatRGBA8, texSize,
+ texSize,
+ driver.FilterNearest, driver.FilterNearest,
+ driver.BufferBindingShaderStorage|driver.BufferBindingFramebuffer)
+ if err != nil {
+ return fmt.Errorf("compute: failed to create material atlas: %v",
+ err)
+ }
+ m.tex = handle
+ fbo, err := g.ctx.NewFramebuffer(handle, 0)
+ if err != nil {
+ return fmt.Errorf("compute: failed to create material framebuffer: %v",
+ err)
+ }
+ m.fbo = fbo
+ }
+ // TODO: move to shaders.
+ // Transform to clip space: [-1, -1] - [1, 1].
+ clip := f32.Affine2D{}.Scale(f32.Pt(0, 0),
+ f32.Pt(2/float32(texSize), 2/float32(texSize))).Offset(f32.Pt(-1, -1))
+ for i, v := range m.quads {
+ p := clip.Transform(f32.Pt(v.posX, v.posY))
+ m.quads[i].posX = p.X
+ m.quads[i].posY = p.Y
+ }
+ vertexData := byteslice.Slice(m.quads)
+ if len(vertexData) > m.bufSize {
+ if m.buffer != nil {
+ m.buffer.Release()
+ m.buffer = nil
+ }
+ n := pow2Ceil(len(vertexData))
+ buf, err := g.ctx.NewBuffer(driver.BufferBindingVertices, n)
+ if err != nil {
+ return err
+ }
+ m.bufSize = n
+ m.buffer = buf
+ }
+ m.buffer.Upload(vertexData)
+ g.ctx.BindTexture(0, g.images.tex)
+ g.ctx.BindFramebuffer(m.fbo)
+ g.ctx.Viewport(0, 0, texSize, texSize)
+ if reclaimed {
+ g.ctx.Clear(0, 0, 0, 0)
+ }
+ g.ctx.BindProgram(m.prog)
+ g.ctx.BindVertexBuffer(m.buffer, int(unsafe.Sizeof(m.quads[0])), 0)
+ g.ctx.BindInputLayout(m.layout)
+ g.ctx.DrawArrays(driver.DrawModeTriangles, 0, len(m.quads))
+ return nil
+}
+
+func (g *compute) uploadImages() error {
+ // padding is the number of pixels added to the right and below
+ // images, to avoid atlas filtering artifacts.
+ const padding = 1
+
+ a := &g.images
+ var uploads map[interface{}]*image.RGBA
+ resize := false
+ reclaimed := false
+restart:
+ for {
+ for i, op := range g.texOps {
+ if pos, exists := a.positions[op.img.handle]; exists {
+ g.texOps[i].pos = pos
+ continue
+ }
+ size := op.img.src.Bounds().Size().Add(image.Pt(padding, padding))
+ place, fits := a.packer.tryAdd(size)
+ if !fits {
+ a.positions = nil
+ uploads = nil
+ a.packer.clear()
+ if !reclaimed {
+ // Some images may no longer be in use, try again
+ // after clearing existing maps.
+ reclaimed = true
+ } else {
+ a.packer.maxDim += 256
+ resize = true
+ if a.packer.maxDim > g.maxTextureDim {
+ return errors.New("compute: no space left in image atlas")
+ }
+ }
+ a.packer.newPage()
+ continue restart
+ }
+ if a.positions == nil {
+ a.positions = make(map[interface{}]image.Point)
+ }
+ a.positions[op.img.handle] = place.Pos
+ g.texOps[i].pos = place.Pos
+ if uploads == nil {
+ uploads = make(map[interface{}]*image.RGBA)
+ }
+ uploads[op.img.handle] = op.img.src
+ }
+ break
+ }
+ if len(uploads) == 0 {
+ return nil
+ }
+ if resize {
+ if a.tex != nil {
+ a.tex.Release()
+ a.tex = nil
+ }
+ sz := a.packer.maxDim
+ handle, err := g.ctx.NewTexture(driver.TextureFormatSRGB, sz, sz,
+ driver.FilterLinear, driver.FilterLinear,
+ driver.BufferBindingTexture)
+ if err != nil {
+ return fmt.Errorf("compute: failed to create image atlas: %v", err)
+ }
+ a.tex = handle
+ }
+ for h, img := range uploads {
+ pos, ok := a.positions[h]
+ if !ok {
+ panic("compute: internal error: image not placed")
+ }
+ size := img.Bounds().Size()
+ driver.UploadImage(a.tex, pos, img)
+ rightPadding := image.Pt(padding, size.Y)
+ a.tex.Upload(image.Pt(pos.X+size.X, pos.Y), rightPadding,
+ g.zeros(rightPadding.X*rightPadding.Y*4))
+ bottomPadding := image.Pt(size.X, padding)
+ a.tex.Upload(image.Pt(pos.X, pos.Y+size.Y), bottomPadding,
+ g.zeros(bottomPadding.X*bottomPadding.Y*4))
+ }
+ return nil
+}
+
+func pow2Ceil(v int) int {
+ exp := bits.Len(uint(v))
+ if bits.OnesCount(uint(v)) == 1 {
+ exp--
+ }
+ return 1 << exp
+}
+
+// materialQuad constructs a quad that represents the transformed image. It returns the quad
+// and its bounds.
+func (g *compute) materialQuad(M f32.Affine2D, img imageOpData,
+ uvPos image.Point) ([4]materialVertex, image.Rectangle) {
+ imgSize := layout.FPt(img.src.Bounds().Size())
+ sx, hx, ox, hy, sy, oy := M.Elems()
+ transOff := f32.Pt(ox, oy)
+ // The 4 corners of the image rectangle transformed by M, excluding its offset, are:
+ //
+ // q0: M * (0, 0) q3: M * (w, 0)
+ // q1: M * (0, h) q2: M * (w, h)
+ //
+ // Note that q0 = M*0 = 0, q2 = q1 + q3.
+ q0 := f32.Pt(0, 0)
+ q1 := f32.Pt(hx*imgSize.Y, sy*imgSize.Y)
+ q3 := f32.Pt(sx*imgSize.X, hy*imgSize.X)
+ q2 := q1.Add(q3)
+ q0 = q0.Add(transOff)
+ q1 = q1.Add(transOff)
+ q2 = q2.Add(transOff)
+ q3 = q3.Add(transOff)
+
+ boundsf := f32.Rectangle{
+ Min: min(min(q0, q1), min(q2, q3)),
+ Max: max(max(q0, q1), max(q2, q3)),
+ }
+
+ bounds := boundRectF(boundsf)
+ uvPosf := layout.FPt(uvPos)
+ atlasScale := 1 / float32(g.images.packer.maxDim)
+ uvBounds := f32.Rectangle{
+ Min: uvPosf.Mul(atlasScale),
+ Max: uvPosf.Add(imgSize).Mul(atlasScale),
+ }
+ quad := [4]materialVertex{
+ {posX: q0.X, posY: q0.Y, u: uvBounds.Min.X, v: uvBounds.Min.Y},
+ {posX: q1.X, posY: q1.Y, u: uvBounds.Min.X, v: uvBounds.Max.Y},
+ {posX: q2.X, posY: q2.Y, u: uvBounds.Max.X, v: uvBounds.Max.Y},
+ {posX: q3.X, posY: q3.Y, u: uvBounds.Max.X, v: uvBounds.Min.Y},
+ }
+ return quad, bounds
+}
+
+func max(p1, p2 f32.Point) f32.Point {
+ p := p1
+ if p2.X > p.X {
+ p.X = p2.X
+ }
+ if p2.Y > p.Y {
+ p.Y = p2.Y
+ }
+ return p
+}
+
+func min(p1, p2 f32.Point) f32.Point {
+ p := p1
+ if p2.X < p.X {
+ p.X = p2.X
+ }
+ if p2.Y < p.Y {
+ p.Y = p2.Y
+ }
+ return p
+}
+
+func (g *compute) encodeOps(trans f32.Affine2D, viewport image.Point,
+ ops []imageOp) error {
+ for _, op := range ops {
+ bounds := layout.FRect(op.clip)
+ // clip is the union of all drawing affected by the clipping
+ // operation. TODO: tighten.
+ clip := f32.Rect(0, 0, float32(viewport.X), float32(viewport.Y))
+ nclips := g.encodeClipStack(clip, bounds, op.path, false)
+ m := op.material
+ switch m.material {
+ case materialTexture:
+ t := trans.Mul(m.trans)
+ g.texOps = append(g.texOps, textureOp{
+ sceneIdx: len(g.enc.scene),
+ img: m.data,
+ key: textureKey{
+ transform: t,
+ handle: m.data.handle,
+ },
+ })
+ // Add fill command, its offset is resolved and filled in renderMaterials.
+ g.enc.fillImage(0)
+ case materialColor:
+ g.enc.fillColor(f32color.NRGBAToRGBA(op.material.color.SRGB()))
+ case materialLinearGradient:
+ // TODO: implement.
+ g.enc.fillColor(f32color.NRGBAToRGBA(op.material.color1.SRGB()))
+ default:
+ panic("not implemented")
+ }
+ if op.path != nil && op.path.path {
+ g.enc.fillMode(scene.FillModeNonzero)
+ g.enc.transform(op.path.trans.Invert())
+ }
+ // Pop the clip stack.
+ for i := 0; i < nclips; i++ {
+ g.enc.endClip(clip)
+ }
+ }
+ return nil
+}
+
+// encodeClips encodes a stack of clip paths and return the stack depth.
+func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp,
+ begin bool) int {
+ nclips := 0
+ if p != nil && p.parent != nil {
+ nclips += g.encodeClipStack(clip, bounds, p.parent, true)
+ nclips += 1
+ }
+ isStroke := p.stroke.Width > 0
+ if p != nil && p.path {
+ if isStroke {
+ g.enc.fillMode(scene.FillModeStroke)
+ g.enc.lineWidth(p.stroke.Width)
+ }
+ pathData, _ := g.drawOps.pathCache.get(p.pathKey)
+ g.enc.transform(p.trans)
+ g.enc.append(pathData.computePath)
+ } else {
+ g.enc.rect(bounds)
+ }
+ if begin {
+ g.enc.beginClip(clip)
+ if isStroke {
+ g.enc.fillMode(scene.FillModeNonzero)
+ }
+ if p != nil && p.path {
+ g.enc.transform(p.trans.Invert())
+ }
+ }
+ return nclips
+}
+
+func encodePath(verts []byte) encoder {
+ var enc encoder
+ for len(verts) >= scene.CommandSize+4 {
+ cmd := ops.DecodeCommand(verts[4:])
+ enc.scene = append(enc.scene, cmd)
+ enc.npathseg++
+ verts = verts[scene.CommandSize+4:]
+ }
+ return enc
+}
+
+func (g *compute) render(tileDims image.Point) error {
+ const (
+ // wgSize is the largest and most common workgroup size.
+ wgSize = 128
+ // PARTITION_SIZE from elements.comp
+ partitionSize = 32 * 4
+ )
+ widthInBins := (tileDims.X + 15) / 16
+ heightInBins := (tileDims.Y + 7) / 8
+ if widthInBins*heightInBins > wgSize {
+ return fmt.Errorf("gpu: output too large (%dx%d)",
+ tileDims.X*tileWidthPx, tileDims.Y*tileHeightPx)
+ }
+
+ // Pad scene with zeroes to avoid reading garbage in elements.comp.
+ scenePadding := partitionSize - len(g.enc.scene)%partitionSize
+ g.enc.scene = append(g.enc.scene, make([]scene.Command, scenePadding)...)
+
+ realloced := false
+ scene := byteslice.Slice(g.enc.scene)
+ if s := len(scene); s > g.buffers.scene.size {
+ realloced = true
+ paddedCap := s * 11 / 10
+ if err := g.buffers.scene.ensureCapacity(g.ctx, paddedCap); err != nil {
+ return err
+ }
+ }
+ g.buffers.scene.buffer.Upload(scene)
+
+ w, h := tileDims.X*tileWidthPx, tileDims.Y*tileHeightPx
+ if g.output.size.X != w || g.output.size.Y != h {
+ if err := g.resizeOutput(image.Pt(w, h)); err != nil {
+ return err
+ }
+ }
+ g.ctx.BindImageTexture(kernel4OutputUnit, g.output.image,
+ driver.AccessWrite, driver.TextureFormatRGBA8)
+ if t := g.materials.tex; t != nil {
+ g.ctx.BindImageTexture(kernel4AtlasUnit, t, driver.AccessRead,
+ driver.TextureFormatRGBA8)
+ }
+
+ // alloc is the number of allocated bytes for static buffers.
+ var alloc uint32
+ round := func(v, quantum int) int {
+ return (v + quantum - 1) &^ (quantum - 1)
+ }
+ malloc := func(size int) memAlloc {
+ size = round(size, 4)
+ offset := alloc
+ alloc += uint32(size)
+ return memAlloc{offset /*, uint32(size)*/}
+ }
+
+ *g.conf = config{
+ n_elements: uint32(g.enc.npath),
+ n_pathseg: uint32(g.enc.npathseg),
+ width_in_tiles: uint32(tileDims.X),
+ height_in_tiles: uint32(tileDims.Y),
+ tile_alloc: malloc(g.enc.npath * pathSize),
+ bin_alloc: malloc(round(g.enc.npath, wgSize) * binSize),
+ ptcl_alloc: malloc(tileDims.X * tileDims.Y * ptclInitialAlloc),
+ pathseg_alloc: malloc(g.enc.npathseg * pathsegSize),
+ anno_alloc: malloc(g.enc.npath * annoSize),
+ trans_alloc: malloc(g.enc.ntrans * transSize),
+ }
+
+ numPartitions := (g.enc.numElements() + 127) / 128
+ // clearSize is the atomic partition counter plus flag and 2 states per partition.
+ clearSize := 4 + numPartitions*stateStride
+ if clearSize > g.buffers.state.size {
+ realloced = true
+ paddedCap := clearSize * 11 / 10
+ if err := g.buffers.state.ensureCapacity(g.ctx, paddedCap); err != nil {
+ return err
+ }
+ }
+
+ g.buffers.config.Upload(byteslice.Struct(g.conf))
+
+ minSize := int(unsafe.Sizeof(memoryHeader{})) + int(alloc)
+ if minSize > g.buffers.memory.size {
+ realloced = true
+ // Add space for dynamic GPU allocations.
+ const sizeBump = 4 * 1024 * 1024
+ minSize += sizeBump
+ if err := g.buffers.memory.ensureCapacity(g.ctx, minSize); err != nil {
+ return err
+ }
+ }
+ for {
+ *g.memHeader = memoryHeader{
+ mem_offset: alloc,
+ }
+ g.buffers.memory.buffer.Upload(byteslice.Struct(g.memHeader))
+ g.buffers.state.buffer.Upload(g.zeros(clearSize))
+
+ if realloced {
+ realloced = false
+ g.bindBuffers()
+ }
+ t := &g.timers
+ g.ctx.MemoryBarrier()
+ t.elements.begin()
+ g.ctx.BindProgram(g.programs.elements)
+ g.ctx.DispatchCompute(numPartitions, 1, 1)
+ g.ctx.MemoryBarrier()
+ t.elements.end()
+ t.tileAlloc.begin()
+ g.ctx.BindProgram(g.programs.tileAlloc)
+ g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1)
+ g.ctx.MemoryBarrier()
+ t.tileAlloc.end()
+ t.pathCoarse.begin()
+ g.ctx.BindProgram(g.programs.pathCoarse)
+ g.ctx.DispatchCompute((g.enc.npathseg+31)/32, 1, 1)
+ g.ctx.MemoryBarrier()
+ t.pathCoarse.end()
+ t.backdropBinning.begin()
+ g.ctx.BindProgram(g.programs.backdrop)
+ g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1)
+ // No barrier needed between backdrop and binning.
+ g.ctx.BindProgram(g.programs.binning)
+ g.ctx.DispatchCompute((g.enc.npath+wgSize-1)/wgSize, 1, 1)
+ g.ctx.MemoryBarrier()
+ t.backdropBinning.end()
+ t.coarse.begin()
+ g.ctx.BindProgram(g.programs.coarse)
+ g.ctx.DispatchCompute(widthInBins, heightInBins, 1)
+ g.ctx.MemoryBarrier()
+ t.coarse.end()
+ t.kernel4.begin()
+ g.ctx.BindProgram(g.programs.kernel4)
+ g.ctx.DispatchCompute(tileDims.X, tileDims.Y, 1)
+ g.ctx.MemoryBarrier()
+ t.kernel4.end()
+
+ if err := g.buffers.memory.buffer.Download(byteslice.Struct(g.memHeader)); err != nil {
+ if err == driver.ErrContentLost {
+ continue
+ }
+ return err
+ }
+ switch errCode := g.memHeader.mem_error; errCode {
+ case memNoError:
+ return nil
+ case memMallocFailed:
+ // Resize memory and try again.
+ realloced = true
+ sz := g.buffers.memory.size * 15 / 10
+ if err := g.buffers.memory.ensureCapacity(g.ctx, sz); err != nil {
+ return err
+ }
+ continue
+ default:
+ return fmt.Errorf("compute: shader program failed with error %d",
+ errCode)
+ }
+ }
+}
+
+// zeros returns a byte slice with size bytes of zeros.
+func (g *compute) zeros(size int) []byte {
+ if cap(g.zeroSlice) < size {
+ g.zeroSlice = append(g.zeroSlice, make([]byte, size)...)
+ }
+ return g.zeroSlice[:size]
+}
+
+func (g *compute) resizeOutput(size image.Point) error {
+ if g.output.image != nil {
+ g.output.image.Release()
+ g.output.image = nil
+ }
+ img, err := g.ctx.NewTexture(driver.TextureFormatRGBA8, size.X, size.Y,
+ driver.FilterNearest,
+ driver.FilterNearest,
+ driver.BufferBindingShaderStorage|driver.BufferBindingTexture)
+ if err != nil {
+ return err
+ }
+ g.output.image = img
+ g.output.size = size
+ return nil
+}
+
+func (g *compute) Release() {
+ if g.drawOps.pathCache != nil {
+ g.drawOps.pathCache.release()
+ }
+ if g.cache != nil {
+ g.cache.release()
+ }
+ progs := []driver.Program{
+ g.programs.elements,
+ g.programs.tileAlloc,
+ g.programs.pathCoarse,
+ g.programs.backdrop,
+ g.programs.binning,
+ g.programs.coarse,
+ g.programs.kernel4,
+ }
+ if p := g.output.blitProg; p != nil {
+ p.Release()
+ }
+ for _, p := range progs {
+ if p != nil {
+ p.Release()
+ }
+ }
+ g.buffers.scene.release()
+ g.buffers.state.release()
+ g.buffers.memory.release()
+ if b := g.buffers.config; b != nil {
+ b.Release()
+ }
+ if g.output.image != nil {
+ g.output.image.Release()
+ }
+ if g.images.tex != nil {
+ g.images.tex.Release()
+ }
+ if g.materials.layout != nil {
+ g.materials.layout.Release()
+ }
+ if g.materials.prog != nil {
+ g.materials.prog.Release()
+ }
+ if g.materials.fbo != nil {
+ g.materials.fbo.Release()
+ }
+ if g.materials.tex != nil {
+ g.materials.tex.Release()
+ }
+ if g.materials.buffer != nil {
+ g.materials.buffer.Release()
+ }
+ if g.timers.t != nil {
+ g.timers.t.release()
+ }
+
+ *g = compute{}
+}
+
+func (g *compute) bindBuffers() {
+ bindStorageBuffers(g.programs.elements, g.buffers.memory.buffer,
+ g.buffers.config, g.buffers.scene.buffer, g.buffers.state.buffer)
+ bindStorageBuffers(g.programs.tileAlloc, g.buffers.memory.buffer,
+ g.buffers.config)
+ bindStorageBuffers(g.programs.pathCoarse, g.buffers.memory.buffer,
+ g.buffers.config)
+ bindStorageBuffers(g.programs.backdrop, g.buffers.memory.buffer,
+ g.buffers.config)
+ bindStorageBuffers(g.programs.binning, g.buffers.memory.buffer,
+ g.buffers.config)
+ bindStorageBuffers(g.programs.coarse, g.buffers.memory.buffer,
+ g.buffers.config)
+ bindStorageBuffers(g.programs.kernel4, g.buffers.memory.buffer,
+ g.buffers.config)
+}
+
+func (b *sizedBuffer) release() {
+ if b.buffer == nil {
+ return
+ }
+ b.buffer.Release()
+ *b = sizedBuffer{}
+}
+
+func (b *sizedBuffer) ensureCapacity(ctx driver.Device, size int) error {
+ if b.size >= size {
+ return nil
+ }
+ if b.buffer != nil {
+ b.release()
+ }
+ buf, err := ctx.NewBuffer(driver.BufferBindingShaderStorage, size)
+ if err != nil {
+ return err
+ }
+ b.buffer = buf
+ b.size = size
+ return nil
+}
+
+func bindStorageBuffers(prog driver.Program, buffers ...driver.Buffer) {
+ for i, buf := range buffers {
+ prog.SetStorageBuffer(i, buf)
+ }
+}
+
+var bo = binary.LittleEndian
+
+func (e *encoder) reset() {
+ e.scene = e.scene[:0]
+ e.npath = 0
+ e.npathseg = 0
+ e.ntrans = 0
+}
+
+func (e *encoder) numElements() int {
+ return len(e.scene)
+}
+
+func (e *encoder) append(e2 encoder) {
+ e.scene = append(e.scene, e2.scene...)
+ e.npath += e2.npath
+ e.npathseg += e2.npathseg
+ e.ntrans += e2.ntrans
+}
+
+func (e *encoder) transform(m f32.Affine2D) {
+ e.scene = append(e.scene, scene.Transform(m))
+ e.ntrans++
+}
+
+func (e *encoder) lineWidth(width float32) {
+ e.scene = append(e.scene, scene.SetLineWidth(width))
+}
+
+func (e *encoder) fillMode(mode scene.FillMode) {
+ e.scene = append(e.scene, scene.SetFillMode(mode))
+}
+
+func (e *encoder) beginClip(bbox f32.Rectangle) {
+ e.scene = append(e.scene, scene.BeginClip(bbox))
+ e.npath++
+}
+
+func (e *encoder) endClip(bbox f32.Rectangle) {
+ e.scene = append(e.scene, scene.EndClip(bbox))
+ e.npath++
+}
+
+func (e *encoder) rect(r f32.Rectangle) {
+ // Rectangle corners, clock-wise.
+ c0, c1, c2, c3 := r.Min, f32.Pt(r.Min.X, r.Max.Y), r.Max, f32.Pt(r.Max.X,
+ r.Min.Y)
+ e.line(c0, c1)
+ e.line(c1, c2)
+ e.line(c2, c3)
+ e.line(c3, c0)
+}
+
+func (e *encoder) fillColor(col color.RGBA) {
+ e.scene = append(e.scene, scene.FillColor(col))
+ e.npath++
+}
+
+func (e *encoder) setFillImageOffset(index int, offset image.Point) {
+ x := int16(offset.X)
+ y := int16(offset.Y)
+ e.scene[index][2] = uint32(uint16(x)) | uint32(uint16(y))<<16
+}
+
+func (e *encoder) fillImage(index int) {
+ e.scene = append(e.scene, scene.FillImage(index))
+ e.npath++
+}
+
+func (e *encoder) line(start, end f32.Point) {
+ e.scene = append(e.scene, scene.Line(start, end))
+ e.npathseg++
+}
+
+func (e *encoder) quad(start, ctrl, end f32.Point) {
+ e.scene = append(e.scene, scene.Quad(start, ctrl, end))
+ e.npathseg++
+}
diff --git a/gio/gpu/gen.go b/gio/gpu/gen.go
new file mode 100644
index 0000000..238f002
--- /dev/null
+++ b/gio/gpu/gen.go
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+//go:generate go run ./internal/convertshaders -package gpu
diff --git a/gio/gpu/gpu.go b/gio/gpu/gpu.go
new file mode 100644
index 0000000..7ff12e5
--- /dev/null
+++ b/gio/gpu/gpu.go
@@ -0,0 +1,1505 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package gpu implements the rendering of Gio drawing operations. It
+is used by package app and package app/headless and is otherwise not
+useful except for integrating with external window implementations.
+*/
+package gpu
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "image"
+ "image/color"
+ "math"
+ "os"
+ "reflect"
+ "time"
+ "unsafe"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/internal/byteslice"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/internal/scene"
+ "realy.lol/gio/internal/stroke"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+
+ // Register backends.
+ _ "realy.lol/gio/gpu/internal/d3d11"
+ _ "realy.lol/gio/gpu/internal/opengl"
+)
+
+type GPU interface {
+ // Release non-Go resources. The GPU is no longer valid after Release.
+ Release()
+ // Clear sets the clear color for the next Frame.
+ Clear(color color.NRGBA)
+ // Collect the graphics operations from frame, given the viewport.
+ Collect(viewport image.Point, frame *op.Ops)
+ // Frame clears the color buffer and draws the collected operations.
+ Frame() error
+ // Profile returns the last available profiling information. Profiling
+ // information is requested when Collect sees a ProfileOp, and the result
+ // is available through Profile at some later time.
+ Profile() string
+}
+
+type gpu struct {
+ cache *resourceCache
+
+ profile string
+ timers *timers
+ frameStart time.Time
+ zopsTimer, stencilTimer, coverTimer, cleanupTimer *timer
+ drawOps drawOps
+ ctx driver.Device
+ renderer *renderer
+}
+
+type renderer struct {
+ ctx driver.Device
+ blitter *blitter
+ pather *pather
+ packer packer
+ intersections packer
+}
+
+type drawOps struct {
+ profile bool
+ reader ops.Reader
+ states []drawState
+ cache *resourceCache
+ vertCache []byte
+ viewport image.Point
+ clear bool
+ clearColor f32color.RGBA
+ // allImageOps is the combined list of imageOps and
+ // zimageOps, in drawing order.
+ allImageOps []imageOp
+ imageOps []imageOp
+ // zimageOps are the rectangle clipped opaque images
+ // that can use fast front-to-back rendering with z-test
+ // and no blending.
+ zimageOps []imageOp
+ pathOps []*pathOp
+ pathOpCache []pathOp
+ qs quadSplitter
+ pathCache *opCache
+ // hack for the compute renderer to access
+ // converted path data.
+ compute bool
+}
+
+type drawState struct {
+ clip f32.Rectangle
+ t f32.Affine2D
+ cpath *pathOp
+ rect bool
+
+ matType materialType
+ // Current paint.ImageOp
+ image imageOpData
+ // Current paint.ColorOp, if any.
+ color color.NRGBA
+
+ // Current paint.LinearGradientOp.
+ stop1 f32.Point
+ stop2 f32.Point
+ color1 color.NRGBA
+ color2 color.NRGBA
+}
+
+type pathOp struct {
+ off f32.Point
+ // clip is the union of all
+ // later clip rectangles.
+ clip image.Rectangle
+ bounds f32.Rectangle
+ pathKey ops.Key
+ path bool
+ pathVerts []byte
+ parent *pathOp
+ place placement
+
+ // For compute
+ trans f32.Affine2D
+ stroke clip.StrokeStyle
+}
+
+type imageOp struct {
+ z float32
+ path *pathOp
+ clip image.Rectangle
+ material material
+ clipType clipType
+ place placement
+}
+
+func decodeStrokeOp(data []byte) clip.StrokeStyle {
+ _ = data[4]
+ if opconst.OpType(data[0]) != opconst.TypeStroke {
+ panic("invalid op")
+ }
+ bo := binary.LittleEndian
+ return clip.StrokeStyle{
+ Width: math.Float32frombits(bo.Uint32(data[1:])),
+ }
+}
+
+type quadsOp struct {
+ key ops.Key
+ aux []byte
+}
+
+type material struct {
+ material materialType
+ opaque bool
+ // For materialTypeColor.
+ color f32color.RGBA
+ // For materialTypeLinearGradient.
+ color1 f32color.RGBA
+ color2 f32color.RGBA
+ // For materialTypeTexture.
+ data imageOpData
+ uvTrans f32.Affine2D
+
+ // For the compute backend.
+ trans f32.Affine2D
+}
+
+// clipOp is the shadow of clip.Op.
+type clipOp struct {
+ // TODO: Use image.Rectangle?
+ bounds f32.Rectangle
+ outline bool
+}
+
+// imageOpData is the shadow of paint.ImageOp.
+type imageOpData struct {
+ src *image.RGBA
+ handle interface{}
+}
+
+type linearGradientOpData struct {
+ stop1 f32.Point
+ color1 color.NRGBA
+ stop2 f32.Point
+ color2 color.NRGBA
+}
+
+func (op *clipOp) decode(data []byte) {
+ if opconst.OpType(data[0]) != opconst.TypeClip {
+ panic("invalid op")
+ }
+ bo := binary.LittleEndian
+ r := image.Rectangle{
+ Min: image.Point{
+ X: int(int32(bo.Uint32(data[1:]))),
+ Y: int(int32(bo.Uint32(data[5:]))),
+ },
+ Max: image.Point{
+ X: int(int32(bo.Uint32(data[9:]))),
+ Y: int(int32(bo.Uint32(data[13:]))),
+ },
+ }
+ *op = clipOp{
+ bounds: layout.FRect(r),
+ outline: data[17] == 1,
+ }
+}
+
+func decodeImageOp(data []byte, refs []interface{}) imageOpData {
+ if opconst.OpType(data[0]) != opconst.TypeImage {
+ panic("invalid op")
+ }
+ handle := refs[1]
+ if handle == nil {
+ return imageOpData{}
+ }
+ return imageOpData{
+ src: refs[0].(*image.RGBA),
+ handle: handle,
+ }
+}
+
+func decodeColorOp(data []byte) color.NRGBA {
+ if opconst.OpType(data[0]) != opconst.TypeColor {
+ panic("invalid op")
+ }
+ return color.NRGBA{
+ R: data[1],
+ G: data[2],
+ B: data[3],
+ A: data[4],
+ }
+}
+
+func decodeLinearGradientOp(data []byte) linearGradientOpData {
+ if opconst.OpType(data[0]) != opconst.TypeLinearGradient {
+ panic("invalid op")
+ }
+ bo := binary.LittleEndian
+ return linearGradientOpData{
+ stop1: f32.Point{
+ X: math.Float32frombits(bo.Uint32(data[1:])),
+ Y: math.Float32frombits(bo.Uint32(data[5:])),
+ },
+ stop2: f32.Point{
+ X: math.Float32frombits(bo.Uint32(data[9:])),
+ Y: math.Float32frombits(bo.Uint32(data[13:])),
+ },
+ color1: color.NRGBA{
+ R: data[17+0],
+ G: data[17+1],
+ B: data[17+2],
+ A: data[17+3],
+ },
+ color2: color.NRGBA{
+ R: data[21+0],
+ G: data[21+1],
+ B: data[21+2],
+ A: data[21+3],
+ },
+ }
+}
+
+type clipType uint8
+
+type resource interface {
+ release()
+}
+
+type texture struct {
+ src *image.RGBA
+ tex driver.Texture
+}
+
+type blitter struct {
+ ctx driver.Device
+ viewport image.Point
+ prog [3]*program
+ layout driver.InputLayout
+ colUniforms *blitColUniforms
+ texUniforms *blitTexUniforms
+ linearGradientUniforms *blitLinearGradientUniforms
+ quadVerts driver.Buffer
+}
+
+type blitColUniforms struct {
+ vert struct {
+ blitUniforms
+ _ [12]byte // Padding to a multiple of 16.
+ }
+ frag struct {
+ colorUniforms
+ }
+}
+
+type blitTexUniforms struct {
+ vert struct {
+ blitUniforms
+ _ [12]byte // Padding to a multiple of 16.
+ }
+}
+
+type blitLinearGradientUniforms struct {
+ vert struct {
+ blitUniforms
+ _ [12]byte // Padding to a multiple of 16.
+ }
+ frag struct {
+ gradientUniforms
+ }
+}
+
+type uniformBuffer struct {
+ buf driver.Buffer
+ ptr []byte
+}
+
+type program struct {
+ prog driver.Program
+ vertUniforms *uniformBuffer
+ fragUniforms *uniformBuffer
+}
+
+type blitUniforms struct {
+ transform [4]float32
+ uvTransformR1 [4]float32
+ uvTransformR2 [4]float32
+ z float32
+}
+
+type colorUniforms struct {
+ color f32color.RGBA
+}
+
+type gradientUniforms struct {
+ color1 f32color.RGBA
+ color2 f32color.RGBA
+}
+
+type materialType uint8
+
+const (
+ clipTypeNone clipType = iota
+ clipTypePath
+ clipTypeIntersection
+)
+
+const (
+ materialColor materialType = iota
+ materialLinearGradient
+ materialTexture
+)
+
+func New(api API) (GPU, error) {
+ d, err := driver.NewDevice(api)
+ if err != nil {
+ return nil, err
+ }
+ forceCompute := os.Getenv("GIORENDERER") == "forcecompute"
+ feats := d.Caps().Features
+ switch {
+ case !forceCompute && feats.Has(driver.FeatureFloatRenderTargets):
+ return newGPU(d)
+ case feats.Has(driver.FeatureCompute):
+ return newCompute(d)
+ default:
+ return nil, errors.New("gpu: no support for float render targets nor compute")
+ }
+}
+
+func newGPU(ctx driver.Device) (*gpu, error) {
+ g := &gpu{
+ cache: newResourceCache(),
+ }
+ g.drawOps.pathCache = newOpCache()
+ if err := g.init(ctx); err != nil {
+ return nil, err
+ }
+ return g, nil
+}
+
+func (g *gpu) init(ctx driver.Device) error {
+ g.ctx = ctx
+ g.renderer = newRenderer(ctx)
+ return nil
+}
+
+func (g *gpu) Clear(col color.NRGBA) {
+ g.drawOps.clear = true
+ g.drawOps.clearColor = f32color.LinearFromSRGB(col)
+}
+
+func (g *gpu) Release() {
+ g.renderer.release()
+ g.drawOps.pathCache.release()
+ g.cache.release()
+ if g.timers != nil {
+ g.timers.release()
+ }
+ g.ctx.Release()
+}
+
+func (g *gpu) Collect(viewport image.Point, frameOps *op.Ops) {
+ g.renderer.blitter.viewport = viewport
+ g.renderer.pather.viewport = viewport
+ g.drawOps.reset(g.cache, viewport)
+ g.drawOps.collect(g.ctx, g.cache, frameOps, viewport)
+ g.frameStart = time.Now()
+ if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
+ g.timers = newTimers(g.ctx)
+ g.zopsTimer = g.timers.newTimer()
+ g.stencilTimer = g.timers.newTimer()
+ g.coverTimer = g.timers.newTimer()
+ g.cleanupTimer = g.timers.newTimer()
+ }
+}
+
+func (g *gpu) Frame() error {
+ defFBO := g.ctx.BeginFrame()
+ defer g.ctx.EndFrame()
+ viewport := g.renderer.blitter.viewport
+ for _, img := range g.drawOps.imageOps {
+ expandPathOp(img.path, img.clip)
+ }
+ if g.drawOps.profile {
+ g.zopsTimer.begin()
+ }
+ g.ctx.BindFramebuffer(defFBO)
+ g.ctx.DepthFunc(driver.DepthFuncGreater)
+ // Note that Clear must be before ClearDepth if nothing else is rendered
+ // (len(zimageOps) == 0). If not, the Fairphone 2 will corrupt the depth buffer.
+ if g.drawOps.clear {
+ g.drawOps.clear = false
+ g.ctx.Clear(g.drawOps.clearColor.Float32())
+ }
+ g.ctx.ClearDepth(0.0)
+ g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
+ g.renderer.drawZOps(g.cache, g.drawOps.zimageOps)
+ g.zopsTimer.end()
+ g.stencilTimer.begin()
+ g.ctx.SetBlend(true)
+ g.renderer.packStencils(&g.drawOps.pathOps)
+ g.renderer.stencilClips(g.drawOps.pathCache, g.drawOps.pathOps)
+ g.renderer.packIntersections(g.drawOps.imageOps)
+ g.renderer.intersect(g.drawOps.imageOps)
+ g.stencilTimer.end()
+ g.coverTimer.begin()
+ g.ctx.BindFramebuffer(defFBO)
+ g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
+ g.renderer.drawOps(g.cache, g.drawOps.imageOps)
+ g.ctx.SetBlend(false)
+ g.renderer.pather.stenciler.invalidateFBO()
+ g.coverTimer.end()
+ g.ctx.BindFramebuffer(defFBO)
+ g.cleanupTimer.begin()
+ g.cache.frame()
+ g.drawOps.pathCache.frame()
+ g.cleanupTimer.end()
+ if g.drawOps.profile && g.timers.ready() {
+ zt, st, covt, cleant := g.zopsTimer.Elapsed, g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
+ ft := zt + st + covt + cleant
+ q := 100 * time.Microsecond
+ zt, st, covt = zt.Round(q), st.Round(q), covt.Round(q)
+ frameDur := time.Since(g.frameStart).Round(q)
+ ft = ft.Round(q)
+ g.profile = fmt.Sprintf("draw:%7s gpu:%7s zt:%7s st:%7s cov:%7s",
+ frameDur, ft, zt, st, covt)
+ }
+ return nil
+}
+
+func (g *gpu) Profile() string {
+ return g.profile
+}
+
+func (r *renderer) texHandle(cache *resourceCache,
+ data imageOpData) driver.Texture {
+ var tex *texture
+ t, exists := cache.get(data.handle)
+ if !exists {
+ t = &texture{
+ src: data.src,
+ }
+ cache.put(data.handle, t)
+ }
+ tex = t.(*texture)
+ if tex.tex != nil {
+ return tex.tex
+ }
+ handle, err := r.ctx.NewTexture(driver.TextureFormatSRGB,
+ data.src.Bounds().Dx(), data.src.Bounds().Dy(), driver.FilterLinear,
+ driver.FilterLinear, driver.BufferBindingTexture)
+ if err != nil {
+ panic(err)
+ }
+ driver.UploadImage(handle, image.Pt(0, 0), data.src)
+ tex.tex = handle
+ return tex.tex
+}
+
+func (t *texture) release() {
+ if t.tex != nil {
+ t.tex.Release()
+ }
+}
+
+func newRenderer(ctx driver.Device) *renderer {
+ r := &renderer{
+ ctx: ctx,
+ blitter: newBlitter(ctx),
+ pather: newPather(ctx),
+ }
+
+ maxDim := ctx.Caps().MaxTextureSize
+ // Large atlas textures cause artifacts due to precision loss in
+ // shaders.
+ if cap := 8192; maxDim > cap {
+ maxDim = cap
+ }
+
+ r.packer.maxDim = maxDim
+ r.intersections.maxDim = maxDim
+ return r
+}
+
+func (r *renderer) release() {
+ r.pather.release()
+ r.blitter.release()
+}
+
+func newBlitter(ctx driver.Device) *blitter {
+ quadVerts, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices,
+ byteslice.Slice([]float32{
+ -1, +1, 0, 0,
+ +1, +1, 1, 0,
+ -1, -1, 0, 1,
+ +1, -1, 1, 1,
+ }),
+ )
+ if err != nil {
+ panic(err)
+ }
+ b := &blitter{
+ ctx: ctx,
+ quadVerts: quadVerts,
+ }
+ b.colUniforms = new(blitColUniforms)
+ b.texUniforms = new(blitTexUniforms)
+ b.linearGradientUniforms = new(blitLinearGradientUniforms)
+ prog, layout, err := createColorPrograms(ctx, shader_blit_vert,
+ shader_blit_frag,
+ [3]interface{}{&b.colUniforms.vert, &b.linearGradientUniforms.vert,
+ &b.texUniforms.vert},
+ [3]interface{}{&b.colUniforms.frag, &b.linearGradientUniforms.frag,
+ nil},
+ )
+ if err != nil {
+ panic(err)
+ }
+ b.prog = prog
+ b.layout = layout
+ return b
+}
+
+func (b *blitter) release() {
+ b.quadVerts.Release()
+ for _, p := range b.prog {
+ p.Release()
+ }
+ b.layout.Release()
+}
+
+func createColorPrograms(b driver.Device, vsSrc driver.ShaderSources,
+ fsSrc [3]driver.ShaderSources,
+ vertUniforms, fragUniforms [3]interface{}) ([3]*program, driver.InputLayout,
+ error) {
+ var progs [3]*program
+ {
+ prog, err := b.NewProgram(vsSrc, fsSrc[materialTexture])
+ if err != nil {
+ return progs, nil, err
+ }
+ var vertBuffer, fragBuffer *uniformBuffer
+ if u := vertUniforms[materialTexture]; u != nil {
+ vertBuffer = newUniformBuffer(b, u)
+ prog.SetVertexUniforms(vertBuffer.buf)
+ }
+ if u := fragUniforms[materialTexture]; u != nil {
+ fragBuffer = newUniformBuffer(b, u)
+ prog.SetFragmentUniforms(fragBuffer.buf)
+ }
+ progs[materialTexture] = newProgram(prog, vertBuffer, fragBuffer)
+ }
+ {
+ var vertBuffer, fragBuffer *uniformBuffer
+ prog, err := b.NewProgram(vsSrc, fsSrc[materialColor])
+ if err != nil {
+ progs[materialTexture].Release()
+ return progs, nil, err
+ }
+ if u := vertUniforms[materialColor]; u != nil {
+ vertBuffer = newUniformBuffer(b, u)
+ prog.SetVertexUniforms(vertBuffer.buf)
+ }
+ if u := fragUniforms[materialColor]; u != nil {
+ fragBuffer = newUniformBuffer(b, u)
+ prog.SetFragmentUniforms(fragBuffer.buf)
+ }
+ progs[materialColor] = newProgram(prog, vertBuffer, fragBuffer)
+ }
+ {
+ var vertBuffer, fragBuffer *uniformBuffer
+ prog, err := b.NewProgram(vsSrc, fsSrc[materialLinearGradient])
+ if err != nil {
+ progs[materialTexture].Release()
+ progs[materialColor].Release()
+ return progs, nil, err
+ }
+ if u := vertUniforms[materialLinearGradient]; u != nil {
+ vertBuffer = newUniformBuffer(b, u)
+ prog.SetVertexUniforms(vertBuffer.buf)
+ }
+ if u := fragUniforms[materialLinearGradient]; u != nil {
+ fragBuffer = newUniformBuffer(b, u)
+ prog.SetFragmentUniforms(fragBuffer.buf)
+ }
+ progs[materialLinearGradient] = newProgram(prog, vertBuffer, fragBuffer)
+ }
+ layout, err := b.NewInputLayout(vsSrc, []driver.InputDesc{
+ {Type: driver.DataTypeFloat, Size: 2, Offset: 0},
+ {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2},
+ })
+ if err != nil {
+ progs[materialTexture].Release()
+ progs[materialColor].Release()
+ progs[materialLinearGradient].Release()
+ return progs, nil, err
+ }
+ return progs, layout, nil
+}
+
+func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) {
+ if len(r.packer.sizes) == 0 {
+ return
+ }
+ fbo := -1
+ r.pather.begin(r.packer.sizes)
+ for _, p := range ops {
+ if fbo != p.place.Idx {
+ fbo = p.place.Idx
+ f := r.pather.stenciler.cover(fbo)
+ r.ctx.BindFramebuffer(f.fbo)
+ r.ctx.Clear(0.0, 0.0, 0.0, 0.0)
+ }
+ v, _ := pathCache.get(p.pathKey)
+ r.pather.stencilPath(p.clip, p.off, p.place.Pos, v.data)
+ }
+}
+
+func (r *renderer) intersect(ops []imageOp) {
+ if len(r.intersections.sizes) == 0 {
+ return
+ }
+ fbo := -1
+ r.pather.stenciler.beginIntersect(r.intersections.sizes)
+ r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0)
+ r.ctx.BindInputLayout(r.pather.stenciler.iprog.layout)
+ for _, img := range ops {
+ if img.clipType != clipTypeIntersection {
+ continue
+ }
+ if fbo != img.place.Idx {
+ fbo = img.place.Idx
+ f := r.pather.stenciler.intersections.fbos[fbo]
+ r.ctx.BindFramebuffer(f.fbo)
+ r.ctx.Clear(1.0, 0.0, 0.0, 0.0)
+ }
+ r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(),
+ img.clip.Dy())
+ r.intersectPath(img.path, img.clip)
+ }
+}
+
+func (r *renderer) intersectPath(p *pathOp, clip image.Rectangle) {
+ if p.parent != nil {
+ r.intersectPath(p.parent, clip)
+ }
+ if !p.path {
+ return
+ }
+ uv := image.Rectangle{
+ Min: p.place.Pos,
+ Max: p.place.Pos.Add(p.clip.Size()),
+ }
+ o := clip.Min.Sub(p.clip.Min)
+ sub := image.Rectangle{
+ Min: o,
+ Max: o.Add(clip.Size()),
+ }
+ fbo := r.pather.stenciler.cover(p.place.Idx)
+ r.ctx.BindTexture(0, fbo.tex)
+ coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size)
+ subScale, subOff := texSpaceTransform(layout.FRect(sub), p.clip.Size())
+ r.pather.stenciler.iprog.uniforms.vert.uvTransform = [4]float32{coverScale.X,
+ coverScale.Y, coverOff.X, coverOff.Y}
+ r.pather.stenciler.iprog.uniforms.vert.subUVTransform = [4]float32{subScale.X,
+ subScale.Y, subOff.X, subOff.Y}
+ r.pather.stenciler.iprog.prog.UploadUniforms()
+ r.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4)
+}
+
+func (r *renderer) packIntersections(ops []imageOp) {
+ r.intersections.clear()
+ for i, img := range ops {
+ var npaths int
+ var onePath *pathOp
+ for p := img.path; p != nil; p = p.parent {
+ if p.path {
+ onePath = p
+ npaths++
+ }
+ }
+ switch npaths {
+ case 0:
+ case 1:
+ place := onePath.place
+ place.Pos = place.Pos.Sub(onePath.clip.Min).Add(img.clip.Min)
+ ops[i].place = place
+ ops[i].clipType = clipTypePath
+ default:
+ sz := image.Point{X: img.clip.Dx(), Y: img.clip.Dy()}
+ place, ok := r.intersections.add(sz)
+ if !ok {
+ panic("internal error: if the intersection fit, the intersection should fit as well")
+ }
+ ops[i].clipType = clipTypeIntersection
+ ops[i].place = place
+ }
+ }
+}
+
+func (r *renderer) packStencils(pops *[]*pathOp) {
+ r.packer.clear()
+ ops := *pops
+ // Allocate atlas space for cover textures.
+ var i int
+ for i < len(ops) {
+ p := ops[i]
+ if p.clip.Empty() {
+ ops[i] = ops[len(ops)-1]
+ ops = ops[:len(ops)-1]
+ continue
+ }
+ sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()}
+ place, ok := r.packer.add(sz)
+ if !ok {
+ // The clip area is at most the entire screen. Hopefully no
+ // screen is larger than GL_MAX_TEXTURE_SIZE.
+ panic(fmt.Errorf("clip area %v is larger than maximum texture size %dx%d",
+ p.clip, r.packer.maxDim, r.packer.maxDim))
+ }
+ p.place = place
+ i++
+ }
+ *pops = ops
+}
+
+// intersects intersects clip and b where b is offset by off.
+// ceilRect returns a bounding image.Rectangle for a f32.Rectangle.
+func boundRectF(r f32.Rectangle) image.Rectangle {
+ return image.Rectangle{
+ Min: image.Point{
+ X: int(floor(r.Min.X)),
+ Y: int(floor(r.Min.Y)),
+ },
+ Max: image.Point{
+ X: int(ceil(r.Max.X)),
+ Y: int(ceil(r.Max.Y)),
+ },
+ }
+}
+
+func ceil(v float32) int {
+ return int(math.Ceil(float64(v)))
+}
+
+func floor(v float32) int {
+ return int(math.Floor(float64(v)))
+}
+
+func (d *drawOps) reset(cache *resourceCache, viewport image.Point) {
+ d.profile = false
+ d.cache = cache
+ d.viewport = viewport
+ d.imageOps = d.imageOps[:0]
+ d.allImageOps = d.allImageOps[:0]
+ d.zimageOps = d.zimageOps[:0]
+ d.pathOps = d.pathOps[:0]
+ d.pathOpCache = d.pathOpCache[:0]
+ d.vertCache = d.vertCache[:0]
+}
+
+func (d *drawOps) collect(ctx driver.Device, cache *resourceCache, root *op.Ops,
+ viewport image.Point) {
+ clip := f32.Rectangle{
+ Max: f32.Point{X: float32(viewport.X), Y: float32(viewport.Y)},
+ }
+ d.reader.Reset(root)
+ state := drawState{
+ clip: clip,
+ rect: true,
+ color: color.NRGBA{A: 0xff},
+ }
+ d.collectOps(&d.reader, state)
+ for _, p := range d.pathOps {
+ if v, exists := d.pathCache.get(p.pathKey); !exists || v.data.data == nil {
+ data := buildPath(ctx, p.pathVerts)
+ var computePath encoder
+ if d.compute {
+ computePath = encodePath(p.pathVerts)
+ }
+ d.pathCache.put(p.pathKey, opCacheValue{
+ data: data,
+ bounds: p.bounds,
+ computePath: computePath,
+ })
+ }
+ p.pathVerts = nil
+ }
+}
+
+func (d *drawOps) newPathOp() *pathOp {
+ d.pathOpCache = append(d.pathOpCache, pathOp{})
+ return &d.pathOpCache[len(d.pathOpCache)-1]
+}
+
+func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key,
+ bounds f32.Rectangle, off f32.Point, tr f32.Affine2D,
+ stroke clip.StrokeStyle) {
+ npath := d.newPathOp()
+ *npath = pathOp{
+ parent: state.cpath,
+ bounds: bounds,
+ off: off,
+ trans: tr,
+ stroke: stroke,
+ }
+ state.cpath = npath
+ if len(aux) > 0 {
+ state.rect = false
+ state.cpath.pathKey = auxKey
+ state.cpath.path = true
+ state.cpath.pathVerts = aux
+ d.pathOps = append(d.pathOps, state.cpath)
+ }
+}
+
+// split a transform into two parts, one which is pur offset and the
+// other representing the scaling, shearing and rotation part
+func splitTransform(t f32.Affine2D) (srs f32.Affine2D, offset f32.Point) {
+ sx, hx, ox, hy, sy, oy := t.Elems()
+ offset = f32.Point{X: ox, Y: oy}
+ srs = f32.NewAffine2D(sx, hx, 0, hy, sy, 0)
+ return
+}
+
+func (d *drawOps) save(id int, state drawState) {
+ if extra := id - len(d.states) + 1; extra > 0 {
+ d.states = append(d.states, make([]drawState, extra)...)
+ }
+ d.states[id] = state
+}
+
+func (d *drawOps) collectOps(r *ops.Reader, state drawState) {
+ var (
+ quads quadsOp
+ str clip.StrokeStyle
+ z int
+ )
+ d.save(opconst.InitialStateID, state)
+loop:
+ for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
+ switch opconst.OpType(encOp.Data[0]) {
+ case opconst.TypeProfile:
+ d.profile = true
+ case opconst.TypeTransform:
+ dop := ops.DecodeTransform(encOp.Data)
+ state.t = state.t.Mul(dop)
+
+ case opconst.TypeStroke:
+ str = decodeStrokeOp(encOp.Data)
+
+ case opconst.TypePath:
+ encOp, ok = r.Decode()
+ if !ok {
+ break loop
+ }
+ quads.aux = encOp.Data[opconst.TypeAuxLen:]
+ quads.key = encOp.Key
+
+ case opconst.TypeClip:
+ var op clipOp
+ op.decode(encOp.Data)
+ bounds := op.bounds
+ trans, off := splitTransform(state.t)
+ if len(quads.aux) > 0 {
+ // There is a clipping path, build the gpu data and update the
+ // cache key such that it will be equal only if the transform is the
+ // same also. Use cached data if we have it.
+ quads.key = quads.key.SetTransform(trans)
+ if v, ok := d.pathCache.get(quads.key); ok {
+ // Since the GPU data exists in the cache aux will not be used.
+ // Why is this not used for the offset shapes?
+ op.bounds = v.bounds
+ } else {
+ pathData, bounds := d.buildVerts(
+ quads.aux, trans, op.outline, str,
+ )
+ op.bounds = bounds
+ if !d.compute {
+ quads.aux = pathData
+ }
+ // add it to the cache, without GPU data, so the transform can be
+ // reused.
+ d.pathCache.put(quads.key, opCacheValue{bounds: op.bounds})
+ }
+ } else {
+ quads.aux, op.bounds, _ = d.boundsForTransformedRect(bounds,
+ trans)
+ quads.key = encOp.Key
+ quads.key.SetTransform(trans)
+ }
+ state.clip = state.clip.Intersect(op.bounds.Add(off))
+ d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, state.t,
+ str)
+ quads = quadsOp{}
+ str = clip.StrokeStyle{}
+
+ case opconst.TypeColor:
+ state.matType = materialColor
+ state.color = decodeColorOp(encOp.Data)
+ case opconst.TypeLinearGradient:
+ state.matType = materialLinearGradient
+ op := decodeLinearGradientOp(encOp.Data)
+ state.stop1 = op.stop1
+ state.stop2 = op.stop2
+ state.color1 = op.color1
+ state.color2 = op.color2
+ case opconst.TypeImage:
+ state.matType = materialTexture
+ state.image = decodeImageOp(encOp.Data, encOp.Refs)
+ case opconst.TypePaint:
+ // Transform (if needed) the painting rectangle and if so generate a clip path,
+ // for those cases also compute a partialTrans that maps texture coordinates between
+ // the new bounding rectangle and the transformed original paint rectangle.
+ trans, off := splitTransform(state.t)
+ // Fill the clip area, unless the material is a (bounded) image.
+ // TODO: Find a tighter bound.
+ inf := float32(1e6)
+ dst := f32.Rect(-inf, -inf, inf, inf)
+ if state.matType == materialTexture {
+ dst = layout.FRect(state.image.src.Rect)
+ }
+ clipData, bnd, partialTrans := d.boundsForTransformedRect(dst,
+ trans)
+ cl := state.clip.Intersect(bnd.Add(off))
+ if cl.Empty() {
+ continue
+ }
+
+ wasrect := state.rect
+ if clipData != nil {
+ // The paint operation is sheared or rotated, add a clip path representing
+ // this transformed rectangle.
+ encOp.Key.SetTransform(trans)
+ d.addClipPath(&state, clipData, encOp.Key, bnd, off, state.t,
+ clip.StrokeStyle{})
+ }
+
+ bounds := boundRectF(cl)
+ mat := state.materialFor(bnd, off, partialTrans, bounds, state.t)
+
+ if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && state.rect && mat.opaque && (mat.material == materialColor) {
+ // The image is a uniform opaque color and takes up the whole screen.
+ // Scrap images up to and including this image and set clear color.
+ d.allImageOps = d.allImageOps[:0]
+ d.zimageOps = d.zimageOps[:0]
+ d.imageOps = d.imageOps[:0]
+ z = 0
+ d.clearColor = mat.color.Opaque()
+ d.clear = true
+ continue
+ }
+ z++
+ if z != int(uint16(z)) {
+ // TODO(eliasnaur) realy.lol/gio/issue/127.
+ panic("more than 65k paint objects not supported")
+ }
+ // Assume 16-bit depth buffer.
+ const zdepth = 1 << 16
+ // Convert z to window-space, assuming depth range [0;1].
+ zf := float32(z)*2/zdepth - 1.0
+ img := imageOp{
+ z: zf,
+ path: state.cpath,
+ clip: bounds,
+ material: mat,
+ }
+
+ d.allImageOps = append(d.allImageOps, img)
+ if state.rect && img.material.opaque {
+ d.zimageOps = append(d.zimageOps, img)
+ } else {
+ d.imageOps = append(d.imageOps, img)
+ }
+ if clipData != nil {
+ // we added a clip path that should not remain
+ state.cpath = state.cpath.parent
+ state.rect = wasrect
+ }
+ case opconst.TypeSave:
+ id := ops.DecodeSave(encOp.Data)
+ d.save(id, state)
+ case opconst.TypeLoad:
+ id, mask := ops.DecodeLoad(encOp.Data)
+ s := d.states[id]
+ if mask&opconst.TransformState != 0 {
+ state.t = s.t
+ }
+ if mask&^opconst.TransformState != 0 {
+ state = s
+ }
+ }
+ }
+}
+
+func expandPathOp(p *pathOp, clip image.Rectangle) {
+ for p != nil {
+ pclip := p.clip
+ if !pclip.Empty() {
+ clip = clip.Union(pclip)
+ }
+ p.clip = clip
+ p = p.parent
+ }
+}
+
+func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point,
+ partTrans f32.Affine2D, clip image.Rectangle, trans f32.Affine2D) material {
+ var m material
+ switch d.matType {
+ case materialColor:
+ m.material = materialColor
+ m.color = f32color.LinearFromSRGB(d.color)
+ m.opaque = m.color.A == 1.0
+ case materialLinearGradient:
+ m.material = materialLinearGradient
+
+ m.color1 = f32color.LinearFromSRGB(d.color1)
+ m.color2 = f32color.LinearFromSRGB(d.color2)
+ m.opaque = m.color1.A == 1.0 && m.color2.A == 1.0
+
+ m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1,
+ d.stop2))
+ case materialTexture:
+ m.material = materialTexture
+ dr := boundRectF(rect.Add(off))
+ sz := d.image.src.Bounds().Size()
+ sr := f32.Rectangle{
+ Max: f32.Point{
+ X: float32(sz.X),
+ Y: float32(sz.Y),
+ },
+ }
+ dx := float32(dr.Dx())
+ sdx := sr.Dx()
+ sr.Min.X += float32(clip.Min.X-dr.Min.X) * sdx / dx
+ sr.Max.X -= float32(dr.Max.X-clip.Max.X) * sdx / dx
+ dy := float32(dr.Dy())
+ sdy := sr.Dy()
+ sr.Min.Y += float32(clip.Min.Y-dr.Min.Y) * sdy / dy
+ sr.Max.Y -= float32(dr.Max.Y-clip.Max.Y) * sdy / dy
+ uvScale, uvOffset := texSpaceTransform(sr, sz)
+ m.uvTrans = partTrans.Mul(f32.Affine2D{}.Scale(f32.Point{},
+ uvScale).Offset(uvOffset))
+ m.trans = trans
+ m.data = d.image
+ }
+ return m
+}
+
+func (r *renderer) drawZOps(cache *resourceCache, ops []imageOp) {
+ r.ctx.SetDepthTest(true)
+ r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0)
+ r.ctx.BindInputLayout(r.blitter.layout)
+ // Render front to back.
+ for i := len(ops) - 1; i >= 0; i-- {
+ img := ops[i]
+ m := img.material
+ switch m.material {
+ case materialTexture:
+ r.ctx.BindTexture(0, r.texHandle(cache, m.data))
+ }
+ drc := img.clip
+ scale, off := clipSpaceTransform(drc, r.blitter.viewport)
+ r.blitter.blit(img.z, m.material, m.color, m.color1, m.color2, scale,
+ off, m.uvTrans)
+ }
+ r.ctx.SetDepthTest(false)
+}
+
+func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
+ r.ctx.SetDepthTest(true)
+ r.ctx.DepthMask(false)
+ r.ctx.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOneMinusSrcAlpha)
+ r.ctx.BindVertexBuffer(r.blitter.quadVerts, 4*4, 0)
+ r.ctx.BindInputLayout(r.pather.coverer.layout)
+ var coverTex driver.Texture
+ for _, img := range ops {
+ m := img.material
+ switch m.material {
+ case materialTexture:
+ r.ctx.BindTexture(0, r.texHandle(cache, m.data))
+ }
+ drc := img.clip
+
+ scale, off := clipSpaceTransform(drc, r.blitter.viewport)
+ var fbo stencilFBO
+ switch img.clipType {
+ case clipTypeNone:
+ r.blitter.blit(img.z, m.material, m.color, m.color1, m.color2,
+ scale, off, m.uvTrans)
+ continue
+ case clipTypePath:
+ fbo = r.pather.stenciler.cover(img.place.Idx)
+ case clipTypeIntersection:
+ fbo = r.pather.stenciler.intersections.fbos[img.place.Idx]
+ }
+ if coverTex != fbo.tex {
+ coverTex = fbo.tex
+ r.ctx.BindTexture(1, coverTex)
+ }
+ uv := image.Rectangle{
+ Min: img.place.Pos,
+ Max: img.place.Pos.Add(drc.Size()),
+ }
+ coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size)
+ r.pather.cover(img.z, m.material, m.color, m.color1, m.color2, scale,
+ off, m.uvTrans, coverScale, coverOff)
+ }
+ r.ctx.DepthMask(true)
+ r.ctx.SetDepthTest(false)
+}
+
+func (b *blitter) blit(z float32, mat materialType, col f32color.RGBA,
+ col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) {
+ p := b.prog[mat]
+ b.ctx.BindProgram(p.prog)
+ var uniforms *blitUniforms
+ switch mat {
+ case materialColor:
+ b.colUniforms.frag.color = col
+ uniforms = &b.colUniforms.vert.blitUniforms
+ case materialTexture:
+ t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
+ b.texUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3,
+ 0}
+ b.texUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6,
+ 0}
+ uniforms = &b.texUniforms.vert.blitUniforms
+ case materialLinearGradient:
+ b.linearGradientUniforms.frag.color1 = col1
+ b.linearGradientUniforms.frag.color2 = col2
+
+ t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
+ b.linearGradientUniforms.vert.blitUniforms.uvTransformR1 = [4]float32{t1,
+ t2, t3, 0}
+ b.linearGradientUniforms.vert.blitUniforms.uvTransformR2 = [4]float32{t4,
+ t5, t6, 0}
+ uniforms = &b.linearGradientUniforms.vert.blitUniforms
+ }
+ uniforms.z = z
+ uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
+ p.UploadUniforms()
+ b.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4)
+}
+
+// newUniformBuffer creates a new GPU uniform buffer backed by the
+// structure uniformBlock points to.
+func newUniformBuffer(b driver.Device,
+ uniformBlock interface{}) *uniformBuffer {
+ ref := reflect.ValueOf(uniformBlock)
+ // Determine the size of the uniforms structure, *uniforms.
+ size := ref.Elem().Type().Size()
+ // Map the uniforms structure as a byte slice.
+ ptr := (*[1 << 30]byte)(unsafe.Pointer(ref.Pointer()))[:size:size]
+ ubuf, err := b.NewBuffer(driver.BufferBindingUniforms, len(ptr))
+ if err != nil {
+ panic(err)
+ }
+ return &uniformBuffer{buf: ubuf, ptr: ptr}
+}
+
+func (u *uniformBuffer) Upload() {
+ u.buf.Upload(u.ptr)
+}
+
+func (u *uniformBuffer) Release() {
+ u.buf.Release()
+ u.buf = nil
+}
+
+func newProgram(prog driver.Program,
+ vertUniforms, fragUniforms *uniformBuffer) *program {
+ if vertUniforms != nil {
+ prog.SetVertexUniforms(vertUniforms.buf)
+ }
+ if fragUniforms != nil {
+ prog.SetFragmentUniforms(fragUniforms.buf)
+ }
+ return &program{prog: prog, vertUniforms: vertUniforms,
+ fragUniforms: fragUniforms}
+}
+
+func (p *program) UploadUniforms() {
+ if p.vertUniforms != nil {
+ p.vertUniforms.Upload()
+ }
+ if p.fragUniforms != nil {
+ p.fragUniforms.Upload()
+ }
+}
+
+func (p *program) Release() {
+ p.prog.Release()
+ p.prog = nil
+ if p.vertUniforms != nil {
+ p.vertUniforms.Release()
+ p.vertUniforms = nil
+ }
+ if p.fragUniforms != nil {
+ p.fragUniforms.Release()
+ p.fragUniforms = nil
+ }
+}
+
+// texSpaceTransform return the scale and offset that transforms the given subimage
+// into quad texture coordinates.
+func texSpaceTransform(r f32.Rectangle, bounds image.Point) (f32.Point,
+ f32.Point) {
+ size := f32.Point{X: float32(bounds.X), Y: float32(bounds.Y)}
+ scale := f32.Point{X: r.Dx() / size.X, Y: r.Dy() / size.Y}
+ offset := f32.Point{X: r.Min.X / size.X, Y: r.Min.Y / size.Y}
+ return scale, offset
+}
+
+// gradientSpaceTransform transforms stop1 and stop2 to [(0,0), (1,1)].
+func gradientSpaceTransform(clip image.Rectangle, off f32.Point,
+ stop1, stop2 f32.Point) f32.Affine2D {
+ d := stop2.Sub(stop1)
+ l := float32(math.Sqrt(float64(d.X*d.X + d.Y*d.Y)))
+ a := float32(math.Atan2(float64(-d.Y), float64(d.X)))
+
+ // TODO: optimize
+ zp := f32.Point{}
+ return f32.Affine2D{}.
+ Scale(zp, layout.FPt(clip.Size())). // scale to pixel space
+ Offset(zp.Sub(off).Add(layout.FPt(clip.Min))). // offset to clip space
+ Offset(zp.Sub(stop1)). // offset to first stop point
+ Rotate(zp, a). // rotate to align gradient
+ Scale(zp, f32.Pt(1/l, 1/l)) // scale gradient to right size
+}
+
+// clipSpaceTransform returns the scale and offset that transforms the given
+// rectangle from a viewport into OpenGL clip space.
+func clipSpaceTransform(r image.Rectangle, viewport image.Point) (f32.Point,
+ f32.Point) {
+ // First, transform UI coordinates to OpenGL coordinates:
+ //
+ // [(-1, +1) (+1, +1)]
+ // [(-1, -1) (+1, -1)]
+ //
+ x, y := float32(r.Min.X), float32(r.Min.Y)
+ w, h := float32(r.Dx()), float32(r.Dy())
+ vx, vy := 2/float32(viewport.X), 2/float32(viewport.Y)
+ x = x*vx - 1
+ y = 1 - y*vy
+ w *= vx
+ h *= vy
+
+ // Then, compute the transformation from the fullscreen quad to
+ // the rectangle at (x, y) and dimensions (w, h).
+ scale := f32.Point{X: w * .5, Y: h * .5}
+ offset := f32.Point{X: x + w*.5, Y: y - h*.5}
+
+ return scale, offset
+}
+
+// Fill in maximal Y coordinates of the NW and NE corners.
+func fillMaxY(verts []byte) {
+ contour := 0
+ bo := binary.LittleEndian
+ for len(verts) > 0 {
+ maxy := float32(math.Inf(-1))
+ i := 0
+ for ; i+vertStride*4 <= len(verts); i += vertStride * 4 {
+ vert := verts[i : i+vertStride]
+ // MaxY contains the integer contour index.
+ pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).MaxY)):]))
+ if contour != pathContour {
+ contour = pathContour
+ break
+ }
+ fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).FromY)):]))
+ ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).CtrlY)):]))
+ toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).ToY)):]))
+ if fromy > maxy {
+ maxy = fromy
+ }
+ if ctrly > maxy {
+ maxy = ctrly
+ }
+ if toy > maxy {
+ maxy = toy
+ }
+ }
+ fillContourMaxY(maxy, verts[:i])
+ verts = verts[i:]
+ }
+}
+
+func fillContourMaxY(maxy float32, verts []byte) {
+ bo := binary.LittleEndian
+ for i := 0; i < len(verts); i += vertStride {
+ off := int(unsafe.Offsetof(((*vertex)(nil)).MaxY))
+ bo.PutUint32(verts[i+off:], math.Float32bits(maxy))
+ }
+}
+
+func (d *drawOps) writeVertCache(n int) []byte {
+ d.vertCache = append(d.vertCache, make([]byte, n)...)
+ return d.vertCache[len(d.vertCache)-n:]
+}
+
+// transform, split paths as needed, calculate maxY, bounds and create GPU vertices.
+func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool,
+ str clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) {
+ inf := float32(math.Inf(+1))
+ d.qs.bounds = f32.Rectangle{
+ Min: f32.Point{X: inf, Y: inf},
+ Max: f32.Point{X: -inf, Y: -inf},
+ }
+ d.qs.d = d
+ startLength := len(d.vertCache)
+
+ switch {
+ case str.Width > 0:
+ // Stroke path.
+ ss := stroke.StrokeStyle{
+ Width: str.Width,
+ Miter: str.Miter,
+ Cap: stroke.StrokeCap(str.Cap),
+ Join: stroke.StrokeJoin(str.Join),
+ }
+ quads := stroke.StrokePathCommands(ss, stroke.DashOp{}, pathData)
+ for _, quad := range quads {
+ d.qs.contour = quad.Contour
+ quad.Quad = quad.Quad.Transform(tr)
+
+ d.qs.splitAndEncode(quad.Quad)
+ }
+
+ case outline:
+ decodeToOutlineQuads(&d.qs, tr, pathData)
+ }
+
+ fillMaxY(d.vertCache[startLength:])
+ return d.vertCache[startLength:], d.qs.bounds
+}
+
+// decodeOutlineQuads decodes scene commands, splits them into quadratic bƩziers
+// as needed and feeds them to the supplied splitter.
+func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
+ for len(pathData) >= scene.CommandSize+4 {
+ qs.contour = bo.Uint32(pathData)
+ cmd := ops.DecodeCommand(pathData[4:])
+ switch cmd.Op() {
+ case scene.OpLine:
+ var q stroke.QuadSegment
+ q.From, q.To = scene.DecodeLine(cmd)
+ q.Ctrl = q.From.Add(q.To).Mul(.5)
+ q = q.Transform(tr)
+ qs.splitAndEncode(q)
+ case scene.OpQuad:
+ var q stroke.QuadSegment
+ q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
+ q = q.Transform(tr)
+ qs.splitAndEncode(q)
+ case scene.OpCubic:
+ for _, q := range stroke.SplitCubic(scene.DecodeCubic(cmd)) {
+ q = q.Transform(tr)
+ qs.splitAndEncode(q)
+ }
+ default:
+ panic("unsupported scene command")
+ }
+ pathData = pathData[scene.CommandSize+4:]
+ }
+}
+
+// create GPU vertices for transformed r, find the bounds and establish texture transform.
+func (d *drawOps) boundsForTransformedRect(r f32.Rectangle,
+ tr f32.Affine2D) (aux []byte, bnd f32.Rectangle, ptr f32.Affine2D) {
+ if isPureOffset(tr) {
+ // fast-path to allow blitting of pure rectangles
+ _, _, ox, _, _, oy := tr.Elems()
+ off := f32.Pt(ox, oy)
+ bnd.Min = r.Min.Add(off)
+ bnd.Max = r.Max.Add(off)
+ return
+ }
+
+ // transform all corners, find new bounds
+ corners := [4]f32.Point{
+ tr.Transform(r.Min), tr.Transform(f32.Pt(r.Max.X, r.Min.Y)),
+ tr.Transform(r.Max), tr.Transform(f32.Pt(r.Min.X, r.Max.Y)),
+ }
+ bnd.Min = f32.Pt(math.MaxFloat32, math.MaxFloat32)
+ bnd.Max = f32.Pt(-math.MaxFloat32, -math.MaxFloat32)
+ for _, c := range corners {
+ if c.X < bnd.Min.X {
+ bnd.Min.X = c.X
+ }
+ if c.Y < bnd.Min.Y {
+ bnd.Min.Y = c.Y
+ }
+ if c.X > bnd.Max.X {
+ bnd.Max.X = c.X
+ }
+ if c.Y > bnd.Max.Y {
+ bnd.Max.Y = c.Y
+ }
+ }
+
+ // build the GPU vertices
+ l := len(d.vertCache)
+ if !d.compute {
+ d.vertCache = append(d.vertCache, make([]byte, vertStride*4*4)...)
+ aux = d.vertCache[l:]
+ encodeQuadTo(aux, 0, corners[0], corners[0].Add(corners[1]).Mul(0.5),
+ corners[1])
+ encodeQuadTo(aux[vertStride*4:], 0, corners[1],
+ corners[1].Add(corners[2]).Mul(0.5), corners[2])
+ encodeQuadTo(aux[vertStride*4*2:], 0, corners[2],
+ corners[2].Add(corners[3]).Mul(0.5), corners[3])
+ encodeQuadTo(aux[vertStride*4*3:], 0, corners[3],
+ corners[3].Add(corners[0]).Mul(0.5), corners[0])
+ fillMaxY(aux)
+ } else {
+ d.vertCache = append(d.vertCache,
+ make([]byte, (scene.CommandSize+4)*4)...)
+ aux = d.vertCache[l:]
+ buf := aux
+ bo := binary.LittleEndian
+ bo.PutUint32(buf, 0) // Contour
+ ops.EncodeCommand(buf[4:], scene.Line(r.Min, f32.Pt(r.Max.X, r.Min.Y)))
+ buf = buf[4+scene.CommandSize:]
+ bo.PutUint32(buf, 0)
+ ops.EncodeCommand(buf[4:], scene.Line(f32.Pt(r.Max.X, r.Min.Y), r.Max))
+ buf = buf[4+scene.CommandSize:]
+ bo.PutUint32(buf, 0)
+ ops.EncodeCommand(buf[4:], scene.Line(r.Max, f32.Pt(r.Min.X, r.Max.Y)))
+ buf = buf[4+scene.CommandSize:]
+ bo.PutUint32(buf, 0)
+ ops.EncodeCommand(buf[4:], scene.Line(f32.Pt(r.Min.X, r.Max.Y), r.Min))
+ }
+
+ // establish the transform mapping from bounds rectangle to transformed corners
+ var P1, P2, P3 f32.Point
+ P1.X = (corners[1].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
+ P1.Y = (corners[1].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
+ P2.X = (corners[2].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
+ P2.Y = (corners[2].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
+ P3.X = (corners[3].X - bnd.Min.X) / (bnd.Max.X - bnd.Min.X)
+ P3.Y = (corners[3].Y - bnd.Min.Y) / (bnd.Max.Y - bnd.Min.Y)
+ sx, sy := P2.X-P3.X, P2.Y-P3.Y
+ ptr = f32.NewAffine2D(sx, P2.X-P1.X, P1.X-sx, sy, P2.Y-P1.Y,
+ P1.Y-sy).Invert()
+
+ return
+}
+
+func isPureOffset(t f32.Affine2D) bool {
+ a, b, _, d, e, _ := t.Elems()
+ return a == 1 && b == 0 && d == 0 && e == 1
+}
diff --git a/gio/gpu/headless/driver_test.go b/gio/gpu/headless/driver_test.go
new file mode 100644
index 0000000..07c0b06
--- /dev/null
+++ b/gio/gpu/headless/driver_test.go
@@ -0,0 +1,211 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package headless
+
+import (
+ "bytes"
+ "flag"
+ "image"
+ "image/color"
+ "image/png"
+ "io/ioutil"
+ "runtime"
+ "testing"
+
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/internal/byteslice"
+ "realy.lol/gio/internal/f32color"
+)
+
+var dumpImages = flag.Bool("saveimages", false, "save test images")
+
+var clearCol = color.NRGBA{A: 0xff, R: 0xde, G: 0xad, B: 0xbe}
+var clearColExpect = f32color.NRGBAToRGBA(clearCol)
+
+func TestFramebufferClear(t *testing.T) {
+ b := newDriver(t)
+ sz := image.Point{X: 800, Y: 600}
+ fbo := setupFBO(t, b, sz)
+ img := screenshot(t, b, fbo, sz)
+ if got := img.RGBAAt(0, 0); got != clearColExpect {
+ t.Errorf("got color %v, expected %v", got, clearColExpect)
+ }
+}
+
+func TestSimpleShader(t *testing.T) {
+ b := newDriver(t)
+ sz := image.Point{X: 800, Y: 600}
+ fbo := setupFBO(t, b, sz)
+ p, err := b.NewProgram(shader_simple_vert, shader_simple_frag)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer p.Release()
+ b.BindProgram(p)
+ b.DrawArrays(driver.DrawModeTriangles, 0, 3)
+ img := screenshot(t, b, fbo, sz)
+ if got := img.RGBAAt(0, 0); got != clearColExpect {
+ t.Errorf("got color %v, expected %v", got, clearColExpect)
+ }
+ // Just off the center to catch inverted triangles.
+ cx, cy := 300, 400
+ shaderCol := f32color.RGBA{R: .25, G: .55, B: .75, A: 1.0}
+ if got, exp := img.RGBAAt(cx,
+ cy), shaderCol.SRGB(); got != f32color.NRGBAToRGBA(exp) {
+ t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(exp))
+ }
+}
+
+func TestInputShader(t *testing.T) {
+ b := newDriver(t)
+ sz := image.Point{X: 800, Y: 600}
+ fbo := setupFBO(t, b, sz)
+ p, err := b.NewProgram(shader_input_vert, shader_simple_frag)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer p.Release()
+ b.BindProgram(p)
+ buf, err := b.NewImmutableBuffer(driver.BufferBindingVertices,
+ byteslice.Slice([]float32{
+ 0, .5, .5, 1,
+ -.5, -.5, .5, 1,
+ .5, -.5, .5, 1,
+ }),
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer buf.Release()
+ b.BindVertexBuffer(buf, 4*4, 0)
+ layout, err := b.NewInputLayout(shader_input_vert, []driver.InputDesc{
+ {
+ Type: driver.DataTypeFloat,
+ Size: 4,
+ Offset: 0,
+ },
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer layout.Release()
+ b.BindInputLayout(layout)
+ b.DrawArrays(driver.DrawModeTriangles, 0, 3)
+ img := screenshot(t, b, fbo, sz)
+ if got := img.RGBAAt(0, 0); got != clearColExpect {
+ t.Errorf("got color %v, expected %v", got, clearColExpect)
+ }
+ cx, cy := 300, 400
+ shaderCol := f32color.RGBA{R: .25, G: .55, B: .75, A: 1.0}
+ if got, exp := img.RGBAAt(cx,
+ cy), shaderCol.SRGB(); got != f32color.NRGBAToRGBA(exp) {
+ t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(exp))
+ }
+}
+
+func TestFramebuffers(t *testing.T) {
+ b := newDriver(t)
+ sz := image.Point{X: 800, Y: 600}
+ fbo1 := newFBO(t, b, sz)
+ fbo2 := newFBO(t, b, sz)
+ var (
+ col1 = color.NRGBA{R: 0xac, G: 0xbd, B: 0xef, A: 0xde}
+ col2 = color.NRGBA{R: 0xfe, G: 0xba, B: 0xbe, A: 0xca}
+ )
+ fcol1, fcol2 := f32color.LinearFromSRGB(col1), f32color.LinearFromSRGB(col2)
+ b.BindFramebuffer(fbo1)
+ b.Clear(fcol1.Float32())
+ b.BindFramebuffer(fbo2)
+ b.Clear(fcol2.Float32())
+ img := screenshot(t, b, fbo1, sz)
+ if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col1) {
+ t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col1))
+ }
+ img = screenshot(t, b, fbo2, sz)
+ if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col2) {
+ t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col2))
+ }
+}
+
+func setupFBO(t *testing.T, b driver.Device,
+ size image.Point) driver.Framebuffer {
+ fbo := newFBO(t, b, size)
+ b.BindFramebuffer(fbo)
+ // ClearColor accepts linear RGBA colors, while 8-bit colors
+ // are in the sRGB color space.
+ col := f32color.LinearFromSRGB(clearCol)
+ b.Clear(col.Float32())
+ b.ClearDepth(0.0)
+ b.Viewport(0, 0, size.X, size.Y)
+ return fbo
+}
+
+func newFBO(t *testing.T, b driver.Device,
+ size image.Point) driver.Framebuffer {
+ fboTex, err := b.NewTexture(
+ driver.TextureFormatSRGB,
+ size.X, size.Y,
+ driver.FilterNearest, driver.FilterNearest,
+ driver.BufferBindingFramebuffer,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ fboTex.Release()
+ })
+ const depthBits = 16
+ fbo, err := b.NewFramebuffer(fboTex, depthBits)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ fbo.Release()
+ })
+ return fbo
+}
+
+func newDriver(t *testing.T) driver.Device {
+ ctx, err := newContext()
+ if err != nil {
+ t.Skipf("no context available: %v", err)
+ }
+ runtime.LockOSThread()
+ if err := ctx.MakeCurrent(); err != nil {
+ t.Fatal(err)
+ }
+ b, err := driver.NewDevice(ctx.API())
+ if err != nil {
+ t.Fatal(err)
+ }
+ b.BeginFrame()
+ t.Cleanup(func() {
+ b.EndFrame()
+ ctx.ReleaseCurrent()
+ runtime.UnlockOSThread()
+ ctx.Release()
+ })
+ return b
+}
+
+func screenshot(t *testing.T, d driver.Device, fbo driver.Framebuffer,
+ size image.Point) *image.RGBA {
+ img, err := driver.DownloadImage(d, fbo, image.Rectangle{Max: size})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if *dumpImages {
+ if err := saveImage(t.Name()+".png", img); err != nil {
+ t.Error(err)
+ }
+ }
+ return img
+}
+
+func saveImage(file string, img image.Image) error {
+ var buf bytes.Buffer
+ if err := png.Encode(&buf, img); err != nil {
+ return err
+ }
+ return ioutil.WriteFile(file, buf.Bytes(), 0666)
+}
diff --git a/gio/gpu/headless/gen.go b/gio/gpu/headless/gen.go
new file mode 100644
index 0000000..b9e1fed
--- /dev/null
+++ b/gio/gpu/headless/gen.go
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package headless
+
+//go:generate go run ../internal/convertshaders -package headless
diff --git a/gio/gpu/headless/headless.go b/gio/gpu/headless/headless.go
new file mode 100644
index 0000000..0f2e172
--- /dev/null
+++ b/gio/gpu/headless/headless.go
@@ -0,0 +1,148 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package headless implements headless windows for rendering
+// an operation list to an image.
+package headless
+
+import (
+ "image"
+ "image/color"
+ "runtime"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/op"
+)
+
+// Window is a headless window.
+type Window struct {
+ size image.Point
+ ctx context
+ dev driver.Device
+ gpu gpu.GPU
+ fboTex driver.Texture
+ fbo driver.Framebuffer
+}
+
+type context interface {
+ API() gpu.API
+ MakeCurrent() error
+ ReleaseCurrent()
+ Release()
+}
+
+// NewWindow creates a new headless window.
+func NewWindow(width, height int) (*Window, error) {
+ ctx, err := newContext()
+ if err != nil {
+ return nil, err
+ }
+ w := &Window{
+ size: image.Point{X: width, Y: height},
+ ctx: ctx,
+ }
+ err = contextDo(ctx, func() error {
+ api := ctx.API()
+ dev, err := driver.NewDevice(api)
+ if err != nil {
+ return err
+ }
+ dev.Viewport(0, 0, width, height)
+ fboTex, err := dev.NewTexture(
+ driver.TextureFormatSRGB,
+ width, height,
+ driver.FilterNearest, driver.FilterNearest,
+ driver.BufferBindingFramebuffer,
+ )
+ if err != nil {
+ return nil
+ }
+ const depthBits = 16
+ fbo, err := dev.NewFramebuffer(fboTex, depthBits)
+ if err != nil {
+ fboTex.Release()
+ return err
+ }
+ gp, err := gpu.New(api)
+ if err != nil {
+ fbo.Release()
+ fboTex.Release()
+ return err
+ }
+ w.fboTex = fboTex
+ w.fbo = fbo
+ w.gpu = gp
+ w.dev = dev
+ return err
+ })
+ if err != nil {
+ ctx.Release()
+ return nil, err
+ }
+ return w, nil
+}
+
+// Release resources associated with the window.
+func (w *Window) Release() {
+ contextDo(w.ctx, func() error {
+ if w.fbo != nil {
+ w.fbo.Release()
+ w.fbo = nil
+ }
+ if w.fboTex != nil {
+ w.fboTex.Release()
+ w.fboTex = nil
+ }
+ if w.gpu != nil {
+ w.gpu.Release()
+ w.gpu = nil
+ }
+ return nil
+ })
+ if w.ctx != nil {
+ w.ctx.Release()
+ w.ctx = nil
+ }
+}
+
+// Frame replace the window content and state with the
+// operation list.
+func (w *Window) Frame(frame *op.Ops) error {
+ return contextDo(w.ctx, func() error {
+ w.dev.BindFramebuffer(w.fbo)
+ w.gpu.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
+ w.gpu.Collect(w.size, frame)
+ return w.gpu.Frame()
+ })
+}
+
+// Screenshot returns an image with the content of the window.
+func (w *Window) Screenshot() (*image.RGBA, error) {
+ var img *image.RGBA
+ err := contextDo(w.ctx, func() error {
+ var err error
+ img, err = driver.DownloadImage(w.dev, w.fbo,
+ image.Rectangle{Max: w.size})
+ return err
+ })
+ if err != nil {
+ return nil, err
+ }
+ return img, nil
+}
+
+func contextDo(ctx context, f func() error) error {
+ errCh := make(chan error)
+ go func() {
+ runtime.LockOSThread()
+ defer runtime.UnlockOSThread()
+ if err := ctx.MakeCurrent(); err != nil {
+ errCh <- err
+ return
+ }
+ err := f()
+ ctx.ReleaseCurrent()
+ errCh <- err
+ }()
+ return <-errCh
+}
diff --git a/gio/gpu/headless/headless_darwin.go b/gio/gpu/headless/headless_darwin.go
new file mode 100644
index 0000000..75a233a
--- /dev/null
+++ b/gio/gpu/headless/headless_darwin.go
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package headless
+
+import (
+ "realy.lol/gio/gpu"
+ _ "realy.lol/gio/internal/cocoainit"
+)
+
+/*
+#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c
+
+#include
+
+__attribute__ ((visibility ("hidden"))) CFTypeRef gio_headless_newContext(void);
+__attribute__ ((visibility ("hidden"))) void gio_headless_releaseContext(CFTypeRef ctxRef);
+__attribute__ ((visibility ("hidden"))) void gio_headless_clearCurrentContext(CFTypeRef ctxRef);
+__attribute__ ((visibility ("hidden"))) void gio_headless_makeCurrentContext(CFTypeRef ctxRef);
+__attribute__ ((visibility ("hidden"))) void gio_headless_prepareContext(CFTypeRef ctxRef);
+*/
+import "C"
+
+type nsContext struct {
+ ctx C.CFTypeRef
+ prepared bool
+}
+
+func newGLContext() (context, error) {
+ ctx := C.gio_headless_newContext()
+ return &nsContext{ctx: ctx}, nil
+}
+
+func (c *nsContext) API() gpu.API {
+ return gpu.OpenGL{}
+}
+
+func (c *nsContext) MakeCurrent() error {
+ C.gio_headless_makeCurrentContext(c.ctx)
+ if !c.prepared {
+ C.gio_headless_prepareContext(c.ctx)
+ c.prepared = true
+ }
+ return nil
+}
+
+func (c *nsContext) ReleaseCurrent() {
+ C.gio_headless_clearCurrentContext(c.ctx)
+}
+
+func (d *nsContext) Release() {
+ if d.ctx != 0 {
+ C.gio_headless_releaseContext(d.ctx)
+ d.ctx = 0
+ }
+}
diff --git a/gio/gpu/headless/headless_egl.go b/gio/gpu/headless/headless_egl.go
new file mode 100644
index 0000000..7d8c1e4
--- /dev/null
+++ b/gio/gpu/headless/headless_egl.go
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build linux || freebsd || windows || openbsd
+// +build linux freebsd windows openbsd
+
+package headless
+
+import (
+ "realy.lol/gio/internal/egl"
+)
+
+func newGLContext() (context, error) {
+ return egl.NewContext(egl.EGL_DEFAULT_DISPLAY)
+}
diff --git a/gio/gpu/headless/headless_gl.go b/gio/gpu/headless/headless_gl.go
new file mode 100644
index 0000000..c00083e
--- /dev/null
+++ b/gio/gpu/headless/headless_gl.go
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build !windows
+
+package headless
+
+func newContext() (context, error) {
+ return newGLContext()
+}
diff --git a/gio/gpu/headless/headless_ios.m b/gio/gpu/headless/headless_ios.m
new file mode 100644
index 0000000..fd72d25
--- /dev/null
+++ b/gio/gpu/headless/headless_ios.m
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build darwin,ios
+
+@import OpenGLES;
+
+#include
+#include "_cgo_export.h"
+
+void gio_headless_releaseContext(CFTypeRef ctxRef) {
+ CFBridgingRelease(ctxRef);
+}
+
+CFTypeRef gio_headless_newContext(void) {
+ EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
+ if (ctx == nil) {
+ return nil;
+ }
+ return CFBridgingRetain(ctx);
+}
+
+void gio_headless_clearCurrentContext(CFTypeRef ctxRef) {
+ [EAGLContext setCurrentContext:nil];
+}
+
+void gio_headless_makeCurrentContext(CFTypeRef ctxRef) {
+ EAGLContext *ctx = (__bridge EAGLContext *)ctxRef;
+ [EAGLContext setCurrentContext:ctx];
+}
+
+void gio_headless_prepareContext(CFTypeRef ctxRef) {
+}
diff --git a/gio/gpu/headless/headless_js.go b/gio/gpu/headless/headless_js.go
new file mode 100644
index 0000000..f79963e
--- /dev/null
+++ b/gio/gpu/headless/headless_js.go
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package headless
+
+import (
+ "errors"
+ "syscall/js"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/internal/gl"
+)
+
+type jsContext struct {
+ ctx js.Value
+}
+
+func newGLContext() (context, error) {
+ doc := js.Global().Get("document")
+ cnv := doc.Call("createElement", "canvas")
+ ctx := cnv.Call("getContext", "webgl2")
+ if ctx.IsNull() {
+ ctx = cnv.Call("getContext", "webgl")
+ }
+ if ctx.IsNull() {
+ return nil, errors.New("headless: webgl is not supported")
+ }
+ c := &jsContext{
+ ctx: ctx,
+ }
+ return c, nil
+}
+
+func (c *jsContext) API() gpu.API {
+ return gpu.OpenGL{Context: gl.Context(c.ctx)}
+}
+
+func (c *jsContext) Release() {
+}
+
+func (c *jsContext) ReleaseCurrent() {
+}
+
+func (c *jsContext) MakeCurrent() error {
+ return nil
+}
diff --git a/gio/gpu/headless/headless_macos.m b/gio/gpu/headless/headless_macos.m
new file mode 100644
index 0000000..46deb37
--- /dev/null
+++ b/gio/gpu/headless/headless_macos.m
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build darwin,!ios
+
+@import AppKit;
+@import OpenGL;
+@import OpenGL.GL;
+@import OpenGL.GL3;
+
+#include
+#include "_cgo_export.h"
+
+void gio_headless_releaseContext(CFTypeRef ctxRef) {
+ CFBridgingRelease(ctxRef);
+}
+
+CFTypeRef gio_headless_newContext(void) {
+ NSOpenGLPixelFormatAttribute attr[] = {
+ NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
+ NSOpenGLPFAColorSize, 24,
+ NSOpenGLPFAAccelerated,
+ // Opt-in to automatic GPU switching. CGL-only property.
+ kCGLPFASupportsAutomaticGraphicsSwitching,
+ NSOpenGLPFAAllowOfflineRenderers,
+ 0
+ };
+ NSOpenGLPixelFormat *pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
+ if (pixFormat == nil) {
+ return NULL;
+ }
+ NSOpenGLContext *ctx = [[NSOpenGLContext alloc] initWithFormat:pixFormat shareContext:nil];
+ return CFBridgingRetain(ctx);
+}
+
+void gio_headless_clearCurrentContext(CFTypeRef ctxRef) {
+ NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef;
+ CGLUnlockContext([ctx CGLContextObj]);
+ [NSOpenGLContext clearCurrentContext];
+}
+
+void gio_headless_makeCurrentContext(CFTypeRef ctxRef) {
+ NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef;
+ [ctx makeCurrentContext];
+ CGLLockContext([ctx CGLContextObj]);
+}
+
+void gio_headless_prepareContext(CFTypeRef ctxRef) {
+ // Bind a default VBA to emulate OpenGL ES 2.
+ GLuint defVBA;
+ glGenVertexArrays(1, &defVBA);
+ glBindVertexArray(defVBA);
+ glEnable(GL_FRAMEBUFFER_SRGB);
+}
diff --git a/gio/gpu/headless/headless_test.go b/gio/gpu/headless/headless_test.go
new file mode 100644
index 0000000..3ceec3f
--- /dev/null
+++ b/gio/gpu/headless/headless_test.go
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package headless
+
+import (
+ "image"
+ "image/color"
+ "testing"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+)
+
+func TestHeadless(t *testing.T) {
+ w, release := newTestWindow(t)
+ defer release()
+
+ sz := w.size
+ col := color.NRGBA{A: 0xff, R: 0xca, G: 0xfe}
+ var ops op.Ops
+ paint.ColorOp{Color: col}.Add(&ops)
+ // Paint only part of the screen to avoid the glClear optimization.
+ paint.FillShape(&ops, col,
+ clip.Rect(image.Rect(0, 0, sz.X-100, sz.Y-100)).Op())
+ if err := w.Frame(&ops); err != nil {
+ t.Fatal(err)
+ }
+
+ img, err := w.Screenshot()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if isz := img.Bounds().Size(); isz != sz {
+ t.Errorf("got %v screenshot, expected %v", isz, sz)
+ }
+ if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col) {
+ t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col))
+ }
+}
+
+func TestClipping(t *testing.T) {
+ w, release := newTestWindow(t)
+ defer release()
+
+ col := color.NRGBA{A: 0xff, R: 0xca, G: 0xfe}
+ col2 := color.NRGBA{A: 0xff, R: 0x00, G: 0xfe}
+ var ops op.Ops
+ paint.ColorOp{Color: col}.Add(&ops)
+ clip.RRect{
+ Rect: f32.Rectangle{
+ Min: f32.Point{X: 50, Y: 50},
+ Max: f32.Point{X: 250, Y: 250},
+ },
+ SE: 75,
+ }.Add(&ops)
+ paint.PaintOp{}.Add(&ops)
+ paint.ColorOp{Color: col2}.Add(&ops)
+ clip.RRect{
+ Rect: f32.Rectangle{
+ Min: f32.Point{X: 100, Y: 100},
+ Max: f32.Point{X: 350, Y: 350},
+ },
+ NW: 75,
+ }.Add(&ops)
+ paint.PaintOp{}.Add(&ops)
+ if err := w.Frame(&ops); err != nil {
+ t.Fatal(err)
+ }
+
+ img, err := w.Screenshot()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if *dumpImages {
+ if err := saveImage("clip.png", img); err != nil {
+ t.Fatal(err)
+ }
+ }
+ bg := color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}
+ tests := []struct {
+ x, y int
+ color color.NRGBA
+ }{
+ {120, 120, col},
+ {130, 130, col2},
+ {210, 210, col2},
+ {230, 230, bg},
+ }
+ for _, test := range tests {
+ if got := img.RGBAAt(test.x,
+ test.y); got != f32color.NRGBAToRGBA(test.color) {
+ t.Errorf("(%d,%d): got color %v, expected %v", test.x, test.y, got,
+ f32color.NRGBAToRGBA(test.color))
+ }
+ }
+}
+
+func TestDepth(t *testing.T) {
+ w, release := newTestWindow(t)
+ defer release()
+ var ops op.Ops
+
+ blue := color.NRGBA{B: 0xFF, A: 0xFF}
+ paint.FillShape(&ops, blue, clip.Rect(image.Rect(0, 0, 50, 100)).Op())
+ red := color.NRGBA{R: 0xFF, A: 0xFF}
+ paint.FillShape(&ops, red, clip.Rect(image.Rect(0, 0, 100, 50)).Op())
+ if err := w.Frame(&ops); err != nil {
+ t.Fatal(err)
+ }
+
+ img, err := w.Screenshot()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if *dumpImages {
+ if err := saveImage("depth.png", img); err != nil {
+ t.Fatal(err)
+ }
+ }
+ tests := []struct {
+ x, y int
+ color color.NRGBA
+ }{
+ {25, 25, red},
+ {75, 25, red},
+ {25, 75, blue},
+ }
+ for _, test := range tests {
+ if got := img.RGBAAt(test.x,
+ test.y); got != f32color.NRGBAToRGBA(test.color) {
+ t.Errorf("(%d,%d): got color %v, expected %v", test.x, test.y, got,
+ f32color.NRGBAToRGBA(test.color))
+ }
+ }
+}
+
+func newTestWindow(t *testing.T) (*Window, func()) {
+ t.Helper()
+ sz := image.Point{X: 800, Y: 600}
+ w, err := NewWindow(sz.X, sz.Y)
+ if err != nil {
+ t.Skipf("headless windows not supported: %v", err)
+ }
+ return w, func() {
+ w.Release()
+ }
+}
diff --git a/gio/gpu/headless/headless_windows.go b/gio/gpu/headless/headless_windows.go
new file mode 100644
index 0000000..bd42d12
--- /dev/null
+++ b/gio/gpu/headless/headless_windows.go
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package headless
+
+import (
+ "unsafe"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/internal/d3d11"
+)
+
+type d3d11Context struct {
+ dev *d3d11.Device
+}
+
+func newContext() (context, error) {
+ dev, ctx, _, err := d3d11.CreateDevice(
+ d3d11.DRIVER_TYPE_HARDWARE,
+ 0,
+ )
+ if err != nil {
+ return nil, err
+ }
+ // Don't need it.
+ d3d11.IUnknownRelease(unsafe.Pointer(ctx), ctx.Vtbl.Release)
+ return &d3d11Context{dev: dev}, nil
+}
+
+func (c *d3d11Context) API() gpu.API {
+ return gpu.Direct3D11{Device: unsafe.Pointer(c.dev)}
+}
+
+func (c *d3d11Context) MakeCurrent() error {
+ return nil
+}
+
+func (c *d3d11Context) ReleaseCurrent() {
+}
+
+func (c *d3d11Context) Release() {
+ d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release)
+ c.dev = nil
+}
diff --git a/gio/gpu/headless/shaders.go b/gio/gpu/headless/shaders.go
new file mode 100644
index 0000000..95e05b2
--- /dev/null
+++ b/gio/gpu/headless/shaders.go
@@ -0,0 +1,233 @@
+// Code generated by build.go. DO NOT EDIT.
+
+package headless
+
+import "realy.lol/gio/gpu/internal/driver"
+
+var (
+ shader_input_vert = driver.ShaderSources{
+ Name: "input.vert",
+ Inputs: []driver.InputLocation{{Name: "position", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 4}},
+ GLSL100ES: `#version 100
+
+attribute vec4 position;
+
+void main()
+{
+ gl_Position = position;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+layout(location = 0) in vec4 position;
+
+void main()
+{
+ gl_Position = position;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+in vec4 position;
+
+void main()
+{
+ gl_Position = position;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+in vec4 position;
+
+void main()
+{
+ gl_Position = position;
+}
+
+`,
+ HLSL: "DXBC\x1eĀ»\x11\xd3iX7\xd4F\xb9\xa4\xf4R\xf9J\x01\x00\x00\x00\x10\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\x9c\x00\x00\x00\xe0\x00\x00\x00\\\x01\x00\x00\xa8\x01\x00\x00\xdc\x01\x00\x00Aon9\\\x00\x00\x00\\\x00\x00\x00\x00\x02\xfe\xff4\x00\x00\x00(\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x01\x00$\x00\x00\x00\x00\x00\x00\x02\xfe\xff\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x04\x00\x00\x04\x00\x00\x03\xc0\x00\x00\xff\x90\x00\x00\xe4\xa0\x00\x00\xe4\x90\x01\x00\x00\x02\x00\x00\f\xc0\x00\x00\xe4\x90\xff\xff\x00\x00SHDR<\x00\x00\x00@\x00\x01\x00\x0f\x00\x00\x00_\x00\x00\x03\xf2\x10\x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x006\x00\x00\x05\xf2 \x10\x00\x00\x00\x00\x00F\x1e\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x0f\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00",
+ }
+ shader_simple_frag = driver.ShaderSources{
+ Name: "simple.frag",
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+void main()
+{
+ gl_FragData[0] = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0);
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+layout(location = 0) out vec4 fragColor;
+
+void main()
+{
+ fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0);
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+out vec4 fragColor;
+
+void main()
+{
+ fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0);
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+out vec4 fragColor;
+
+void main()
+{
+ fragColor = vec4(0.25, 0.550000011920928955078125, 0.75, 1.0);
+}
+
+`,
+ HLSL: "DXBC\xf5F\xdef$)\xa8\xbbV\xeas\xb5ks\x12r\x01\x00\x00\x00\xdc\x01\x00\x00\x06\x00\x00\x008\x00\x00\x00\x90\x00\x00\x00\xd0\x00\x00\x00L\x01\x00\x00\x98\x01\x00\x00\xa8\x01\x00\x00Aon9P\x00\x00\x00P\x00\x00\x00\x00\x02\xff\xff,\x00\x00\x00$\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x80>\xcd\xcc\f?\x00\x00@?\x00\x00\x80?\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\xa0\xff\xff\x00\x00SHDR8\x00\x00\x00@\x00\x00\x00\x0e\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x006\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80>\xcd\xcc\f?\x00\x00@?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\b\x00\x00\x00\x00\x00\x00\x00\b\x00\x00\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ }
+ shader_simple_vert = driver.ShaderSources{
+ Name: "simple.vert",
+ GLSL100ES: `#version 100
+
+void main()
+{
+ float x;
+ float y;
+ if (gl_VertexID == 0)
+ {
+ x = 0.0;
+ y = 0.5;
+ }
+ else
+ {
+ if (gl_VertexID == 1)
+ {
+ x = 0.5;
+ y = -0.5;
+ }
+ else
+ {
+ x = -0.5;
+ y = -0.5;
+ }
+ }
+ gl_Position = vec4(x, y, 0.5, 1.0);
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+void main()
+{
+ float x;
+ float y;
+ if (gl_VertexID == 0)
+ {
+ x = 0.0;
+ y = 0.5;
+ }
+ else
+ {
+ if (gl_VertexID == 1)
+ {
+ x = 0.5;
+ y = -0.5;
+ }
+ else
+ {
+ x = -0.5;
+ y = -0.5;
+ }
+ }
+ gl_Position = vec4(x, y, 0.5, 1.0);
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+void main()
+{
+ float x;
+ float y;
+ if (gl_VertexID == 0)
+ {
+ x = 0.0;
+ y = 0.5;
+ }
+ else
+ {
+ if (gl_VertexID == 1)
+ {
+ x = 0.5;
+ y = -0.5;
+ }
+ else
+ {
+ x = -0.5;
+ y = -0.5;
+ }
+ }
+ gl_Position = vec4(x, y, 0.5, 1.0);
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+void main()
+{
+ float x;
+ float y;
+ if (gl_VertexID == 0)
+ {
+ x = 0.0;
+ y = 0.5;
+ }
+ else
+ {
+ if (gl_VertexID == 1)
+ {
+ x = 0.5;
+ y = -0.5;
+ }
+ else
+ {
+ x = -0.5;
+ y = -0.5;
+ }
+ }
+ gl_Position = vec4(x, y, 0.5, 1.0);
+}
+
+`,
+ HLSL: "DXBC\xc8 \\\"\xec\xe9\xb2)@\xdf|Z(\xea\f\xb8\x01\x00\x00\x00H\x02\x00\x00\x05\x00\x00\x004\x00\x00\x00\x80\x00\x00\x00\xb4\x00\x00\x00\xe8\x00\x00\x00\xcc\x01\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00SV_VertexID\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00SHDR\xdc\x00\x00\x00@\x00\x01\x007\x00\x00\x00`\x00\x00\x04\x12\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00 \x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x01\x00\x00\x007\x00\x00\x0f2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x007\x00\x00\f2 \x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ }
+)
diff --git a/gio/gpu/headless/shaders/input.vert b/gio/gpu/headless/shaders/input.vert
new file mode 100644
index 0000000..ed9a4bd
--- /dev/null
+++ b/gio/gpu/headless/shaders/input.vert
@@ -0,0 +1,11 @@
+#version 310 es
+
+// SPDX-License-Identifier: Unlicense OR MIT
+
+precision highp float;
+
+layout(location=0) in vec4 position;
+
+void main() {
+ gl_Position = position;
+}
diff --git a/gio/gpu/headless/shaders/simple.frag b/gio/gpu/headless/shaders/simple.frag
new file mode 100644
index 0000000..4614f33
--- /dev/null
+++ b/gio/gpu/headless/shaders/simple.frag
@@ -0,0 +1,11 @@
+#version 310 es
+
+// SPDX-License-Identifier: Unlicense OR MIT
+
+precision mediump float;
+
+layout(location = 0) out vec4 fragColor;
+
+void main() {
+ fragColor = vec4(.25, .55, .75, 1.0);
+}
diff --git a/gio/gpu/headless/shaders/simple.vert b/gio/gpu/headless/shaders/simple.vert
new file mode 100644
index 0000000..a226816
--- /dev/null
+++ b/gio/gpu/headless/shaders/simple.vert
@@ -0,0 +1,20 @@
+#version 310 es
+
+// SPDX-License-Identifier: Unlicense OR MIT
+
+precision highp float;
+
+void main() {
+ float x, y;
+ if (gl_VertexIndex == 0) {
+ x = 0.0;
+ y = .5;
+ } else if (gl_VertexIndex == 1) {
+ x = .5;
+ y = -.5;
+ } else {
+ x = -.5;
+ y = -.5;
+ }
+ gl_Position = vec4(x, y, 0.5, 1.0);
+}
diff --git a/gio/gpu/internal/convertshaders/glslvalidate.go b/gio/gpu/internal/convertshaders/glslvalidate.go
new file mode 100644
index 0000000..0d02a29
--- /dev/null
+++ b/gio/gpu/internal/convertshaders/glslvalidate.go
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os/exec"
+ "path/filepath"
+)
+
+// GLSLValidator is OpenGL reference compiler.
+type GLSLValidator struct {
+ Bin string
+ WorkDir WorkDir
+}
+
+func NewGLSLValidator() *GLSLValidator { return &GLSLValidator{Bin: "glslangValidator"} }
+
+// Convert converts a glsl shader to spirv.
+func (glsl *GLSLValidator) Convert(path, variant string, hlsl bool, input []byte) ([]byte, error) {
+ base := glsl.WorkDir.Path(filepath.Base(path), variant)
+ pathout := base + ".out"
+
+ cmd := exec.Command(glsl.Bin,
+ "--stdin",
+ "-I"+filepath.Dir(path),
+ "-V", // OpenGL ES 3.1.
+ "-w", // Suppress warnings.
+ "-S", filepath.Ext(path)[1:],
+ "-o", pathout,
+ )
+ if hlsl {
+ cmd.Args = append(cmd.Args, "-DHLSL")
+ }
+ cmd.Stdin = bytes.NewBuffer(input)
+
+ out, err := cmd.Output()
+ if err != nil {
+ return nil, fmt.Errorf("%s\nfailed to run %v: %w", out, cmd.Args, err)
+ }
+
+ compiled, err := ioutil.ReadFile(pathout)
+ if err != nil {
+ return nil, fmt.Errorf("unable to read output %q: %w", pathout, err)
+ }
+
+ return compiled, nil
+}
diff --git a/gio/gpu/internal/convertshaders/hlsl.go b/gio/gpu/internal/convertshaders/hlsl.go
new file mode 100644
index 0000000..a007925
--- /dev/null
+++ b/gio/gpu/internal/convertshaders/hlsl.go
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+// FXC is hlsl compiler that targets ShaderModel 5.x and lower.
+type FXC struct {
+ Bin string
+ WorkDir WorkDir
+}
+
+func NewFXC() *FXC { return &FXC{Bin: "fxc.exe"} }
+
+// Compile compiles the input shader.
+func (fxc *FXC) Compile(path, variant string, input []byte, entryPoint string, profileVersion string) (string, error) {
+ base := fxc.WorkDir.Path(filepath.Base(path), variant, profileVersion)
+ pathin := base + ".in"
+ pathout := base + ".out"
+ result := pathout
+
+ if err := fxc.WorkDir.WriteFile(pathin, input); err != nil {
+ return "", fmt.Errorf("unable to write shader to disk: %w", err)
+ }
+
+ cmd := exec.Command(fxc.Bin)
+ if runtime.GOOS != "windows" {
+ cmd = exec.Command("wine", fxc.Bin)
+ if err := winepath(&pathin, &pathout); err != nil {
+ return "", err
+ }
+ }
+
+ var profile string
+ switch filepath.Ext(path) {
+ case ".frag":
+ profile = "ps_" + profileVersion
+ case ".vert":
+ profile = "vs_" + profileVersion
+ case ".comp":
+ profile = "cs_" + profileVersion
+ default:
+ return "", fmt.Errorf("unrecognized shader type %s", path)
+ }
+
+ cmd.Args = append(cmd.Args,
+ "/Fo", pathout,
+ "/T", profile,
+ "/E", entryPoint,
+ pathin,
+ )
+
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ info := ""
+ if runtime.GOOS != "windows" {
+ info = "If the fxc tool cannot be found, set WINEPATH to the Windows path for the Windows SDK.\n"
+ }
+ return "", fmt.Errorf("%s\n%sfailed to run %v: %w", output, info, cmd.Args, err)
+ }
+
+ compiled, err := ioutil.ReadFile(result)
+ if err != nil {
+ return "", fmt.Errorf("unable to read output %q: %w", pathout, err)
+ }
+
+ return string(compiled), nil
+}
+
+// DXC is hlsl compiler that targets ShaderModel 6.0 and newer.
+type DXC struct {
+ Bin string
+ WorkDir WorkDir
+}
+
+func NewDXC() *DXC { return &DXC{Bin: "dxc"} }
+
+// Compile compiles the input shader.
+func (dxc *DXC) Compile(path, variant string, input []byte, entryPoint string, profile string) (string, error) {
+ base := dxc.WorkDir.Path(filepath.Base(path), variant, profile)
+ pathin := base + ".in"
+ pathout := base + ".out"
+ result := pathout
+
+ if err := dxc.WorkDir.WriteFile(pathin, input); err != nil {
+ return "", fmt.Errorf("unable to write shader to disk: %w", err)
+ }
+
+ cmd := exec.Command(dxc.Bin)
+
+ cmd.Args = append(cmd.Args,
+ "-Fo", pathout,
+ "-T", profile,
+ "-E", entryPoint,
+ "-Qstrip_reflect",
+ pathin,
+ )
+
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", fmt.Errorf("%s\nfailed to run %v: %w", output, cmd.Args, err)
+ }
+
+ compiled, err := ioutil.ReadFile(result)
+ if err != nil {
+ return "", fmt.Errorf("unable to read output %q: %w", pathout, err)
+ }
+
+ return string(compiled), nil
+}
+
+// winepath uses the winepath tool to convert a paths to Windows format.
+// The returned path can be used as arguments for Windows command line tools.
+func winepath(paths ...*string) error {
+ winepath := exec.Command("winepath", "--windows")
+ for _, path := range paths {
+ winepath.Args = append(winepath.Args, *path)
+ }
+ // Use a pipe instead of Output, because winepath may have left wineserver
+ // running for several seconds as a grandchild.
+ out, err := winepath.StdoutPipe()
+ if err != nil {
+ return fmt.Errorf("unable to start winepath: %w", err)
+ }
+ if err := winepath.Start(); err != nil {
+ return fmt.Errorf("unable to start winepath: %w", err)
+ }
+ var buf bytes.Buffer
+ if _, err := io.Copy(&buf, out); err != nil {
+ return fmt.Errorf("unable to run winepath: %w", err)
+ }
+ winPaths := strings.Split(strings.TrimSpace(buf.String()), "\n")
+ for i, path := range paths {
+ *path = winPaths[i]
+ }
+ return nil
+}
diff --git a/gio/gpu/internal/convertshaders/main.go b/gio/gpu/internal/convertshaders/main.go
new file mode 100644
index 0000000..a0589dc
--- /dev/null
+++ b/gio/gpu/internal/convertshaders/main.go
@@ -0,0 +1,436 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "text/template"
+
+ "realy.lol/gio/gpu/internal/driver"
+)
+
+func main() {
+ packageName := flag.String("package", "", "specify Go package name")
+ workdir := flag.String("work", "",
+ "temporary working directory (default TEMP)")
+ shadersDir := flag.String("dir", "shaders", "shaders directory")
+ directCompute := flag.Bool("directcompute", false,
+ "enable compiling DirectCompute shaders")
+
+ flag.Parse()
+
+ var work WorkDir
+ cleanup := func() {}
+ if *workdir == "" {
+ tempdir, err := ioutil.TempDir("", "shader-convert")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to create tempdir: %v\n", err)
+ os.Exit(1)
+ }
+ cleanup = func() { os.RemoveAll(tempdir) }
+ defer cleanup()
+
+ work = WorkDir(tempdir)
+ } else {
+ if abs, err := filepath.Abs(*workdir); err == nil {
+ *workdir = abs
+ }
+ work = WorkDir(*workdir)
+ }
+
+ var out bytes.Buffer
+ conv := NewConverter(work, *packageName, *shadersDir, *directCompute)
+ if err := conv.Run(&out); err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ cleanup()
+ os.Exit(1)
+ }
+
+ if err := ioutil.WriteFile("shaders.go", out.Bytes(), 0644); err != nil {
+ fmt.Fprintf(os.Stderr, "failed to create shaders: %v\n", err)
+ cleanup()
+ os.Exit(1)
+ }
+
+ cmd := exec.Command("gofmt", "-s", "-w", "shaders.go")
+ cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
+ if err := cmd.Run(); err != nil {
+ fmt.Fprintf(os.Stderr, "formatting shaders.go failed: %v\n", err)
+ cleanup()
+ os.Exit(1)
+ }
+}
+
+type Converter struct {
+ workDir WorkDir
+ shadersDir string
+ directCompute bool
+
+ packageName string
+
+ glslvalidator *GLSLValidator
+ spirv *SPIRVCross
+ fxc *FXC
+}
+
+func NewConverter(workDir WorkDir, packageName, shadersDir string,
+ directCompute bool) *Converter {
+ if abs, err := filepath.Abs(shadersDir); err == nil {
+ shadersDir = abs
+ }
+
+ conv := &Converter{}
+ conv.workDir = workDir
+ conv.shadersDir = shadersDir
+ conv.directCompute = directCompute
+
+ conv.packageName = packageName
+
+ conv.glslvalidator = NewGLSLValidator()
+ conv.spirv = NewSPIRVCross()
+ conv.fxc = NewFXC()
+
+ verifyBinaryPath(&conv.glslvalidator.Bin)
+ verifyBinaryPath(&conv.spirv.Bin)
+ // We cannot check fxc since it may depend on wine.
+
+ conv.glslvalidator.WorkDir = workDir.Dir("glslvalidator")
+ conv.fxc.WorkDir = workDir.Dir("fxc")
+ conv.spirv.WorkDir = workDir.Dir("spirv")
+
+ return conv
+}
+
+func verifyBinaryPath(bin *string) {
+ new, err := exec.LookPath(*bin)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "unable to find %q: %v\n", *bin, err)
+ } else {
+ *bin = new
+ }
+}
+
+func (conv *Converter) Run(out io.Writer) error {
+ shaders, err := filepath.Glob(filepath.Join(conv.shadersDir, "*"))
+ if len(shaders) == 0 || err != nil {
+ return fmt.Errorf("failed to list shaders in %q: %w", conv.shadersDir,
+ err)
+ }
+
+ sort.Strings(shaders)
+
+ var workers Workers
+
+ type ShaderResult struct {
+ Path string
+ Shaders []driver.ShaderSources
+ Error error
+ }
+ shaderResults := make([]ShaderResult, len(shaders))
+
+ for i, shaderPath := range shaders {
+ i, shaderPath := i, shaderPath
+
+ switch filepath.Ext(shaderPath) {
+ case ".vert", ".frag":
+ workers.Go(func() {
+ shaders, err := conv.Shader(shaderPath)
+ shaderResults[i] = ShaderResult{
+ Path: shaderPath,
+ Shaders: shaders,
+ Error: err,
+ }
+ })
+ case ".comp":
+ workers.Go(func() {
+ shaders, err := conv.ComputeShader(shaderPath)
+ shaderResults[i] = ShaderResult{
+ Path: shaderPath,
+ Shaders: shaders,
+ Error: err,
+ }
+ })
+ default:
+ continue
+ }
+ }
+
+ workers.Wait()
+
+ var allErrors string
+ for _, r := range shaderResults {
+ if r.Error != nil {
+ if len(allErrors) > 0 {
+ allErrors += "\n\n"
+ }
+ allErrors += "--- " + r.Path + " --- \n\n" + r.Error.Error() + "\n"
+ }
+ }
+ if len(allErrors) > 0 {
+ return errors.New(allErrors)
+ }
+
+ fmt.Fprintf(out, "// Code generated by build.go. DO NOT EDIT.\n\n")
+ fmt.Fprintf(out, "package %s\n\n", conv.packageName)
+ fmt.Fprintf(out, "import %q\n\n", "realy.lol/gio/gpu/internal/driver")
+
+ fmt.Fprintf(out, "var (\n")
+
+ for _, r := range shaderResults {
+ if len(r.Shaders) == 0 {
+ continue
+ }
+
+ name := filepath.Base(r.Path)
+ name = strings.ReplaceAll(name, ".", "_")
+ fmt.Fprintf(out, "\tshader_%s = ", name)
+
+ multiVariant := len(r.Shaders) > 1
+ if multiVariant {
+ fmt.Fprintf(out, "[...]driver.ShaderSources{\n")
+ }
+
+ for _, src := range r.Shaders {
+ fmt.Fprintf(out, "driver.ShaderSources{\n")
+ fmt.Fprintf(out, "Name: %#v,\n", src.Name)
+ if len(src.Inputs) > 0 {
+ fmt.Fprintf(out, "Inputs: %#v,\n", src.Inputs)
+ }
+ if u := src.Uniforms; len(u.Blocks) > 0 {
+ fmt.Fprintf(out, "Uniforms: driver.UniformsReflection{\n")
+ fmt.Fprintf(out, "Blocks: %#v,\n", u.Blocks)
+ fmt.Fprintf(out, "Locations: %#v,\n", u.Locations)
+ fmt.Fprintf(out, "Size: %d,\n", u.Size)
+ fmt.Fprintf(out, "},\n")
+ }
+ if len(src.Textures) > 0 {
+ fmt.Fprintf(out, "Textures: %#v,\n", src.Textures)
+ }
+ if len(src.GLSL100ES) > 0 {
+ fmt.Fprintf(out, "GLSL100ES: `%s`,\n", src.GLSL100ES)
+ }
+ if len(src.GLSL300ES) > 0 {
+ fmt.Fprintf(out, "GLSL300ES: `%s`,\n", src.GLSL300ES)
+ }
+ if len(src.GLSL310ES) > 0 {
+ fmt.Fprintf(out, "GLSL310ES: `%s`,\n", src.GLSL310ES)
+ }
+ if len(src.GLSL130) > 0 {
+ fmt.Fprintf(out, "GLSL130: `%s`,\n", src.GLSL130)
+ }
+ if len(src.GLSL150) > 0 {
+ fmt.Fprintf(out, "GLSL150: `%s`,\n", src.GLSL150)
+ }
+ if len(src.HLSL) > 0 {
+ fmt.Fprintf(out, "HLSL: %q,\n", src.HLSL)
+ }
+ fmt.Fprintf(out, "}")
+ if multiVariant {
+ fmt.Fprintf(out, ",")
+ }
+ fmt.Fprintf(out, "\n")
+ }
+ if multiVariant {
+ fmt.Fprintf(out, "}\n")
+ }
+ }
+ fmt.Fprintf(out, ")\n")
+
+ return nil
+}
+
+func (conv *Converter) Shader(shaderPath string) ([]driver.ShaderSources,
+ error) {
+ type Variant struct {
+ FetchColorExpr string
+ Header string
+ }
+ variantArgs := [...]Variant{
+ {
+ FetchColorExpr: `_color.color`,
+ Header: `layout(binding=0) uniform Color { vec4 color; } _color;`,
+ },
+ {
+ FetchColorExpr: `mix(_gradient.color1, _gradient.color2, clamp(vUV.x, 0.0, 1.0))`,
+ Header: `layout(binding=0) uniform Gradient { vec4 color1; vec4 color2; } _gradient;`,
+ },
+ {
+ FetchColorExpr: `texture(tex, vUV)`,
+ Header: `layout(binding=0) uniform sampler2D tex;`,
+ },
+ }
+
+ shaderTemplate, err := template.ParseFiles(shaderPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse template %q: %w", shaderPath,
+ err)
+ }
+
+ var variants []driver.ShaderSources
+ for i, variantArg := range variantArgs {
+ variantName := strconv.Itoa(i)
+ var buf bytes.Buffer
+ err := shaderTemplate.Execute(&buf, variantArg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute template %q with %#v: %w",
+ shaderPath, variantArg, err)
+ }
+
+ var sources driver.ShaderSources
+ sources.Name = filepath.Base(shaderPath)
+
+ // Ignore error; some shaders are not meant to run in GLSL 1.00.
+ sources.GLSL100ES, _, _ = conv.ShaderVariant(shaderPath, variantName,
+ buf.Bytes(), "es", "100")
+
+ var metadata Metadata
+ sources.GLSL300ES, metadata, err = conv.ShaderVariant(shaderPath,
+ variantName, buf.Bytes(), "es", "300")
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert GLSL300ES:\n%w", err)
+ }
+
+ sources.GLSL130, _, err = conv.ShaderVariant(shaderPath, variantName,
+ buf.Bytes(), "glsl", "130")
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert GLSL130:\n%w", err)
+ }
+
+ hlsl, _, err := conv.ShaderVariant(shaderPath, variantName, buf.Bytes(),
+ "hlsl", "40")
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert HLSL:\n%w", err)
+ }
+ sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName,
+ []byte(hlsl), "main", "4_0_level_9_1")
+ if err != nil {
+ // Attempt shader model 4.0. Only the gpu/headless
+ // test shaders use features not supported by level
+ // 9.1.
+ sources.HLSL, err = conv.fxc.Compile(shaderPath, variantName,
+ []byte(hlsl), "main", "4_0")
+ if err != nil {
+ return nil, fmt.Errorf("failed to compile HLSL: %w", err)
+ }
+ }
+
+ sources.GLSL150, _, err = conv.ShaderVariant(shaderPath, variantName,
+ buf.Bytes(), "glsl", "150")
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert GLSL150:\n%w", err)
+ }
+
+ sources.Uniforms = metadata.Uniforms
+ sources.Inputs = metadata.Inputs
+ sources.Textures = metadata.Textures
+
+ variants = append(variants, sources)
+ }
+
+ // If the shader don't use the variant arguments, output only a single version.
+ if variants[0].GLSL100ES == variants[1].GLSL100ES {
+ variants = variants[:1]
+ }
+
+ return variants, nil
+}
+
+func (conv *Converter) ShaderVariant(shaderPath, variant string, src []byte,
+ lang, profile string) (string, Metadata, error) {
+ spirv, err := conv.glslvalidator.Convert(shaderPath, variant,
+ lang == "hlsl", src)
+ if err != nil {
+ return "", Metadata{}, fmt.Errorf("failed to generate SPIR-V for %q: %w",
+ shaderPath, err)
+ }
+
+ dst, err := conv.spirv.Convert(shaderPath, variant, spirv, lang, profile)
+ if err != nil {
+ return "", Metadata{}, fmt.Errorf("failed to convert shader %q: %w",
+ shaderPath, err)
+ }
+
+ meta, err := conv.spirv.Metadata(shaderPath, variant, spirv)
+ if err != nil {
+ return "", Metadata{}, fmt.Errorf("failed to extract metadata for shader %q: %w",
+ shaderPath, err)
+ }
+
+ return dst, meta, nil
+}
+
+func (conv *Converter) ComputeShader(shaderPath string) ([]driver.ShaderSources,
+ error) {
+ shader, err := ioutil.ReadFile(shaderPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load shader %q: %w", shaderPath, err)
+ }
+
+ spirv, err := conv.glslvalidator.Convert(shaderPath, "", false, shader)
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert compute shader %q: %w",
+ shaderPath, err)
+ }
+
+ var sources driver.ShaderSources
+ sources.Name = filepath.Base(shaderPath)
+
+ sources.GLSL310ES, err = conv.spirv.Convert(shaderPath, "", spirv, "es",
+ "310")
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert es compute shader %q: %w",
+ shaderPath, err)
+ }
+ sources.GLSL310ES = unixLineEnding(sources.GLSL310ES)
+
+ hlslSource, err := conv.spirv.Convert(shaderPath, "", spirv, "hlsl", "50")
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert hlsl compute shader %q: %w",
+ shaderPath, err)
+ }
+
+ dxil, err := conv.fxc.Compile(shaderPath, "0", []byte(hlslSource), "main",
+ "5_0")
+ if err != nil {
+ return nil, fmt.Errorf("failed to compile hlsl compute shader %q: %w",
+ shaderPath, err)
+ }
+ if conv.directCompute {
+ sources.HLSL = dxil
+ }
+
+ return []driver.ShaderSources{sources}, nil
+}
+
+// Workers implements wait group with synchronous logging.
+type Workers struct {
+ running sync.WaitGroup
+}
+
+func (lg *Workers) Go(fn func()) {
+ lg.running.Add(1)
+ go func() {
+ defer lg.running.Done()
+ fn()
+ }()
+}
+
+func (lg *Workers) Wait() {
+ lg.running.Wait()
+}
+
+func unixLineEnding(s string) string {
+ return strings.ReplaceAll(s, "\r\n", "\n")
+}
diff --git a/gio/gpu/internal/convertshaders/spirvcross.go b/gio/gpu/internal/convertshaders/spirvcross.go
new file mode 100644
index 0000000..4252469
--- /dev/null
+++ b/gio/gpu/internal/convertshaders/spirvcross.go
@@ -0,0 +1,218 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "os/exec"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "realy.lol/gio/gpu/internal/driver"
+)
+
+// Metadata contains reflection data about a shader.
+type Metadata struct {
+ Uniforms driver.UniformsReflection
+ Inputs []driver.InputLocation
+ Textures []driver.TextureBinding
+}
+
+// SPIRVCross cross-compiles spirv shaders to es, hlsl and others.
+type SPIRVCross struct {
+ Bin string
+ WorkDir WorkDir
+}
+
+func NewSPIRVCross() *SPIRVCross { return &SPIRVCross{Bin: "spirv-cross"} }
+
+// Convert converts compute shader from spirv format to a target format.
+func (spirv *SPIRVCross) Convert(path, variant string, shader []byte,
+ target, version string) (string, error) {
+ base := spirv.WorkDir.Path(filepath.Base(path), variant)
+
+ if err := spirv.WorkDir.WriteFile(base, shader); err != nil {
+ return "", fmt.Errorf("unable to write shader to disk: %w", err)
+ }
+
+ var cmd *exec.Cmd
+ switch target {
+ case "glsl":
+ cmd = exec.Command(spirv.Bin,
+ "--no-es",
+ "--version", version,
+ )
+ case "es":
+ cmd = exec.Command(spirv.Bin,
+ "--es",
+ "--version", version,
+ )
+ case "hlsl":
+ cmd = exec.Command(spirv.Bin,
+ "--hlsl",
+ "--shader-model", version,
+ )
+ default:
+ return "", fmt.Errorf("unknown target %q", target)
+ }
+ cmd.Args = append(cmd.Args, "--no-420pack-extension", base)
+
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", fmt.Errorf("%s\nfailed to run %v: %w", out, cmd.Args, err)
+ }
+ s := string(out)
+ if target != "hlsl" {
+ // Strip Windows \r in line endings.
+ s = unixLineEnding(s)
+ }
+
+ return s, nil
+}
+
+// Metadata extracts metadata for a SPIR-V shader.
+func (spirv *SPIRVCross) Metadata(path, variant string,
+ shader []byte) (Metadata, error) {
+ base := spirv.WorkDir.Path(filepath.Base(path), variant)
+
+ if err := spirv.WorkDir.WriteFile(base, shader); err != nil {
+ return Metadata{}, fmt.Errorf("unable to write shader to disk: %w", err)
+ }
+
+ cmd := exec.Command(spirv.Bin,
+ base,
+ "--reflect",
+ )
+
+ out, err := cmd.Output()
+ if err != nil {
+ return Metadata{}, fmt.Errorf("failed to run %v: %w", cmd.Args, err)
+ }
+
+ meta, err := parseMetadata(out)
+ if err != nil {
+ return Metadata{}, fmt.Errorf("%s\nfailed to parse metadata: %w", out,
+ err)
+ }
+
+ return meta, nil
+}
+
+func parseMetadata(data []byte) (Metadata, error) {
+ var reflect struct {
+ Types map[string]struct {
+ Name string `json:"name"`
+ Members []struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Offset int `json:"offset"`
+ } `json:"members"`
+ } `json:"types"`
+ Inputs []struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Location int `json:"location"`
+ } `json:"inputs"`
+ Textures []struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Set int `json:"set"`
+ Binding int `json:"binding"`
+ } `json:"textures"`
+ UBOs []struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ BlockSize int `json:"block_size"`
+ Set int `json:"set"`
+ Binding int `json:"binding"`
+ } `json:"ubos"`
+ }
+ if err := json.Unmarshal(data, &reflect); err != nil {
+ return Metadata{}, fmt.Errorf("failed to parse reflection data: %w",
+ err)
+ }
+
+ var m Metadata
+
+ for _, input := range reflect.Inputs {
+ dataType, dataSize, err := parseDataType(input.Type)
+ if err != nil {
+ return Metadata{}, fmt.Errorf("parseReflection: %v", err)
+ }
+ m.Inputs = append(m.Inputs, driver.InputLocation{
+ Name: input.Name,
+ Location: input.Location,
+ Semantic: "TEXCOORD",
+ SemanticIndex: input.Location,
+ Type: dataType,
+ Size: dataSize,
+ })
+ }
+
+ sort.Slice(m.Inputs, func(i, j int) bool {
+ return m.Inputs[i].Location < m.Inputs[j].Location
+ })
+
+ blockOffset := 0
+ for _, block := range reflect.UBOs {
+ m.Uniforms.Blocks = append(m.Uniforms.Blocks, driver.UniformBlock{
+ Name: block.Name,
+ Binding: block.Binding,
+ })
+ t := reflect.Types[block.Type]
+ // By convention uniform block variables are named by prepending an underscore
+ // and converting to lowercase.
+ blockVar := "_" + strings.ToLower(block.Name)
+ for _, member := range t.Members {
+ dataType, size, err := parseDataType(member.Type)
+ if err != nil {
+ return Metadata{}, fmt.Errorf("failed to parse reflection data: %v",
+ err)
+ }
+ m.Uniforms.Locations = append(m.Uniforms.Locations,
+ driver.UniformLocation{
+ Name: fmt.Sprintf("%s.%s", blockVar, member.Name),
+ Type: dataType,
+ Size: size,
+ Offset: blockOffset + member.Offset,
+ })
+ }
+ blockOffset += block.BlockSize
+ }
+ m.Uniforms.Size = blockOffset
+
+ for _, texture := range reflect.Textures {
+ m.Textures = append(m.Textures, driver.TextureBinding{
+ Name: texture.Name,
+ Binding: texture.Binding,
+ })
+ }
+
+ // return m, fmt.Errorf("not yet!: %+v", reflect)
+ return m, nil
+}
+
+func parseDataType(t string) (driver.DataType, int, error) {
+ switch t {
+ case "float":
+ return driver.DataTypeFloat, 1, nil
+ case "vec2":
+ return driver.DataTypeFloat, 2, nil
+ case "vec3":
+ return driver.DataTypeFloat, 3, nil
+ case "vec4":
+ return driver.DataTypeFloat, 4, nil
+ case "int":
+ return driver.DataTypeInt, 1, nil
+ case "int2":
+ return driver.DataTypeInt, 2, nil
+ case "int3":
+ return driver.DataTypeInt, 3, nil
+ case "int4":
+ return driver.DataTypeInt, 4, nil
+ default:
+ return 0, 0, fmt.Errorf("unsupported input data type: %s", t)
+ }
+}
diff --git a/gio/gpu/internal/convertshaders/workdir.go b/gio/gpu/internal/convertshaders/workdir.go
new file mode 100644
index 0000000..4c1c092
--- /dev/null
+++ b/gio/gpu/internal/convertshaders/workdir.go
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+type WorkDir string
+
+func (wd WorkDir) Dir(path string) WorkDir {
+ dirname := filepath.Join(string(wd), path)
+ if err := os.Mkdir(dirname, 0755); err != nil {
+ if !os.IsExist(err) {
+ fmt.Fprintf(os.Stderr, "failed to create %q: %v\n", dirname, err)
+ }
+ }
+ return WorkDir(dirname)
+}
+
+func (wd WorkDir) Path(path ...string) (fullpath string) {
+ return filepath.Join(string(wd), strings.Join(path, "."))
+}
+
+func (wd WorkDir) WriteFile(path string, data []byte) error {
+ err := ioutil.WriteFile(path, data, 0644)
+ if err != nil {
+ return fmt.Errorf("unable to create %v: %w", path, err)
+ }
+ return nil
+}
diff --git a/gio/gpu/internal/d3d11/d3d11.go b/gio/gpu/internal/d3d11/d3d11.go
new file mode 100644
index 0000000..3ddf7c3
--- /dev/null
+++ b/gio/gpu/internal/d3d11/d3d11.go
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// This file exists so this package builds on non-Windows platforms.
+
+package d3d11
diff --git a/gio/gpu/internal/d3d11/d3d11_windows.go b/gio/gpu/internal/d3d11/d3d11_windows.go
new file mode 100644
index 0000000..217ea98
--- /dev/null
+++ b/gio/gpu/internal/d3d11/d3d11_windows.go
@@ -0,0 +1,787 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package d3d11
+
+import (
+ "errors"
+ "fmt"
+ "image"
+ "math"
+ "reflect"
+ "unsafe"
+
+ "golang.org/x/sys/windows"
+
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/internal/d3d11"
+)
+
+type Backend struct {
+ dev *d3d11.Device
+ ctx *d3d11.DeviceContext
+
+ // Temporary storage to avoid garbage.
+ clearColor [4]float32
+ viewport d3d11.VIEWPORT
+ depthState depthState
+ blendState blendState
+
+ // Current program.
+ prog *Program
+
+ caps driver.Caps
+
+ // fbo is the currently bound fbo.
+ fbo *Framebuffer
+
+ floatFormat uint32
+
+ // cached state objects.
+ depthStates map[depthState]*d3d11.DepthStencilState
+ blendStates map[blendState]*d3d11.BlendState
+}
+
+type blendState struct {
+ enable bool
+ sfactor driver.BlendFactor
+ dfactor driver.BlendFactor
+}
+
+type depthState struct {
+ enable bool
+ mask bool
+ fn driver.DepthFunc
+}
+
+type Texture struct {
+ backend *Backend
+ format uint32
+ bindings driver.BufferBinding
+ tex *d3d11.Texture2D
+ sampler *d3d11.SamplerState
+ resView *d3d11.ShaderResourceView
+ width int
+ height int
+}
+
+type Program struct {
+ backend *Backend
+
+ vert struct {
+ shader *d3d11.VertexShader
+ uniforms *Buffer
+ }
+ frag struct {
+ shader *d3d11.PixelShader
+ uniforms *Buffer
+ }
+}
+
+type Framebuffer struct {
+ dev *d3d11.Device
+ ctx *d3d11.DeviceContext
+ format uint32
+ resource *d3d11.Resource
+ renderTarget *d3d11.RenderTargetView
+ depthView *d3d11.DepthStencilView
+ foreign bool
+}
+
+type Buffer struct {
+ backend *Backend
+ bind uint32
+ buf *d3d11.Buffer
+ immutable bool
+}
+
+type InputLayout struct {
+ layout *d3d11.InputLayout
+}
+
+func init() {
+ driver.NewDirect3D11Device = newDirect3D11Device
+}
+
+func detectFloatFormat(dev *d3d11.Device) (uint32, bool) {
+ formats := []uint32{
+ d3d11.DXGI_FORMAT_R16_FLOAT,
+ d3d11.DXGI_FORMAT_R32_FLOAT,
+ d3d11.DXGI_FORMAT_R16G16_FLOAT,
+ d3d11.DXGI_FORMAT_R32G32_FLOAT,
+ // These last two are really wasteful, but c'est la vie.
+ d3d11.DXGI_FORMAT_R16G16B16A16_FLOAT,
+ d3d11.DXGI_FORMAT_R32G32B32A32_FLOAT,
+ }
+ for _, format := range formats {
+ need := uint32(d3d11.FORMAT_SUPPORT_TEXTURE2D | d3d11.FORMAT_SUPPORT_RENDER_TARGET)
+ if support, _ := dev.CheckFormatSupport(format); support&need == need {
+ return format, true
+ }
+ }
+ return 0, false
+}
+
+func newDirect3D11Device(api driver.Direct3D11) (driver.Device, error) {
+ dev := (*d3d11.Device)(api.Device)
+ b := &Backend{
+ dev: dev,
+ ctx: dev.GetImmediateContext(),
+ caps: driver.Caps{
+ MaxTextureSize: 2048, // 9.1 maximum
+ },
+ depthStates: make(map[depthState]*d3d11.DepthStencilState),
+ blendStates: make(map[blendState]*d3d11.BlendState),
+ }
+ featLvl := dev.GetFeatureLevel()
+ if featLvl < d3d11.FEATURE_LEVEL_9_1 {
+ d3d11.IUnknownRelease(unsafe.Pointer(dev), dev.Vtbl.Release)
+ d3d11.IUnknownRelease(unsafe.Pointer(b.ctx), b.ctx.Vtbl.Release)
+ return nil, fmt.Errorf("d3d11: feature level too low: %d", featLvl)
+ }
+ switch {
+ case featLvl >= d3d11.FEATURE_LEVEL_11_0:
+ b.caps.MaxTextureSize = 16384
+ case featLvl >= d3d11.FEATURE_LEVEL_9_3:
+ b.caps.MaxTextureSize = 4096
+ }
+ if fmt, ok := detectFloatFormat(dev); ok {
+ b.floatFormat = fmt
+ b.caps.Features |= driver.FeatureFloatRenderTargets
+ }
+ // Enable depth mask to match OpenGL.
+ b.depthState.mask = true
+ // Disable backface culling to match OpenGL.
+ state, err := dev.CreateRasterizerState(&d3d11.RASTERIZER_DESC{
+ CullMode: d3d11.CULL_NONE,
+ FillMode: d3d11.FILL_SOLID,
+ DepthClipEnable: 1,
+ })
+ if err != nil {
+ return nil, err
+ }
+ defer d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release)
+ b.ctx.RSSetState(state)
+ return b, nil
+}
+
+func (b *Backend) BeginFrame() driver.Framebuffer {
+ renderTarget, depthView := b.ctx.OMGetRenderTargets()
+ // Assume someone else is holding on to the render targets.
+ if renderTarget != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(renderTarget),
+ renderTarget.Vtbl.Release)
+ }
+ if depthView != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(depthView), depthView.Vtbl.Release)
+ }
+ return &Framebuffer{ctx: b.ctx, dev: b.dev, renderTarget: renderTarget,
+ depthView: depthView, foreign: true}
+}
+
+func (b *Backend) EndFrame() {
+}
+
+func (b *Backend) Caps() driver.Caps {
+ return b.caps
+}
+
+func (b *Backend) NewTimer() driver.Timer {
+ panic("timers not supported")
+}
+
+func (b *Backend) IsTimeContinuous() bool {
+ panic("timers not supported")
+}
+
+func (b *Backend) Release() {
+ for _, state := range b.depthStates {
+ d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release)
+ }
+ for _, state := range b.blendStates {
+ d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release)
+ }
+ d3d11.IUnknownRelease(unsafe.Pointer(b.ctx), b.ctx.Vtbl.Release)
+ *b = Backend{}
+}
+
+func (b *Backend) NewTexture(format driver.TextureFormat, width, height int,
+ minFilter, magFilter driver.TextureFilter,
+ bindings driver.BufferBinding) (driver.Texture, error) {
+ var d3dfmt uint32
+ switch format {
+ case driver.TextureFormatFloat:
+ d3dfmt = b.floatFormat
+ case driver.TextureFormatSRGB:
+ d3dfmt = d3d11.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB
+ default:
+ return nil, fmt.Errorf("unsupported texture format %d", format)
+ }
+ tex, err := b.dev.CreateTexture2D(&d3d11.TEXTURE2D_DESC{
+ Width: uint32(width),
+ Height: uint32(height),
+ MipLevels: 1,
+ ArraySize: 1,
+ Format: d3dfmt,
+ SampleDesc: d3d11.DXGI_SAMPLE_DESC{
+ Count: 1,
+ Quality: 0,
+ },
+ BindFlags: convBufferBinding(bindings),
+ })
+ if err != nil {
+ return nil, err
+ }
+ var (
+ sampler *d3d11.SamplerState
+ resView *d3d11.ShaderResourceView
+ )
+ if bindings&driver.BufferBindingTexture != 0 {
+ var filter uint32
+ switch {
+ case minFilter == driver.FilterNearest && magFilter == driver.FilterNearest:
+ filter = d3d11.FILTER_MIN_MAG_MIP_POINT
+ case minFilter == driver.FilterLinear && magFilter == driver.FilterLinear:
+ filter = d3d11.FILTER_MIN_MAG_LINEAR_MIP_POINT
+ default:
+ d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release)
+ return nil, fmt.Errorf("unsupported texture filter combination %d, %d",
+ minFilter, magFilter)
+ }
+ var err error
+ sampler, err = b.dev.CreateSamplerState(&d3d11.SAMPLER_DESC{
+ Filter: filter,
+ AddressU: d3d11.TEXTURE_ADDRESS_CLAMP,
+ AddressV: d3d11.TEXTURE_ADDRESS_CLAMP,
+ AddressW: d3d11.TEXTURE_ADDRESS_CLAMP,
+ MaxAnisotropy: 1,
+ MinLOD: -math.MaxFloat32,
+ MaxLOD: math.MaxFloat32,
+ })
+ if err != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release)
+ return nil, err
+ }
+ resView, err = b.dev.CreateShaderResourceViewTEX2D(
+ (*d3d11.Resource)(unsafe.Pointer(tex)),
+ &d3d11.SHADER_RESOURCE_VIEW_DESC_TEX2D{
+ SHADER_RESOURCE_VIEW_DESC: d3d11.SHADER_RESOURCE_VIEW_DESC{
+ Format: d3dfmt,
+ ViewDimension: d3d11.SRV_DIMENSION_TEXTURE2D,
+ },
+ Texture2D: d3d11.TEX2D_SRV{
+ MostDetailedMip: 0,
+ MipLevels: ^uint32(0),
+ },
+ },
+ )
+ if err != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release)
+ d3d11.IUnknownRelease(unsafe.Pointer(sampler), sampler.Vtbl.Release)
+ return nil, err
+ }
+ }
+ return &Texture{backend: b, format: d3dfmt, tex: tex, sampler: sampler,
+ resView: resView, bindings: bindings, width: width, height: height}, nil
+}
+
+func (b *Backend) NewFramebuffer(tex driver.Texture,
+ depthBits int) (driver.Framebuffer, error) {
+ d3dtex := tex.(*Texture)
+ if d3dtex.bindings&driver.BufferBindingFramebuffer == 0 {
+ return nil, errors.New("the texture was created without BufferBindingFramebuffer binding")
+ }
+ resource := (*d3d11.Resource)(unsafe.Pointer(d3dtex.tex))
+ renderTarget, err := b.dev.CreateRenderTargetView(resource)
+ if err != nil {
+ return nil, err
+ }
+ fbo := &Framebuffer{ctx: b.ctx, dev: b.dev, format: d3dtex.format,
+ resource: resource, renderTarget: renderTarget}
+ if depthBits > 0 {
+ depthView, err := d3d11.CreateDepthView(b.dev, d3dtex.width,
+ d3dtex.height, depthBits)
+ if err != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(renderTarget),
+ renderTarget.Vtbl.Release)
+ return nil, err
+ }
+ fbo.depthView = depthView
+ }
+ return fbo, nil
+}
+
+func (b *Backend) NewInputLayout(vertexShader driver.ShaderSources,
+ layout []driver.InputDesc) (driver.InputLayout, error) {
+ if len(vertexShader.Inputs) != len(layout) {
+ return nil, fmt.Errorf("NewInputLayout: got %d inputs, expected %d",
+ len(layout), len(vertexShader.Inputs))
+ }
+ descs := make([]d3d11.INPUT_ELEMENT_DESC, len(layout))
+ for i, l := range layout {
+ inp := vertexShader.Inputs[i]
+ cname, err := windows.BytePtrFromString(inp.Semantic)
+ if err != nil {
+ return nil, err
+ }
+ var format uint32
+ switch l.Type {
+ case driver.DataTypeFloat:
+ switch l.Size {
+ case 1:
+ format = d3d11.DXGI_FORMAT_R32_FLOAT
+ case 2:
+ format = d3d11.DXGI_FORMAT_R32G32_FLOAT
+ case 3:
+ format = d3d11.DXGI_FORMAT_R32G32B32_FLOAT
+ case 4:
+ format = d3d11.DXGI_FORMAT_R32G32B32A32_FLOAT
+ default:
+ panic("unsupported data size")
+ }
+ case driver.DataTypeShort:
+ switch l.Size {
+ case 1:
+ format = d3d11.DXGI_FORMAT_R16_SINT
+ case 2:
+ format = d3d11.DXGI_FORMAT_R16G16_SINT
+ default:
+ panic("unsupported data size")
+ }
+ default:
+ panic("unsupported data type")
+ }
+ descs[i] = d3d11.INPUT_ELEMENT_DESC{
+ SemanticName: cname,
+ SemanticIndex: uint32(inp.SemanticIndex),
+ Format: format,
+ AlignedByteOffset: uint32(l.Offset),
+ }
+ }
+ l, err := b.dev.CreateInputLayout(descs, []byte(vertexShader.HLSL))
+ if err != nil {
+ return nil, err
+ }
+ return &InputLayout{layout: l}, nil
+}
+
+func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer,
+ error) {
+ if typ&driver.BufferBindingUniforms != 0 {
+ if typ != driver.BufferBindingUniforms {
+ return nil, errors.New("uniform buffers cannot have other bindings")
+ }
+ if size%16 != 0 {
+ return nil, fmt.Errorf("constant buffer size is %d, expected a multiple of 16",
+ size)
+ }
+ }
+ bind := convBufferBinding(typ)
+ buf, err := b.dev.CreateBuffer(&d3d11.BUFFER_DESC{
+ ByteWidth: uint32(size),
+ BindFlags: bind,
+ }, nil)
+ if err != nil {
+ return nil, err
+ }
+ return &Buffer{backend: b, buf: buf, bind: bind}, nil
+}
+
+func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding,
+ data []byte) (driver.Buffer, error) {
+ if typ&driver.BufferBindingUniforms != 0 {
+ if typ != driver.BufferBindingUniforms {
+ return nil, errors.New("uniform buffers cannot have other bindings")
+ }
+ if len(data)%16 != 0 {
+ return nil, fmt.Errorf("constant buffer size is %d, expected a multiple of 16",
+ len(data))
+ }
+ }
+ bind := convBufferBinding(typ)
+ buf, err := b.dev.CreateBuffer(&d3d11.BUFFER_DESC{
+ ByteWidth: uint32(len(data)),
+ Usage: d3d11.USAGE_IMMUTABLE,
+ BindFlags: bind,
+ }, data)
+ if err != nil {
+ return nil, err
+ }
+ return &Buffer{backend: b, buf: buf, bind: bind, immutable: true}, nil
+}
+
+func (b *Backend) NewComputeProgram(shader driver.ShaderSources) (driver.Program,
+ error) {
+ panic("not implemented")
+}
+
+func (b *Backend) NewProgram(vertexShader, fragmentShader driver.ShaderSources) (driver.Program,
+ error) {
+ vs, err := b.dev.CreateVertexShader([]byte(vertexShader.HLSL))
+ if err != nil {
+ return nil, err
+ }
+ ps, err := b.dev.CreatePixelShader([]byte(fragmentShader.HLSL))
+ if err != nil {
+ return nil, err
+ }
+ p := &Program{backend: b}
+ p.vert.shader = vs
+ p.frag.shader = ps
+ return p, nil
+}
+
+func (b *Backend) Clear(colr, colg, colb, cola float32) {
+ b.clearColor = [4]float32{colr, colg, colb, cola}
+ b.ctx.ClearRenderTargetView(b.fbo.renderTarget, &b.clearColor)
+}
+
+func (b *Backend) ClearDepth(depth float32) {
+ if b.fbo.depthView != nil {
+ b.ctx.ClearDepthStencilView(b.fbo.depthView,
+ d3d11.CLEAR_DEPTH|d3d11.CLEAR_STENCIL, depth, 0)
+ }
+}
+
+func (b *Backend) Viewport(x, y, width, height int) {
+ b.viewport = d3d11.VIEWPORT{
+ TopLeftX: float32(x),
+ TopLeftY: float32(y),
+ Width: float32(width),
+ Height: float32(height),
+ MinDepth: 0.0,
+ MaxDepth: 1.0,
+ }
+ b.ctx.RSSetViewports(&b.viewport)
+}
+
+func (b *Backend) DrawArrays(mode driver.DrawMode, off, count int) {
+ b.prepareDraw(mode)
+ b.ctx.Draw(uint32(count), uint32(off))
+}
+
+func (b *Backend) DrawElements(mode driver.DrawMode, off, count int) {
+ b.prepareDraw(mode)
+ b.ctx.DrawIndexed(uint32(count), uint32(off), 0)
+}
+
+func (b *Backend) prepareDraw(mode driver.DrawMode) {
+ if p := b.prog; p != nil {
+ b.ctx.VSSetShader(p.vert.shader)
+ b.ctx.PSSetShader(p.frag.shader)
+ if buf := p.vert.uniforms; buf != nil {
+ b.ctx.VSSetConstantBuffers(buf.buf)
+ }
+ if buf := p.frag.uniforms; buf != nil {
+ b.ctx.PSSetConstantBuffers(buf.buf)
+ }
+ }
+ var topology uint32
+ switch mode {
+ case driver.DrawModeTriangles:
+ topology = d3d11.PRIMITIVE_TOPOLOGY_TRIANGLELIST
+ case driver.DrawModeTriangleStrip:
+ topology = d3d11.PRIMITIVE_TOPOLOGY_TRIANGLESTRIP
+ default:
+ panic("unsupported draw mode")
+ }
+ b.ctx.IASetPrimitiveTopology(topology)
+
+ depthState, ok := b.depthStates[b.depthState]
+ if !ok {
+ var desc d3d11.DEPTH_STENCIL_DESC
+ if b.depthState.enable {
+ desc.DepthEnable = 1
+ }
+ if b.depthState.mask {
+ desc.DepthWriteMask = d3d11.DEPTH_WRITE_MASK_ALL
+ }
+ switch b.depthState.fn {
+ case driver.DepthFuncGreater:
+ desc.DepthFunc = d3d11.COMPARISON_GREATER
+ case driver.DepthFuncGreaterEqual:
+ desc.DepthFunc = d3d11.COMPARISON_GREATER_EQUAL
+ default:
+ panic("unsupported depth func")
+ }
+ var err error
+ depthState, err = b.dev.CreateDepthStencilState(&desc)
+ if err != nil {
+ panic(err)
+ }
+ b.depthStates[b.depthState] = depthState
+ }
+ b.ctx.OMSetDepthStencilState(depthState, 0)
+
+ blendState, ok := b.blendStates[b.blendState]
+ if !ok {
+ var desc d3d11.BLEND_DESC
+ t0 := &desc.RenderTarget[0]
+ t0.RenderTargetWriteMask = d3d11.COLOR_WRITE_ENABLE_ALL
+ t0.BlendOp = d3d11.BLEND_OP_ADD
+ t0.BlendOpAlpha = d3d11.BLEND_OP_ADD
+ if b.blendState.enable {
+ t0.BlendEnable = 1
+ }
+ scol, salpha := toBlendFactor(b.blendState.sfactor)
+ dcol, dalpha := toBlendFactor(b.blendState.dfactor)
+ t0.SrcBlend = scol
+ t0.SrcBlendAlpha = salpha
+ t0.DestBlend = dcol
+ t0.DestBlendAlpha = dalpha
+ var err error
+ blendState, err = b.dev.CreateBlendState(&desc)
+ if err != nil {
+ panic(err)
+ }
+ b.blendStates[b.blendState] = blendState
+ }
+ b.ctx.OMSetBlendState(blendState, nil, 0xffffffff)
+}
+
+func (b *Backend) DepthFunc(f driver.DepthFunc) {
+ b.depthState.fn = f
+}
+
+func (b *Backend) SetBlend(enable bool) {
+ b.blendState.enable = enable
+}
+
+func (b *Backend) SetDepthTest(enable bool) {
+ b.depthState.enable = enable
+}
+
+func (b *Backend) DepthMask(mask bool) {
+ b.depthState.mask = mask
+}
+
+func (b *Backend) BlendFunc(sfactor, dfactor driver.BlendFactor) {
+ b.blendState.sfactor = sfactor
+ b.blendState.dfactor = dfactor
+}
+
+func (b *Backend) BindImageTexture(unit int, tex driver.Texture,
+ access driver.AccessBits, f driver.TextureFormat) {
+ panic("not implemented")
+}
+
+func (b *Backend) MemoryBarrier() {
+ panic("not implemented")
+}
+
+func (b *Backend) DispatchCompute(x, y, z int) {
+ panic("not implemented")
+}
+
+func (t *Texture) Upload(offset, size image.Point, pixels []byte) {
+ stride := size.X * 4
+ dst := &d3d11.BOX{
+ Left: uint32(offset.X),
+ Top: uint32(offset.Y),
+ Right: uint32(offset.X + size.X),
+ Bottom: uint32(offset.Y + size.Y),
+ Front: 0,
+ Back: 1,
+ }
+ res := (*d3d11.Resource)(unsafe.Pointer(t.tex))
+ t.backend.ctx.UpdateSubresource(res, dst, uint32(stride),
+ uint32(len(pixels)), pixels)
+}
+
+func (t *Texture) Release() {
+ d3d11.IUnknownRelease(unsafe.Pointer(t.tex), t.tex.Vtbl.Release)
+ t.tex = nil
+ if t.sampler != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(t.sampler), t.sampler.Vtbl.Release)
+ t.sampler = nil
+ }
+ if t.resView != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(t.resView), t.resView.Vtbl.Release)
+ t.resView = nil
+ }
+}
+
+func (b *Backend) BindTexture(unit int, tex driver.Texture) {
+ t := tex.(*Texture)
+ b.ctx.PSSetSamplers(uint32(unit), t.sampler)
+ b.ctx.PSSetShaderResources(uint32(unit), t.resView)
+}
+
+func (b *Backend) BindProgram(prog driver.Program) {
+ b.prog = prog.(*Program)
+}
+
+func (p *Program) Release() {
+ d3d11.IUnknownRelease(unsafe.Pointer(p.vert.shader),
+ p.vert.shader.Vtbl.Release)
+ d3d11.IUnknownRelease(unsafe.Pointer(p.frag.shader),
+ p.frag.shader.Vtbl.Release)
+ p.vert.shader = nil
+ p.frag.shader = nil
+}
+
+func (p *Program) SetStorageBuffer(binding int, buffer driver.Buffer) {
+ panic("not implemented")
+}
+
+func (p *Program) SetVertexUniforms(buf driver.Buffer) {
+ p.vert.uniforms = buf.(*Buffer)
+}
+
+func (p *Program) SetFragmentUniforms(buf driver.Buffer) {
+ p.frag.uniforms = buf.(*Buffer)
+}
+
+func (b *Backend) BindVertexBuffer(buf driver.Buffer, stride, offset int) {
+ b.ctx.IASetVertexBuffers(buf.(*Buffer).buf, uint32(stride), uint32(offset))
+}
+
+func (b *Backend) BindIndexBuffer(buf driver.Buffer) {
+ b.ctx.IASetIndexBuffer(buf.(*Buffer).buf, d3d11.DXGI_FORMAT_R16_UINT, 0)
+}
+
+func (b *Buffer) Download(data []byte) error {
+ panic("not implemented")
+}
+
+func (b *Buffer) Upload(data []byte) {
+ b.backend.ctx.UpdateSubresource((*d3d11.Resource)(unsafe.Pointer(b.buf)),
+ nil, 0, 0, data)
+}
+
+func (b *Buffer) Release() {
+ d3d11.IUnknownRelease(unsafe.Pointer(b.buf), b.buf.Vtbl.Release)
+ b.buf = nil
+}
+
+func (f *Framebuffer) ReadPixels(src image.Rectangle, pixels []byte) error {
+ if f.resource == nil {
+ return errors.New("framebuffer does not support ReadPixels")
+ }
+ w, h := src.Dx(), src.Dy()
+ tex, err := f.dev.CreateTexture2D(&d3d11.TEXTURE2D_DESC{
+ Width: uint32(w),
+ Height: uint32(h),
+ MipLevels: 1,
+ ArraySize: 1,
+ Format: f.format,
+ SampleDesc: d3d11.DXGI_SAMPLE_DESC{
+ Count: 1,
+ Quality: 0,
+ },
+ Usage: d3d11.USAGE_STAGING,
+ CPUAccessFlags: d3d11.CPU_ACCESS_READ,
+ })
+ if err != nil {
+ return fmt.Errorf("ReadPixels: %v", err)
+ }
+ defer d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release)
+ res := (*d3d11.Resource)(unsafe.Pointer(tex))
+ f.ctx.CopySubresourceRegion(
+ res,
+ 0, // Destination subresource.
+ 0, 0, 0, // Destination coordinates (x, y, z).
+ f.resource,
+ 0, // Source subresource.
+ &d3d11.BOX{
+ Left: uint32(src.Min.X),
+ Top: uint32(src.Min.Y),
+ Right: uint32(src.Max.X),
+ Bottom: uint32(src.Max.Y),
+ Front: 0,
+ Back: 1,
+ },
+ )
+ resMap, err := f.ctx.Map(res, 0, d3d11.MAP_READ, 0)
+ if err != nil {
+ return fmt.Errorf("ReadPixels: %v", err)
+ }
+ defer f.ctx.Unmap(res, 0)
+ srcPitch := w * 4
+ dstPitch := int(resMap.RowPitch)
+ mapSize := dstPitch * h
+ data := sliceOf(resMap.PData, mapSize)
+ width := w * 4
+ for r := 0; r < h; r++ {
+ pixels := pixels[r*srcPitch:]
+ copy(pixels[:width], data[r*dstPitch:])
+ }
+ return nil
+}
+
+func (b *Backend) BindFramebuffer(fbo driver.Framebuffer) {
+ b.fbo = fbo.(*Framebuffer)
+ b.ctx.OMSetRenderTargets(b.fbo.renderTarget, b.fbo.depthView)
+}
+
+func (f *Framebuffer) Invalidate() {
+}
+
+func (f *Framebuffer) Release() {
+ if f.foreign {
+ panic("framebuffer not created by NewFramebuffer")
+ }
+ if f.renderTarget != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(f.renderTarget),
+ f.renderTarget.Vtbl.Release)
+ f.renderTarget = nil
+ }
+ if f.depthView != nil {
+ d3d11.IUnknownRelease(unsafe.Pointer(f.depthView),
+ f.depthView.Vtbl.Release)
+ f.depthView = nil
+ }
+}
+
+func (b *Backend) BindInputLayout(layout driver.InputLayout) {
+ b.ctx.IASetInputLayout(layout.(*InputLayout).layout)
+}
+
+func (l *InputLayout) Release() {
+ d3d11.IUnknownRelease(unsafe.Pointer(l.layout), l.layout.Vtbl.Release)
+ l.layout = nil
+}
+
+func convBufferBinding(typ driver.BufferBinding) uint32 {
+ var bindings uint32
+ if typ&driver.BufferBindingVertices != 0 {
+ bindings |= d3d11.BIND_VERTEX_BUFFER
+ }
+ if typ&driver.BufferBindingIndices != 0 {
+ bindings |= d3d11.BIND_INDEX_BUFFER
+ }
+ if typ&driver.BufferBindingUniforms != 0 {
+ bindings |= d3d11.BIND_CONSTANT_BUFFER
+ }
+ if typ&driver.BufferBindingTexture != 0 {
+ bindings |= d3d11.BIND_SHADER_RESOURCE
+ }
+ if typ&driver.BufferBindingFramebuffer != 0 {
+ bindings |= d3d11.BIND_RENDER_TARGET
+ }
+ return bindings
+}
+
+func toBlendFactor(f driver.BlendFactor) (uint32, uint32) {
+ switch f {
+ case driver.BlendFactorOne:
+ return d3d11.BLEND_ONE, d3d11.BLEND_ONE
+ case driver.BlendFactorOneMinusSrcAlpha:
+ return d3d11.BLEND_INV_SRC_ALPHA, d3d11.BLEND_INV_SRC_ALPHA
+ case driver.BlendFactorZero:
+ return d3d11.BLEND_ZERO, d3d11.BLEND_ZERO
+ case driver.BlendFactorDstColor:
+ return d3d11.BLEND_DEST_COLOR, d3d11.BLEND_DEST_ALPHA
+ default:
+ panic("unsupported blend source factor")
+ }
+}
+
+// sliceOf returns a slice from a (native) pointer.
+func sliceOf(ptr uintptr, cap int) []byte {
+ var data []byte
+ h := (*reflect.SliceHeader)(unsafe.Pointer(&data))
+ h.Data = ptr
+ h.Cap = cap
+ h.Len = cap
+ return data
+}
diff --git a/gio/gpu/internal/driver/api.go b/gio/gpu/internal/driver/api.go
new file mode 100644
index 0000000..6e0d846
--- /dev/null
+++ b/gio/gpu/internal/driver/api.go
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package driver
+
+import (
+ "fmt"
+ "unsafe"
+
+ "realy.lol/gio/internal/gl"
+)
+
+// See gpu/api.go for documentation for the API types
+
+type API interface {
+ implementsAPI()
+}
+
+type OpenGL struct {
+ // Context contains the WebGL context for WebAssembly platforms. It is
+ // empty for all other platforms; an OpenGL context is assumed current when
+ // calling NewDevice.
+ Context gl.Context
+}
+
+type Direct3D11 struct {
+ // Device contains a *ID3D11Device.
+ Device unsafe.Pointer
+}
+
+// API specific device constructors.
+var (
+ NewOpenGLDevice func(api OpenGL) (Device, error)
+ NewDirect3D11Device func(api Direct3D11) (Device, error)
+)
+
+// NewDevice creates a new Device given the api.
+//
+// Note that the device does not assume ownership of the resources contained in
+// api; the caller must ensure the resources are valid until the device is
+// released.
+func NewDevice(api API) (Device, error) {
+ switch api := api.(type) {
+ case OpenGL:
+ if NewOpenGLDevice != nil {
+ return NewOpenGLDevice(api)
+ }
+ case Direct3D11:
+ if NewDirect3D11Device != nil {
+ return NewDirect3D11Device(api)
+ }
+ }
+ return nil, fmt.Errorf("driver: no driver available for the API %T", api)
+}
+
+func (OpenGL) implementsAPI() {}
+func (Direct3D11) implementsAPI() {}
diff --git a/gio/gpu/internal/driver/driver.go b/gio/gpu/internal/driver/driver.go
new file mode 100644
index 0000000..14d3d85
--- /dev/null
+++ b/gio/gpu/internal/driver/driver.go
@@ -0,0 +1,270 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package driver
+
+import (
+ "errors"
+ "image"
+ "time"
+)
+
+// Device represents the abstraction of underlying GPU
+// APIs such as OpenGL, Direct3D useful for rendering Gio
+// operations.
+type Device interface {
+ BeginFrame() Framebuffer
+ EndFrame()
+ Caps() Caps
+ NewTimer() Timer
+ // IsContinuousTime reports whether all timer measurements
+ // are valid at the point of call.
+ IsTimeContinuous() bool
+ NewTexture(format TextureFormat, width, height int, minFilter, magFilter TextureFilter, bindings BufferBinding) (Texture, error)
+ NewFramebuffer(tex Texture, depthBits int) (Framebuffer, error)
+ NewImmutableBuffer(typ BufferBinding, data []byte) (Buffer, error)
+ NewBuffer(typ BufferBinding, size int) (Buffer, error)
+ NewComputeProgram(shader ShaderSources) (Program, error)
+ NewProgram(vertexShader, fragmentShader ShaderSources) (Program, error)
+ NewInputLayout(vertexShader ShaderSources, layout []InputDesc) (InputLayout, error)
+
+ DepthFunc(f DepthFunc)
+ ClearDepth(d float32)
+ Clear(r, g, b, a float32)
+ Viewport(x, y, width, height int)
+ DrawArrays(mode DrawMode, off, count int)
+ DrawElements(mode DrawMode, off, count int)
+ SetBlend(enable bool)
+ SetDepthTest(enable bool)
+ DepthMask(mask bool)
+ BlendFunc(sfactor, dfactor BlendFactor)
+
+ BindInputLayout(i InputLayout)
+ BindProgram(p Program)
+ BindFramebuffer(f Framebuffer)
+ BindTexture(unit int, t Texture)
+ BindVertexBuffer(b Buffer, stride, offset int)
+ BindIndexBuffer(b Buffer)
+ BindImageTexture(unit int, texture Texture, access AccessBits, format TextureFormat)
+
+ MemoryBarrier()
+ DispatchCompute(x, y, z int)
+
+ Release()
+}
+
+type ShaderSources struct {
+ Name string
+ GLSL100ES string
+ GLSL300ES string
+ GLSL310ES string
+ GLSL130 string
+ GLSL150 string
+ HLSL string
+ Uniforms UniformsReflection
+ Inputs []InputLocation
+ Textures []TextureBinding
+}
+
+type UniformsReflection struct {
+ Blocks []UniformBlock
+ Locations []UniformLocation
+ Size int
+}
+
+type TextureBinding struct {
+ Name string
+ Binding int
+}
+
+type UniformBlock struct {
+ Name string
+ Binding int
+}
+
+type UniformLocation struct {
+ Name string
+ Type DataType
+ Size int
+ Offset int
+}
+
+type InputLocation struct {
+ // For GLSL.
+ Name string
+ Location int
+ // For HLSL.
+ Semantic string
+ SemanticIndex int
+
+ Type DataType
+ Size int
+}
+
+// InputDesc describes a vertex attribute as laid out in a Buffer.
+type InputDesc struct {
+ Type DataType
+ Size int
+
+ Offset int
+}
+
+// InputLayout is the driver specific representation of the mapping
+// between Buffers and shader attributes.
+type InputLayout interface {
+ Release()
+}
+
+type AccessBits uint8
+
+type BlendFactor uint8
+
+type DrawMode uint8
+
+type TextureFilter uint8
+type TextureFormat uint8
+
+type BufferBinding uint8
+
+type DataType uint8
+
+type DepthFunc uint8
+
+type Features uint
+
+type Caps struct {
+ // BottomLeftOrigin is true if the driver has the origin in the lower left
+ // corner. The OpenGL driver returns true.
+ BottomLeftOrigin bool
+ Features Features
+ MaxTextureSize int
+}
+
+type Program interface {
+ Release()
+ SetStorageBuffer(binding int, buf Buffer)
+ SetVertexUniforms(buf Buffer)
+ SetFragmentUniforms(buf Buffer)
+}
+
+type Buffer interface {
+ Release()
+ Upload(data []byte)
+ Download(data []byte) error
+}
+
+type Framebuffer interface {
+ Invalidate()
+ Release()
+ ReadPixels(src image.Rectangle, pixels []byte) error
+}
+
+type Timer interface {
+ Begin()
+ End()
+ Duration() (time.Duration, bool)
+ Release()
+}
+
+type Texture interface {
+ Upload(offset, size image.Point, pixels []byte)
+ Release()
+}
+
+const (
+ DepthFuncGreater DepthFunc = iota
+ DepthFuncGreaterEqual
+)
+
+const (
+ DataTypeFloat DataType = iota
+ DataTypeInt
+ DataTypeShort
+)
+
+const (
+ BufferBindingIndices BufferBinding = 1 << iota
+ BufferBindingVertices
+ BufferBindingUniforms
+ BufferBindingTexture
+ BufferBindingFramebuffer
+ BufferBindingShaderStorage
+)
+
+const (
+ TextureFormatSRGB TextureFormat = iota
+ TextureFormatFloat
+ TextureFormatRGBA8
+)
+
+const (
+ AccessRead AccessBits = 1 + iota
+ AccessWrite
+)
+
+const (
+ FilterNearest TextureFilter = iota
+ FilterLinear
+)
+
+const (
+ FeatureTimers Features = 1 << iota
+ FeatureFloatRenderTargets
+ FeatureCompute
+)
+
+const (
+ DrawModeTriangleStrip DrawMode = iota
+ DrawModeTriangles
+)
+
+const (
+ BlendFactorOne BlendFactor = iota
+ BlendFactorOneMinusSrcAlpha
+ BlendFactorZero
+ BlendFactorDstColor
+)
+
+var ErrContentLost = errors.New("buffer content lost")
+
+func (f Features) Has(feats Features) bool {
+ return f&feats == feats
+}
+
+func DownloadImage(d Device, f Framebuffer, r image.Rectangle) (*image.RGBA, error) {
+ img := image.NewRGBA(r)
+ if err := f.ReadPixels(r, img.Pix); err != nil {
+ return nil, err
+ }
+ if d.Caps().BottomLeftOrigin {
+ // OpenGL origin is in the lower-left corner. Flip the image to
+ // match.
+ flipImageY(r.Dx()*4, r.Dy(), img.Pix)
+ }
+ return img, nil
+}
+
+func flipImageY(stride, height int, pixels []byte) {
+ // Flip image in y-direction. OpenGL's origin is in the lower
+ // left corner.
+ row := make([]uint8, stride)
+ for y := 0; y < height/2; y++ {
+ y1 := height - y - 1
+ dest := y1 * stride
+ src := y * stride
+ copy(row, pixels[dest:])
+ copy(pixels[dest:], pixels[src:src+len(row)])
+ copy(pixels[src:], row)
+ }
+}
+
+func UploadImage(t Texture, offset image.Point, img *image.RGBA) {
+ var pixels []byte
+ size := img.Bounds().Size()
+ if img.Stride != size.X*4 {
+ panic("unsupported stride")
+ }
+ start := img.PixOffset(0, 0)
+ end := img.PixOffset(size.X, size.Y-1)
+ pixels = img.Pix[start:end]
+ t.Upload(offset, size, pixels)
+}
diff --git a/gio/gpu/internal/opengl/opengl.go b/gio/gpu/internal/opengl/opengl.go
new file mode 100644
index 0000000..e41dbc8
--- /dev/null
+++ b/gio/gpu/internal/opengl/opengl.go
@@ -0,0 +1,998 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package opengl
+
+import (
+ "errors"
+ "fmt"
+ "image"
+ "strings"
+ "time"
+ "unsafe"
+
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/internal/gl"
+)
+
+// Backend implements driver.Device.
+type Backend struct {
+ funcs *gl.Functions
+
+ state glstate
+
+ glver [2]int
+ gles bool
+ ubo bool
+ feats driver.Caps
+ // floatTriple holds the settings for floating point
+ // textures.
+ floatTriple textureTriple
+ // Single channel alpha textures.
+ alphaTriple textureTriple
+ srgbaTriple textureTriple
+}
+
+// State tracking.
+type glstate struct {
+ // nattr is the current number of enabled vertex arrays.
+ nattr int
+ prog *gpuProgram
+ texUnits [4]*gpuTexture
+ layout *gpuInputLayout
+ buffer bufferBinding
+}
+
+type bufferBinding struct {
+ buf *gpuBuffer
+ offset int
+ stride int
+}
+
+type gpuTimer struct {
+ funcs *gl.Functions
+ obj gl.Query
+}
+
+type gpuTexture struct {
+ backend *Backend
+ obj gl.Texture
+ triple textureTriple
+ width int
+ height int
+}
+
+type gpuFramebuffer struct {
+ backend *Backend
+ obj gl.Framebuffer
+ hasDepth bool
+ depthBuf gl.Renderbuffer
+ foreign bool
+}
+
+type gpuBuffer struct {
+ backend *Backend
+ hasBuffer bool
+ obj gl.Buffer
+ typ driver.BufferBinding
+ size int
+ immutable bool
+ version int
+ // For emulation of uniform buffers.
+ data []byte
+}
+
+type gpuProgram struct {
+ backend *Backend
+ obj gl.Program
+ nattr int
+ vertUniforms uniformsTracker
+ fragUniforms uniformsTracker
+ storage [storageBindings]*gpuBuffer
+}
+
+type uniformsTracker struct {
+ locs []uniformLocation
+ size int
+ buf *gpuBuffer
+ version int
+}
+
+type uniformLocation struct {
+ uniform gl.Uniform
+ offset int
+ typ driver.DataType
+ size int
+}
+
+type gpuInputLayout struct {
+ inputs []driver.InputLocation
+ layout []driver.InputDesc
+}
+
+// textureTriple holds the type settings for
+// a TexImage2D call.
+type textureTriple struct {
+ internalFormat gl.Enum
+ format gl.Enum
+ typ gl.Enum
+}
+
+type Context = gl.Context
+
+const (
+ storageBindings = 32
+)
+
+func init() {
+ driver.NewOpenGLDevice = newOpenGLDevice
+}
+
+func newOpenGLDevice(api driver.OpenGL) (driver.Device, error) {
+ f, err := gl.NewFunctions(api.Context)
+ if err != nil {
+ return nil, err
+ }
+ exts := strings.Split(f.GetString(gl.EXTENSIONS), " ")
+ glVer := f.GetString(gl.VERSION)
+ ver, gles, err := gl.ParseGLVersion(glVer)
+ if err != nil {
+ return nil, err
+ }
+ floatTriple, ffboErr := floatTripleFor(f, ver, exts)
+ srgbaTriple, err := srgbaTripleFor(ver, exts)
+ if err != nil {
+ return nil, err
+ }
+ gles30 := gles && ver[0] >= 3
+ gles31 := gles && (ver[0] > 3 || (ver[0] == 3 && ver[1] >= 1))
+ gl40 := !gles && ver[0] >= 4
+ b := &Backend{
+ glver: ver,
+ gles: gles,
+ ubo: gles30 || gl40,
+ funcs: f,
+ floatTriple: floatTriple,
+ alphaTriple: alphaTripleFor(ver),
+ srgbaTriple: srgbaTriple,
+ }
+ b.feats.BottomLeftOrigin = true
+ if ffboErr == nil {
+ b.feats.Features |= driver.FeatureFloatRenderTargets
+ }
+ if gles31 {
+ b.feats.Features |= driver.FeatureCompute
+ }
+ if hasExtension(exts,
+ "GL_EXT_disjoint_timer_query_webgl2") || hasExtension(exts,
+ "GL_EXT_disjoint_timer_query") {
+ b.feats.Features |= driver.FeatureTimers
+ }
+ b.feats.MaxTextureSize = f.GetInteger(gl.MAX_TEXTURE_SIZE)
+ return b, nil
+}
+
+func (b *Backend) BeginFrame() driver.Framebuffer {
+ // Assume GL state is reset between frames.
+ b.state = glstate{}
+ fboID := gl.Framebuffer(b.funcs.GetBinding(gl.FRAMEBUFFER_BINDING))
+ return &gpuFramebuffer{backend: b, obj: fboID, foreign: true}
+}
+
+func (b *Backend) EndFrame() {
+ b.funcs.ActiveTexture(gl.TEXTURE0)
+}
+
+func (b *Backend) Caps() driver.Caps {
+ return b.feats
+}
+
+func (b *Backend) NewTimer() driver.Timer {
+ return &gpuTimer{
+ funcs: b.funcs,
+ obj: b.funcs.CreateQuery(),
+ }
+}
+
+func (b *Backend) IsTimeContinuous() bool {
+ return b.funcs.GetInteger(gl.GPU_DISJOINT_EXT) == gl.FALSE
+}
+
+func (b *Backend) NewFramebuffer(tex driver.Texture,
+ depthBits int) (driver.Framebuffer, error) {
+ glErr(b.funcs)
+ gltex := tex.(*gpuTexture)
+ fb := b.funcs.CreateFramebuffer()
+ fbo := &gpuFramebuffer{backend: b, obj: fb}
+ b.BindFramebuffer(fbo)
+ if err := glErr(b.funcs); err != nil {
+ fbo.Release()
+ return nil, err
+ }
+ b.funcs.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
+ gl.TEXTURE_2D, gltex.obj, 0)
+ if depthBits > 0 {
+ size := gl.Enum(gl.DEPTH_COMPONENT16)
+ switch {
+ case depthBits > 24:
+ size = gl.DEPTH_COMPONENT32F
+ case depthBits > 16:
+ size = gl.DEPTH_COMPONENT24
+ }
+ depthBuf := b.funcs.CreateRenderbuffer()
+ b.funcs.BindRenderbuffer(gl.RENDERBUFFER, depthBuf)
+ b.funcs.RenderbufferStorage(gl.RENDERBUFFER, size, gltex.width,
+ gltex.height)
+ b.funcs.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT,
+ gl.RENDERBUFFER, depthBuf)
+ fbo.depthBuf = depthBuf
+ fbo.hasDepth = true
+ if err := glErr(b.funcs); err != nil {
+ fbo.Release()
+ return nil, err
+ }
+ }
+ if st := b.funcs.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
+ fbo.Release()
+ return nil, fmt.Errorf("incomplete framebuffer, status = 0x%x, err = %d",
+ st, b.funcs.GetError())
+ }
+ return fbo, nil
+}
+
+func (b *Backend) NewTexture(format driver.TextureFormat, width, height int,
+ minFilter, magFilter driver.TextureFilter,
+ binding driver.BufferBinding) (driver.Texture, error) {
+ glErr(b.funcs)
+ tex := &gpuTexture{backend: b, obj: b.funcs.CreateTexture(), width: width,
+ height: height}
+ switch format {
+ case driver.TextureFormatFloat:
+ tex.triple = b.floatTriple
+ case driver.TextureFormatSRGB:
+ tex.triple = b.srgbaTriple
+ case driver.TextureFormatRGBA8:
+ tex.triple = textureTriple{gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE}
+ default:
+ return nil, errors.New("unsupported texture format")
+ }
+ b.BindTexture(0, tex)
+ b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER,
+ toTexFilter(magFilter))
+ b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER,
+ toTexFilter(minFilter))
+ b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
+ b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
+ if b.gles && b.glver[0] >= 3 {
+ // Immutable textures are required for BindImageTexture, and can't hurt otherwise.
+ b.funcs.TexStorage2D(gl.TEXTURE_2D, 1, tex.triple.internalFormat, width,
+ height)
+ } else {
+ b.funcs.TexImage2D(gl.TEXTURE_2D, 0, tex.triple.internalFormat, width,
+ height, tex.triple.format, tex.triple.typ)
+ }
+ if err := glErr(b.funcs); err != nil {
+ tex.Release()
+ return nil, err
+ }
+ return tex, nil
+}
+
+func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer,
+ error) {
+ glErr(b.funcs)
+ buf := &gpuBuffer{backend: b, typ: typ, size: size}
+ if typ&driver.BufferBindingUniforms != 0 {
+ if typ != driver.BufferBindingUniforms {
+ return nil, errors.New("uniforms buffers cannot be bound as anything else")
+ }
+ if !b.ubo {
+ // GLES 2 doesn't support uniform buffers.
+ buf.data = make([]byte, size)
+ }
+ }
+ if typ&^driver.BufferBindingUniforms != 0 || b.ubo {
+ buf.hasBuffer = true
+ buf.obj = b.funcs.CreateBuffer()
+ if err := glErr(b.funcs); err != nil {
+ buf.Release()
+ return nil, err
+ }
+ firstBinding := firstBufferType(typ)
+ b.funcs.BindBuffer(firstBinding, buf.obj)
+ b.funcs.BufferData(firstBinding, size, gl.DYNAMIC_DRAW)
+ }
+ return buf, nil
+}
+
+func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding,
+ data []byte) (driver.Buffer, error) {
+ glErr(b.funcs)
+ obj := b.funcs.CreateBuffer()
+ buf := &gpuBuffer{backend: b, obj: obj, typ: typ, size: len(data),
+ hasBuffer: true}
+ firstBinding := firstBufferType(typ)
+ b.funcs.BindBuffer(firstBinding, buf.obj)
+ b.funcs.BufferData(firstBinding, len(data), gl.STATIC_DRAW)
+ buf.Upload(data)
+ buf.immutable = true
+ if err := glErr(b.funcs); err != nil {
+ buf.Release()
+ return nil, err
+ }
+ return buf, nil
+}
+
+func glErr(f *gl.Functions) error {
+ if st := f.GetError(); st != gl.NO_ERROR {
+ return fmt.Errorf("glGetError: %#x", st)
+ }
+ return nil
+}
+
+func (b *Backend) Release() {
+}
+
+func (b *Backend) MemoryBarrier() {
+ b.funcs.MemoryBarrier(gl.ALL_BARRIER_BITS)
+}
+
+func (b *Backend) DispatchCompute(x, y, z int) {
+ if p := b.state.prog; p != nil {
+ for binding, buf := range p.storage {
+ if buf != nil {
+ b.funcs.BindBufferBase(gl.SHADER_STORAGE_BUFFER, binding,
+ buf.obj)
+ }
+ }
+ }
+ b.funcs.DispatchCompute(x, y, z)
+}
+
+func (b *Backend) BindImageTexture(unit int, tex driver.Texture,
+ access driver.AccessBits, f driver.TextureFormat) {
+ t := tex.(*gpuTexture)
+ var acc gl.Enum
+ switch access {
+ case driver.AccessWrite:
+ acc = gl.WRITE_ONLY
+ case driver.AccessRead:
+ acc = gl.READ_ONLY
+ default:
+ panic("unsupported access bits")
+ }
+ var format gl.Enum
+ switch f {
+ case driver.TextureFormatRGBA8:
+ format = gl.RGBA8
+ default:
+ panic("unsupported format")
+ }
+ b.funcs.BindImageTexture(unit, t.obj, 0, false, 0, acc, format)
+}
+
+func (b *Backend) bindTexture(unit int, t *gpuTexture) {
+ if b.state.texUnits[unit] != t {
+ b.funcs.ActiveTexture(gl.TEXTURE0 + gl.Enum(unit))
+ b.funcs.BindTexture(gl.TEXTURE_2D, t.obj)
+ b.state.texUnits[unit] = t
+ }
+}
+
+func (b *Backend) useProgram(p *gpuProgram) {
+ if b.state.prog != p {
+ p.backend.funcs.UseProgram(p.obj)
+ b.state.prog = p
+ }
+}
+
+func (b *Backend) enableVertexArrays(n int) {
+ // Enable needed arrays.
+ for i := b.state.nattr; i < n; i++ {
+ b.funcs.EnableVertexAttribArray(gl.Attrib(i))
+ }
+ // Disable extra arrays.
+ for i := n; i < b.state.nattr; i++ {
+ b.funcs.DisableVertexAttribArray(gl.Attrib(i))
+ }
+ b.state.nattr = n
+}
+
+func (b *Backend) SetDepthTest(enable bool) {
+ if enable {
+ b.funcs.Enable(gl.DEPTH_TEST)
+ } else {
+ b.funcs.Disable(gl.DEPTH_TEST)
+ }
+}
+
+func (b *Backend) BlendFunc(sfactor, dfactor driver.BlendFactor) {
+ b.funcs.BlendFunc(toGLBlendFactor(sfactor), toGLBlendFactor(dfactor))
+}
+
+func toGLBlendFactor(f driver.BlendFactor) gl.Enum {
+ switch f {
+ case driver.BlendFactorOne:
+ return gl.ONE
+ case driver.BlendFactorOneMinusSrcAlpha:
+ return gl.ONE_MINUS_SRC_ALPHA
+ case driver.BlendFactorZero:
+ return gl.ZERO
+ case driver.BlendFactorDstColor:
+ return gl.DST_COLOR
+ default:
+ panic("unsupported blend factor")
+ }
+}
+
+func (b *Backend) DepthMask(mask bool) {
+ b.funcs.DepthMask(mask)
+}
+
+func (b *Backend) SetBlend(enable bool) {
+ if enable {
+ b.funcs.Enable(gl.BLEND)
+ } else {
+ b.funcs.Disable(gl.BLEND)
+ }
+}
+
+func (b *Backend) DrawElements(mode driver.DrawMode, off, count int) {
+ b.prepareDraw()
+ // off is in 16-bit indices, but DrawElements take a byte offset.
+ byteOff := off * 2
+ b.funcs.DrawElements(toGLDrawMode(mode), count, gl.UNSIGNED_SHORT, byteOff)
+}
+
+func (b *Backend) DrawArrays(mode driver.DrawMode, off, count int) {
+ b.prepareDraw()
+ b.funcs.DrawArrays(toGLDrawMode(mode), off, count)
+}
+
+func (b *Backend) prepareDraw() {
+ nattr := b.state.prog.nattr
+ b.enableVertexArrays(nattr)
+ if nattr > 0 {
+ b.setupVertexArrays()
+ }
+ if p := b.state.prog; p != nil {
+ p.updateUniforms()
+ }
+}
+
+func toGLDrawMode(mode driver.DrawMode) gl.Enum {
+ switch mode {
+ case driver.DrawModeTriangleStrip:
+ return gl.TRIANGLE_STRIP
+ case driver.DrawModeTriangles:
+ return gl.TRIANGLES
+ default:
+ panic("unsupported draw mode")
+ }
+}
+
+func (b *Backend) Viewport(x, y, width, height int) {
+ b.funcs.Viewport(x, y, width, height)
+}
+
+func (b *Backend) Clear(colR, colG, colB, colA float32) {
+ b.funcs.ClearColor(colR, colG, colB, colA)
+ b.funcs.Clear(gl.COLOR_BUFFER_BIT)
+}
+
+func (b *Backend) ClearDepth(d float32) {
+ b.funcs.ClearDepthf(d)
+ b.funcs.Clear(gl.DEPTH_BUFFER_BIT)
+}
+
+func (b *Backend) DepthFunc(f driver.DepthFunc) {
+ var glfunc gl.Enum
+ switch f {
+ case driver.DepthFuncGreater:
+ glfunc = gl.GREATER
+ case driver.DepthFuncGreaterEqual:
+ glfunc = gl.GEQUAL
+ default:
+ panic("unsupported depth func")
+ }
+ b.funcs.DepthFunc(glfunc)
+}
+
+func (b *Backend) NewInputLayout(vs driver.ShaderSources,
+ layout []driver.InputDesc) (driver.InputLayout, error) {
+ if len(vs.Inputs) != len(layout) {
+ return nil, fmt.Errorf("NewInputLayout: got %d inputs, expected %d",
+ len(layout), len(vs.Inputs))
+ }
+ for i, inp := range vs.Inputs {
+ if exp, got := inp.Size, layout[i].Size; exp != got {
+ return nil, fmt.Errorf("NewInputLayout: data size mismatch for %q: got %d expected %d",
+ inp.Name, got, exp)
+ }
+ }
+ return &gpuInputLayout{
+ inputs: vs.Inputs,
+ layout: layout,
+ }, nil
+}
+
+func (b *Backend) NewComputeProgram(src driver.ShaderSources) (driver.Program,
+ error) {
+ p, err := gl.CreateComputeProgram(b.funcs, src.GLSL310ES)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %v", src.Name, err)
+ }
+ gpuProg := &gpuProgram{
+ backend: b,
+ obj: p,
+ }
+ return gpuProg, nil
+}
+
+func (b *Backend) NewProgram(vertShader, fragShader driver.ShaderSources) (driver.Program,
+ error) {
+ attr := make([]string, len(vertShader.Inputs))
+ for _, inp := range vertShader.Inputs {
+ attr[inp.Location] = inp.Name
+ }
+ vsrc, fsrc := vertShader.GLSL100ES, fragShader.GLSL100ES
+ if b.glver[0] >= 3 {
+ // OpenGL (ES) 3.0.
+ switch {
+ case b.gles:
+ vsrc, fsrc = vertShader.GLSL300ES, fragShader.GLSL300ES
+ case b.glver[0] >= 4 || b.glver[1] >= 2:
+ // OpenGL 3.2 Core only accepts glsl 1.50 or newer.
+ vsrc, fsrc = vertShader.GLSL150, fragShader.GLSL150
+ default:
+ vsrc, fsrc = vertShader.GLSL130, fragShader.GLSL130
+ }
+ }
+ p, err := gl.CreateProgram(b.funcs, vsrc, fsrc, attr)
+ if err != nil {
+ return nil, err
+ }
+ gpuProg := &gpuProgram{
+ backend: b,
+ obj: p,
+ nattr: len(attr),
+ }
+ b.BindProgram(gpuProg)
+ // Bind texture uniforms.
+ for _, tex := range vertShader.Textures {
+ u := b.funcs.GetUniformLocation(p, tex.Name)
+ if u.Valid() {
+ b.funcs.Uniform1i(u, tex.Binding)
+ }
+ }
+ for _, tex := range fragShader.Textures {
+ u := b.funcs.GetUniformLocation(p, tex.Name)
+ if u.Valid() {
+ b.funcs.Uniform1i(u, tex.Binding)
+ }
+ }
+ if b.ubo {
+ for _, block := range vertShader.Uniforms.Blocks {
+ blockIdx := b.funcs.GetUniformBlockIndex(p, block.Name)
+ if blockIdx != gl.INVALID_INDEX {
+ b.funcs.UniformBlockBinding(p, blockIdx, uint(block.Binding))
+ }
+ }
+ // To match Direct3D 11 with separate vertex and fragment
+ // shader uniform buffers, offset all fragment blocks to be
+ // located after the vertex blocks.
+ off := len(vertShader.Uniforms.Blocks)
+ for _, block := range fragShader.Uniforms.Blocks {
+ blockIdx := b.funcs.GetUniformBlockIndex(p, block.Name)
+ if blockIdx != gl.INVALID_INDEX {
+ b.funcs.UniformBlockBinding(p, blockIdx,
+ uint(block.Binding+off))
+ }
+ }
+ } else {
+ gpuProg.vertUniforms.setup(b.funcs, p, vertShader.Uniforms.Size,
+ vertShader.Uniforms.Locations)
+ gpuProg.fragUniforms.setup(b.funcs, p, fragShader.Uniforms.Size,
+ fragShader.Uniforms.Locations)
+ }
+ return gpuProg, nil
+}
+
+func lookupUniform(funcs *gl.Functions, p gl.Program,
+ loc driver.UniformLocation) uniformLocation {
+ u := funcs.GetUniformLocation(p, loc.Name)
+ if !u.Valid() {
+ panic(fmt.Errorf("uniform %q not found", loc.Name))
+ }
+ return uniformLocation{uniform: u, offset: loc.Offset, typ: loc.Type,
+ size: loc.Size}
+}
+
+func (p *gpuProgram) SetStorageBuffer(binding int, buffer driver.Buffer) {
+ buf := buffer.(*gpuBuffer)
+ if buf.typ&driver.BufferBindingShaderStorage == 0 {
+ panic("not a shader storage buffer")
+ }
+ p.storage[binding] = buf
+}
+
+func (p *gpuProgram) SetVertexUniforms(buffer driver.Buffer) {
+ p.vertUniforms.setBuffer(buffer)
+}
+
+func (p *gpuProgram) SetFragmentUniforms(buffer driver.Buffer) {
+ p.fragUniforms.setBuffer(buffer)
+}
+
+func (p *gpuProgram) updateUniforms() {
+ f := p.backend.funcs
+ if p.backend.ubo {
+ if b := p.vertUniforms.buf; b != nil {
+ f.BindBufferBase(gl.UNIFORM_BUFFER, 0, b.obj)
+ }
+ if b := p.fragUniforms.buf; b != nil {
+ f.BindBufferBase(gl.UNIFORM_BUFFER, 1, b.obj)
+ }
+ } else {
+ p.vertUniforms.update(f)
+ p.fragUniforms.update(f)
+ }
+}
+
+func (b *Backend) BindProgram(prog driver.Program) {
+ p := prog.(*gpuProgram)
+ b.useProgram(p)
+}
+
+func (p *gpuProgram) Release() {
+ p.backend.funcs.DeleteProgram(p.obj)
+}
+
+func (u *uniformsTracker) setup(funcs *gl.Functions, p gl.Program,
+ uniformSize int, uniforms []driver.UniformLocation) {
+ u.locs = make([]uniformLocation, len(uniforms))
+ for i, uniform := range uniforms {
+ u.locs[i] = lookupUniform(funcs, p, uniform)
+ }
+ u.size = uniformSize
+}
+
+func (u *uniformsTracker) setBuffer(buffer driver.Buffer) {
+ buf := buffer.(*gpuBuffer)
+ if buf.typ&driver.BufferBindingUniforms == 0 {
+ panic("not a uniform buffer")
+ }
+ if buf.size < u.size {
+ panic(fmt.Errorf("uniform buffer too small, got %d need %d", buf.size,
+ u.size))
+ }
+ u.buf = buf
+ // Force update.
+ u.version = buf.version - 1
+}
+
+func (p *uniformsTracker) update(funcs *gl.Functions) {
+ b := p.buf
+ if b == nil || b.version == p.version {
+ return
+ }
+ p.version = b.version
+ data := b.data
+ for _, u := range p.locs {
+ data := data[u.offset:]
+ switch {
+ case u.typ == driver.DataTypeFloat && u.size == 1:
+ data := data[:4]
+ v := *(*[1]float32)(unsafe.Pointer(&data[0]))
+ funcs.Uniform1f(u.uniform, v[0])
+ case u.typ == driver.DataTypeFloat && u.size == 2:
+ data := data[:8]
+ v := *(*[2]float32)(unsafe.Pointer(&data[0]))
+ funcs.Uniform2f(u.uniform, v[0], v[1])
+ case u.typ == driver.DataTypeFloat && u.size == 3:
+ data := data[:12]
+ v := *(*[3]float32)(unsafe.Pointer(&data[0]))
+ funcs.Uniform3f(u.uniform, v[0], v[1], v[2])
+ case u.typ == driver.DataTypeFloat && u.size == 4:
+ data := data[:16]
+ v := *(*[4]float32)(unsafe.Pointer(&data[0]))
+ funcs.Uniform4f(u.uniform, v[0], v[1], v[2], v[3])
+ default:
+ panic("unsupported uniform data type or size")
+ }
+ }
+}
+
+func (b *gpuBuffer) Upload(data []byte) {
+ if b.immutable {
+ panic("immutable buffer")
+ }
+ if len(data) > b.size {
+ panic("buffer size overflow")
+ }
+ b.version++
+ copy(b.data, data)
+ if b.hasBuffer {
+ firstBinding := firstBufferType(b.typ)
+ b.backend.funcs.BindBuffer(firstBinding, b.obj)
+ if len(data) == b.size {
+ // the iOS GL implementation doesn't recognize when BufferSubData
+ // clears the entire buffer. Tell it and avoid GPU stalls.
+ // See also https://github.com/godotengine/godot/issues/23956.
+ b.backend.funcs.BufferData(firstBinding, b.size, gl.DYNAMIC_DRAW)
+ }
+ b.backend.funcs.BufferSubData(firstBinding, 0, data)
+ }
+}
+
+func (b *gpuBuffer) Download(data []byte) error {
+ if len(data) > b.size {
+ panic("buffer size overflow")
+ }
+ if !b.hasBuffer {
+ copy(data, b.data)
+ return nil
+ }
+ firstBinding := firstBufferType(b.typ)
+ b.backend.funcs.BindBuffer(firstBinding, b.obj)
+ bufferMap := b.backend.funcs.MapBufferRange(firstBinding, 0, len(data),
+ gl.MAP_READ_BIT)
+ if bufferMap == nil {
+ return fmt.Errorf("MapBufferRange: error %#x",
+ b.backend.funcs.GetError())
+ }
+ copy(data, bufferMap)
+ if !b.backend.funcs.UnmapBuffer(firstBinding) {
+ return driver.ErrContentLost
+ }
+ return nil
+}
+
+func (b *gpuBuffer) Release() {
+ if b.hasBuffer {
+ b.backend.funcs.DeleteBuffer(b.obj)
+ b.hasBuffer = false
+ }
+}
+
+func (b *Backend) BindVertexBuffer(buf driver.Buffer, stride, offset int) {
+ gbuf := buf.(*gpuBuffer)
+ if gbuf.typ&driver.BufferBindingVertices == 0 {
+ panic("not a vertex buffer")
+ }
+ b.state.buffer = bufferBinding{buf: gbuf, stride: stride, offset: offset}
+}
+
+func (b *Backend) setupVertexArrays() {
+ layout := b.state.layout
+ if layout == nil {
+ return
+ }
+ buf := b.state.buffer
+ b.funcs.BindBuffer(gl.ARRAY_BUFFER, buf.buf.obj)
+ for i, inp := range layout.inputs {
+ l := layout.layout[i]
+ var gltyp gl.Enum
+ switch l.Type {
+ case driver.DataTypeFloat:
+ gltyp = gl.FLOAT
+ case driver.DataTypeShort:
+ gltyp = gl.SHORT
+ default:
+ panic("unsupported data type")
+ }
+ b.funcs.VertexAttribPointer(gl.Attrib(inp.Location), l.Size, gltyp,
+ false, buf.stride, buf.offset+l.Offset)
+ }
+}
+
+func (b *Backend) BindIndexBuffer(buf driver.Buffer) {
+ gbuf := buf.(*gpuBuffer)
+ if gbuf.typ&driver.BufferBindingIndices == 0 {
+ panic("not an index buffer")
+ }
+ b.funcs.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, gbuf.obj)
+}
+
+func (b *Backend) BlitFramebuffer(dst, src driver.Framebuffer,
+ srect, drect image.Rectangle) {
+ b.funcs.BindFramebuffer(gl.DRAW_FRAMEBUFFER, dst.(*gpuFramebuffer).obj)
+ b.funcs.BindFramebuffer(gl.READ_FRAMEBUFFER, src.(*gpuFramebuffer).obj)
+ b.funcs.BlitFramebuffer(
+ srect.Min.X, srect.Min.Y, srect.Max.X, srect.Max.Y,
+ drect.Min.X, drect.Min.Y, drect.Max.X, drect.Max.Y,
+ gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT|gl.STENCIL_BUFFER_BIT,
+ gl.NEAREST)
+}
+
+func (f *gpuFramebuffer) ReadPixels(src image.Rectangle, pixels []byte) error {
+ glErr(f.backend.funcs)
+ f.backend.BindFramebuffer(f)
+ if len(pixels) < src.Dx()*src.Dy()*4 {
+ return errors.New("unexpected RGBA size")
+ }
+ f.backend.funcs.ReadPixels(src.Min.X, src.Min.Y, src.Dx(), src.Dy(),
+ gl.RGBA, gl.UNSIGNED_BYTE, pixels)
+ return glErr(f.backend.funcs)
+}
+
+func (b *Backend) BindFramebuffer(fbo driver.Framebuffer) {
+ b.funcs.BindFramebuffer(gl.FRAMEBUFFER, fbo.(*gpuFramebuffer).obj)
+}
+
+func (f *gpuFramebuffer) Invalidate() {
+ f.backend.BindFramebuffer(f)
+ f.backend.funcs.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0)
+}
+
+func (f *gpuFramebuffer) Release() {
+ if f.foreign {
+ panic("framebuffer not created by NewFramebuffer")
+ }
+ f.backend.funcs.DeleteFramebuffer(f.obj)
+ if f.hasDepth {
+ f.backend.funcs.DeleteRenderbuffer(f.depthBuf)
+ }
+}
+
+func toTexFilter(f driver.TextureFilter) int {
+ switch f {
+ case driver.FilterNearest:
+ return gl.NEAREST
+ case driver.FilterLinear:
+ return gl.LINEAR
+ default:
+ panic("unsupported texture filter")
+ }
+}
+
+func (b *Backend) BindTexture(unit int, t driver.Texture) {
+ b.bindTexture(unit, t.(*gpuTexture))
+}
+
+func (t *gpuTexture) Release() {
+ t.backend.funcs.DeleteTexture(t.obj)
+}
+
+func (t *gpuTexture) Upload(offset, size image.Point, pixels []byte) {
+ if min := size.X * size.Y * 4; min > len(pixels) {
+ panic(fmt.Errorf("size %d larger than data %d", min, len(pixels)))
+ }
+ t.backend.BindTexture(0, t)
+ t.backend.funcs.TexSubImage2D(gl.TEXTURE_2D, 0, offset.X, offset.Y, size.X,
+ size.Y, t.triple.format, t.triple.typ, pixels)
+}
+
+func (t *gpuTimer) Begin() {
+ t.funcs.BeginQuery(gl.TIME_ELAPSED_EXT, t.obj)
+}
+
+func (t *gpuTimer) End() {
+ t.funcs.EndQuery(gl.TIME_ELAPSED_EXT)
+}
+
+func (t *gpuTimer) ready() bool {
+ return t.funcs.GetQueryObjectuiv(t.obj,
+ gl.QUERY_RESULT_AVAILABLE) == gl.TRUE
+}
+
+func (t *gpuTimer) Release() {
+ t.funcs.DeleteQuery(t.obj)
+}
+
+func (t *gpuTimer) Duration() (time.Duration, bool) {
+ if !t.ready() {
+ return 0, false
+ }
+ nanos := t.funcs.GetQueryObjectuiv(t.obj, gl.QUERY_RESULT)
+ return time.Duration(nanos), true
+}
+
+func (b *Backend) BindInputLayout(l driver.InputLayout) {
+ b.state.layout = l.(*gpuInputLayout)
+}
+
+func (l *gpuInputLayout) Release() {}
+
+// floatTripleFor determines the best texture triple for floating point FBOs.
+func floatTripleFor(f *gl.Functions, ver [2]int, exts []string) (textureTriple,
+ error) {
+ var triples []textureTriple
+ if ver[0] >= 3 {
+ triples = append(triples,
+ textureTriple{gl.R16F, gl.Enum(gl.RED), gl.Enum(gl.HALF_FLOAT)})
+ }
+ // According to the OES_texture_half_float specification, EXT_color_buffer_half_float is needed to
+ // render to FBOs. However, the Safari WebGL1 implementation does support half-float FBOs but does not
+ // report EXT_color_buffer_half_float support. The triples are verified below, so it doesn't matter if we're
+ // wrong.
+ if hasExtension(exts, "GL_OES_texture_half_float") || hasExtension(exts,
+ "GL_EXT_color_buffer_half_float") {
+ // Try single channel.
+ triples = append(triples,
+ textureTriple{gl.LUMINANCE, gl.Enum(gl.LUMINANCE),
+ gl.Enum(gl.HALF_FLOAT_OES)})
+ // Fallback to 4 channels.
+ triples = append(triples, textureTriple{gl.RGBA, gl.Enum(gl.RGBA),
+ gl.Enum(gl.HALF_FLOAT_OES)})
+ }
+ if hasExtension(exts, "GL_OES_texture_float") || hasExtension(exts,
+ "GL_EXT_color_buffer_float") {
+ triples = append(triples,
+ textureTriple{gl.RGBA, gl.Enum(gl.RGBA), gl.Enum(gl.FLOAT)})
+ }
+ tex := f.CreateTexture()
+ defer f.DeleteTexture(tex)
+ f.BindTexture(gl.TEXTURE_2D, tex)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
+ fbo := f.CreateFramebuffer()
+ defer f.DeleteFramebuffer(fbo)
+ defFBO := gl.Framebuffer(f.GetBinding(gl.FRAMEBUFFER_BINDING))
+ f.BindFramebuffer(gl.FRAMEBUFFER, fbo)
+ defer f.BindFramebuffer(gl.FRAMEBUFFER, defFBO)
+ var attempts []string
+ for _, tt := range triples {
+ const size = 256
+ f.TexImage2D(gl.TEXTURE_2D, 0, tt.internalFormat, size, size, tt.format,
+ tt.typ)
+ f.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
+ gl.TEXTURE_2D, tex, 0)
+ st := f.CheckFramebufferStatus(gl.FRAMEBUFFER)
+ if st == gl.FRAMEBUFFER_COMPLETE {
+ return tt, nil
+ }
+ attempts = append(attempts,
+ fmt.Sprintf("(0x%x, 0x%x, 0x%x): 0x%x", tt.internalFormat,
+ tt.format, tt.typ, st))
+ }
+ return textureTriple{}, fmt.Errorf("floating point fbos not supported (attempted %s)",
+ attempts)
+}
+
+func srgbaTripleFor(ver [2]int, exts []string) (textureTriple, error) {
+ switch {
+ case ver[0] >= 3:
+ return textureTriple{gl.SRGB8_ALPHA8, gl.Enum(gl.RGBA),
+ gl.Enum(gl.UNSIGNED_BYTE)}, nil
+ case hasExtension(exts, "GL_EXT_sRGB"):
+ return textureTriple{gl.SRGB_ALPHA_EXT, gl.Enum(gl.SRGB_ALPHA_EXT),
+ gl.Enum(gl.UNSIGNED_BYTE)}, nil
+ default:
+ return textureTriple{}, errors.New("no sRGB texture formats found")
+ }
+}
+
+func alphaTripleFor(ver [2]int) textureTriple {
+ intf, f := gl.Enum(gl.R8), gl.Enum(gl.RED)
+ if ver[0] < 3 {
+ // R8, RED not supported on OpenGL ES 2.0.
+ intf, f = gl.LUMINANCE, gl.Enum(gl.LUMINANCE)
+ }
+ return textureTriple{intf, f, gl.UNSIGNED_BYTE}
+}
+
+func hasExtension(exts []string, ext string) bool {
+ for _, e := range exts {
+ if ext == e {
+ return true
+ }
+ }
+ return false
+}
+
+func firstBufferType(typ driver.BufferBinding) gl.Enum {
+ switch {
+ case typ&driver.BufferBindingIndices != 0:
+ return gl.ELEMENT_ARRAY_BUFFER
+ case typ&driver.BufferBindingVertices != 0:
+ return gl.ARRAY_BUFFER
+ case typ&driver.BufferBindingUniforms != 0:
+ return gl.UNIFORM_BUFFER
+ case typ&driver.BufferBindingShaderStorage != 0:
+ return gl.SHADER_STORAGE_BUFFER
+ default:
+ panic("unsupported buffer type")
+ }
+}
diff --git a/gio/gpu/internal/rendertest/bench_test.go b/gio/gpu/internal/rendertest/bench_test.go
new file mode 100644
index 0000000..ac4ec5f
--- /dev/null
+++ b/gio/gpu/internal/rendertest/bench_test.go
@@ -0,0 +1,321 @@
+package rendertest
+
+import (
+ "image"
+ "image/color"
+ "math"
+ "testing"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/font/gofont"
+ "realy.lol/gio/gpu/headless"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/widget/material"
+)
+
+// use some global variables for benchmarking so as to not pollute
+// the reported allocs with allocations that we do not want to count.
+var (
+ c1, c2, c3 = make(chan op.CallOp), make(chan op.CallOp), make(chan op.CallOp)
+ op1, op2, op3 op.Ops
+)
+
+func setupBenchmark(b *testing.B) (layout.Context, *headless.Window,
+ *material.Theme) {
+ sz := image.Point{X: 1024, Y: 1200}
+ w := newWindow(b, sz.X, sz.Y)
+ ops := new(op.Ops)
+ gtx := layout.Context{
+ Ops: ops,
+ Constraints: layout.Exact(sz),
+ }
+ th := material.NewTheme(gofont.Collection())
+ return gtx, w, th
+}
+
+func resetOps(gtx layout.Context) {
+ gtx.Ops.Reset()
+ op1.Reset()
+ op2.Reset()
+ op3.Reset()
+}
+
+func finishBenchmark(b *testing.B, w *headless.Window) {
+ b.StopTimer()
+ if *dumpImages {
+ img, err := w.Screenshot()
+ w.Release()
+ if err != nil {
+ b.Error(err)
+ }
+ if err := saveImage(b.Name()+".png", img); err != nil {
+ b.Error(err)
+ }
+ }
+}
+
+func BenchmarkDrawUICached(b *testing.B) {
+ // As BenchmarkDraw but the same op.Ops every time that is not reset - this
+ // should thus allow for maximal cache usage.
+ gtx, w, th := setupBenchmark(b)
+ drawCore(gtx, th)
+ w.Frame(gtx.Ops)
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ w.Frame(gtx.Ops)
+ }
+ finishBenchmark(b, w)
+}
+
+func BenchmarkDrawUI(b *testing.B) {
+ // BenchmarkDraw is intended as a reasonable overall benchmark for
+ // the drawing performance of the full drawing pipeline, in each iteration
+ // resetting the ops and drawing, similar to how a typical UI would function.
+ // This will allow font caching across frames.
+ gtx, w, th := setupBenchmark(b)
+ drawCore(gtx, th)
+ w.Frame(gtx.Ops)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ resetOps(gtx)
+
+ p := op.Save(gtx.Ops)
+ off := float32(math.Mod(float64(i)/10, 10))
+ op.Offset(f32.Pt(off, off)).Add(gtx.Ops)
+
+ drawCore(gtx, th)
+
+ p.Load()
+ w.Frame(gtx.Ops)
+ }
+ finishBenchmark(b, w)
+}
+
+func BenchmarkDrawUITransformed(b *testing.B) {
+ // Like BenchmarkDraw UI but transformed at every frame
+ gtx, w, th := setupBenchmark(b)
+ drawCore(gtx, th)
+ w.Frame(gtx.Ops)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ resetOps(gtx)
+
+ p := op.Save(gtx.Ops)
+ angle := float32(math.Mod(float64(i)/1000, 0.05))
+ a := f32.Affine2D{}.Shear(f32.Point{}, angle, angle).Rotate(f32.Point{},
+ angle)
+ op.Affine(a).Add(gtx.Ops)
+
+ drawCore(gtx, th)
+
+ p.Load()
+ w.Frame(gtx.Ops)
+ }
+ finishBenchmark(b, w)
+}
+
+func Benchmark1000Circles(b *testing.B) {
+ // Benchmark1000Shapes draws 1000 individual shapes such that no caching between
+ // shapes will be possible and resets buffers on each operation to prevent caching
+ // between frames.
+ gtx, w, _ := setupBenchmark(b)
+ draw1000Circles(gtx)
+ w.Frame(gtx.Ops)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ resetOps(gtx)
+ draw1000Circles(gtx)
+ w.Frame(gtx.Ops)
+ }
+ finishBenchmark(b, w)
+}
+
+func Benchmark1000CirclesInstanced(b *testing.B) {
+ // Like Benchmark1000Circles but will record them and thus allow for caching between
+ // them.
+ gtx, w, _ := setupBenchmark(b)
+ draw1000CirclesInstanced(gtx)
+ w.Frame(gtx.Ops)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ resetOps(gtx)
+ draw1000CirclesInstanced(gtx)
+ w.Frame(gtx.Ops)
+ }
+ finishBenchmark(b, w)
+}
+
+func draw1000Circles(gtx layout.Context) {
+ ops := gtx.Ops
+ for x := 0; x < 100; x++ {
+ p := op.Save(ops)
+ op.Offset(f32.Pt(float32(x*10), 0)).Add(ops)
+ for y := 0; y < 10; y++ {
+ paint.FillShape(ops,
+ color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100,
+ A: 120},
+ clip.RRect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5,
+ NW: 5}.Op(ops),
+ )
+ op.Offset(f32.Pt(0, float32(100))).Add(ops)
+ }
+ p.Load()
+ }
+}
+
+func draw1000CirclesInstanced(gtx layout.Context) {
+ ops := gtx.Ops
+
+ r := op.Record(ops)
+ clip.RRect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5,
+ NW: 5}.Add(ops)
+ paint.PaintOp{}.Add(ops)
+ c := r.Stop()
+
+ for x := 0; x < 100; x++ {
+ p := op.Save(ops)
+ op.Offset(f32.Pt(float32(x*10), 0)).Add(ops)
+ for y := 0; y < 10; y++ {
+ pi := op.Save(ops)
+ paint.ColorOp{Color: color.NRGBA{R: 100 + uint8(x),
+ G: 100 + uint8(y), B: 100, A: 120}}.Add(ops)
+ c.Add(ops)
+ pi.Load()
+ op.Offset(f32.Pt(0, float32(100))).Add(ops)
+ }
+ p.Load()
+ }
+}
+
+func drawCore(gtx layout.Context, th *material.Theme) {
+ c1 := drawIndividualShapes(gtx, th)
+ c2 := drawShapeInstances(gtx, th)
+ c3 := drawText(gtx, th)
+
+ (<-c1).Add(gtx.Ops)
+ (<-c2).Add(gtx.Ops)
+ (<-c3).Add(gtx.Ops)
+}
+
+func drawIndividualShapes(gtx layout.Context,
+ th *material.Theme) chan op.CallOp {
+ // draw 81 rounded rectangles of different solid colors - each one individually
+ go func() {
+ ops := &op1
+ c := op.Record(ops)
+ for x := 0; x < 9; x++ {
+ p := op.Save(ops)
+ op.Offset(f32.Pt(float32(x*50), 0)).Add(ops)
+ for y := 0; y < 9; y++ {
+ paint.FillShape(ops,
+ color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100,
+ A: 120},
+ clip.RRect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10,
+ SW: 10, NW: 10}.Op(ops),
+ )
+ op.Offset(f32.Pt(0, float32(50))).Add(ops)
+ }
+ p.Load()
+ }
+ c1 <- c.Stop()
+ }()
+ return c1
+}
+
+func drawShapeInstances(gtx layout.Context, th *material.Theme) chan op.CallOp {
+ // draw 400 textured circle instances, each with individual transform
+ go func() {
+ ops := &op2
+ co := op.Record(ops)
+
+ r := op.Record(ops)
+ clip.RRect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10,
+ NW: 10}.Add(ops)
+ paint.PaintOp{}.Add(ops)
+ c := r.Stop()
+
+ squares.Add(ops)
+ rad := float32(0)
+ for x := 0; x < 20; x++ {
+ for y := 0; y < 20; y++ {
+ p := op.Save(ops)
+ op.Offset(f32.Pt(float32(x*50+25), float32(y*50+25))).Add(ops)
+ c.Add(ops)
+ p.Load()
+ rad += math.Pi * 2 / 400
+ }
+ }
+ c2 <- co.Stop()
+ }()
+ return c2
+}
+
+func drawText(gtx layout.Context, th *material.Theme) chan op.CallOp {
+ // draw 40 lines of text with different transforms.
+ go func() {
+ ops := &op3
+ c := op.Record(ops)
+
+ txt := material.H6(th, "")
+ for x := 0; x < 40; x++ {
+ txt.Text = textRows[x]
+ p := op.Save(ops)
+ op.Offset(f32.Pt(float32(0), float32(24*x))).Add(ops)
+ gtx.Ops = ops
+ txt.Layout(gtx)
+ p.Load()
+ }
+ c3 <- c.Stop()
+ }()
+ return c3
+}
+
+var textRows = []string{
+ "1. I learned from my grandfather, Verus, to use good manners, and to",
+ "put restraint on anger. 2. In the famous memory of my father I had a",
+ "pattern of modesty and manliness. 3. Of my mother I learned to be",
+ "pious and generous; to keep myself not only from evil deeds, but even",
+ "from evil thoughts; and to live with a simplicity which is far from",
+ "customary among the rich. 4. I owe it to my great-grandfather that I",
+ "did not attend public lectures and discussions, but had good and able",
+ "teachers at home; and I owe him also the knowledge that for things of",
+ "this nature a man should count no expense too great.",
+ "5. My tutor taught me not to favour either green or blue at the",
+ "chariot races, nor, in the contests of gladiators, to be a supporter",
+ "either of light or heavy armed. He taught me also to endure labour;",
+ "not to need many things; to serve myself without troubling others; not",
+ "to intermeddle in the affairs of others, and not easily to listen to",
+ "slanders against them.",
+ "6. Of Diognetus I had the lesson not to busy myself about vain things;",
+ "not to credit the great professions of such as pretend to work",
+ "wonders, or of sorcerers about their charms, and their expelling of",
+ "Demons and the like; not to keep quails (for fighting or divination),",
+ "nor to run after such things; to suffer freedom of speech in others,",
+ "and to apply myself heartily to philosophy. Him also I must thank for",
+ "my hearing first Bacchius, then Tandasis and Marcianus; that I wrote",
+ "dialogues in my youth, and took a liking to the philosopher's pallet",
+ "and skins, and to the other things which, by the Grecian discipline,",
+ "belong to that profession.",
+ "7. To Rusticus I owe my first apprehensions that my nature needed",
+ "reform and cure; and that I did not fall into the ambition of the",
+ "common Sophists, either by composing speculative writings or by",
+ "declaiming harangues of exhortation in public; further, that I never",
+ "strove to be admired by ostentation of great patience in an ascetic",
+ "life, or by display of activity and application; that I gave over the",
+ "study of rhetoric, poetry, and the graces of language; and that I did",
+ "not pace my house in my senatorial robes, or practise any similar",
+ "affectation. I observed also the simplicity of style in his letters,",
+ "particularly in that which he wrote to my mother from Sinuessa. I",
+ "learned from him to be easily appeased, and to be readily reconciled",
+ "with those who had displeased me or given cause of offence, so soon as",
+ "they inclined to make their peace; to read with care; not to rest",
+ "satisfied with a slight and superficial knowledge; nor quickly to",
+ "assent to great talkers. I have him to thank that I met with the",
+}
diff --git a/gio/gpu/internal/rendertest/clip_test.go b/gio/gpu/internal/rendertest/clip_test.go
new file mode 100644
index 0000000..d12bb90
--- /dev/null
+++ b/gio/gpu/internal/rendertest/clip_test.go
@@ -0,0 +1,581 @@
+package rendertest
+
+import (
+ "image"
+ "math"
+ "testing"
+
+ "golang.org/x/image/colornames"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+)
+
+func TestPaintRect(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
+ }, func(r result) {
+ r.expect(0, 0, colornames.Red)
+ r.expect(49, 0, colornames.Red)
+ r.expect(50, 0, transparent)
+ r.expect(10, 50, transparent)
+ })
+}
+
+func TestPaintClippedRect(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ clip.RRect{Rect: f32.Rect(25, 25, 60, 60)}.Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(24, 35, transparent)
+ r.expect(25, 35, colornames.Red)
+ r.expect(50, 0, transparent)
+ r.expect(10, 50, transparent)
+ })
+}
+
+func TestPaintClippedCircle(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ r := float32(10)
+ clip.RRect{Rect: f32.Rect(20, 20, 40, 40), SE: r, SW: r, NW: r,
+ NE: r}.Add(o)
+ clip.Rect(image.Rect(0, 0, 30, 50)).Add(o)
+ paint.Fill(o, red)
+ }, func(r result) {
+ r.expect(21, 21, transparent)
+ r.expect(25, 30, colornames.Red)
+ r.expect(31, 30, transparent)
+ })
+}
+
+func TestPaintArc(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(0, 20))
+ p.Line(f32.Pt(10, 0))
+ p.Arc(f32.Pt(10, 0), f32.Pt(40, 0), math.Pi)
+ p.Line(f32.Pt(30, 0))
+ p.Line(f32.Pt(0, 25))
+ p.Arc(f32.Pt(-10, 5), f32.Pt(10, 15), -math.Pi)
+ p.Line(f32.Pt(0, 25))
+ p.Arc(f32.Pt(10, 10), f32.Pt(10, 10), 2*math.Pi)
+ p.Line(f32.Pt(-10, 0))
+ p.Arc(f32.Pt(-10, 0), f32.Pt(-40, 0), -math.Pi)
+ p.Line(f32.Pt(-10, 0))
+ p.Line(f32.Pt(0, -10))
+ p.Arc(f32.Pt(-10, -20), f32.Pt(10, -5), math.Pi)
+ p.Line(f32.Pt(0, -10))
+ p.Line(f32.Pt(-50, 0))
+ p.Close()
+ clip.Outline{
+ Path: p.End(),
+ }.Op().Add(o)
+
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op())
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(0, 25, colornames.Red)
+ r.expect(0, 15, transparent)
+ })
+}
+
+func TestPaintAbsolute(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(100,
+ 100)) // offset the initial pen position to test "MoveTo"
+
+ p.MoveTo(f32.Pt(20, 20))
+ p.LineTo(f32.Pt(80, 20))
+ p.QuadTo(f32.Pt(80, 80), f32.Pt(20, 80))
+ p.Close()
+ clip.Outline{
+ Path: p.End(),
+ }.Op().Add(o)
+
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op())
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(30, 30, colornames.Red)
+ r.expect(79, 79, transparent)
+ r.expect(90, 90, transparent)
+ })
+}
+
+func TestPaintTexture(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ squares.Add(o)
+ scale(80.0/512, 80.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(0, 0, colornames.Blue)
+ r.expect(79, 10, colornames.Green)
+ r.expect(80, 0, transparent)
+ r.expect(10, 80, transparent)
+ })
+}
+
+func TestTexturedStrokeClipped(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ smallSquares.Add(o)
+ op.Offset(f32.Pt(50, 50)).Add(o)
+ clip.Stroke{
+ Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o),
+ Style: clip.StrokeStyle{
+ Width: 10,
+ },
+ }.Op().Add(o)
+ clip.RRect{Rect: f32.Rect(-30, -30, 60, 60)}.Add(o)
+ op.Offset(f32.Pt(-10, -10)).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ })
+}
+
+func TestTexturedStroke(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ smallSquares.Add(o)
+ op.Offset(f32.Pt(50, 50)).Add(o)
+ clip.Stroke{
+ Path: clip.RRect{Rect: f32.Rect(0, 0, 30, 30)}.Path(o),
+ Style: clip.StrokeStyle{
+ Width: 10,
+ },
+ }.Op().Add(o)
+ op.Offset(f32.Pt(-10, -10)).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ })
+}
+
+func TestPaintClippedTexture(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ squares.Add(o)
+ clip.RRect{Rect: f32.Rect(0, 0, 40, 40)}.Add(o)
+ scale(80.0/512, 80.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(40, 40, transparent)
+ r.expect(25, 35, colornames.Blue)
+ })
+}
+
+func TestStrokedPathBevelFlat(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ p := newStrokedPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2.5,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+
+ paint.Fill(o, red)
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(10, 50, colornames.Red)
+ })
+}
+
+func TestStrokedPathBevelRound(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ p := newStrokedPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2.5,
+ Cap: clip.RoundCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+
+ paint.Fill(o, red)
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(10, 50, colornames.Red)
+ })
+}
+
+func TestStrokedPathBevelSquare(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ p := newStrokedPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2.5,
+ Cap: clip.SquareCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+
+ paint.Fill(o, red)
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(10, 50, colornames.Red)
+ })
+}
+
+func TestStrokedPathRoundRound(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ p := newStrokedPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2.5,
+ Cap: clip.RoundCap,
+ Join: clip.RoundJoin,
+ },
+ }.Op().Add(o)
+
+ paint.Fill(o, red)
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(10, 50, colornames.Red)
+ })
+}
+
+func TestStrokedPathFlatMiter(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 10,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ Miter: 5,
+ },
+ }.Op().Add(o)
+ paint.Fill(o, red)
+ stk.Load()
+ }
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+ paint.Fill(o, black)
+ stk.Load()
+ }
+
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(40, 10, colornames.Black)
+ r.expect(40, 12, colornames.Red)
+ })
+}
+
+func TestStrokedPathFlatMiterInf(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 10,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ Miter: float32(math.Inf(+1)),
+ },
+ }.Op().Add(o)
+ paint.Fill(o, red)
+ stk.Load()
+ }
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+ paint.Fill(o, black)
+ stk.Load()
+ }
+
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(40, 10, colornames.Black)
+ r.expect(40, 12, colornames.Red)
+ })
+}
+
+func TestStrokedPathZeroWidth(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(10, 50))
+ p.Line(f32.Pt(50, 0))
+ clip.Stroke{
+ Path: p.End(),
+ Style: clip.StrokeStyle{
+ Width: 2,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+
+ paint.Fill(o, black)
+ stk.Load()
+ }
+
+ {
+ stk := op.Save(o)
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(10, 50))
+ p.Line(f32.Pt(30, 0))
+ clip.Stroke{
+ Path: p.End(),
+ }.Op().Add(o) // width=0, disable stroke
+
+ paint.Fill(o, red)
+ stk.Load()
+ }
+
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(10, 50, colornames.Black)
+ r.expect(30, 50, colornames.Black)
+ r.expect(65, 50, transparent)
+ })
+}
+
+func TestDashedPathFlatCapEllipse(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ p := newEllipsePath(o)
+
+ var dash clip.Dash
+ dash.Begin(o)
+ dash.Dash(5)
+ dash.Dash(3)
+
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 10,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ Miter: float32(math.Inf(+1)),
+ },
+ Dashes: dash.End(),
+ }.Op().Add(o)
+
+ paint.Fill(
+ o,
+ red,
+ )
+ stk.Load()
+ }
+ {
+ stk := op.Save(o)
+ p := newEllipsePath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2,
+ },
+ }.Op().Add(o)
+
+ paint.Fill(
+ o,
+ black,
+ )
+ stk.Load()
+ }
+
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(0, 62, colornames.Red)
+ r.expect(0, 65, colornames.Black)
+ })
+}
+
+func TestDashedPathFlatCapZ(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ var dash clip.Dash
+ dash.Begin(o)
+ dash.Dash(5)
+ dash.Dash(3)
+
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 10,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ Miter: float32(math.Inf(+1)),
+ },
+ Dashes: dash.End(),
+ }.Op().Add(o)
+ paint.Fill(o, red)
+ stk.Load()
+ }
+
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+ paint.Fill(o, black)
+ stk.Load()
+ }
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(40, 10, colornames.Black)
+ r.expect(40, 12, colornames.Red)
+ r.expect(46, 12, transparent)
+ })
+}
+
+func TestDashedPathFlatCapZNoDash(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ var dash clip.Dash
+ dash.Begin(o)
+ dash.Phase(1)
+
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 10,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ Miter: float32(math.Inf(+1)),
+ },
+ Dashes: dash.End(),
+ }.Op().Add(o)
+ paint.Fill(o, red)
+ stk.Load()
+ }
+ {
+ stk := op.Save(o)
+ clip.Stroke{
+ Path: newZigZagPath(o),
+ Style: clip.StrokeStyle{
+ Width: 2,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+ paint.Fill(o, black)
+ stk.Load()
+ }
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(40, 10, colornames.Black)
+ r.expect(40, 12, colornames.Red)
+ r.expect(46, 12, colornames.Red)
+ })
+}
+
+func TestDashedPathFlatCapZNoPath(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ {
+ stk := op.Save(o)
+ var dash clip.Dash
+ dash.Begin(o)
+ dash.Dash(0)
+ clip.Stroke{
+ Path: newZigZagPath(o),
+ Style: clip.StrokeStyle{
+ Width: 10,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ Miter: float32(math.Inf(+1)),
+ },
+ Dashes: dash.End(),
+ }.Op().Add(o)
+ paint.Fill(o, red)
+ stk.Load()
+ }
+ {
+ stk := op.Save(o)
+ p := newZigZagPath(o)
+ clip.Stroke{
+ Path: p,
+ Style: clip.StrokeStyle{
+ Width: 2,
+ Cap: clip.FlatCap,
+ Join: clip.BevelJoin,
+ },
+ }.Op().Add(o)
+ paint.Fill(o, black)
+ stk.Load()
+ }
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(40, 10, colornames.Black)
+ r.expect(40, 12, transparent)
+ r.expect(46, 12, transparent)
+ })
+}
+
+func newStrokedPath(o *op.Ops) clip.PathSpec {
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(10, 50))
+ p.Line(f32.Pt(10, 0))
+ p.Arc(f32.Pt(10, 0), f32.Pt(20, 0), math.Pi)
+ p.Line(f32.Pt(10, 0))
+ p.Line(f32.Pt(10, 10))
+ p.Arc(f32.Pt(0, 30), f32.Pt(0, 30), 2*math.Pi)
+ p.Line(f32.Pt(-20, 0))
+ p.Quad(f32.Pt(-10, -10), f32.Pt(-30, 30))
+ return p.End()
+}
+
+func newZigZagPath(o *op.Ops) clip.PathSpec {
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(40, 10))
+ p.Line(f32.Pt(50, 0))
+ p.Line(f32.Pt(-50, 50))
+ p.Line(f32.Pt(50, 0))
+ p.Quad(f32.Pt(-50, 20), f32.Pt(-50, 50))
+ p.Line(f32.Pt(50, 0))
+ return p.End()
+}
+
+func newEllipsePath(o *op.Ops) clip.PathSpec {
+ p := new(clip.Path)
+ p.Begin(o)
+ p.Move(f32.Pt(0, 65))
+ p.Line(f32.Pt(20, 0))
+ p.Arc(f32.Pt(20, 0), f32.Pt(70, 0), 2*math.Pi)
+ return p.End()
+}
diff --git a/gio/gpu/internal/rendertest/doc.go b/gio/gpu/internal/rendertest/doc.go
new file mode 100644
index 0000000..9f6948e
--- /dev/null
+++ b/gio/gpu/internal/rendertest/doc.go
@@ -0,0 +1,2 @@
+// Package rendertest is intended for testing of drawing ops only.
+package rendertest
diff --git a/gio/gpu/internal/rendertest/refs/TestBuildOffscreen.png b/gio/gpu/internal/rendertest/refs/TestBuildOffscreen.png
new file mode 100644
index 0000000..fb50427
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestBuildOffscreen.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png b/gio/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png
new file mode 100644
index 0000000..8ff717b
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestClipOffset.png b/gio/gpu/internal/rendertest/refs/TestClipOffset.png
new file mode 100644
index 0000000..6396fb4
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestClipOffset.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestClipPaintOffset.png b/gio/gpu/internal/rendertest/refs/TestClipPaintOffset.png
new file mode 100644
index 0000000..0fe37e6
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestClipPaintOffset.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestClipRotate.png b/gio/gpu/internal/rendertest/refs/TestClipRotate.png
new file mode 100644
index 0000000..e6c15e3
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestClipRotate.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestClipScale.png b/gio/gpu/internal/rendertest/refs/TestClipScale.png
new file mode 100644
index 0000000..6396fb4
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestClipScale.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestComplicatedTransform.png b/gio/gpu/internal/rendertest/refs/TestComplicatedTransform.png
new file mode 100644
index 0000000..4a92e3c
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestComplicatedTransform.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png
new file mode 100644
index 0000000..79bae38
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapEllipse.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png
new file mode 100644
index 0000000..12212e9
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZ.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png
new file mode 100644
index 0000000..d315f0f
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoDash.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png
new file mode 100644
index 0000000..94c160e
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestDashedPathFlatCapZNoPath.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestDeferredPaint.png b/gio/gpu/internal/rendertest/refs/TestDeferredPaint.png
new file mode 100644
index 0000000..b562f12
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestDeferredPaint.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestDepthOverlap.png b/gio/gpu/internal/rendertest/refs/TestDepthOverlap.png
new file mode 100644
index 0000000..9d416b9
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestDepthOverlap.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestLinearGradient.png b/gio/gpu/internal/rendertest/refs/TestLinearGradient.png
new file mode 100644
index 0000000..c3c007c
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestLinearGradient.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestLinearGradientAngled.png b/gio/gpu/internal/rendertest/refs/TestLinearGradientAngled.png
new file mode 100644
index 0000000..3ba0734
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestLinearGradientAngled.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestNegativeOverlaps.png b/gio/gpu/internal/rendertest/refs/TestNegativeOverlaps.png
new file mode 100644
index 0000000..fb50427
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestNegativeOverlaps.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestNoClipFromPaint.png b/gio/gpu/internal/rendertest/refs/TestNoClipFromPaint.png
new file mode 100644
index 0000000..e774064
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestNoClipFromPaint.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png b/gio/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png
new file mode 100644
index 0000000..515a4d2
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestOffsetTexture.png b/gio/gpu/internal/rendertest/refs/TestOffsetTexture.png
new file mode 100644
index 0000000..87386e8
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestOffsetTexture.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestPaintAbsolute.png b/gio/gpu/internal/rendertest/refs/TestPaintAbsolute.png
new file mode 100644
index 0000000..dd09760
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintAbsolute.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestPaintArc.png b/gio/gpu/internal/rendertest/refs/TestPaintArc.png
new file mode 100644
index 0000000..f432914
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintArc.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedBorder.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedBorder.png
new file mode 100644
index 0000000..f8fcfbb
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintClippedBorder.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedCircle.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedCircle.png
new file mode 100644
index 0000000..bdf1fce
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintClippedCircle.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedCirle.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedCirle.png
new file mode 100644
index 0000000..c8cf2f6
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintClippedCirle.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedRect.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedRect.png
new file mode 100644
index 0000000..c1dd7a0
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintClippedRect.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedTexture.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedTexture.png
new file mode 100644
index 0000000..ae0e066
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintClippedTexture.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestPaintOffset.png b/gio/gpu/internal/rendertest/refs/TestPaintOffset.png
new file mode 100644
index 0000000..82394d5
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintOffset.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestPaintRect.png b/gio/gpu/internal/rendertest/refs/TestPaintRect.png
new file mode 100644
index 0000000..f942601
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintRect.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestPaintRotate.png b/gio/gpu/internal/rendertest/refs/TestPaintRotate.png
new file mode 100644
index 0000000..fe15d7d
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintRotate.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestPaintShear.png b/gio/gpu/internal/rendertest/refs/TestPaintShear.png
new file mode 100644
index 0000000..6d1a4c9
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintShear.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestPaintTexture.png b/gio/gpu/internal/rendertest/refs/TestPaintTexture.png
new file mode 100644
index 0000000..9120231
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestPaintTexture.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png b/gio/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png
new file mode 100644
index 0000000..da201dc
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestReuseStencil.png b/gio/gpu/internal/rendertest/refs/TestReuseStencil.png
new file mode 100644
index 0000000..349db1f
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestReuseStencil.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestRotateClipTexture.png b/gio/gpu/internal/rendertest/refs/TestRotateClipTexture.png
new file mode 100644
index 0000000..56c3182
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestRotateClipTexture.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestRotateTexture.png b/gio/gpu/internal/rendertest/refs/TestRotateTexture.png
new file mode 100644
index 0000000..e56c972
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestRotateTexture.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png
new file mode 100644
index 0000000..9d442f5
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelFlat.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png
new file mode 100644
index 0000000..a37235c
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelRound.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png
new file mode 100644
index 0000000..8d2919d
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathBevelSquare.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png
new file mode 100644
index 0000000..ae6472a
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiter.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png
new file mode 100644
index 0000000..d315f0f
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathFlatMiterInf.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png
new file mode 100644
index 0000000..8ef5a94
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathRoundRound.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png
new file mode 100644
index 0000000..0fc6fe8
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestTexturedStroke.png b/gio/gpu/internal/rendertest/refs/TestTexturedStroke.png
new file mode 100644
index 0000000..637c932
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestTexturedStroke.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png b/gio/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png
new file mode 100644
index 0000000..637c932
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestTransformMacro.png b/gio/gpu/internal/rendertest/refs/TestTransformMacro.png
new file mode 100644
index 0000000..a9cce29
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestTransformMacro.png differ
diff --git a/gio/gpu/internal/rendertest/refs/TestTransformOrder.png b/gio/gpu/internal/rendertest/refs/TestTransformOrder.png
new file mode 100644
index 0000000..720ca3c
Binary files /dev/null and b/gio/gpu/internal/rendertest/refs/TestTransformOrder.png differ
diff --git a/gio/gpu/internal/rendertest/render_test.go b/gio/gpu/internal/rendertest/render_test.go
new file mode 100644
index 0000000..efa60a6
--- /dev/null
+++ b/gio/gpu/internal/rendertest/render_test.go
@@ -0,0 +1,358 @@
+package rendertest
+
+import (
+ "image"
+ "image/color"
+ "math"
+ "testing"
+
+ "golang.org/x/image/colornames"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+)
+
+func TestTransformMacro(t *testing.T) {
+ // testcase resulting from original bug when rendering layout.Stacked
+
+ // Build clip-path.
+ c := constSqPath()
+
+ run(t, func(o *op.Ops) {
+
+ // render the first Stacked item
+ m1 := op.Record(o)
+ dr := image.Rect(0, 0, 128, 50)
+ paint.FillShape(o, black, clip.Rect(dr).Op())
+ c1 := m1.Stop()
+
+ // Render the second stacked item
+ m2 := op.Record(o)
+ paint.ColorOp{Color: red}.Add(o)
+ // Simulate a draw text call
+ stack := op.Save(o)
+ op.Offset(f32.Pt(0, 10)).Add(o)
+
+ // Apply the clip-path.
+ c.Add(o)
+
+ paint.PaintOp{}.Add(o)
+ stack.Load()
+
+ c2 := m2.Stop()
+
+ // Call each of them in a transform
+ s1 := op.Save(o)
+ op.Offset(f32.Pt(0, 0)).Add(o)
+ c1.Add(o)
+ s1.Load()
+ s2 := op.Save(o)
+ op.Offset(f32.Pt(0, 0)).Add(o)
+ c2.Add(o)
+ s2.Load()
+ }, func(r result) {
+ r.expect(5, 15, colornames.Red)
+ r.expect(15, 15, colornames.Black)
+ r.expect(11, 51, transparent)
+ })
+}
+
+func TestRepeatedPaintsZ(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ // Draw a rectangle
+ paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 128, 50)).Op())
+
+ builder := clip.Path{}
+ builder.Begin(o)
+ builder.Move(f32.Pt(0, 0))
+ builder.Line(f32.Pt(10, 0))
+ builder.Line(f32.Pt(0, 10))
+ builder.Line(f32.Pt(-10, 0))
+ builder.Line(f32.Pt(0, -10))
+ p := builder.End()
+ clip.Outline{
+ Path: p,
+ }.Op().Add(o)
+ paint.Fill(o, red)
+ }, func(r result) {
+ r.expect(5, 5, colornames.Red)
+ r.expect(11, 15, colornames.Black)
+ r.expect(11, 51, transparent)
+ })
+}
+
+func TestNoClipFromPaint(t *testing.T) {
+ // ensure that a paint operation does not pollute the state
+ // by leaving any clip paths in place.
+ run(t, func(o *op.Ops) {
+ a := f32.Affine2D{}.Rotate(f32.Pt(20, 20), math.Pi/4)
+ op.Affine(a).Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(10, 10, 30, 30)).Op())
+ a = f32.Affine2D{}.Rotate(f32.Pt(20, 20), -math.Pi/4)
+ op.Affine(a).Add(o)
+
+ paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
+ }, func(r result) {
+ r.expect(1, 1, colornames.Black)
+ r.expect(20, 20, colornames.Black)
+ r.expect(49, 49, colornames.Black)
+ r.expect(51, 51, transparent)
+ })
+}
+
+func TestDeferredPaint(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ state := op.Save(o)
+ clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o)
+ paint.ColorOp{Color: color.NRGBA{A: 0xff, G: 0xff}}.Add(o)
+ paint.PaintOp{}.Add(o)
+
+ op.Affine(f32.Affine2D{}.Offset(f32.Pt(20, 20))).Add(o)
+ m := op.Record(o)
+ clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o)
+ paint.ColorOp{Color: color.NRGBA{A: 0xff, R: 0xff, G: 0xff}}.Add(o)
+ paint.PaintOp{}.Add(o)
+ paintMacro := m.Stop()
+ op.Defer(o, paintMacro)
+
+ state.Load()
+ op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o)
+ clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o)
+ paint.ColorOp{Color: color.NRGBA{A: 0xff, B: 0xff}}.Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ })
+}
+
+func constSqPath() op.CallOp {
+ innerOps := new(op.Ops)
+ m := op.Record(innerOps)
+ builder := clip.Path{}
+ builder.Begin(innerOps)
+ builder.Move(f32.Pt(0, 0))
+ builder.Line(f32.Pt(10, 0))
+ builder.Line(f32.Pt(0, 10))
+ builder.Line(f32.Pt(-10, 0))
+ builder.Line(f32.Pt(0, -10))
+ p := builder.End()
+ clip.Outline{Path: p}.Op().Add(innerOps)
+ return m.Stop()
+}
+
+func constSqCirc() op.CallOp {
+ innerOps := new(op.Ops)
+ m := op.Record(innerOps)
+ clip.RRect{Rect: f32.Rect(0, 0, 40, 40),
+ NW: 20, NE: 20, SW: 20, SE: 20}.Add(innerOps)
+ return m.Stop()
+}
+
+func drawChild(ops *op.Ops, text op.CallOp) op.CallOp {
+ r1 := op.Record(ops)
+ text.Add(ops)
+ paint.PaintOp{}.Add(ops)
+ return r1.Stop()
+}
+
+func TestReuseStencil(t *testing.T) {
+ txt := constSqPath()
+ run(t, func(ops *op.Ops) {
+ c1 := drawChild(ops, txt)
+ c2 := drawChild(ops, txt)
+
+ // lay out the children
+ stack1 := op.Save(ops)
+ c1.Add(ops)
+ stack1.Load()
+
+ stack2 := op.Save(ops)
+ op.Offset(f32.Pt(0, 50)).Add(ops)
+ c2.Add(ops)
+ stack2.Load()
+ }, func(r result) {
+ r.expect(5, 5, colornames.Black)
+ r.expect(5, 55, colornames.Black)
+ })
+}
+
+func TestBuildOffscreen(t *testing.T) {
+ // Check that something we in one frame build outside the screen
+ // still is rendered correctly if moved into the screen in a later
+ // frame.
+
+ txt := constSqCirc()
+ draw := func(off float32, o *op.Ops) {
+ s := op.Save(o)
+ op.Offset(f32.Pt(0, off)).Add(o)
+ txt.Add(o)
+ paint.PaintOp{}.Add(o)
+ s.Load()
+ }
+
+ multiRun(t,
+ frame(
+ func(ops *op.Ops) {
+ draw(-100, ops)
+ }, func(r result) {
+ r.expect(5, 5, transparent)
+ r.expect(20, 20, transparent)
+ }),
+ frame(
+ func(ops *op.Ops) {
+ draw(0, ops)
+ }, func(r result) {
+ r.expect(2, 2, transparent)
+ r.expect(20, 20, colornames.Black)
+ r.expect(38, 38, transparent)
+ }))
+}
+
+func TestNegativeOverlaps(t *testing.T) {
+ run(t, func(ops *op.Ops) {
+ clip.RRect{Rect: f32.Rect(50, 50, 100, 100)}.Add(ops)
+ clip.Rect(image.Rect(0, 120, 100, 122)).Add(ops)
+ paint.PaintOp{}.Add(ops)
+ }, func(r result) {
+ r.expect(60, 60, transparent)
+ r.expect(60, 110, transparent)
+ r.expect(60, 120, transparent)
+ r.expect(60, 122, transparent)
+ })
+}
+
+func TestDepthOverlap(t *testing.T) {
+ run(t, func(ops *op.Ops) {
+ stack := op.Save(ops)
+ paint.FillShape(ops, red, clip.Rect{Max: image.Pt(128, 64)}.Op())
+ stack.Load()
+
+ stack = op.Save(ops)
+ paint.FillShape(ops, green, clip.Rect{Max: image.Pt(64, 128)}.Op())
+ stack.Load()
+ }, func(r result) {
+ r.expect(96, 32, colornames.Red)
+ r.expect(32, 96, colornames.Green)
+ r.expect(32, 32, colornames.Green)
+ })
+}
+
+type Gradient struct {
+ From, To color.NRGBA
+}
+
+var gradients = []Gradient{
+ {From: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF},
+ To: color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}},
+ {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF},
+ To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}},
+ {From: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF},
+ To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}},
+ {From: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF},
+ To: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}},
+ {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0xFF, A: 0xFF},
+ To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}},
+ {From: color.NRGBA{R: 0xFF, G: 0xFF, B: 0x19, A: 0xFF},
+ To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}},
+}
+
+func TestLinearGradient(t *testing.T) {
+ t.Skip("linear gradients don't support transformations")
+
+ const gradienth = 8
+ // 0.5 offset from ends to ensure that the center of the pixel
+ // aligns with gradient from and to colors.
+ pixelAligned := f32.Rect(0.5, 0, 127.5, gradienth)
+ samples := []int{0, 12, 32, 64, 96, 115, 127}
+
+ run(t, func(ops *op.Ops) {
+ gr := f32.Rect(0, 0, 128, gradienth)
+ for _, g := range gradients {
+ paint.LinearGradientOp{
+ Stop1: f32.Pt(gr.Min.X, gr.Min.Y),
+ Color1: g.From,
+ Stop2: f32.Pt(gr.Max.X, gr.Min.Y),
+ Color2: g.To,
+ }.Add(ops)
+ st := op.Save(ops)
+ clip.RRect{Rect: gr}.Add(ops)
+ op.Affine(f32.Affine2D{}.Offset(pixelAligned.Min)).Add(ops)
+ scale(pixelAligned.Dx()/128, 1).Add(ops)
+ paint.PaintOp{}.Add(ops)
+ st.Load()
+ gr = gr.Add(f32.Pt(0, gradienth))
+ }
+ }, func(r result) {
+ gr := pixelAligned
+ for _, g := range gradients {
+ from := f32color.LinearFromSRGB(g.From)
+ to := f32color.LinearFromSRGB(g.To)
+ for _, p := range samples {
+ exp := lerp(from, to, float32(p)/float32(r.img.Bounds().Dx()-1))
+ r.expect(p, int(gr.Min.Y+gradienth/2),
+ f32color.NRGBAToRGBA(exp.SRGB()))
+ }
+ gr = gr.Add(f32.Pt(0, gradienth))
+ }
+ })
+}
+
+func TestLinearGradientAngled(t *testing.T) {
+ run(t, func(ops *op.Ops) {
+ paint.LinearGradientOp{
+ Stop1: f32.Pt(64, 64),
+ Color1: black,
+ Stop2: f32.Pt(0, 0),
+ Color2: red,
+ }.Add(ops)
+ st := op.Save(ops)
+ clip.Rect(image.Rect(0, 0, 64, 64)).Add(ops)
+ paint.PaintOp{}.Add(ops)
+ st.Load()
+
+ paint.LinearGradientOp{
+ Stop1: f32.Pt(64, 64),
+ Color1: white,
+ Stop2: f32.Pt(128, 0),
+ Color2: green,
+ }.Add(ops)
+ st = op.Save(ops)
+ clip.Rect(image.Rect(64, 0, 128, 64)).Add(ops)
+ paint.PaintOp{}.Add(ops)
+ st.Load()
+
+ paint.LinearGradientOp{
+ Stop1: f32.Pt(64, 64),
+ Color1: black,
+ Stop2: f32.Pt(128, 128),
+ Color2: blue,
+ }.Add(ops)
+ st = op.Save(ops)
+ clip.Rect(image.Rect(64, 64, 128, 128)).Add(ops)
+ paint.PaintOp{}.Add(ops)
+ st.Load()
+
+ paint.LinearGradientOp{
+ Stop1: f32.Pt(64, 64),
+ Color1: white,
+ Stop2: f32.Pt(0, 128),
+ Color2: magenta,
+ }.Add(ops)
+ st = op.Save(ops)
+ clip.Rect(image.Rect(0, 64, 64, 128)).Add(ops)
+ paint.PaintOp{}.Add(ops)
+ st.Load()
+ }, func(r result) {})
+}
+
+// lerp calculates linear interpolation with color b and p.
+func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
+ return f32color.RGBA{
+ R: a.R*(1-p) + b.R*p,
+ G: a.G*(1-p) + b.G*p,
+ B: a.B*(1-p) + b.B*p,
+ A: a.A*(1-p) + b.A*p,
+ }
+}
diff --git a/gio/gpu/internal/rendertest/transform_test.go b/gio/gpu/internal/rendertest/transform_test.go
new file mode 100644
index 0000000..b00aa7e
--- /dev/null
+++ b/gio/gpu/internal/rendertest/transform_test.go
@@ -0,0 +1,204 @@
+package rendertest
+
+import (
+ "image"
+ "math"
+ "testing"
+
+ "golang.org/x/image/colornames"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+)
+
+func TestPaintOffset(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ op.Offset(f32.Pt(10, 20)).Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(59, 30, colornames.Red)
+ r.expect(60, 30, transparent)
+ r.expect(10, 70, transparent)
+ })
+}
+
+func TestPaintRotate(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/8)
+ op.Affine(a).Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(20, 20, 60, 60)).Op())
+ }, func(r result) {
+ r.expect(40, 40, colornames.Red)
+ r.expect(50, 19, colornames.Red)
+ r.expect(59, 19, transparent)
+ r.expect(21, 21, transparent)
+ })
+}
+
+func TestPaintShear(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0)
+ op.Affine(a).Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 40, 40)).Op())
+ }, func(r result) {
+ r.expect(10, 30, transparent)
+ })
+}
+
+func TestClipPaintOffset(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ clip.RRect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o)
+ op.Offset(f32.Pt(20, 20)).Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op())
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(19, 19, transparent)
+ r.expect(20, 20, colornames.Red)
+ r.expect(30, 30, transparent)
+ })
+}
+
+func TestClipOffset(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ op.Offset(f32.Pt(20, 20)).Add(o)
+ clip.RRect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op())
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(29, 29, transparent)
+ r.expect(30, 30, colornames.Red)
+ r.expect(49, 49, colornames.Red)
+ r.expect(50, 50, transparent)
+ })
+}
+
+func TestClipScale(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ a := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 2)).Offset(f32.Pt(10,
+ 10))
+ op.Affine(a).Add(o)
+ clip.RRect{Rect: f32.Rect(10, 10, 20, 20)}.Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 1000, 1000)).Op())
+ }, func(r result) {
+ r.expect(19+10, 19+10, transparent)
+ r.expect(20+10, 20+10, colornames.Red)
+ r.expect(39+10, 39+10, colornames.Red)
+ r.expect(40+10, 40+10, transparent)
+ })
+}
+
+func TestClipRotate(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ op.Affine(f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/4)).Add(o)
+ clip.RRect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 40, 100, 100)).Op())
+ }, func(r result) {
+ r.expect(39, 39, transparent)
+ r.expect(41, 41, colornames.Red)
+ r.expect(50, 50, transparent)
+ })
+}
+
+func TestOffsetTexture(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ op.Offset(f32.Pt(15, 15)).Add(o)
+ squares.Add(o)
+ scale(50.0/512, 50.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(14, 20, transparent)
+ r.expect(66, 20, transparent)
+ r.expect(16, 64, colornames.Green)
+ r.expect(64, 16, colornames.Green)
+ })
+}
+
+func TestOffsetScaleTexture(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ op.Offset(f32.Pt(15, 15)).Add(o)
+ squares.Add(o)
+ op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 1))).Add(o)
+ scale(50.0/512, 50.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(114, 64, colornames.Blue)
+ r.expect(116, 64, transparent)
+ })
+}
+
+func TestRotateTexture(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ defer op.Save(o).Load()
+ squares.Add(o)
+ a := f32.Affine2D{}.Offset(f32.Pt(30, 30)).Rotate(f32.Pt(40, 40),
+ math.Pi/4)
+ op.Affine(a).Add(o)
+ scale(20.0/512, 20.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(40, 40-12, colornames.Blue)
+ r.expect(40+12, 40, colornames.Green)
+ })
+}
+
+func TestRotateClipTexture(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ squares.Add(o)
+ a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), math.Pi/8)
+ op.Affine(a).Add(o)
+ clip.RRect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o)
+ op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o)
+ scale(60.0/512, 60.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(0, 0, transparent)
+ r.expect(37, 39, colornames.Green)
+ r.expect(36, 39, colornames.Green)
+ r.expect(35, 39, colornames.Green)
+ r.expect(34, 39, colornames.Green)
+ r.expect(33, 39, colornames.Green)
+ })
+}
+
+func TestComplicatedTransform(t *testing.T) {
+ run(t, func(o *op.Ops) {
+ squares.Add(o)
+
+ clip.RRect{Rect: f32.Rect(0, 0, 100, 100), SE: 50, SW: 50, NW: 50,
+ NE: 50}.Add(o)
+
+ a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0)
+ op.Affine(a).Add(o)
+ clip.RRect{Rect: f32.Rect(0, 0, 50, 40)}.Add(o)
+
+ scale(50.0/512, 50.0/512).Add(o)
+ paint.PaintOp{}.Add(o)
+ }, func(r result) {
+ r.expect(20, 5, transparent)
+ })
+}
+
+func TestTransformOrder(t *testing.T) {
+ // check the ordering of operations bot in affine and in gpu stack.
+ run(t, func(o *op.Ops) {
+ a := f32.Affine2D{}.Offset(f32.Pt(64, 64))
+ op.Affine(a).Add(o)
+
+ b := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(8, 8))
+ op.Affine(b).Add(o)
+
+ c := f32.Affine2D{}.Offset(f32.Pt(-10, -10)).Scale(f32.Point{},
+ f32.Pt(0.5, 0.5))
+ op.Affine(c).Add(o)
+ paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 20, 20)).Op())
+ }, func(r result) {
+ // centered and with radius 40
+ r.expect(64-41, 64, transparent)
+ r.expect(64-39, 64, colornames.Red)
+ r.expect(64+39, 64, colornames.Red)
+ r.expect(64+41, 64, transparent)
+ })
+}
diff --git a/gio/gpu/internal/rendertest/util_test.go b/gio/gpu/internal/rendertest/util_test.go
new file mode 100644
index 0000000..74c6f5f
--- /dev/null
+++ b/gio/gpu/internal/rendertest/util_test.go
@@ -0,0 +1,290 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package rendertest
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "image"
+ "image/color"
+ "image/draw"
+ "image/png"
+ "io/ioutil"
+ "path/filepath"
+ "strconv"
+ "testing"
+
+ "golang.org/x/image/colornames"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/gpu/headless"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/paint"
+)
+
+var (
+ dumpImages = flag.Bool("saveimages", false, "save test images")
+ squares paint.ImageOp
+ smallSquares paint.ImageOp
+)
+
+var (
+ red = f32color.RGBAToNRGBA(colornames.Red)
+ green = f32color.RGBAToNRGBA(colornames.Green)
+ blue = f32color.RGBAToNRGBA(colornames.Blue)
+ magenta = f32color.RGBAToNRGBA(colornames.Magenta)
+ black = f32color.RGBAToNRGBA(colornames.Black)
+ white = f32color.RGBAToNRGBA(colornames.White)
+ transparent = color.RGBA{}
+)
+
+func init() {
+ squares = buildSquares(512)
+ smallSquares = buildSquares(50)
+}
+
+func buildSquares(size int) paint.ImageOp {
+ sub := size / 4
+ im := image.NewNRGBA(image.Rect(0, 0, size, size))
+ c1, c2 := image.NewUniform(colornames.Green), image.NewUniform(colornames.Blue)
+ for r := 0; r < 4; r++ {
+ for c := 0; c < 4; c++ {
+ c1, c2 = c2, c1
+ draw.Draw(im, image.Rect(r*sub, c*sub, r*sub+sub, c*sub+sub), c1,
+ image.Point{}, draw.Over)
+ }
+ c1, c2 = c2, c1
+ }
+ return paint.NewImageOp(im)
+}
+
+func drawImage(t *testing.T, size int, ops *op.Ops,
+ draw func(o *op.Ops)) (im *image.RGBA, err error) {
+ sz := image.Point{X: size, Y: size}
+ w := newWindow(t, sz.X, sz.Y)
+ draw(ops)
+ if err := w.Frame(ops); err != nil {
+ return nil, err
+ }
+ return w.Screenshot()
+}
+
+func run(t *testing.T, f func(o *op.Ops), c func(r result)) {
+ // draw a few times and check that it is correct each time, to
+ // ensure any caching effects still generate the correct images.
+ var img *image.RGBA
+ var err error
+ ops := new(op.Ops)
+ for i := 0; i < 3; i++ {
+ ops.Reset()
+ img, err = drawImage(t, 128, ops, f)
+ if err != nil {
+ t.Error("error rendering:", err)
+ return
+ }
+ // check for a reference image and make sure we are identical.
+ if !verifyRef(t, img, 0) {
+ name := fmt.Sprintf("%s-%d-bad.png", t.Name(), i)
+ if err := saveImage(name, img); err != nil {
+ t.Error(err)
+ }
+ }
+ c(result{t: t, img: img})
+ }
+
+ if *dumpImages {
+ if err := saveImage(t.Name()+".png", img); err != nil {
+ t.Error(err)
+ }
+ }
+}
+
+func frame(f func(o *op.Ops), c func(r result)) frameT {
+ return frameT{f: f, c: c}
+}
+
+type frameT struct {
+ f func(o *op.Ops)
+ c func(r result)
+}
+
+// multiRun is used to run test cases over multiple frames, typically
+// to test caching interactions.
+func multiRun(t *testing.T, frames ...frameT) {
+ // draw a few times and check that it is correct each time, to
+ // ensure any caching effects still generate the correct images.
+ var img *image.RGBA
+ var err error
+ sz := image.Point{X: 128, Y: 128}
+ w := newWindow(t, sz.X, sz.Y)
+ ops := new(op.Ops)
+ for i := range frames {
+ ops.Reset()
+ frames[i].f(ops)
+ if err := w.Frame(ops); err != nil {
+ t.Errorf("rendering failed: %v", err)
+ continue
+ }
+ img, err = w.Screenshot()
+ if err != nil {
+ t.Errorf("screenshot failed: %v", err)
+ continue
+ }
+ // Check for a reference image and make sure they are identical.
+ ok := verifyRef(t, img, i)
+ if frames[i].c != nil {
+ frames[i].c(result{t: t, img: img})
+ }
+ if *dumpImages || !ok {
+ name := t.Name() + ".png"
+ if i != 0 {
+ name = t.Name() + "_" + strconv.Itoa(i) + ".png"
+ }
+ if err := saveImage(name, img); err != nil {
+ t.Error(err)
+ }
+ }
+ }
+
+}
+
+func verifyRef(t *testing.T, img *image.RGBA, frame int) (ok bool) {
+ // ensure identical to ref data
+ path := filepath.Join("refs", t.Name()+".png")
+ if frame != 0 {
+ path = filepath.Join("refs", t.Name()+"_"+strconv.Itoa(frame)+".png")
+ }
+ b, err := ioutil.ReadFile(path)
+ if err != nil {
+ t.Error("could not open ref:", err)
+ return
+ }
+ r, err := png.Decode(bytes.NewReader(b))
+ if err != nil {
+ t.Error("could not decode ref:", err)
+ return
+ }
+ if img.Bounds() != r.Bounds() {
+ t.Errorf("reference image is %v, expected %v", r.Bounds(), img.Bounds())
+ return false
+ }
+ var ref *image.RGBA
+ switch r := r.(type) {
+ case *image.RGBA:
+ ref = r
+ case *image.NRGBA:
+ ref = image.NewRGBA(r.Bounds())
+ bnd := r.Bounds()
+ for x := bnd.Min.X; x < bnd.Max.X; x++ {
+ for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
+ ref.SetRGBA(x, y, f32color.NRGBAToRGBA(r.NRGBAAt(x, y)))
+ }
+ }
+ default:
+ t.Fatalf("reference image is a %T, expected *image.NRGBA or *image.RGBA",
+ r)
+ }
+ bnd := img.Bounds()
+ for x := bnd.Min.X; x < bnd.Max.X; x++ {
+ for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
+ exp := ref.RGBAAt(x, y)
+ got := img.RGBAAt(x, y)
+ if !colorsClose(exp, got) {
+ t.Error("not equal to ref at", x, y, " ", got, exp)
+ return false
+ }
+ }
+ }
+ return true
+}
+
+func colorsClose(c1, c2 color.RGBA) bool {
+ const delta = 0.01 // magic value obtained from experimentation.
+ return yiqEqApprox(c1, c2, delta)
+}
+
+// yiqEqApprox compares the colors of 2 pixels, in the NTSC YIQ color space,
+// as described in:
+//
+// Measuring perceived color difference using YIQ NTSC
+// transmission color space in mobile applications.
+// Yuriy Kotsarenko, Fernando Ramos.
+//
+// An electronic version is available at:
+//
+// - http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf
+func yiqEqApprox(c1, c2 color.RGBA, d2 float64) bool {
+ const max = 35215.0 // difference between 2 maximally different pixels.
+
+ var (
+ r1 = float64(c1.R)
+ g1 = float64(c1.G)
+ b1 = float64(c1.B)
+
+ r2 = float64(c2.R)
+ g2 = float64(c2.G)
+ b2 = float64(c2.B)
+
+ y1 = r1*0.29889531 + g1*0.58662247 + b1*0.11448223
+ i1 = r1*0.59597799 - g1*0.27417610 - b1*0.32180189
+ q1 = r1*0.21147017 - g1*0.52261711 + b1*0.31114694
+
+ y2 = r2*0.29889531 + g2*0.58662247 + b2*0.11448223
+ i2 = r2*0.59597799 - g2*0.27417610 - b2*0.32180189
+ q2 = r2*0.21147017 - g2*0.52261711 + b2*0.31114694
+
+ y = y1 - y2
+ i = i1 - i2
+ q = q1 - q2
+
+ diff = 0.5053*y*y + 0.299*i*i + 0.1957*q*q
+ )
+ return diff <= max*d2
+}
+
+func (r result) expect(x, y int, col color.RGBA) {
+ r.t.Helper()
+ if r.img == nil {
+ return
+ }
+ c := r.img.RGBAAt(x, y)
+ if !colorsClose(c, col) {
+ r.t.Error("expected ", col, " at ", "(", x, ",", y, ") but got ", c)
+ }
+}
+
+type result struct {
+ t *testing.T
+ img *image.RGBA
+}
+
+func saveImage(file string, img *image.RGBA) error {
+ // Only NRGBA images are losslessly encoded by png.Encode.
+ nrgba := image.NewNRGBA(img.Bounds())
+ bnd := img.Bounds()
+ for x := bnd.Min.X; x < bnd.Max.X; x++ {
+ for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
+ nrgba.SetNRGBA(x, y, f32color.RGBAToNRGBA(img.RGBAAt(x, y)))
+ }
+ }
+ var buf bytes.Buffer
+ if err := png.Encode(&buf, nrgba); err != nil {
+ return err
+ }
+ return ioutil.WriteFile(file, buf.Bytes(), 0666)
+}
+
+func newWindow(t testing.TB, width, height int) *headless.Window {
+ w, err := headless.NewWindow(width, height)
+ if err != nil {
+ t.Skipf("failed to create headless window, skipping: %v", err)
+ }
+ t.Cleanup(w.Release)
+ return w
+}
+
+func scale(sx, sy float32) op.TransformOp {
+ return op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(sx, sy)))
+}
diff --git a/gio/gpu/pack.go b/gio/gpu/pack.go
new file mode 100644
index 0000000..c4dbaad
--- /dev/null
+++ b/gio/gpu/pack.go
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+import (
+ "image"
+)
+
+// packer packs a set of many smaller rectangles into
+// much fewer larger atlases.
+type packer struct {
+ maxDim int
+ spaces []image.Rectangle
+
+ sizes []image.Point
+ pos image.Point
+}
+
+type placement struct {
+ Idx int
+ Pos image.Point
+}
+
+// add adds the given rectangle to the atlases and
+// return the allocated position.
+func (p *packer) add(s image.Point) (placement, bool) {
+ if place, ok := p.tryAdd(s); ok {
+ return place, true
+ }
+ p.newPage()
+ return p.tryAdd(s)
+}
+
+func (p *packer) clear() {
+ p.sizes = p.sizes[:0]
+ p.spaces = p.spaces[:0]
+}
+
+func (p *packer) newPage() {
+ p.pos = image.Point{}
+ p.sizes = append(p.sizes, image.Point{})
+ p.spaces = p.spaces[:0]
+ p.spaces = append(p.spaces, image.Rectangle{
+ Max: image.Point{X: p.maxDim, Y: p.maxDim},
+ })
+}
+
+func (p *packer) tryAdd(s image.Point) (placement, bool) {
+ // Go backwards to prioritize smaller spaces first.
+ for i := len(p.spaces) - 1; i >= 0; i-- {
+ space := p.spaces[i]
+ rightSpace := space.Dx() - s.X
+ bottomSpace := space.Dy() - s.Y
+ if rightSpace >= 0 && bottomSpace >= 0 {
+ // Remove space.
+ p.spaces[i] = p.spaces[len(p.spaces)-1]
+ p.spaces = p.spaces[:len(p.spaces)-1]
+ // Put s in the top left corner and add the (at most)
+ // two smaller spaces.
+ pos := space.Min
+ if bottomSpace > 0 {
+ p.spaces = append(p.spaces, image.Rectangle{
+ Min: image.Point{X: pos.X, Y: pos.Y + s.Y},
+ Max: image.Point{X: space.Max.X, Y: space.Max.Y},
+ })
+ }
+ if rightSpace > 0 {
+ p.spaces = append(p.spaces, image.Rectangle{
+ Min: image.Point{X: pos.X + s.X, Y: pos.Y},
+ Max: image.Point{X: space.Max.X, Y: pos.Y + s.Y},
+ })
+ }
+ idx := len(p.sizes) - 1
+ size := &p.sizes[idx]
+ if x := pos.X + s.X; x > size.X {
+ size.X = x
+ }
+ if y := pos.Y + s.Y; y > size.Y {
+ size.Y = y
+ }
+ return placement{Idx: idx, Pos: pos}, true
+ }
+ }
+ return placement{}, false
+}
diff --git a/gio/gpu/path.go b/gio/gpu/path.go
new file mode 100644
index 0000000..4670f03
--- /dev/null
+++ b/gio/gpu/path.go
@@ -0,0 +1,438 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+// GPU accelerated path drawing using the algorithms from
+// Pathfinder (https://github.com/servo/pathfinder).
+
+import (
+ "encoding/binary"
+ "image"
+ "math"
+ "unsafe"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/gpu/internal/driver"
+ "realy.lol/gio/internal/byteslice"
+ "realy.lol/gio/internal/f32color"
+)
+
+type pather struct {
+ ctx driver.Device
+
+ viewport image.Point
+
+ stenciler *stenciler
+ coverer *coverer
+}
+
+type coverer struct {
+ ctx driver.Device
+ prog [3]*program
+ texUniforms *coverTexUniforms
+ colUniforms *coverColUniforms
+ linearGradientUniforms *coverLinearGradientUniforms
+ layout driver.InputLayout
+}
+
+type coverTexUniforms struct {
+ vert struct {
+ coverUniforms
+ _ [12]byte // Padding to multiple of 16.
+ }
+}
+
+type coverColUniforms struct {
+ vert struct {
+ coverUniforms
+ _ [12]byte // Padding to multiple of 16.
+ }
+ frag struct {
+ colorUniforms
+ }
+}
+
+type coverLinearGradientUniforms struct {
+ vert struct {
+ coverUniforms
+ _ [12]byte // Padding to multiple of 16.
+ }
+ frag struct {
+ gradientUniforms
+ }
+}
+
+type coverUniforms struct {
+ transform [4]float32
+ uvCoverTransform [4]float32
+ uvTransformR1 [4]float32
+ uvTransformR2 [4]float32
+ z float32
+}
+
+type stenciler struct {
+ ctx driver.Device
+ prog struct {
+ prog *program
+ uniforms *stencilUniforms
+ layout driver.InputLayout
+ }
+ iprog struct {
+ prog *program
+ uniforms *intersectUniforms
+ layout driver.InputLayout
+ }
+ fbos fboSet
+ intersections fboSet
+ indexBuf driver.Buffer
+}
+
+type stencilUniforms struct {
+ vert struct {
+ transform [4]float32
+ pathOffset [2]float32
+ _ [8]byte // Padding to multiple of 16.
+ }
+}
+
+type intersectUniforms struct {
+ vert struct {
+ uvTransform [4]float32
+ subUVTransform [4]float32
+ }
+}
+
+type fboSet struct {
+ fbos []stencilFBO
+}
+
+type stencilFBO struct {
+ size image.Point
+ fbo driver.Framebuffer
+ tex driver.Texture
+}
+
+type pathData struct {
+ ncurves int
+ data driver.Buffer
+}
+
+// vertex data suitable for passing to vertex programs.
+type vertex struct {
+ // Corner encodes the corner: +0.5 for south, +.25 for east.
+ Corner float32
+ MaxY float32
+ FromX, FromY float32
+ CtrlX, CtrlY float32
+ ToX, ToY float32
+}
+
+func (v vertex) encode(d []byte, maxy uint32) {
+ bo := binary.LittleEndian
+ bo.PutUint32(d[0:], math.Float32bits(v.Corner))
+ bo.PutUint32(d[4:], maxy)
+ bo.PutUint32(d[8:], math.Float32bits(v.FromX))
+ bo.PutUint32(d[12:], math.Float32bits(v.FromY))
+ bo.PutUint32(d[16:], math.Float32bits(v.CtrlX))
+ bo.PutUint32(d[20:], math.Float32bits(v.CtrlY))
+ bo.PutUint32(d[24:], math.Float32bits(v.ToX))
+ bo.PutUint32(d[28:], math.Float32bits(v.ToY))
+}
+
+const (
+ // Number of path quads per draw batch.
+ pathBatchSize = 10000
+ // Size of a vertex as sent to gpu
+ vertStride = 8 * 4
+)
+
+func newPather(ctx driver.Device) *pather {
+ return &pather{
+ ctx: ctx,
+ stenciler: newStenciler(ctx),
+ coverer: newCoverer(ctx),
+ }
+}
+
+func newCoverer(ctx driver.Device) *coverer {
+ c := &coverer{
+ ctx: ctx,
+ }
+ c.colUniforms = new(coverColUniforms)
+ c.texUniforms = new(coverTexUniforms)
+ c.linearGradientUniforms = new(coverLinearGradientUniforms)
+ prog, layout, err := createColorPrograms(ctx, shader_cover_vert,
+ shader_cover_frag,
+ [3]interface{}{&c.colUniforms.vert, &c.linearGradientUniforms.vert,
+ &c.texUniforms.vert},
+ [3]interface{}{&c.colUniforms.frag, &c.linearGradientUniforms.frag,
+ nil},
+ )
+ if err != nil {
+ panic(err)
+ }
+ c.prog = prog
+ c.layout = layout
+ return c
+}
+
+func newStenciler(ctx driver.Device) *stenciler {
+ // Allocate a suitably large index buffer for drawing paths.
+ indices := make([]uint16, pathBatchSize*6)
+ for i := 0; i < pathBatchSize; i++ {
+ i := uint16(i)
+ indices[i*6+0] = i*4 + 0
+ indices[i*6+1] = i*4 + 1
+ indices[i*6+2] = i*4 + 2
+ indices[i*6+3] = i*4 + 2
+ indices[i*6+4] = i*4 + 1
+ indices[i*6+5] = i*4 + 3
+ }
+ indexBuf, err := ctx.NewImmutableBuffer(driver.BufferBindingIndices,
+ byteslice.Slice(indices))
+ if err != nil {
+ panic(err)
+ }
+ progLayout, err := ctx.NewInputLayout(shader_stencil_vert,
+ []driver.InputDesc{
+ {Type: driver.DataTypeFloat, Size: 1,
+ Offset: int(unsafe.Offsetof((*(*vertex)(nil)).Corner))},
+ {Type: driver.DataTypeFloat, Size: 1,
+ Offset: int(unsafe.Offsetof((*(*vertex)(nil)).MaxY))},
+ {Type: driver.DataTypeFloat, Size: 2,
+ Offset: int(unsafe.Offsetof((*(*vertex)(nil)).FromX))},
+ {Type: driver.DataTypeFloat, Size: 2,
+ Offset: int(unsafe.Offsetof((*(*vertex)(nil)).CtrlX))},
+ {Type: driver.DataTypeFloat, Size: 2,
+ Offset: int(unsafe.Offsetof((*(*vertex)(nil)).ToX))},
+ })
+ if err != nil {
+ panic(err)
+ }
+ iprogLayout, err := ctx.NewInputLayout(shader_intersect_vert,
+ []driver.InputDesc{
+ {Type: driver.DataTypeFloat, Size: 2, Offset: 0},
+ {Type: driver.DataTypeFloat, Size: 2, Offset: 4 * 2},
+ })
+ if err != nil {
+ panic(err)
+ }
+ st := &stenciler{
+ ctx: ctx,
+ indexBuf: indexBuf,
+ }
+ prog, err := ctx.NewProgram(shader_stencil_vert, shader_stencil_frag)
+ if err != nil {
+ panic(err)
+ }
+ st.prog.uniforms = new(stencilUniforms)
+ vertUniforms := newUniformBuffer(ctx, &st.prog.uniforms.vert)
+ st.prog.prog = newProgram(prog, vertUniforms, nil)
+ st.prog.layout = progLayout
+ iprog, err := ctx.NewProgram(shader_intersect_vert, shader_intersect_frag)
+ if err != nil {
+ panic(err)
+ }
+ st.iprog.uniforms = new(intersectUniforms)
+ vertUniforms = newUniformBuffer(ctx, &st.iprog.uniforms.vert)
+ st.iprog.prog = newProgram(iprog, vertUniforms, nil)
+ st.iprog.layout = iprogLayout
+ return st
+}
+
+func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) {
+ // Add fbos.
+ for i := len(s.fbos); i < len(sizes); i++ {
+ s.fbos = append(s.fbos, stencilFBO{})
+ }
+ // Resize fbos.
+ for i, sz := range sizes {
+ f := &s.fbos[i]
+ // Resizing or recreating FBOs can introduce rendering stalls.
+ // Avoid if the space waste is not too high.
+ resize := sz.X > f.size.X || sz.Y > f.size.Y
+ waste := float32(sz.X*sz.Y) / float32(f.size.X*f.size.Y)
+ resize = resize || waste > 1.2
+ if resize {
+ if f.fbo != nil {
+ f.fbo.Release()
+ f.tex.Release()
+ }
+ tex, err := ctx.NewTexture(driver.TextureFormatFloat, sz.X, sz.Y,
+ driver.FilterNearest, driver.FilterNearest,
+ driver.BufferBindingTexture|driver.BufferBindingFramebuffer)
+ if err != nil {
+ panic(err)
+ }
+ fbo, err := ctx.NewFramebuffer(tex, 0)
+ if err != nil {
+ panic(err)
+ }
+ f.size = sz
+ f.tex = tex
+ f.fbo = fbo
+ }
+ }
+ // Delete extra fbos.
+ s.delete(ctx, len(sizes))
+}
+
+func (s *fboSet) invalidate(ctx driver.Device) {
+ for _, f := range s.fbos {
+ f.fbo.Invalidate()
+ }
+}
+
+func (s *fboSet) delete(ctx driver.Device, idx int) {
+ for i := idx; i < len(s.fbos); i++ {
+ f := s.fbos[i]
+ f.fbo.Release()
+ f.tex.Release()
+ }
+ s.fbos = s.fbos[:idx]
+}
+
+func (s *stenciler) release() {
+ s.fbos.delete(s.ctx, 0)
+ s.prog.layout.Release()
+ s.prog.prog.Release()
+ s.iprog.layout.Release()
+ s.iprog.prog.Release()
+ s.indexBuf.Release()
+}
+
+func (p *pather) release() {
+ p.stenciler.release()
+ p.coverer.release()
+}
+
+func (c *coverer) release() {
+ for _, p := range c.prog {
+ p.Release()
+ }
+ c.layout.Release()
+}
+
+func buildPath(ctx driver.Device, p []byte) pathData {
+ buf, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices, p)
+ if err != nil {
+ panic(err)
+ }
+ return pathData{
+ ncurves: len(p) / vertStride,
+ data: buf,
+ }
+}
+
+func (p pathData) release() {
+ p.data.Release()
+}
+
+func (p *pather) begin(sizes []image.Point) {
+ p.stenciler.begin(sizes)
+}
+
+func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point,
+ uv image.Point, data pathData) {
+ p.stenciler.stencilPath(bounds, offset, uv, data)
+}
+
+func (s *stenciler) beginIntersect(sizes []image.Point) {
+ s.ctx.BlendFunc(driver.BlendFactorDstColor, driver.BlendFactorZero)
+ // 8 bit coverage is enough, but OpenGL ES only supports single channel
+ // floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if
+ // no floating point support is available.
+ s.intersections.resize(s.ctx, sizes)
+ s.ctx.BindProgram(s.iprog.prog.prog)
+}
+
+func (s *stenciler) invalidateFBO() {
+ s.intersections.invalidate(s.ctx)
+ s.fbos.invalidate(s.ctx)
+}
+
+func (s *stenciler) cover(idx int) stencilFBO {
+ return s.fbos.fbos[idx]
+}
+
+func (s *stenciler) begin(sizes []image.Point) {
+ s.ctx.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOne)
+ s.fbos.resize(s.ctx, sizes)
+ s.ctx.BindProgram(s.prog.prog.prog)
+ s.ctx.BindInputLayout(s.prog.layout)
+ s.ctx.BindIndexBuffer(s.indexBuf)
+}
+
+func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point,
+ uv image.Point, data pathData) {
+ s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy())
+ // Transform UI coordinates to OpenGL coordinates.
+ texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())}
+ scale := f32.Point{X: 2 / texSize.X, Y: 2 / texSize.Y}
+ orig := f32.Point{X: -1 - float32(bounds.Min.X)*2/texSize.X,
+ Y: -1 - float32(bounds.Min.Y)*2/texSize.Y}
+ s.prog.uniforms.vert.transform = [4]float32{scale.X, scale.Y, orig.X,
+ orig.Y}
+ s.prog.uniforms.vert.pathOffset = [2]float32{offset.X, offset.Y}
+ s.prog.prog.UploadUniforms()
+ // Draw in batches that fit in uint16 indices.
+ start := 0
+ nquads := data.ncurves / 4
+ for start < nquads {
+ batch := nquads - start
+ if max := pathBatchSize; batch > max {
+ batch = max
+ }
+ off := vertStride * start * 4
+ s.ctx.BindVertexBuffer(data.data, vertStride, off)
+ s.ctx.DrawElements(driver.DrawModeTriangles, 0, batch*6)
+ start += batch
+ }
+}
+
+func (p *pather) cover(z float32, mat materialType, col f32color.RGBA,
+ col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D,
+ coverScale, coverOff f32.Point) {
+ p.coverer.cover(z, mat, col, col1, col2, scale, off, uvTrans, coverScale,
+ coverOff)
+}
+
+func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA,
+ col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D,
+ coverScale, coverOff f32.Point) {
+ p := c.prog[mat]
+ c.ctx.BindProgram(p.prog)
+ var uniforms *coverUniforms
+ switch mat {
+ case materialColor:
+ c.colUniforms.frag.color = col
+ uniforms = &c.colUniforms.vert.coverUniforms
+ case materialLinearGradient:
+ c.linearGradientUniforms.frag.color1 = col1
+ c.linearGradientUniforms.frag.color2 = col2
+
+ t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
+ c.linearGradientUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0}
+ c.linearGradientUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0}
+ uniforms = &c.linearGradientUniforms.vert.coverUniforms
+ case materialTexture:
+ t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
+ c.texUniforms.vert.uvTransformR1 = [4]float32{t1, t2, t3, 0}
+ c.texUniforms.vert.uvTransformR2 = [4]float32{t4, t5, t6, 0}
+ uniforms = &c.texUniforms.vert.coverUniforms
+ }
+ uniforms.z = z
+ uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
+ uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y,
+ coverOff.X, coverOff.Y}
+ p.UploadUniforms()
+ c.ctx.DrawArrays(driver.DrawModeTriangleStrip, 0, 4)
+}
+
+func init() {
+ // Check that struct vertex has the expected size and
+ // that it contains no padding.
+ if unsafe.Sizeof(*(*vertex)(nil)) != vertStride {
+ panic("unexpected struct size")
+ }
+}
diff --git a/gio/gpu/shaders.go b/gio/gpu/shaders.go
new file mode 100644
index 0000000..7df7cb5
--- /dev/null
+++ b/gio/gpu/shaders.go
@@ -0,0 +1,6694 @@
+// Code generated by build.go. DO NOT EDIT.
+
+package gpu
+
+import "realy.lol/gio/gpu/internal/driver"
+
+var (
+ shader_backdrop_comp = driver.ShaderSources{
+ Name: "backdrop.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct AnnotatedRef
+{
+ uint offset;
+};
+
+struct AnnotatedTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct PathRef
+{
+ uint offset;
+};
+
+struct TileRef
+{
+ uint offset;
+};
+
+struct Path
+{
+ uvec4 bbox;
+ TileRef tiles;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _77;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _191;
+
+shared uint sh_row_width[128];
+shared Alloc sh_row_alloc[128];
+shared uint sh_row_count[128];
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+uint read_mem(Alloc alloc, uint offset)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return 0u;
+ }
+ uint v = _77.memory[offset];
+ return v;
+}
+
+AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint tag_and_flags = read_mem(param, param_1);
+ return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+uint fill_mode_from_flags(uint flags)
+{
+ return flags & 1u;
+}
+
+Path Path_read(Alloc a, PathRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Path s;
+ s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16));
+ s.tiles = TileRef(raw2);
+ return s;
+}
+
+Alloc new_alloc(uint offset, uint size)
+{
+ Alloc a;
+ a.offset = offset;
+ return a;
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _77.memory[offset] = val;
+}
+
+void main()
+{
+ if (_77.mem_error != 0u)
+ {
+ return;
+ }
+ uint th_ix = gl_LocalInvocationID.x;
+ uint element_ix = gl_GlobalInvocationID.x;
+ AnnotatedRef ref = AnnotatedRef(_191.conf.anno_alloc.offset + (element_ix * 32u));
+ uint row_count = 0u;
+ if (element_ix < _191.conf.n_elements)
+ {
+ Alloc param;
+ param.offset = _191.conf.anno_alloc.offset;
+ AnnotatedRef param_1 = ref;
+ AnnotatedTag tag = Annotated_tag(param, param_1);
+ switch (tag.tag)
+ {
+ case 2u:
+ case 3u:
+ case 1u:
+ {
+ uint param_2 = tag.flags;
+ if (fill_mode_from_flags(param_2) != 0u)
+ {
+ break;
+ }
+ PathRef path_ref = PathRef(_191.conf.tile_alloc.offset + (element_ix * 12u));
+ Alloc param_3;
+ param_3.offset = _191.conf.tile_alloc.offset;
+ PathRef param_4 = path_ref;
+ Path path = Path_read(param_3, param_4);
+ sh_row_width[th_ix] = path.bbox.z - path.bbox.x;
+ row_count = path.bbox.w - path.bbox.y;
+ bool _267 = row_count == 1u;
+ bool _273;
+ if (_267)
+ {
+ _273 = path.bbox.y > 0u;
+ }
+ else
+ {
+ _273 = _267;
+ }
+ if (_273)
+ {
+ row_count = 0u;
+ }
+ uint param_5 = path.tiles.offset;
+ uint param_6 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u;
+ Alloc path_alloc = new_alloc(param_5, param_6);
+ sh_row_alloc[th_ix] = path_alloc;
+ break;
+ }
+ }
+ }
+ sh_row_count[th_ix] = row_count;
+ for (uint i = 0u; i < 7u; i++)
+ {
+ barrier();
+ if (th_ix >= uint(1 << int(i)))
+ {
+ row_count += sh_row_count[th_ix - uint(1 << int(i))];
+ }
+ barrier();
+ sh_row_count[th_ix] = row_count;
+ }
+ barrier();
+ uint total_rows = sh_row_count[127];
+ uint _395;
+ for (uint row = th_ix; row < total_rows; row += 128u)
+ {
+ uint el_ix = 0u;
+ for (uint i_1 = 0u; i_1 < 7u; i_1++)
+ {
+ uint probe = el_ix + uint(64 >> int(i_1));
+ if (row >= sh_row_count[probe - 1u])
+ {
+ el_ix = probe;
+ }
+ }
+ uint width = sh_row_width[el_ix];
+ if (width > 0u)
+ {
+ Alloc tiles_alloc = sh_row_alloc[el_ix];
+ if (el_ix > 0u)
+ {
+ _395 = sh_row_count[el_ix - 1u];
+ }
+ else
+ {
+ _395 = 0u;
+ }
+ uint seq_ix = row - _395;
+ uint tile_el_ix = ((tiles_alloc.offset >> uint(2)) + 1u) + ((seq_ix * 2u) * width);
+ Alloc param_7 = tiles_alloc;
+ uint param_8 = tile_el_ix;
+ uint sum = read_mem(param_7, param_8);
+ for (uint x = 1u; x < width; x++)
+ {
+ tile_el_ix += 2u;
+ Alloc param_9 = tiles_alloc;
+ uint param_10 = tile_el_ix;
+ sum += read_mem(param_9, param_10);
+ Alloc param_11 = tiles_alloc;
+ uint param_12 = tile_el_ix;
+ uint param_13 = sum;
+ write_mem(param_11, param_12, param_13);
+ }
+ }
+ }
+}
+
+`,
+ }
+ shader_binning_comp = driver.ShaderSources{
+ Name: "binning.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct MallocResult
+{
+ Alloc alloc;
+ bool failed;
+};
+
+struct AnnoEndClipRef
+{
+ uint offset;
+};
+
+struct AnnoEndClip
+{
+ vec4 bbox;
+};
+
+struct AnnotatedRef
+{
+ uint offset;
+};
+
+struct AnnotatedTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct BinInstanceRef
+{
+ uint offset;
+};
+
+struct BinInstance
+{
+ uint element_ix;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _88;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _254;
+
+shared uint bitmaps[4][128];
+shared bool sh_alloc_failed;
+shared uint count[4][128];
+shared Alloc sh_chunk_alloc[128];
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+uint read_mem(Alloc alloc, uint offset)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return 0u;
+ }
+ uint v = _88.memory[offset];
+ return v;
+}
+
+AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint tag_and_flags = read_mem(param, param_1);
+ return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+AnnoEndClip AnnoEndClip_read(Alloc a, AnnoEndClipRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ AnnoEndClip s;
+ s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ return s;
+}
+
+AnnoEndClip Annotated_EndClip_read(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ AnnoEndClipRef param_1 = AnnoEndClipRef(ref.offset + 4u);
+ return AnnoEndClip_read(param, param_1);
+}
+
+Alloc new_alloc(uint offset, uint size)
+{
+ Alloc a;
+ a.offset = offset;
+ return a;
+}
+
+MallocResult malloc(uint size)
+{
+ MallocResult r;
+ r.failed = false;
+ uint _94 = atomicAdd(_88.mem_offset, size);
+ uint offset = _94;
+ uint param = offset;
+ uint param_1 = size;
+ r.alloc = new_alloc(param, param_1);
+ if ((offset + size) > uint(int(uint(_88.memory.length())) * 4))
+ {
+ r.failed = true;
+ uint _115 = atomicMax(_88.mem_error, 1u);
+ return r;
+ }
+ return r;
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _88.memory[offset] = val;
+}
+
+void BinInstance_write(Alloc a, BinInstanceRef ref, BinInstance s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.element_ix;
+ write_mem(param, param_1, param_2);
+}
+
+void main()
+{
+ if (_88.mem_error != 0u)
+ {
+ return;
+ }
+ uint my_n_elements = _254.conf.n_elements;
+ uint my_partition = gl_WorkGroupID.x;
+ for (uint i = 0u; i < 4u; i++)
+ {
+ bitmaps[i][gl_LocalInvocationID.x] = 0u;
+ }
+ if (gl_LocalInvocationID.x == 0u)
+ {
+ sh_alloc_failed = false;
+ }
+ barrier();
+ uint element_ix = (my_partition * 128u) + gl_LocalInvocationID.x;
+ AnnotatedRef ref = AnnotatedRef(_254.conf.anno_alloc.offset + (element_ix * 32u));
+ uint tag = 0u;
+ if (element_ix < my_n_elements)
+ {
+ Alloc param;
+ param.offset = _254.conf.anno_alloc.offset;
+ AnnotatedRef param_1 = ref;
+ tag = Annotated_tag(param, param_1).tag;
+ }
+ int x0 = 0;
+ int y0 = 0;
+ int x1 = 0;
+ int y1 = 0;
+ switch (tag)
+ {
+ case 1u:
+ case 2u:
+ case 3u:
+ case 4u:
+ {
+ Alloc param_2;
+ param_2.offset = _254.conf.anno_alloc.offset;
+ AnnotatedRef param_3 = ref;
+ AnnoEndClip clip = Annotated_EndClip_read(param_2, param_3);
+ x0 = int(floor(clip.bbox.x * 0.001953125));
+ y0 = int(floor(clip.bbox.y * 0.00390625));
+ x1 = int(ceil(clip.bbox.z * 0.001953125));
+ y1 = int(ceil(clip.bbox.w * 0.00390625));
+ break;
+ }
+ }
+ uint width_in_bins = ((_254.conf.width_in_tiles + 16u) - 1u) / 16u;
+ uint height_in_bins = ((_254.conf.height_in_tiles + 8u) - 1u) / 8u;
+ x0 = clamp(x0, 0, int(width_in_bins));
+ x1 = clamp(x1, x0, int(width_in_bins));
+ y0 = clamp(y0, 0, int(height_in_bins));
+ y1 = clamp(y1, y0, int(height_in_bins));
+ if (x0 == x1)
+ {
+ y1 = y0;
+ }
+ int x = x0;
+ int y = y0;
+ uint my_slice = gl_LocalInvocationID.x / 32u;
+ uint my_mask = uint(1 << int(gl_LocalInvocationID.x & 31u));
+ while (y < y1)
+ {
+ uint _438 = atomicOr(bitmaps[my_slice][(uint(y) * width_in_bins) + uint(x)], my_mask);
+ x++;
+ if (x == x1)
+ {
+ x = x0;
+ y++;
+ }
+ }
+ barrier();
+ uint element_count = 0u;
+ for (uint i_1 = 0u; i_1 < 4u; i_1++)
+ {
+ element_count += uint(bitCount(bitmaps[i_1][gl_LocalInvocationID.x]));
+ count[i_1][gl_LocalInvocationID.x] = element_count;
+ }
+ uint param_4 = 0u;
+ uint param_5 = 0u;
+ Alloc chunk_alloc = new_alloc(param_4, param_5);
+ if (element_count != 0u)
+ {
+ uint param_6 = element_count * 4u;
+ MallocResult _487 = malloc(param_6);
+ MallocResult chunk = _487;
+ chunk_alloc = chunk.alloc;
+ sh_chunk_alloc[gl_LocalInvocationID.x] = chunk_alloc;
+ if (chunk.failed)
+ {
+ sh_alloc_failed = true;
+ }
+ }
+ uint out_ix = (_254.conf.bin_alloc.offset >> uint(2)) + (((my_partition * 128u) + gl_LocalInvocationID.x) * 2u);
+ Alloc param_7;
+ param_7.offset = _254.conf.bin_alloc.offset;
+ uint param_8 = out_ix;
+ uint param_9 = element_count;
+ write_mem(param_7, param_8, param_9);
+ Alloc param_10;
+ param_10.offset = _254.conf.bin_alloc.offset;
+ uint param_11 = out_ix + 1u;
+ uint param_12 = chunk_alloc.offset;
+ write_mem(param_10, param_11, param_12);
+ barrier();
+ if (sh_alloc_failed)
+ {
+ return;
+ }
+ x = x0;
+ y = y0;
+ while (y < y1)
+ {
+ uint bin_ix = (uint(y) * width_in_bins) + uint(x);
+ uint out_mask = bitmaps[my_slice][bin_ix];
+ if ((out_mask & my_mask) != 0u)
+ {
+ uint idx = uint(bitCount(out_mask & (my_mask - 1u)));
+ if (my_slice > 0u)
+ {
+ idx += count[my_slice - 1u][bin_ix];
+ }
+ Alloc out_alloc = sh_chunk_alloc[bin_ix];
+ uint out_offset = out_alloc.offset + (idx * 4u);
+ Alloc param_13 = out_alloc;
+ BinInstanceRef param_14 = BinInstanceRef(out_offset);
+ BinInstance param_15 = BinInstance(element_ix);
+ BinInstance_write(param_13, param_14, param_15);
+ }
+ x++;
+ if (x == x1)
+ {
+ x = x0;
+ y++;
+ }
+ }
+}
+
+`,
+ }
+ shader_blit_frag = [...]driver.ShaderSources{
+ {
+ Name: "blit.frag",
+ Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Color", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_color.color", Type: 0x0, Size: 4, Offset: 0}},
+ Size: 16,
+ },
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+struct Color
+{
+ vec4 color;
+};
+
+uniform Color _color;
+
+varying vec2 vUV;
+
+void main()
+{
+ gl_FragData[0] = _color.color;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+layout(std140) uniform Color
+{
+ vec4 color;
+} _color;
+
+layout(location = 0) out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = _color.color;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct Color
+{
+ vec4 color;
+};
+
+uniform Color _color;
+
+out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = _color.color;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0, std140) uniform Color
+{
+ vec4 color;
+} _color;
+
+out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = _color.color;
+}
+
+`,
+ HLSL: "DXBC,\xc1\x9c\x85P\xbc\xab\x8a.\x9e\b\xdd\xf7\xd2\x18\xa2\x01\x00\x00\x00t\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\x84\x00\x00\x00\xcc\x00\x00\x00H\x01\x00\x00\f\x02\x00\x00@\x02\x00\x00Aon9D\x00\x00\x00D\x00\x00\x00\x00\x02\xff\xff\x14\x00\x00\x000\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x00\x000\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\xa0\xff\xff\x00\x00SHDR@\x00\x00\x00@\x00\x00\x00\x10\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x006\x00\x00\x06\xf2 \x10\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xbc\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x94\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Color\x00\xab\xab<\x00\x00\x00\x01\x00\x00\x00\\\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00t\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\x84\x00\x00\x00\x00\x00\x00\x00_color_color\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ },
+ {
+ Name: "blit.frag",
+ Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Gradient", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_gradient.color1", Type: 0x0, Size: 4, Offset: 0}, {Name: "_gradient.color2", Type: 0x0, Size: 4, Offset: 16}},
+ Size: 32,
+ },
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+struct Gradient
+{
+ vec4 color1;
+ vec4 color2;
+};
+
+uniform Gradient _gradient;
+
+varying vec2 vUV;
+
+void main()
+{
+ gl_FragData[0] = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+layout(std140) uniform Gradient
+{
+ vec4 color1;
+ vec4 color2;
+} _gradient;
+
+layout(location = 0) out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct Gradient
+{
+ vec4 color1;
+ vec4 color2;
+};
+
+uniform Gradient _gradient;
+
+out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0, std140) uniform Gradient
+{
+ vec4 color1;
+ vec4 color2;
+} _gradient;
+
+out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+}
+
+`,
+ HLSL: "DXBCdZ\xb9AA\xb2\xa5-Ī£c\xb9\xdc\xfd]\xae\x01\x00\x00\x00P\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xcc\x00\x00\x00t\x01\x00\x00\xf0\x01\x00\x00\xe8\x02\x00\x00\x1c\x03\x00\x00Aon9\x8c\x00\x00\x00\x8c\x00\x00\x00\x00\x02\xff\xff\\\x00\x00\x000\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x00\x000\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x01\x00\x00\x02\x00\x00\x18\x80\x00\x00\x00\xb0\x01\x00\x00\x02\x01\x00\x0f\x80\x00\x00\xe4\xa0\x02\x00\x00\x03\x01\x00\x0f\x80\x01\x00\xe4\x81\x01\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\x0f\x80\x00\x00\xff\x80\x01\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xa0\x00\x00\x00@\x00\x00\x00(\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00b\x10\x00\x03\x12\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006 \x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x8e \x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x002\x00\x00\n\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xf0\x00\x00\x00\x01\x00\x00\x00H\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xc5\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Gradient\x00\xab\xab\xab<\x00\x00\x00\x02\x00\x00\x00`\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x00\x00\xb4\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa4\x00\x00\x00\x00\x00\x00\x00_gradient_color1\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_gradient_color2\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x01\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ },
+ {
+ Name: "blit.frag",
+ Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}},
+ Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+
+varying vec2 vUV;
+
+void main()
+{
+ gl_FragData[0] = texture2D(tex, vUV);
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+
+layout(location = 0) out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = texture(tex, vUV);
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+
+out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = texture(tex, vUV);
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+
+out vec4 fragColor;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = texture(tex, vUV);
+}
+
+`,
+ HLSL: "DXBC\xb7?\x1d\xb1\x80Ķ\xa3W\t\xfbZ\x9fV\xd6\xda\x01\x00\x00\x00\x94\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\xa4\x00\x00\x00\x10\x01\x00\x00\x8c\x01\x00\x00,\x02\x00\x00`\x02\x00\x00Aon9d\x00\x00\x00d\x00\x00\x00\x00\x02\xff\xff<\x00\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDRd\x00\x00\x00@\x00\x00\x00\x19\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00E\x00\x00\t\xf2 \x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00m\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00i\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ },
+ }
+ shader_blit_vert = driver.ShaderSources{
+ Name: "blit.vert",
+ Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.uvTransformR1", Type: 0x0, Size: 4, Offset: 16}, {Name: "_block.uvTransformR2", Type: 0x0, Size: 4, Offset: 32}, {Name: "_block.z", Type: 0x0, Size: 1, Offset: 48}},
+ Size: 52,
+ },
+ GLSL100ES: `#version 100
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+struct Block
+{
+ vec4 transform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+};
+
+uniform Block _block;
+
+attribute vec2 pos;
+varying vec2 vUV;
+attribute vec2 uv;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec2 p = (pos * _block.transform.xy) + _block.transform.zw;
+ vec4 param = vec4(p, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+layout(std140) uniform Block
+{
+ vec4 transform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+} _block;
+
+layout(location = 0) in vec2 pos;
+out vec2 vUV;
+layout(location = 1) in vec2 uv;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec2 p = (pos * _block.transform.xy) + _block.transform.zw;
+ vec4 param = vec4(p, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+struct Block
+{
+ vec4 transform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+};
+
+uniform Block _block;
+
+in vec2 pos;
+out vec2 vUV;
+in vec2 uv;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec2 p = (pos * _block.transform.xy) + _block.transform.zw;
+ vec4 param = vec4(p, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+layout(binding = 0, std140) uniform Block
+{
+ vec4 transform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+} _block;
+
+in vec2 pos;
+out vec2 vUV;
+in vec2 uv;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec2 p = (pos * _block.transform.xy) + _block.transform.zw;
+ vec4 param = vec4(p, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+}
+
+`,
+ HLSL: "DXBC\x80\xa7\xa0\x9e\xbb\xa1\xa3\x1b\x85\xac\xb6\xe9\xfb\xe6W\x03\x01\x00\x00\x00\xc8\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00$\x01\x00\x00T\x02\x00\x00\xd0\x02\x00\x00$\x04\x00\x00p\x04\x00\x00Aon9\xe4\x00\x00\x00\xe4\x00\x00\x00\x00\x02\xfe\xff\xb0\x00\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x04\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x05\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00?\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\a\x80\x01\x00Ä\x05\x00Š \x05\x00Å \b\x00\x00\x03\x00\x00\x01\xe0\x02\x00\xe4\xa0\x00\x00\xe4\x80\b\x00\x00\x03\x00\x00\x02\xe0\x03\x00\xe4\xa0\x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x90\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\a\x80\x05\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\f\xc0\x04\x00\x00\xa0\x00\x00d\x80\x00\x00$\x80\xff\xff\x00\x00SHDR(\x01\x00\x00@\x00\x01\x00J\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x04\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x006\x00\x00\x052\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x10\x00\x00\b\x12 \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x10\x00\x00\b\" \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x002\x00\x00\v2 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\nB \x10\x00\x01\x00\x00\x00\n\x80 \x00\x00\x00\x00\x00\x03\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\x01@\x00\x00\x00\x00\x00?6\x00\x00\x05\x82 \x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\b\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFL\x01\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00$\x01\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x04\x00\x00\x00\\\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbc\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\xf5\x00\x00\x00 \x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\n\x01\x00\x000\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x14\x01\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_uvTransformR1\x00_block_uvTransformR2\x00_block_z\x00\xab\x00\x00\x03\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab",
+ }
+ shader_coarse_comp = driver.ShaderSources{
+ Name: "coarse.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct MallocResult
+{
+ Alloc alloc;
+ bool failed;
+};
+
+struct AnnoImageRef
+{
+ uint offset;
+};
+
+struct AnnoImage
+{
+ vec4 bbox;
+ float linewidth;
+ uint index;
+ ivec2 offset;
+};
+
+struct AnnoColorRef
+{
+ uint offset;
+};
+
+struct AnnoColor
+{
+ vec4 bbox;
+ float linewidth;
+ uint rgba_color;
+};
+
+struct AnnoBeginClipRef
+{
+ uint offset;
+};
+
+struct AnnoBeginClip
+{
+ vec4 bbox;
+ float linewidth;
+};
+
+struct AnnotatedRef
+{
+ uint offset;
+};
+
+struct AnnotatedTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct BinInstanceRef
+{
+ uint offset;
+};
+
+struct BinInstance
+{
+ uint element_ix;
+};
+
+struct PathRef
+{
+ uint offset;
+};
+
+struct TileRef
+{
+ uint offset;
+};
+
+struct Path
+{
+ uvec4 bbox;
+ TileRef tiles;
+};
+
+struct TileSegRef
+{
+ uint offset;
+};
+
+struct Tile
+{
+ TileSegRef tile;
+ int backdrop;
+};
+
+struct CmdStrokeRef
+{
+ uint offset;
+};
+
+struct CmdStroke
+{
+ uint tile_ref;
+ float half_width;
+};
+
+struct CmdFillRef
+{
+ uint offset;
+};
+
+struct CmdFill
+{
+ uint tile_ref;
+ int backdrop;
+};
+
+struct CmdColorRef
+{
+ uint offset;
+};
+
+struct CmdColor
+{
+ uint rgba_color;
+};
+
+struct CmdImageRef
+{
+ uint offset;
+};
+
+struct CmdImage
+{
+ uint index;
+ ivec2 offset;
+};
+
+struct CmdJumpRef
+{
+ uint offset;
+};
+
+struct CmdJump
+{
+ uint new_ref;
+};
+
+struct CmdRef
+{
+ uint offset;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _276;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _1066;
+
+shared uint sh_bitmaps[4][128];
+shared Alloc sh_part_elements[128];
+shared uint sh_part_count[128];
+shared uint sh_elements[128];
+shared uint sh_tile_stride[128];
+shared uint sh_tile_width[128];
+shared uint sh_tile_x0[128];
+shared uint sh_tile_y0[128];
+shared uint sh_tile_base[128];
+shared uint sh_tile_count[128];
+
+Alloc new_alloc(uint offset, uint size)
+{
+ Alloc a;
+ a.offset = offset;
+ return a;
+}
+
+Alloc slice_mem(Alloc a, uint offset, uint size)
+{
+ uint param = a.offset + offset;
+ uint param_1 = size;
+ return new_alloc(param, param_1);
+}
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+uint read_mem(Alloc alloc, uint offset)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return 0u;
+ }
+ uint v = _276.memory[offset];
+ return v;
+}
+
+BinInstanceRef BinInstance_index(BinInstanceRef ref, uint index)
+{
+ return BinInstanceRef(ref.offset + (index * 4u));
+}
+
+BinInstance BinInstance_read(Alloc a, BinInstanceRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ BinInstance s;
+ s.element_ix = raw0;
+ return s;
+}
+
+AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint tag_and_flags = read_mem(param, param_1);
+ return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+Path Path_read(Alloc a, PathRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Path s;
+ s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16));
+ s.tiles = TileRef(raw2);
+ return s;
+}
+
+void write_tile_alloc(uint el_ix, Alloc a)
+{
+}
+
+Alloc read_tile_alloc(uint el_ix)
+{
+ uint param = 0u;
+ uint param_1 = uint(int(uint(_276.memory.length())) * 4);
+ return new_alloc(param, param_1);
+}
+
+Tile Tile_read(Alloc a, TileRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Tile s;
+ s.tile = TileSegRef(raw0);
+ s.backdrop = int(raw1);
+ return s;
+}
+
+AnnoColor AnnoColor_read(Alloc a, AnnoColorRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ Alloc param_8 = a;
+ uint param_9 = ix + 4u;
+ uint raw4 = read_mem(param_8, param_9);
+ Alloc param_10 = a;
+ uint param_11 = ix + 5u;
+ uint raw5 = read_mem(param_10, param_11);
+ AnnoColor s;
+ s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.linewidth = uintBitsToFloat(raw4);
+ s.rgba_color = raw5;
+ return s;
+}
+
+AnnoColor Annotated_Color_read(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ AnnoColorRef param_1 = AnnoColorRef(ref.offset + 4u);
+ return AnnoColor_read(param, param_1);
+}
+
+MallocResult malloc(uint size)
+{
+ MallocResult r;
+ r.failed = false;
+ uint _282 = atomicAdd(_276.mem_offset, size);
+ uint offset = _282;
+ uint param = offset;
+ uint param_1 = size;
+ r.alloc = new_alloc(param, param_1);
+ if ((offset + size) > uint(int(uint(_276.memory.length())) * 4))
+ {
+ r.failed = true;
+ uint _303 = atomicMax(_276.mem_error, 1u);
+ return r;
+ }
+ return r;
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _276.memory[offset] = val;
+}
+
+void CmdJump_write(Alloc a, CmdJumpRef ref, CmdJump s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.new_ref;
+ write_mem(param, param_1, param_2);
+}
+
+void Cmd_Jump_write(Alloc a, CmdRef ref, CmdJump s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 9u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ CmdJumpRef param_4 = CmdJumpRef(ref.offset + 4u);
+ CmdJump param_5 = s;
+ CmdJump_write(param_3, param_4, param_5);
+}
+
+bool alloc_cmd(inout Alloc cmd_alloc, inout CmdRef cmd_ref, inout uint cmd_limit)
+{
+ if (cmd_ref.offset < cmd_limit)
+ {
+ return true;
+ }
+ uint param = 1024u;
+ MallocResult _968 = malloc(param);
+ MallocResult new_cmd = _968;
+ if (new_cmd.failed)
+ {
+ return false;
+ }
+ CmdJump jump = CmdJump(new_cmd.alloc.offset);
+ Alloc param_1 = cmd_alloc;
+ CmdRef param_2 = cmd_ref;
+ CmdJump param_3 = jump;
+ Cmd_Jump_write(param_1, param_2, param_3);
+ cmd_alloc = new_cmd.alloc;
+ cmd_ref = CmdRef(cmd_alloc.offset);
+ cmd_limit = (cmd_alloc.offset + 1024u) - 36u;
+ return true;
+}
+
+uint fill_mode_from_flags(uint flags)
+{
+ return flags & 1u;
+}
+
+void CmdFill_write(Alloc a, CmdFillRef ref, CmdFill s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.tile_ref;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = uint(s.backdrop);
+ write_mem(param_3, param_4, param_5);
+}
+
+void Cmd_Fill_write(Alloc a, CmdRef ref, CmdFill s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 1u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ CmdFillRef param_4 = CmdFillRef(ref.offset + 4u);
+ CmdFill param_5 = s;
+ CmdFill_write(param_3, param_4, param_5);
+}
+
+void Cmd_Solid_write(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 3u;
+ write_mem(param, param_1, param_2);
+}
+
+void CmdStroke_write(Alloc a, CmdStrokeRef ref, CmdStroke s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.tile_ref;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.half_width);
+ write_mem(param_3, param_4, param_5);
+}
+
+void Cmd_Stroke_write(Alloc a, CmdRef ref, CmdStroke s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 2u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ CmdStrokeRef param_4 = CmdStrokeRef(ref.offset + 4u);
+ CmdStroke param_5 = s;
+ CmdStroke_write(param_3, param_4, param_5);
+}
+
+void write_fill(Alloc alloc, inout CmdRef cmd_ref, uint flags, Tile tile, float linewidth)
+{
+ uint param = flags;
+ if (fill_mode_from_flags(param) == 0u)
+ {
+ if (tile.tile.offset != 0u)
+ {
+ CmdFill cmd_fill = CmdFill(tile.tile.offset, tile.backdrop);
+ Alloc param_1 = alloc;
+ CmdRef param_2 = cmd_ref;
+ CmdFill param_3 = cmd_fill;
+ Cmd_Fill_write(param_1, param_2, param_3);
+ cmd_ref.offset += 12u;
+ }
+ else
+ {
+ Alloc param_4 = alloc;
+ CmdRef param_5 = cmd_ref;
+ Cmd_Solid_write(param_4, param_5);
+ cmd_ref.offset += 4u;
+ }
+ }
+ else
+ {
+ CmdStroke cmd_stroke = CmdStroke(tile.tile.offset, 0.5 * linewidth);
+ Alloc param_6 = alloc;
+ CmdRef param_7 = cmd_ref;
+ CmdStroke param_8 = cmd_stroke;
+ Cmd_Stroke_write(param_6, param_7, param_8);
+ cmd_ref.offset += 12u;
+ }
+}
+
+void CmdColor_write(Alloc a, CmdColorRef ref, CmdColor s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.rgba_color;
+ write_mem(param, param_1, param_2);
+}
+
+void Cmd_Color_write(Alloc a, CmdRef ref, CmdColor s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 5u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ CmdColorRef param_4 = CmdColorRef(ref.offset + 4u);
+ CmdColor param_5 = s;
+ CmdColor_write(param_3, param_4, param_5);
+}
+
+AnnoImage AnnoImage_read(Alloc a, AnnoImageRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ Alloc param_8 = a;
+ uint param_9 = ix + 4u;
+ uint raw4 = read_mem(param_8, param_9);
+ Alloc param_10 = a;
+ uint param_11 = ix + 5u;
+ uint raw5 = read_mem(param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 6u;
+ uint raw6 = read_mem(param_12, param_13);
+ AnnoImage s;
+ s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.linewidth = uintBitsToFloat(raw4);
+ s.index = raw5;
+ s.offset = ivec2(int(raw6 << uint(16)) >> 16, int(raw6) >> 16);
+ return s;
+}
+
+AnnoImage Annotated_Image_read(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ AnnoImageRef param_1 = AnnoImageRef(ref.offset + 4u);
+ return AnnoImage_read(param, param_1);
+}
+
+void CmdImage_write(Alloc a, CmdImageRef ref, CmdImage s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.index;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = (uint(s.offset.x) & 65535u) | (uint(s.offset.y) << uint(16));
+ write_mem(param_3, param_4, param_5);
+}
+
+void Cmd_Image_write(Alloc a, CmdRef ref, CmdImage s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 6u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ CmdImageRef param_4 = CmdImageRef(ref.offset + 4u);
+ CmdImage param_5 = s;
+ CmdImage_write(param_3, param_4, param_5);
+}
+
+AnnoBeginClip AnnoBeginClip_read(Alloc a, AnnoBeginClipRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ Alloc param_8 = a;
+ uint param_9 = ix + 4u;
+ uint raw4 = read_mem(param_8, param_9);
+ AnnoBeginClip s;
+ s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.linewidth = uintBitsToFloat(raw4);
+ return s;
+}
+
+AnnoBeginClip Annotated_BeginClip_read(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ AnnoBeginClipRef param_1 = AnnoBeginClipRef(ref.offset + 4u);
+ return AnnoBeginClip_read(param, param_1);
+}
+
+void Cmd_BeginClip_write(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 7u;
+ write_mem(param, param_1, param_2);
+}
+
+void Cmd_EndClip_write(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 8u;
+ write_mem(param, param_1, param_2);
+}
+
+void Cmd_End_write(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 0u;
+ write_mem(param, param_1, param_2);
+}
+
+void alloc_write(Alloc a, uint offset, Alloc alloc)
+{
+ Alloc param = a;
+ uint param_1 = offset >> uint(2);
+ uint param_2 = alloc.offset;
+ write_mem(param, param_1, param_2);
+}
+
+void main()
+{
+ if (_276.mem_error != 0u)
+ {
+ return;
+ }
+ uint width_in_bins = ((_1066.conf.width_in_tiles + 16u) - 1u) / 16u;
+ uint bin_ix = (width_in_bins * gl_WorkGroupID.y) + gl_WorkGroupID.x;
+ uint partition_ix = 0u;
+ uint n_partitions = ((_1066.conf.n_elements + 128u) - 1u) / 128u;
+ uint th_ix = gl_LocalInvocationID.x;
+ uint bin_tile_x = 16u * gl_WorkGroupID.x;
+ uint bin_tile_y = 8u * gl_WorkGroupID.y;
+ uint tile_x = gl_LocalInvocationID.x % 16u;
+ uint tile_y = gl_LocalInvocationID.x / 16u;
+ uint this_tile_ix = (((bin_tile_y + tile_y) * _1066.conf.width_in_tiles) + bin_tile_x) + tile_x;
+ Alloc param;
+ param.offset = _1066.conf.ptcl_alloc.offset;
+ uint param_1 = this_tile_ix * 1024u;
+ uint param_2 = 1024u;
+ Alloc cmd_alloc = slice_mem(param, param_1, param_2);
+ CmdRef cmd_ref = CmdRef(cmd_alloc.offset);
+ uint cmd_limit = (cmd_ref.offset + 1024u) - 36u;
+ uint clip_depth = 0u;
+ uint clip_zero_depth = 0u;
+ uint clip_one_mask = 0u;
+ uint rd_ix = 0u;
+ uint wr_ix = 0u;
+ uint part_start_ix = 0u;
+ uint ready_ix = 0u;
+ Alloc param_3 = cmd_alloc;
+ uint param_4 = 0u;
+ uint param_5 = 8u;
+ Alloc scratch_alloc = slice_mem(param_3, param_4, param_5);
+ cmd_ref.offset += 8u;
+ uint num_begin_slots = 0u;
+ uint begin_slot = 0u;
+ Alloc param_6;
+ Alloc param_8;
+ uint _1354;
+ uint element_ix;
+ AnnotatedRef ref;
+ Alloc param_16;
+ Alloc param_18;
+ uint tile_count;
+ Alloc param_24;
+ uint _1667;
+ bool include_tile;
+ Alloc param_29;
+ Tile tile_1;
+ Alloc param_34;
+ Alloc param_50;
+ Alloc param_66;
+ while (true)
+ {
+ for (uint i = 0u; i < 4u; i++)
+ {
+ sh_bitmaps[i][th_ix] = 0u;
+ }
+ bool _1406;
+ for (;;)
+ {
+ if ((ready_ix == wr_ix) && (partition_ix < n_partitions))
+ {
+ part_start_ix = ready_ix;
+ uint count = 0u;
+ bool _1204 = th_ix < 128u;
+ bool _1212;
+ if (_1204)
+ {
+ _1212 = (partition_ix + th_ix) < n_partitions;
+ }
+ else
+ {
+ _1212 = _1204;
+ }
+ if (_1212)
+ {
+ uint in_ix = (_1066.conf.bin_alloc.offset >> uint(2)) + ((((partition_ix + th_ix) * 128u) + bin_ix) * 2u);
+ param_6.offset = _1066.conf.bin_alloc.offset;
+ uint param_7 = in_ix;
+ count = read_mem(param_6, param_7);
+ param_8.offset = _1066.conf.bin_alloc.offset;
+ uint param_9 = in_ix + 1u;
+ uint offset = read_mem(param_8, param_9);
+ uint param_10 = offset;
+ uint param_11 = count * 4u;
+ sh_part_elements[th_ix] = new_alloc(param_10, param_11);
+ }
+ for (uint i_1 = 0u; i_1 < 7u; i_1++)
+ {
+ if (th_ix < 128u)
+ {
+ sh_part_count[th_ix] = count;
+ }
+ barrier();
+ if (th_ix < 128u)
+ {
+ if (th_ix >= uint(1 << int(i_1)))
+ {
+ count += sh_part_count[th_ix - uint(1 << int(i_1))];
+ }
+ }
+ barrier();
+ }
+ if (th_ix < 128u)
+ {
+ sh_part_count[th_ix] = part_start_ix + count;
+ }
+ barrier();
+ ready_ix = sh_part_count[127];
+ partition_ix += 128u;
+ }
+ uint ix = rd_ix + th_ix;
+ if ((ix >= wr_ix) && (ix < ready_ix))
+ {
+ uint part_ix = 0u;
+ for (uint i_2 = 0u; i_2 < 7u; i_2++)
+ {
+ uint probe = part_ix + uint(64 >> int(i_2));
+ if (ix >= sh_part_count[probe - 1u])
+ {
+ part_ix = probe;
+ }
+ }
+ if (part_ix > 0u)
+ {
+ _1354 = sh_part_count[part_ix - 1u];
+ }
+ else
+ {
+ _1354 = part_start_ix;
+ }
+ ix -= _1354;
+ Alloc bin_alloc = sh_part_elements[part_ix];
+ BinInstanceRef inst_ref = BinInstanceRef(bin_alloc.offset);
+ BinInstanceRef param_12 = inst_ref;
+ uint param_13 = ix;
+ Alloc param_14 = bin_alloc;
+ BinInstanceRef param_15 = BinInstance_index(param_12, param_13);
+ BinInstance inst = BinInstance_read(param_14, param_15);
+ sh_elements[th_ix] = inst.element_ix;
+ }
+ barrier();
+ wr_ix = min((rd_ix + 128u), ready_ix);
+ bool _1396 = (wr_ix - rd_ix) < 128u;
+ if (_1396)
+ {
+ _1406 = (wr_ix < ready_ix) || (partition_ix < n_partitions);
+ }
+ else
+ {
+ _1406 = _1396;
+ }
+ if (_1406)
+ {
+ continue;
+ }
+ else
+ {
+ break;
+ }
+ }
+ uint tag = 0u;
+ if ((th_ix + rd_ix) < wr_ix)
+ {
+ element_ix = sh_elements[th_ix];
+ ref = AnnotatedRef(_1066.conf.anno_alloc.offset + (element_ix * 32u));
+ param_16.offset = _1066.conf.anno_alloc.offset;
+ AnnotatedRef param_17 = ref;
+ tag = Annotated_tag(param_16, param_17).tag;
+ }
+ switch (tag)
+ {
+ case 1u:
+ case 2u:
+ case 3u:
+ case 4u:
+ {
+ uint path_ix = element_ix;
+ param_18.offset = _1066.conf.tile_alloc.offset;
+ PathRef param_19 = PathRef(_1066.conf.tile_alloc.offset + (path_ix * 12u));
+ Path path = Path_read(param_18, param_19);
+ uint stride = path.bbox.z - path.bbox.x;
+ sh_tile_stride[th_ix] = stride;
+ int dx = int(path.bbox.x) - int(bin_tile_x);
+ int dy = int(path.bbox.y) - int(bin_tile_y);
+ int x0 = clamp(dx, 0, 16);
+ int y0 = clamp(dy, 0, 8);
+ int x1 = clamp(int(path.bbox.z) - int(bin_tile_x), 0, 16);
+ int y1 = clamp(int(path.bbox.w) - int(bin_tile_y), 0, 8);
+ sh_tile_width[th_ix] = uint(x1 - x0);
+ sh_tile_x0[th_ix] = uint(x0);
+ sh_tile_y0[th_ix] = uint(y0);
+ tile_count = uint(x1 - x0) * uint(y1 - y0);
+ uint base = path.tiles.offset - (((uint(dy) * stride) + uint(dx)) * 8u);
+ sh_tile_base[th_ix] = base;
+ uint param_20 = path.tiles.offset;
+ uint param_21 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u;
+ Alloc path_alloc = new_alloc(param_20, param_21);
+ uint param_22 = th_ix;
+ Alloc param_23 = path_alloc;
+ write_tile_alloc(param_22, param_23);
+ break;
+ }
+ default:
+ {
+ tile_count = 0u;
+ break;
+ }
+ }
+ sh_tile_count[th_ix] = tile_count;
+ for (uint i_3 = 0u; i_3 < 7u; i_3++)
+ {
+ barrier();
+ if (th_ix >= uint(1 << int(i_3)))
+ {
+ tile_count += sh_tile_count[th_ix - uint(1 << int(i_3))];
+ }
+ barrier();
+ sh_tile_count[th_ix] = tile_count;
+ }
+ barrier();
+ uint total_tile_count = sh_tile_count[127];
+ for (uint ix_1 = th_ix; ix_1 < total_tile_count; ix_1 += 128u)
+ {
+ uint el_ix = 0u;
+ for (uint i_4 = 0u; i_4 < 7u; i_4++)
+ {
+ uint probe_1 = el_ix + uint(64 >> int(i_4));
+ if (ix_1 >= sh_tile_count[probe_1 - 1u])
+ {
+ el_ix = probe_1;
+ }
+ }
+ AnnotatedRef ref_1 = AnnotatedRef(_1066.conf.anno_alloc.offset + (sh_elements[el_ix] * 32u));
+ param_24.offset = _1066.conf.anno_alloc.offset;
+ AnnotatedRef param_25 = ref_1;
+ uint tag_1 = Annotated_tag(param_24, param_25).tag;
+ if (el_ix > 0u)
+ {
+ _1667 = sh_tile_count[el_ix - 1u];
+ }
+ else
+ {
+ _1667 = 0u;
+ }
+ uint seq_ix = ix_1 - _1667;
+ uint width = sh_tile_width[el_ix];
+ uint x = sh_tile_x0[el_ix] + (seq_ix % width);
+ uint y = sh_tile_y0[el_ix] + (seq_ix / width);
+ if ((tag_1 == 3u) || (tag_1 == 4u))
+ {
+ include_tile = true;
+ }
+ else
+ {
+ uint param_26 = el_ix;
+ Alloc param_27 = read_tile_alloc(param_26);
+ TileRef param_28 = TileRef(sh_tile_base[el_ix] + (((sh_tile_stride[el_ix] * y) + x) * 8u));
+ Tile tile = Tile_read(param_27, param_28);
+ bool _1728 = tile.tile.offset != 0u;
+ bool _1735;
+ if (!_1728)
+ {
+ _1735 = tile.backdrop != 0;
+ }
+ else
+ {
+ _1735 = _1728;
+ }
+ include_tile = _1735;
+ }
+ if (include_tile)
+ {
+ uint el_slice = el_ix / 32u;
+ uint el_mask = uint(1 << int(el_ix & 31u));
+ uint _1755 = atomicOr(sh_bitmaps[el_slice][(y * 16u) + x], el_mask);
+ }
+ }
+ barrier();
+ uint slice_ix = 0u;
+ uint bitmap = sh_bitmaps[0][th_ix];
+ while (true)
+ {
+ if (bitmap == 0u)
+ {
+ slice_ix++;
+ if (slice_ix == 4u)
+ {
+ break;
+ }
+ bitmap = sh_bitmaps[slice_ix][th_ix];
+ if (bitmap == 0u)
+ {
+ continue;
+ }
+ }
+ uint element_ref_ix = (slice_ix * 32u) + uint(findLSB(bitmap));
+ uint element_ix_1 = sh_elements[element_ref_ix];
+ bitmap &= (bitmap - 1u);
+ ref = AnnotatedRef(_1066.conf.anno_alloc.offset + (element_ix_1 * 32u));
+ param_29.offset = _1066.conf.anno_alloc.offset;
+ AnnotatedRef param_30 = ref;
+ AnnotatedTag tag_2 = Annotated_tag(param_29, param_30);
+ if (clip_zero_depth == 0u)
+ {
+ switch (tag_2.tag)
+ {
+ case 1u:
+ {
+ uint param_31 = element_ref_ix;
+ Alloc param_32 = read_tile_alloc(param_31);
+ TileRef param_33 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u));
+ tile_1 = Tile_read(param_32, param_33);
+ param_34.offset = _1066.conf.anno_alloc.offset;
+ AnnotatedRef param_35 = ref;
+ AnnoColor fill = Annotated_Color_read(param_34, param_35);
+ Alloc param_36 = cmd_alloc;
+ CmdRef param_37 = cmd_ref;
+ uint param_38 = cmd_limit;
+ bool _1865 = alloc_cmd(param_36, param_37, param_38);
+ cmd_alloc = param_36;
+ cmd_ref = param_37;
+ cmd_limit = param_38;
+ if (!_1865)
+ {
+ break;
+ }
+ Alloc param_39 = cmd_alloc;
+ CmdRef param_40 = cmd_ref;
+ uint param_41 = tag_2.flags;
+ Tile param_42 = tile_1;
+ float param_43 = fill.linewidth;
+ write_fill(param_39, param_40, param_41, param_42, param_43);
+ cmd_ref = param_40;
+ Alloc param_44 = cmd_alloc;
+ CmdRef param_45 = cmd_ref;
+ CmdColor param_46 = CmdColor(fill.rgba_color);
+ Cmd_Color_write(param_44, param_45, param_46);
+ cmd_ref.offset += 8u;
+ break;
+ }
+ case 2u:
+ {
+ uint param_47 = element_ref_ix;
+ Alloc param_48 = read_tile_alloc(param_47);
+ TileRef param_49 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u));
+ tile_1 = Tile_read(param_48, param_49);
+ param_50.offset = _1066.conf.anno_alloc.offset;
+ AnnotatedRef param_51 = ref;
+ AnnoImage fill_img = Annotated_Image_read(param_50, param_51);
+ Alloc param_52 = cmd_alloc;
+ CmdRef param_53 = cmd_ref;
+ uint param_54 = cmd_limit;
+ bool _1935 = alloc_cmd(param_52, param_53, param_54);
+ cmd_alloc = param_52;
+ cmd_ref = param_53;
+ cmd_limit = param_54;
+ if (!_1935)
+ {
+ break;
+ }
+ Alloc param_55 = cmd_alloc;
+ CmdRef param_56 = cmd_ref;
+ uint param_57 = tag_2.flags;
+ Tile param_58 = tile_1;
+ float param_59 = fill_img.linewidth;
+ write_fill(param_55, param_56, param_57, param_58, param_59);
+ cmd_ref = param_56;
+ Alloc param_60 = cmd_alloc;
+ CmdRef param_61 = cmd_ref;
+ CmdImage param_62 = CmdImage(fill_img.index, fill_img.offset);
+ Cmd_Image_write(param_60, param_61, param_62);
+ cmd_ref.offset += 12u;
+ break;
+ }
+ case 3u:
+ {
+ uint param_63 = element_ref_ix;
+ Alloc param_64 = read_tile_alloc(param_63);
+ TileRef param_65 = TileRef(sh_tile_base[element_ref_ix] + (((sh_tile_stride[element_ref_ix] * tile_y) + tile_x) * 8u));
+ tile_1 = Tile_read(param_64, param_65);
+ bool _1994 = tile_1.tile.offset == 0u;
+ bool _2000;
+ if (_1994)
+ {
+ _2000 = tile_1.backdrop == 0;
+ }
+ else
+ {
+ _2000 = _1994;
+ }
+ if (_2000)
+ {
+ clip_zero_depth = clip_depth + 1u;
+ }
+ else
+ {
+ if ((tile_1.tile.offset == 0u) && (clip_depth < 32u))
+ {
+ clip_one_mask |= uint(1 << int(clip_depth));
+ }
+ else
+ {
+ param_66.offset = _1066.conf.anno_alloc.offset;
+ AnnotatedRef param_67 = ref;
+ AnnoBeginClip begin_clip = Annotated_BeginClip_read(param_66, param_67);
+ Alloc param_68 = cmd_alloc;
+ CmdRef param_69 = cmd_ref;
+ uint param_70 = cmd_limit;
+ bool _2035 = alloc_cmd(param_68, param_69, param_70);
+ cmd_alloc = param_68;
+ cmd_ref = param_69;
+ cmd_limit = param_70;
+ if (!_2035)
+ {
+ break;
+ }
+ Alloc param_71 = cmd_alloc;
+ CmdRef param_72 = cmd_ref;
+ uint param_73 = tag_2.flags;
+ Tile param_74 = tile_1;
+ float param_75 = begin_clip.linewidth;
+ write_fill(param_71, param_72, param_73, param_74, param_75);
+ cmd_ref = param_72;
+ Alloc param_76 = cmd_alloc;
+ CmdRef param_77 = cmd_ref;
+ Cmd_BeginClip_write(param_76, param_77);
+ cmd_ref.offset += 4u;
+ if (clip_depth < 32u)
+ {
+ clip_one_mask &= uint(~(1 << int(clip_depth)));
+ }
+ begin_slot++;
+ num_begin_slots = max(num_begin_slots, begin_slot);
+ }
+ }
+ clip_depth++;
+ break;
+ }
+ case 4u:
+ {
+ clip_depth--;
+ bool _2087 = clip_depth >= 32u;
+ bool _2097;
+ if (!_2087)
+ {
+ _2097 = (clip_one_mask & uint(1 << int(clip_depth))) == 0u;
+ }
+ else
+ {
+ _2097 = _2087;
+ }
+ if (_2097)
+ {
+ Alloc param_78 = cmd_alloc;
+ CmdRef param_79 = cmd_ref;
+ uint param_80 = cmd_limit;
+ bool _2106 = alloc_cmd(param_78, param_79, param_80);
+ cmd_alloc = param_78;
+ cmd_ref = param_79;
+ cmd_limit = param_80;
+ if (!_2106)
+ {
+ break;
+ }
+ Alloc param_81 = cmd_alloc;
+ CmdRef param_82 = cmd_ref;
+ Cmd_Solid_write(param_81, param_82);
+ cmd_ref.offset += 4u;
+ begin_slot--;
+ Alloc param_83 = cmd_alloc;
+ CmdRef param_84 = cmd_ref;
+ Cmd_EndClip_write(param_83, param_84);
+ cmd_ref.offset += 4u;
+ }
+ break;
+ }
+ }
+ }
+ else
+ {
+ switch (tag_2.tag)
+ {
+ case 3u:
+ {
+ clip_depth++;
+ break;
+ }
+ case 4u:
+ {
+ if (clip_depth == clip_zero_depth)
+ {
+ clip_zero_depth = 0u;
+ }
+ clip_depth--;
+ break;
+ }
+ }
+ }
+ }
+ barrier();
+ rd_ix += 128u;
+ if ((rd_ix >= ready_ix) && (partition_ix >= n_partitions))
+ {
+ break;
+ }
+ }
+ bool _2171 = (bin_tile_x + tile_x) < _1066.conf.width_in_tiles;
+ bool _2180;
+ if (_2171)
+ {
+ _2180 = (bin_tile_y + tile_y) < _1066.conf.height_in_tiles;
+ }
+ else
+ {
+ _2180 = _2171;
+ }
+ if (_2180)
+ {
+ Alloc param_85 = cmd_alloc;
+ CmdRef param_86 = cmd_ref;
+ Cmd_End_write(param_85, param_86);
+ if (num_begin_slots > 0u)
+ {
+ uint scratch_size = (((num_begin_slots * 32u) * 32u) * 2u) * 4u;
+ uint param_87 = scratch_size;
+ MallocResult _2201 = malloc(param_87);
+ MallocResult scratch = _2201;
+ Alloc param_88 = scratch_alloc;
+ uint param_89 = scratch_alloc.offset;
+ Alloc param_90 = scratch.alloc;
+ alloc_write(param_88, param_89, param_90);
+ }
+ }
+}
+
+`,
+ }
+ shader_copy_frag = driver.ShaderSources{
+ Name: "copy.frag",
+ Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}},
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+
+layout(location = 0) out highp vec4 fragColor;
+
+highp vec3 sRGBtoRGB(highp vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375));
+ highp vec3 below = rgb / vec3(12.9200000762939453125);
+ highp vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625));
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ highp vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0);
+ highp vec3 param = texel.xyz;
+ highp vec3 rgb = sRGBtoRGB(param);
+ fragColor = vec4(rgb, texel.w);
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+
+out vec4 fragColor;
+
+vec3 sRGBtoRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375));
+ vec3 below = rgb / vec3(12.9200000762939453125);
+ vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625));
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0);
+ vec3 param = texel.xyz;
+ vec3 rgb = sRGBtoRGB(param);
+ fragColor = vec4(rgb, texel.w);
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+
+out vec4 fragColor;
+
+vec3 sRGBtoRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.040449999272823333740234375));
+ vec3 below = rgb / vec3(12.9200000762939453125);
+ vec3 above = pow((rgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625));
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ vec4 texel = texelFetch(tex, ivec2(gl_FragCoord.xy), 0);
+ vec3 param = texel.xyz;
+ vec3 rgb = sRGBtoRGB(param);
+ fragColor = vec4(rgb, texel.w);
+}
+
+`,
+ HLSL: "DXBC\xe6\x89_t\x8b\xfc\xea8\xd9'\xad5.Ćk\x01\x00\x00\x00H\x03\x00\x00\x05\x00\x00\x004\x00\x00\x00\xa4\x00\x00\x00\xd8\x00\x00\x00\f\x01\x00\x00\xcc\x02\x00\x00RDEFh\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00@\x00\x00\x00<\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x03\x00\x00SV_Position\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xabSHDR\xb8\x01\x00\x00@\x00\x00\x00n\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00d \x00\x042\x10\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x00\x1b\x00\x00\x052\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00-\x00\x00\a\xf2\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\xaeGa=\xaeGa=\xaeGa=\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00o\xa7r?o\xa7r?o\xa7r?\x00\x00\x00\x00/\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00\x9a\x99\x19@\x9a\x99\x19@\x9a\x99\x19@\x00\x00\x00\x00\x19\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x1d\x00\x00\nr\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\xe6\xae%=\xe6\xae%=\xe6\xae%=\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x00\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x91\x83\x9e=\x91\x83\x9e=\x91\x83\x9e=\x00\x00\x00\x006\x00\x00\x05\x82 \x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x007\x00\x00\tr \x10\x00\x00\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\r\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ }
+ shader_copy_vert = driver.ShaderSources{
+ Name: "copy.vert",
+ GLSL100ES: `#version 100
+
+void main()
+{
+ for (int spvDummy6 = 0; spvDummy6 < 1; spvDummy6++)
+ {
+ if (gl_VertexID == 0)
+ {
+ gl_Position = vec4(-1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ else if (gl_VertexID == 1)
+ {
+ gl_Position = vec4(1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ else if (gl_VertexID == 2)
+ {
+ gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ else if (gl_VertexID == 3)
+ {
+ gl_Position = vec4(1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ }
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+void main()
+{
+ switch (gl_VertexID)
+ {
+ case 0:
+ {
+ gl_Position = vec4(-1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ case 1:
+ {
+ gl_Position = vec4(1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ case 2:
+ {
+ gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ case 3:
+ {
+ gl_Position = vec4(1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ }
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+void main()
+{
+ switch (gl_VertexID)
+ {
+ case 0:
+ {
+ gl_Position = vec4(-1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ case 1:
+ {
+ gl_Position = vec4(1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ case 2:
+ {
+ gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ case 3:
+ {
+ gl_Position = vec4(1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ }
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+void main()
+{
+ switch (gl_VertexID)
+ {
+ case 0:
+ {
+ gl_Position = vec4(-1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ case 1:
+ {
+ gl_Position = vec4(1.0, 1.0, 0.0, 1.0);
+ break;
+ }
+ case 2:
+ {
+ gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ case 3:
+ {
+ gl_Position = vec4(1.0, -1.0, 0.0, 1.0);
+ break;
+ }
+ }
+}
+
+`,
+ HLSL: "DXBC\x99\xb4[\xef]IX\xa2Qh\x9f\xb6!\x1cR\xe7\x01\x00\x00\x00\xc0\x02\x00\x00\x05\x00\x00\x004\x00\x00\x00\x80\x00\x00\x00\xb4\x00\x00\x00\xe8\x00\x00\x00D\x02\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00SV_VertexID\x00OSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Position\x00SHDRT\x01\x00\x00@\x00\x01\x00U\x00\x00\x00`\x00\x00\x04\x12\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x00\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00L\x00\x00\x03\n\x10\x10\x00\x00\x00\x00\x00\x06\x00\x00\x03\x01@\x00\x00\x00\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x01\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80?\x00\x00\x80?\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x02\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\x06\x00\x00\x03\x01@\x00\x00\x03\x00\x00\x006\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80?\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x02\x00\x00\x01\n\x00\x00\x016\x00\x00\br\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x01\x17\x00\x00\x016\x00\x00\x05\xb2 \x10\x00\x00\x00\x00\x00F\b\x10\x00\x00\x00\x00\x006\x00\x00\x05B \x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x14\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
+ }
+ shader_cover_frag = [...]driver.ShaderSources{
+ {
+ Name: "cover.frag",
+ Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Color", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_color.color", Type: 0x0, Size: 4, Offset: 0}},
+ Size: 16,
+ },
+ Textures: []driver.TextureBinding{{Name: "cover", Binding: 1}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+struct Color
+{
+ vec4 color;
+};
+
+uniform Color _color;
+
+uniform mediump sampler2D cover;
+
+varying highp vec2 vCoverUV;
+varying vec2 vUV;
+
+void main()
+{
+ gl_FragData[0] = _color.color;
+ float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0);
+ gl_FragData[0] *= cover_1;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+layout(std140) uniform Color
+{
+ vec4 color;
+} _color;
+
+uniform mediump sampler2D cover;
+
+layout(location = 0) out vec4 fragColor;
+in highp vec2 vCoverUV;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = _color.color;
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct Color
+{
+ vec4 color;
+};
+
+uniform Color _color;
+
+layout(binding = 1) uniform sampler2D cover;
+
+out vec4 fragColor;
+in vec2 vCoverUV;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = _color.color;
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0, std140) uniform Color
+{
+ vec4 color;
+} _color;
+
+layout(binding = 1) uniform sampler2D cover;
+
+out vec4 fragColor;
+in vec2 vCoverUV;
+in vec2 vUV;
+
+void main()
+{
+ fragColor = _color.color;
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ HLSL: "DXBC\x88\x01{\x0f\x94\xca3\xeb\xabßø\xa1\xbfL1\xbf\x01\x00\x00\x00\xa4\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xcc\x00\x00\x00\x90\x01\x00\x00\f\x02\x00\x00$\x03\x00\x00p\x03\x00\x00Aon9\x8c\x00\x00\x00\x8c\x00\x00\x00\x00\x02\xff\xffX\x00\x00\x004\x00\x00\x00\x01\x00(\x00\x00\x004\x00\x00\x004\x00\x01\x00$\x00\x00\x004\x00\x01\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0#\x00\x00\x02\x00\x00\x11\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\x00\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xbc\x00\x00\x00@\x00\x00\x00/\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?8\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x10\x01\x00\x00\x01\x00\x00\x00\x98\x00\x00\x00\x03\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xe8\x00\x00\x00|\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x8b\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\x91\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00_cover_sampler\x00cover\x00Color\x00\xab\x91\x00\x00\x00\x01\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xd8\x00\x00\x00\x00\x00\x00\x00_color_color\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x00\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ },
+ {
+ Name: "cover.frag",
+ Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Gradient", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_gradient.color1", Type: 0x0, Size: 4, Offset: 0}, {Name: "_gradient.color2", Type: 0x0, Size: 4, Offset: 16}},
+ Size: 32,
+ },
+ Textures: []driver.TextureBinding{{Name: "cover", Binding: 1}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+struct Gradient
+{
+ vec4 color1;
+ vec4 color2;
+};
+
+uniform Gradient _gradient;
+
+uniform mediump sampler2D cover;
+
+varying vec2 vUV;
+varying highp vec2 vCoverUV;
+
+void main()
+{
+ gl_FragData[0] = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+ float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0);
+ gl_FragData[0] *= cover_1;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+layout(std140) uniform Gradient
+{
+ vec4 color1;
+ vec4 color2;
+} _gradient;
+
+uniform mediump sampler2D cover;
+
+layout(location = 0) out vec4 fragColor;
+in vec2 vUV;
+in highp vec2 vCoverUV;
+
+void main()
+{
+ fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct Gradient
+{
+ vec4 color1;
+ vec4 color2;
+};
+
+uniform Gradient _gradient;
+
+layout(binding = 1) uniform sampler2D cover;
+
+out vec4 fragColor;
+in vec2 vUV;
+in vec2 vCoverUV;
+
+void main()
+{
+ fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0, std140) uniform Gradient
+{
+ vec4 color1;
+ vec4 color2;
+} _gradient;
+
+layout(binding = 1) uniform sampler2D cover;
+
+out vec4 fragColor;
+in vec2 vUV;
+in vec2 vCoverUV;
+
+void main()
+{
+ fragColor = mix(_gradient.color1, _gradient.color2, vec4(clamp(vUV.x, 0.0, 1.0)));
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ HLSL: "DXBCj\xa0\x9e\x8d\x1eĆO\rJ\xea\x8f\x17\x11o\x98\x01\x00\x00\x00\x80\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00\b\x01\x00\x008\x02\x00\x00\xb4\x02\x00\x00\x00\x04\x00\x00L\x04\x00\x00Aon9\xc8\x00\x00\x00\xc8\x00\x00\x00\x00\x02\xff\xff\x94\x00\x00\x004\x00\x00\x00\x01\x00(\x00\x00\x004\x00\x00\x004\x00\x01\x00$\x00\x00\x004\x00\x01\x01\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x01\x00\x00\x02\x00\x00\x12\x80\x00\x00\xff\xb0\x01\x00\x00\x02\x01\x00\x0f\x80\x00\x00\xe4\xa0\x02\x00\x00\x03\x01\x00\x0f\x80\x01\x00\xe4\x81\x01\x00\xe4\xa0\x04\x00\x00\x04\x01\x00\x0f\x80\x00\x00U\x80\x01\x00\xe4\x80\x00\x00\xe4\xa0#\x00\x00\x02\x00\x00\x11\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\x00\x80\x01\x00\xe4\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR(\x01\x00\x00@\x00\x00\x00J\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03B\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006 \x00\x05\x12\x00\x10\x00\x00\x00\x00\x00*\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x8e \x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x01\x00\x00\x002\x00\x00\n\xf2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00F\x8e \x00\x00\x00\x00\x00\x00\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?8\x00\x00\a\xf2 \x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x01\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\a\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x01\x00\x00\x01\x00\x00\x00\x9c\x00\x00\x00\x03\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x19\x01\x00\x00|\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x8b\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\x91\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00_cover_sampler\x00cover\x00Gradient\x00\xab\xab\x91\x00\x00\x00\x02\x00\x00\x00\xb4\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe4\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x00\x00\b\x01\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x00\x00_gradient_color1\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_gradient_color2\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x04\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ },
+ {
+ Name: "cover.frag",
+ Inputs: []driver.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}, {Name: "cover", Binding: 1}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+uniform mediump sampler2D cover;
+
+varying vec2 vUV;
+varying highp vec2 vCoverUV;
+
+void main()
+{
+ gl_FragData[0] = texture2D(tex, vUV);
+ float cover_1 = min(abs(texture2D(cover, vCoverUV).x), 1.0);
+ gl_FragData[0] *= cover_1;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+uniform mediump sampler2D cover;
+
+layout(location = 0) out vec4 fragColor;
+in vec2 vUV;
+in highp vec2 vCoverUV;
+
+void main()
+{
+ fragColor = texture(tex, vUV);
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+layout(binding = 1) uniform sampler2D cover;
+
+out vec4 fragColor;
+in vec2 vUV;
+in vec2 vCoverUV;
+
+void main()
+{
+ fragColor = texture(tex, vUV);
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+layout(binding = 1) uniform sampler2D cover;
+
+out vec4 fragColor;
+in vec2 vUV;
+in vec2 vCoverUV;
+
+void main()
+{
+ fragColor = texture(tex, vUV);
+ float cover_1 = min(abs(texture(cover, vCoverUV).x), 1.0);
+ fragColor *= cover_1;
+}
+
+`,
+ HLSL: "DXBC\x99\x16l`\xf6:k\xa2Y$\xa1,\xfd\xcdJE\x01\x00\x00\x00\xd8\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xec\x00\x00\x00\xe8\x01\x00\x00d\x02\x00\x00X\x03\x00\x00\xa4\x03\x00\x00Aon9\xac\x00\x00\x00\xac\x00\x00\x00\x00\x02\xff\xff\x80\x00\x00\x00,\x00\x00\x00\x00\x00,\x00\x00\x00,\x00\x00\x00,\x00\x02\x00$\x00\x00\x00,\x00\x00\x00\x00\x00\x01\x01\x01\x00\x00\x02\xff\xff\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0\x1f\x00\x00\x02\x00\x00\x00\x90\x01\b\x0f\xa0\x01\x00\x00\x02\x00\x00\x03\x80\x00\x00\x1b\xb0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\x80\x00\b\xe4\xa0B\x00\x00\x03\x01\x00\x0f\x80\x00\x00\xe4\xb0\x01\b\xe4\xa0#\x00\x00\x02\x01\x00\x11\x80\x01\x00\x00\x80\x05\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\x80\x01\x00\x00\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xf4\x00\x00\x00@\x00\x00\x00=\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x01\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00X\x18\x00\x04\x00p\x10\x00\x01\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03\xc2\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x01\x00\x00\x00\x00`\x10\x00\x01\x00\x00\x003\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?E\x00\x00\t\xf2\x00\x10\x00\x01\x00\x00\x00\xe6\x1a\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x008\x00\x00\a\xf2 \x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00F\x0e\x10\x00\x01\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x05\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xec\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\xc2\x00\x00\x00\x9c\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xa9\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\xb8\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00\xbc\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00_cover_sampler\x00tex\x00cover\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\f\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ },
+ }
+ shader_cover_vert = driver.ShaderSources{
+ Name: "cover.vert",
+ Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.uvCoverTransform", Type: 0x0, Size: 4, Offset: 16}, {Name: "_block.uvTransformR1", Type: 0x0, Size: 4, Offset: 32}, {Name: "_block.uvTransformR2", Type: 0x0, Size: 4, Offset: 48}, {Name: "_block.z", Type: 0x0, Size: 1, Offset: 64}},
+ Size: 68,
+ },
+ GLSL100ES: `#version 100
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+struct Block
+{
+ vec4 transform;
+ vec4 uvCoverTransform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+};
+
+uniform Block _block;
+
+attribute vec2 pos;
+varying vec2 vUV;
+attribute vec2 uv;
+varying vec2 vCoverUV;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+ m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_4 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_3, param_4);
+ vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+layout(std140) uniform Block
+{
+ vec4 transform;
+ vec4 uvCoverTransform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+} _block;
+
+layout(location = 0) in vec2 pos;
+out vec2 vUV;
+layout(location = 1) in vec2 uv;
+out vec2 vCoverUV;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+ m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_4 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_3, param_4);
+ vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+struct Block
+{
+ vec4 transform;
+ vec4 uvCoverTransform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+};
+
+uniform Block _block;
+
+in vec2 pos;
+out vec2 vUV;
+in vec2 uv;
+out vec2 vCoverUV;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+ m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_4 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_3, param_4);
+ vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+layout(binding = 0, std140) uniform Block
+{
+ vec4 transform;
+ vec4 uvCoverTransform;
+ vec4 uvTransformR1;
+ vec4 uvTransformR2;
+ float z;
+} _block;
+
+in vec2 pos;
+out vec2 vUV;
+in vec2 uv;
+out vec2 vCoverUV;
+
+vec4 toClipSpace(vec4 pos_1)
+{
+ return pos_1;
+}
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ vec4 param = vec4((pos * _block.transform.xy) + _block.transform.zw, _block.z, 1.0);
+ gl_Position = toClipSpace(param);
+ m3x2 param_1 = m3x2(_block.uvTransformR1.xyz, _block.uvTransformR2.xyz);
+ vec3 param_2 = vec3(uv, 1.0);
+ vUV = transform3x2(param_1, param_2).xy;
+ m3x2 param_3 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_4 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_3, param_4);
+ vCoverUV = ((uv3 * vec3(_block.uvCoverTransform.xy, 1.0)) + vec3(_block.uvCoverTransform.zw, 0.0)).xy;
+}
+
+`,
+ HLSL: "DXBCx\xefn{F\v\x88%\xc6\x05\x8f4h\xe4\xaaP\x01\x00\x00\x00\xd8\x05\x00\x00\x06\x00\x00\x008\x00\x00\x00x\x01\x00\x00\x1c\x03\x00\x00\x98\x03\x00\x00\x1c\x05\x00\x00h\x05\x00\x00Aon98\x01\x00\x008\x01\x00\x00\x00\x02\xfe\xff\x04\x01\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x05\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x06\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x80\xbf\x00\x00\x00?\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\a\x80\x01\x00Ä\x06\x00Š \x06\x00Å \b\x00\x00\x03\x00\x00\b\xe0\x03\x00\xe4\xa0\x00\x00\xe4\x80\b\x00\x00\x03\x00\x00\x04\xe0\x04\x00\xe4\xa0\x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\x80\x01\x00\xe1\x90\x06\x00\xe4\xa0\x06\x00\xe1\xa0\x05\x00\x00\x03\x00\x00\x03\x80\x00\x00\xe4\x80\x06\x00\xe2\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x01\x80\x01\x00\x00\x90\x04\x00\x00\x04\x00\x00\x03\xe0\x00\x00\xe4\x80\x02\x00\xe4\xa0\x02\x00\xee\xa0\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x90\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\v\x80\x06\x00\xe4\xa0\x04\x00\x00\x04\x00\x00\f\xc0\x05\x00\x00\xa0\x00\x00t\x80\x00\x004\x80\xff\xff\x00\x00SHDR\x9c\x01\x00\x00@\x00\x01\x00g\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x05\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00e\x00\x00\x03\xc2 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x02\x00\x00\x006\x00\x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x006\x00\x00\x052\x00\x10\x00\x01\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x0f\x00\x00\n\"\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x96\x05\x10\x00\x01\x00\x00\x002\x00\x00\v2 \x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x01\x00\x00\x00\x10\x00\x00\bB \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x10\x00\x00\b\x82 \x10\x00\x00\x00\x00\x00F\x82 \x00\x00\x00\x00\x00\x03\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x002\x00\x00\v2 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\nB \x10\x00\x01\x00\x00\x00\n\x80 \x00\x00\x00\x00\x00\x04\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\x01@\x00\x00\x00\x00\x00?6\x00\x00\x05\x82 \x10\x00\x01\x00\x00\x00\x01@\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\v\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF|\x01\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00T\x01\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x05\x00\x00\x00\\\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00\xf8\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00\x10\x01\x00\x00 \x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00%\x01\x00\x000\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00:\x01\x00\x00@\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00D\x01\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_uvCoverTransform\x00_block_uvTransformR1\x00_block_uvTransformR2\x00_block_z\x00\xab\x00\x00\x03\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNh\x00\x00\x00\x03\x00\x00\x00\b\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00P\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x03\x00\x00Y\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab",
+ }
+ shader_elements_comp = driver.ShaderSources{
+ Name: "elements.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct ElementRef
+{
+ uint offset;
+};
+
+struct LineSegRef
+{
+ uint offset;
+};
+
+struct LineSeg
+{
+ vec2 p0;
+ vec2 p1;
+};
+
+struct QuadSegRef
+{
+ uint offset;
+};
+
+struct QuadSeg
+{
+ vec2 p0;
+ vec2 p1;
+ vec2 p2;
+};
+
+struct CubicSegRef
+{
+ uint offset;
+};
+
+struct CubicSeg
+{
+ vec2 p0;
+ vec2 p1;
+ vec2 p2;
+ vec2 p3;
+};
+
+struct FillColorRef
+{
+ uint offset;
+};
+
+struct FillColor
+{
+ uint rgba_color;
+};
+
+struct FillImageRef
+{
+ uint offset;
+};
+
+struct FillImage
+{
+ uint index;
+ ivec2 offset;
+};
+
+struct SetLineWidthRef
+{
+ uint offset;
+};
+
+struct SetLineWidth
+{
+ float width;
+};
+
+struct TransformRef
+{
+ uint offset;
+};
+
+struct Transform
+{
+ vec4 mat;
+ vec2 translate;
+};
+
+struct ClipRef
+{
+ uint offset;
+};
+
+struct Clip
+{
+ vec4 bbox;
+};
+
+struct SetFillModeRef
+{
+ uint offset;
+};
+
+struct SetFillMode
+{
+ uint fill_mode;
+};
+
+struct ElementTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct StateRef
+{
+ uint offset;
+};
+
+struct State
+{
+ vec4 mat;
+ vec2 translate;
+ vec4 bbox;
+ float linewidth;
+ uint flags;
+ uint path_count;
+ uint pathseg_count;
+ uint trans_count;
+};
+
+struct AnnoImageRef
+{
+ uint offset;
+};
+
+struct AnnoImage
+{
+ vec4 bbox;
+ float linewidth;
+ uint index;
+ ivec2 offset;
+};
+
+struct AnnoColorRef
+{
+ uint offset;
+};
+
+struct AnnoColor
+{
+ vec4 bbox;
+ float linewidth;
+ uint rgba_color;
+};
+
+struct AnnoBeginClipRef
+{
+ uint offset;
+};
+
+struct AnnoBeginClip
+{
+ vec4 bbox;
+ float linewidth;
+};
+
+struct AnnoEndClipRef
+{
+ uint offset;
+};
+
+struct AnnoEndClip
+{
+ vec4 bbox;
+};
+
+struct AnnotatedRef
+{
+ uint offset;
+};
+
+struct PathCubicRef
+{
+ uint offset;
+};
+
+struct PathCubic
+{
+ vec2 p0;
+ vec2 p1;
+ vec2 p2;
+ vec2 p3;
+ uint path_ix;
+ uint trans_ix;
+ vec2 stroke;
+};
+
+struct PathSegRef
+{
+ uint offset;
+};
+
+struct TransformSegRef
+{
+ uint offset;
+};
+
+struct TransformSeg
+{
+ vec4 mat;
+ vec2 translate;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _294;
+
+layout(binding = 2, std430) readonly buffer SceneBuf
+{
+ uint scene[];
+} _323;
+
+layout(binding = 3, std430) coherent buffer StateBuf
+{
+ uint part_counter;
+ uint state[];
+} _779;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _2435;
+
+shared uint sh_part_ix;
+shared State sh_state[32];
+shared State sh_prefix;
+
+ElementTag Element_tag(ElementRef ref)
+{
+ uint tag_and_flags = _323.scene[ref.offset >> uint(2)];
+ return ElementTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+LineSeg LineSeg_read(LineSegRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ uint raw1 = _323.scene[ix + 1u];
+ uint raw2 = _323.scene[ix + 2u];
+ uint raw3 = _323.scene[ix + 3u];
+ LineSeg s;
+ s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1));
+ s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ return s;
+}
+
+LineSeg Element_Line_read(ElementRef ref)
+{
+ LineSegRef param = LineSegRef(ref.offset + 4u);
+ return LineSeg_read(param);
+}
+
+QuadSeg QuadSeg_read(QuadSegRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ uint raw1 = _323.scene[ix + 1u];
+ uint raw2 = _323.scene[ix + 2u];
+ uint raw3 = _323.scene[ix + 3u];
+ uint raw4 = _323.scene[ix + 4u];
+ uint raw5 = _323.scene[ix + 5u];
+ QuadSeg s;
+ s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1));
+ s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5));
+ return s;
+}
+
+QuadSeg Element_Quad_read(ElementRef ref)
+{
+ QuadSegRef param = QuadSegRef(ref.offset + 4u);
+ return QuadSeg_read(param);
+}
+
+CubicSeg CubicSeg_read(CubicSegRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ uint raw1 = _323.scene[ix + 1u];
+ uint raw2 = _323.scene[ix + 2u];
+ uint raw3 = _323.scene[ix + 3u];
+ uint raw4 = _323.scene[ix + 4u];
+ uint raw5 = _323.scene[ix + 5u];
+ uint raw6 = _323.scene[ix + 6u];
+ uint raw7 = _323.scene[ix + 7u];
+ CubicSeg s;
+ s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1));
+ s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5));
+ s.p3 = vec2(uintBitsToFloat(raw6), uintBitsToFloat(raw7));
+ return s;
+}
+
+CubicSeg Element_Cubic_read(ElementRef ref)
+{
+ CubicSegRef param = CubicSegRef(ref.offset + 4u);
+ return CubicSeg_read(param);
+}
+
+SetLineWidth SetLineWidth_read(SetLineWidthRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ SetLineWidth s;
+ s.width = uintBitsToFloat(raw0);
+ return s;
+}
+
+SetLineWidth Element_SetLineWidth_read(ElementRef ref)
+{
+ SetLineWidthRef param = SetLineWidthRef(ref.offset + 4u);
+ return SetLineWidth_read(param);
+}
+
+Transform Transform_read(TransformRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ uint raw1 = _323.scene[ix + 1u];
+ uint raw2 = _323.scene[ix + 2u];
+ uint raw3 = _323.scene[ix + 3u];
+ uint raw4 = _323.scene[ix + 4u];
+ uint raw5 = _323.scene[ix + 5u];
+ Transform s;
+ s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5));
+ return s;
+}
+
+Transform Element_Transform_read(ElementRef ref)
+{
+ TransformRef param = TransformRef(ref.offset + 4u);
+ return Transform_read(param);
+}
+
+SetFillMode SetFillMode_read(SetFillModeRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ SetFillMode s;
+ s.fill_mode = raw0;
+ return s;
+}
+
+SetFillMode Element_SetFillMode_read(ElementRef ref)
+{
+ SetFillModeRef param = SetFillModeRef(ref.offset + 4u);
+ return SetFillMode_read(param);
+}
+
+State map_element(ElementRef ref)
+{
+ ElementRef param = ref;
+ uint tag = Element_tag(param).tag;
+ State c;
+ c.bbox = vec4(0.0);
+ c.mat = vec4(1.0, 0.0, 0.0, 1.0);
+ c.translate = vec2(0.0);
+ c.linewidth = 1.0;
+ c.flags = 0u;
+ c.path_count = 0u;
+ c.pathseg_count = 0u;
+ c.trans_count = 0u;
+ switch (tag)
+ {
+ case 1u:
+ {
+ ElementRef param_1 = ref;
+ LineSeg line = Element_Line_read(param_1);
+ vec2 _1919 = min(line.p0, line.p1);
+ c.bbox = vec4(_1919.x, _1919.y, c.bbox.z, c.bbox.w);
+ vec2 _1927 = max(line.p0, line.p1);
+ c.bbox = vec4(c.bbox.x, c.bbox.y, _1927.x, _1927.y);
+ c.pathseg_count = 1u;
+ break;
+ }
+ case 2u:
+ {
+ ElementRef param_2 = ref;
+ QuadSeg quad = Element_Quad_read(param_2);
+ vec2 _1944 = min(min(quad.p0, quad.p1), quad.p2);
+ c.bbox = vec4(_1944.x, _1944.y, c.bbox.z, c.bbox.w);
+ vec2 _1955 = max(max(quad.p0, quad.p1), quad.p2);
+ c.bbox = vec4(c.bbox.x, c.bbox.y, _1955.x, _1955.y);
+ c.pathseg_count = 1u;
+ break;
+ }
+ case 3u:
+ {
+ ElementRef param_3 = ref;
+ CubicSeg cubic = Element_Cubic_read(param_3);
+ vec2 _1975 = min(min(cubic.p0, cubic.p1), min(cubic.p2, cubic.p3));
+ c.bbox = vec4(_1975.x, _1975.y, c.bbox.z, c.bbox.w);
+ vec2 _1989 = max(max(cubic.p0, cubic.p1), max(cubic.p2, cubic.p3));
+ c.bbox = vec4(c.bbox.x, c.bbox.y, _1989.x, _1989.y);
+ c.pathseg_count = 1u;
+ break;
+ }
+ case 4u:
+ case 9u:
+ case 7u:
+ {
+ c.flags = 4u;
+ c.path_count = 1u;
+ break;
+ }
+ case 8u:
+ {
+ c.path_count = 1u;
+ break;
+ }
+ case 5u:
+ {
+ ElementRef param_4 = ref;
+ SetLineWidth lw = Element_SetLineWidth_read(param_4);
+ c.linewidth = lw.width;
+ c.flags = 1u;
+ break;
+ }
+ case 6u:
+ {
+ ElementRef param_5 = ref;
+ Transform t = Element_Transform_read(param_5);
+ c.mat = t.mat;
+ c.translate = t.translate;
+ c.trans_count = 1u;
+ break;
+ }
+ case 10u:
+ {
+ ElementRef param_6 = ref;
+ SetFillMode fm = Element_SetFillMode_read(param_6);
+ c.flags = 8u | (fm.fill_mode << uint(4));
+ break;
+ }
+ }
+ return c;
+}
+
+ElementRef Element_index(ElementRef ref, uint index)
+{
+ return ElementRef(ref.offset + (index * 36u));
+}
+
+State combine_state(State a, State b)
+{
+ State c;
+ c.bbox.x = (min(a.mat.x * b.bbox.x, a.mat.x * b.bbox.z) + min(a.mat.z * b.bbox.y, a.mat.z * b.bbox.w)) + a.translate.x;
+ c.bbox.y = (min(a.mat.y * b.bbox.x, a.mat.y * b.bbox.z) + min(a.mat.w * b.bbox.y, a.mat.w * b.bbox.w)) + a.translate.y;
+ c.bbox.z = (max(a.mat.x * b.bbox.x, a.mat.x * b.bbox.z) + max(a.mat.z * b.bbox.y, a.mat.z * b.bbox.w)) + a.translate.x;
+ c.bbox.w = (max(a.mat.y * b.bbox.x, a.mat.y * b.bbox.z) + max(a.mat.w * b.bbox.y, a.mat.w * b.bbox.w)) + a.translate.y;
+ bool _1657 = (a.flags & 4u) == 0u;
+ bool _1665;
+ if (_1657)
+ {
+ _1665 = b.bbox.z <= b.bbox.x;
+ }
+ else
+ {
+ _1665 = _1657;
+ }
+ bool _1673;
+ if (_1665)
+ {
+ _1673 = b.bbox.w <= b.bbox.y;
+ }
+ else
+ {
+ _1673 = _1665;
+ }
+ if (_1673)
+ {
+ c.bbox = a.bbox;
+ }
+ else
+ {
+ bool _1683 = (a.flags & 4u) == 0u;
+ bool _1690;
+ if (_1683)
+ {
+ _1690 = (b.flags & 2u) == 0u;
+ }
+ else
+ {
+ _1690 = _1683;
+ }
+ bool _1707;
+ if (_1690)
+ {
+ bool _1697 = a.bbox.z > a.bbox.x;
+ bool _1706;
+ if (!_1697)
+ {
+ _1706 = a.bbox.w > a.bbox.y;
+ }
+ else
+ {
+ _1706 = _1697;
+ }
+ _1707 = _1706;
+ }
+ else
+ {
+ _1707 = _1690;
+ }
+ if (_1707)
+ {
+ vec2 _1716 = min(a.bbox.xy, c.bbox.xy);
+ c.bbox = vec4(_1716.x, _1716.y, c.bbox.z, c.bbox.w);
+ vec2 _1726 = max(a.bbox.zw, c.bbox.zw);
+ c.bbox = vec4(c.bbox.x, c.bbox.y, _1726.x, _1726.y);
+ }
+ }
+ c.mat.x = (a.mat.x * b.mat.x) + (a.mat.z * b.mat.y);
+ c.mat.y = (a.mat.y * b.mat.x) + (a.mat.w * b.mat.y);
+ c.mat.z = (a.mat.x * b.mat.z) + (a.mat.z * b.mat.w);
+ c.mat.w = (a.mat.y * b.mat.z) + (a.mat.w * b.mat.w);
+ c.translate.x = ((a.mat.x * b.translate.x) + (a.mat.z * b.translate.y)) + a.translate.x;
+ c.translate.y = ((a.mat.y * b.translate.x) + (a.mat.w * b.translate.y)) + a.translate.y;
+ float _1812;
+ if ((b.flags & 1u) == 0u)
+ {
+ _1812 = a.linewidth;
+ }
+ else
+ {
+ _1812 = b.linewidth;
+ }
+ c.linewidth = _1812;
+ c.flags = (a.flags & 11u) | b.flags;
+ c.flags |= ((a.flags & 4u) >> uint(1));
+ uint _1842;
+ if ((b.flags & 8u) == 0u)
+ {
+ _1842 = a.flags;
+ }
+ else
+ {
+ _1842 = b.flags;
+ }
+ uint fill_mode = _1842;
+ fill_mode &= 16u;
+ c.flags = (c.flags & 4294967279u) | fill_mode;
+ c.path_count = a.path_count + b.path_count;
+ c.pathseg_count = a.pathseg_count + b.pathseg_count;
+ c.trans_count = a.trans_count + b.trans_count;
+ return c;
+}
+
+StateRef state_aggregate_ref(uint partition_ix)
+{
+ return StateRef(4u + (partition_ix * 124u));
+}
+
+void State_write(StateRef ref, State s)
+{
+ uint ix = ref.offset >> uint(2);
+ _779.state[ix + 0u] = floatBitsToUint(s.mat.x);
+ _779.state[ix + 1u] = floatBitsToUint(s.mat.y);
+ _779.state[ix + 2u] = floatBitsToUint(s.mat.z);
+ _779.state[ix + 3u] = floatBitsToUint(s.mat.w);
+ _779.state[ix + 4u] = floatBitsToUint(s.translate.x);
+ _779.state[ix + 5u] = floatBitsToUint(s.translate.y);
+ _779.state[ix + 6u] = floatBitsToUint(s.bbox.x);
+ _779.state[ix + 7u] = floatBitsToUint(s.bbox.y);
+ _779.state[ix + 8u] = floatBitsToUint(s.bbox.z);
+ _779.state[ix + 9u] = floatBitsToUint(s.bbox.w);
+ _779.state[ix + 10u] = floatBitsToUint(s.linewidth);
+ _779.state[ix + 11u] = s.flags;
+ _779.state[ix + 12u] = s.path_count;
+ _779.state[ix + 13u] = s.pathseg_count;
+ _779.state[ix + 14u] = s.trans_count;
+}
+
+StateRef state_prefix_ref(uint partition_ix)
+{
+ return StateRef((4u + (partition_ix * 124u)) + 60u);
+}
+
+uint state_flag_index(uint partition_ix)
+{
+ return partition_ix * 31u;
+}
+
+State State_read(StateRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _779.state[ix + 0u];
+ uint raw1 = _779.state[ix + 1u];
+ uint raw2 = _779.state[ix + 2u];
+ uint raw3 = _779.state[ix + 3u];
+ uint raw4 = _779.state[ix + 4u];
+ uint raw5 = _779.state[ix + 5u];
+ uint raw6 = _779.state[ix + 6u];
+ uint raw7 = _779.state[ix + 7u];
+ uint raw8 = _779.state[ix + 8u];
+ uint raw9 = _779.state[ix + 9u];
+ uint raw10 = _779.state[ix + 10u];
+ uint raw11 = _779.state[ix + 11u];
+ uint raw12 = _779.state[ix + 12u];
+ uint raw13 = _779.state[ix + 13u];
+ uint raw14 = _779.state[ix + 14u];
+ State s;
+ s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5));
+ s.bbox = vec4(uintBitsToFloat(raw6), uintBitsToFloat(raw7), uintBitsToFloat(raw8), uintBitsToFloat(raw9));
+ s.linewidth = uintBitsToFloat(raw10);
+ s.flags = raw11;
+ s.path_count = raw12;
+ s.pathseg_count = raw13;
+ s.trans_count = raw14;
+ return s;
+}
+
+uint fill_mode_from_flags(uint flags)
+{
+ return flags & 1u;
+}
+
+vec2 get_linewidth(State st)
+{
+ return vec2(length(st.mat.xz), length(st.mat.yw)) * (0.5 * st.linewidth);
+}
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _294.memory[offset] = val;
+}
+
+void PathCubic_write(Alloc a, PathCubicRef ref, PathCubic s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.p0.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.p0.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.p1.x);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.p1.y);
+ write_mem(param_9, param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 4u;
+ uint param_14 = floatBitsToUint(s.p2.x);
+ write_mem(param_12, param_13, param_14);
+ Alloc param_15 = a;
+ uint param_16 = ix + 5u;
+ uint param_17 = floatBitsToUint(s.p2.y);
+ write_mem(param_15, param_16, param_17);
+ Alloc param_18 = a;
+ uint param_19 = ix + 6u;
+ uint param_20 = floatBitsToUint(s.p3.x);
+ write_mem(param_18, param_19, param_20);
+ Alloc param_21 = a;
+ uint param_22 = ix + 7u;
+ uint param_23 = floatBitsToUint(s.p3.y);
+ write_mem(param_21, param_22, param_23);
+ Alloc param_24 = a;
+ uint param_25 = ix + 8u;
+ uint param_26 = s.path_ix;
+ write_mem(param_24, param_25, param_26);
+ Alloc param_27 = a;
+ uint param_28 = ix + 9u;
+ uint param_29 = s.trans_ix;
+ write_mem(param_27, param_28, param_29);
+ Alloc param_30 = a;
+ uint param_31 = ix + 10u;
+ uint param_32 = floatBitsToUint(s.stroke.x);
+ write_mem(param_30, param_31, param_32);
+ Alloc param_33 = a;
+ uint param_34 = ix + 11u;
+ uint param_35 = floatBitsToUint(s.stroke.y);
+ write_mem(param_33, param_34, param_35);
+}
+
+void PathSeg_Cubic_write(Alloc a, PathSegRef ref, uint flags, PathCubic s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = (flags << uint(16)) | 1u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ PathCubicRef param_4 = PathCubicRef(ref.offset + 4u);
+ PathCubic param_5 = s;
+ PathCubic_write(param_3, param_4, param_5);
+}
+
+FillColor FillColor_read(FillColorRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ FillColor s;
+ s.rgba_color = raw0;
+ return s;
+}
+
+FillColor Element_FillColor_read(ElementRef ref)
+{
+ FillColorRef param = FillColorRef(ref.offset + 4u);
+ return FillColor_read(param);
+}
+
+void AnnoColor_write(Alloc a, AnnoColorRef ref, AnnoColor s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.bbox.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.bbox.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.bbox.z);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.bbox.w);
+ write_mem(param_9, param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 4u;
+ uint param_14 = floatBitsToUint(s.linewidth);
+ write_mem(param_12, param_13, param_14);
+ Alloc param_15 = a;
+ uint param_16 = ix + 5u;
+ uint param_17 = s.rgba_color;
+ write_mem(param_15, param_16, param_17);
+}
+
+void Annotated_Color_write(Alloc a, AnnotatedRef ref, uint flags, AnnoColor s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = (flags << uint(16)) | 1u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ AnnoColorRef param_4 = AnnoColorRef(ref.offset + 4u);
+ AnnoColor param_5 = s;
+ AnnoColor_write(param_3, param_4, param_5);
+}
+
+FillImage FillImage_read(FillImageRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ uint raw1 = _323.scene[ix + 1u];
+ FillImage s;
+ s.index = raw0;
+ s.offset = ivec2(int(raw1 << uint(16)) >> 16, int(raw1) >> 16);
+ return s;
+}
+
+FillImage Element_FillImage_read(ElementRef ref)
+{
+ FillImageRef param = FillImageRef(ref.offset + 4u);
+ return FillImage_read(param);
+}
+
+void AnnoImage_write(Alloc a, AnnoImageRef ref, AnnoImage s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.bbox.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.bbox.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.bbox.z);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.bbox.w);
+ write_mem(param_9, param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 4u;
+ uint param_14 = floatBitsToUint(s.linewidth);
+ write_mem(param_12, param_13, param_14);
+ Alloc param_15 = a;
+ uint param_16 = ix + 5u;
+ uint param_17 = s.index;
+ write_mem(param_15, param_16, param_17);
+ Alloc param_18 = a;
+ uint param_19 = ix + 6u;
+ uint param_20 = (uint(s.offset.x) & 65535u) | (uint(s.offset.y) << uint(16));
+ write_mem(param_18, param_19, param_20);
+}
+
+void Annotated_Image_write(Alloc a, AnnotatedRef ref, uint flags, AnnoImage s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = (flags << uint(16)) | 2u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ AnnoImageRef param_4 = AnnoImageRef(ref.offset + 4u);
+ AnnoImage param_5 = s;
+ AnnoImage_write(param_3, param_4, param_5);
+}
+
+Clip Clip_read(ClipRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ uint raw0 = _323.scene[ix + 0u];
+ uint raw1 = _323.scene[ix + 1u];
+ uint raw2 = _323.scene[ix + 2u];
+ uint raw3 = _323.scene[ix + 3u];
+ Clip s;
+ s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ return s;
+}
+
+Clip Element_BeginClip_read(ElementRef ref)
+{
+ ClipRef param = ClipRef(ref.offset + 4u);
+ return Clip_read(param);
+}
+
+void AnnoBeginClip_write(Alloc a, AnnoBeginClipRef ref, AnnoBeginClip s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.bbox.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.bbox.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.bbox.z);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.bbox.w);
+ write_mem(param_9, param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 4u;
+ uint param_14 = floatBitsToUint(s.linewidth);
+ write_mem(param_12, param_13, param_14);
+}
+
+void Annotated_BeginClip_write(Alloc a, AnnotatedRef ref, uint flags, AnnoBeginClip s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = (flags << uint(16)) | 3u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ AnnoBeginClipRef param_4 = AnnoBeginClipRef(ref.offset + 4u);
+ AnnoBeginClip param_5 = s;
+ AnnoBeginClip_write(param_3, param_4, param_5);
+}
+
+Clip Element_EndClip_read(ElementRef ref)
+{
+ ClipRef param = ClipRef(ref.offset + 4u);
+ return Clip_read(param);
+}
+
+void AnnoEndClip_write(Alloc a, AnnoEndClipRef ref, AnnoEndClip s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.bbox.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.bbox.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.bbox.z);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.bbox.w);
+ write_mem(param_9, param_10, param_11);
+}
+
+void Annotated_EndClip_write(Alloc a, AnnotatedRef ref, AnnoEndClip s)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint param_2 = 4u;
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ AnnoEndClipRef param_4 = AnnoEndClipRef(ref.offset + 4u);
+ AnnoEndClip param_5 = s;
+ AnnoEndClip_write(param_3, param_4, param_5);
+}
+
+void TransformSeg_write(Alloc a, TransformSegRef ref, TransformSeg s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.mat.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.mat.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.mat.z);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.mat.w);
+ write_mem(param_9, param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 4u;
+ uint param_14 = floatBitsToUint(s.translate.x);
+ write_mem(param_12, param_13, param_14);
+ Alloc param_15 = a;
+ uint param_16 = ix + 5u;
+ uint param_17 = floatBitsToUint(s.translate.y);
+ write_mem(param_15, param_16, param_17);
+}
+
+void main()
+{
+ if (_294.mem_error != 0u)
+ {
+ return;
+ }
+ if (gl_LocalInvocationID.x == 0u)
+ {
+ uint _2069 = atomicAdd(_779.part_counter, 1u);
+ sh_part_ix = _2069;
+ }
+ barrier();
+ uint part_ix = sh_part_ix;
+ uint ix = (part_ix * 128u) + (gl_LocalInvocationID.x * 4u);
+ ElementRef ref = ElementRef(ix * 36u);
+ ElementRef param = ref;
+ State th_state[4];
+ th_state[0] = map_element(param);
+ for (uint i = 1u; i < 4u; i++)
+ {
+ ElementRef param_1 = ref;
+ uint param_2 = i;
+ ElementRef param_3 = Element_index(param_1, param_2);
+ State param_4 = th_state[i - 1u];
+ State param_5 = map_element(param_3);
+ th_state[i] = combine_state(param_4, param_5);
+ }
+ State agg = th_state[3];
+ sh_state[gl_LocalInvocationID.x] = agg;
+ for (uint i_1 = 0u; i_1 < 5u; i_1++)
+ {
+ barrier();
+ if (gl_LocalInvocationID.x >= uint(1 << int(i_1)))
+ {
+ State other = sh_state[gl_LocalInvocationID.x - uint(1 << int(i_1))];
+ State param_6 = other;
+ State param_7 = agg;
+ agg = combine_state(param_6, param_7);
+ }
+ barrier();
+ sh_state[gl_LocalInvocationID.x] = agg;
+ }
+ State exclusive;
+ exclusive.bbox = vec4(0.0);
+ exclusive.mat = vec4(1.0, 0.0, 0.0, 1.0);
+ exclusive.translate = vec2(0.0);
+ exclusive.linewidth = 1.0;
+ exclusive.flags = 0u;
+ exclusive.path_count = 0u;
+ exclusive.pathseg_count = 0u;
+ exclusive.trans_count = 0u;
+ if (gl_LocalInvocationID.x == 31u)
+ {
+ uint param_8 = part_ix;
+ StateRef param_9 = state_aggregate_ref(param_8);
+ State param_10 = agg;
+ State_write(param_9, param_10);
+ uint flag = 1u;
+ memoryBarrierBuffer();
+ if (part_ix == 0u)
+ {
+ uint param_11 = part_ix;
+ StateRef param_12 = state_prefix_ref(param_11);
+ State param_13 = agg;
+ State_write(param_12, param_13);
+ flag = 2u;
+ }
+ uint param_14 = part_ix;
+ _779.state[state_flag_index(param_14)] = flag;
+ if (part_ix != 0u)
+ {
+ uint look_back_ix = part_ix - 1u;
+ uint their_ix = 0u;
+ State their_agg;
+ while (true)
+ {
+ uint param_15 = look_back_ix;
+ flag = _779.state[state_flag_index(param_15)];
+ if (flag == 2u)
+ {
+ uint param_16 = look_back_ix;
+ StateRef param_17 = state_prefix_ref(param_16);
+ State their_prefix = State_read(param_17);
+ State param_18 = their_prefix;
+ State param_19 = exclusive;
+ exclusive = combine_state(param_18, param_19);
+ break;
+ }
+ else
+ {
+ if (flag == 1u)
+ {
+ uint param_20 = look_back_ix;
+ StateRef param_21 = state_aggregate_ref(param_20);
+ their_agg = State_read(param_21);
+ State param_22 = their_agg;
+ State param_23 = exclusive;
+ exclusive = combine_state(param_22, param_23);
+ look_back_ix--;
+ their_ix = 0u;
+ continue;
+ }
+ }
+ ElementRef ref_1 = ElementRef(((look_back_ix * 128u) + their_ix) * 36u);
+ ElementRef param_24 = ref_1;
+ State s = map_element(param_24);
+ if (their_ix == 0u)
+ {
+ their_agg = s;
+ }
+ else
+ {
+ State param_25 = their_agg;
+ State param_26 = s;
+ their_agg = combine_state(param_25, param_26);
+ }
+ their_ix++;
+ if (their_ix == 128u)
+ {
+ State param_27 = their_agg;
+ State param_28 = exclusive;
+ exclusive = combine_state(param_27, param_28);
+ if (look_back_ix == 0u)
+ {
+ break;
+ }
+ look_back_ix--;
+ their_ix = 0u;
+ }
+ }
+ State param_29 = exclusive;
+ State param_30 = agg;
+ State inclusive_prefix = combine_state(param_29, param_30);
+ sh_prefix = exclusive;
+ uint param_31 = part_ix;
+ StateRef param_32 = state_prefix_ref(param_31);
+ State param_33 = inclusive_prefix;
+ State_write(param_32, param_33);
+ memoryBarrierBuffer();
+ flag = 2u;
+ uint param_34 = part_ix;
+ _779.state[state_flag_index(param_34)] = flag;
+ }
+ }
+ barrier();
+ if (part_ix != 0u)
+ {
+ exclusive = sh_prefix;
+ }
+ State row = exclusive;
+ if (gl_LocalInvocationID.x > 0u)
+ {
+ State other_1 = sh_state[gl_LocalInvocationID.x - 1u];
+ State param_35 = row;
+ State param_36 = other_1;
+ row = combine_state(param_35, param_36);
+ }
+ PathCubic path_cubic;
+ PathSegRef path_out_ref;
+ Alloc param_45;
+ Alloc param_51;
+ Alloc param_57;
+ AnnoColor anno_fill;
+ AnnotatedRef out_ref;
+ Alloc param_63;
+ AnnoImage anno_img;
+ Alloc param_69;
+ AnnoBeginClip anno_begin_clip;
+ Alloc param_75;
+ Alloc param_80;
+ Alloc param_83;
+ for (uint i_2 = 0u; i_2 < 4u; i_2++)
+ {
+ State param_37 = row;
+ State param_38 = th_state[i_2];
+ State st = combine_state(param_37, param_38);
+ ElementRef param_39 = ref;
+ uint param_40 = i_2;
+ ElementRef this_ref = Element_index(param_39, param_40);
+ ElementRef param_41 = this_ref;
+ ElementTag tag = Element_tag(param_41);
+ uint param_42 = st.flags >> uint(4);
+ uint fill_mode = fill_mode_from_flags(param_42);
+ bool is_stroke = fill_mode == 1u;
+ switch (tag.tag)
+ {
+ case 1u:
+ {
+ ElementRef param_43 = this_ref;
+ LineSeg line = Element_Line_read(param_43);
+ path_cubic.p0 = line.p0;
+ path_cubic.p1 = mix(line.p0, line.p1, vec2(0.3333333432674407958984375));
+ path_cubic.p2 = mix(line.p1, line.p0, vec2(0.3333333432674407958984375));
+ path_cubic.p3 = line.p1;
+ path_cubic.path_ix = st.path_count;
+ path_cubic.trans_ix = st.trans_count;
+ if (is_stroke)
+ {
+ State param_44 = st;
+ path_cubic.stroke = get_linewidth(param_44);
+ }
+ else
+ {
+ path_cubic.stroke = vec2(0.0);
+ }
+ path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u));
+ param_45.offset = _2435.conf.pathseg_alloc.offset;
+ PathSegRef param_46 = path_out_ref;
+ uint param_47 = fill_mode;
+ PathCubic param_48 = path_cubic;
+ PathSeg_Cubic_write(param_45, param_46, param_47, param_48);
+ break;
+ }
+ case 2u:
+ {
+ ElementRef param_49 = this_ref;
+ QuadSeg quad = Element_Quad_read(param_49);
+ path_cubic.p0 = quad.p0;
+ path_cubic.p1 = mix(quad.p1, quad.p0, vec2(0.3333333432674407958984375));
+ path_cubic.p2 = mix(quad.p1, quad.p2, vec2(0.3333333432674407958984375));
+ path_cubic.p3 = quad.p2;
+ path_cubic.path_ix = st.path_count;
+ path_cubic.trans_ix = st.trans_count;
+ if (is_stroke)
+ {
+ State param_50 = st;
+ path_cubic.stroke = get_linewidth(param_50);
+ }
+ else
+ {
+ path_cubic.stroke = vec2(0.0);
+ }
+ path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u));
+ param_51.offset = _2435.conf.pathseg_alloc.offset;
+ PathSegRef param_52 = path_out_ref;
+ uint param_53 = fill_mode;
+ PathCubic param_54 = path_cubic;
+ PathSeg_Cubic_write(param_51, param_52, param_53, param_54);
+ break;
+ }
+ case 3u:
+ {
+ ElementRef param_55 = this_ref;
+ CubicSeg cubic = Element_Cubic_read(param_55);
+ path_cubic.p0 = cubic.p0;
+ path_cubic.p1 = cubic.p1;
+ path_cubic.p2 = cubic.p2;
+ path_cubic.p3 = cubic.p3;
+ path_cubic.path_ix = st.path_count;
+ path_cubic.trans_ix = st.trans_count;
+ if (is_stroke)
+ {
+ State param_56 = st;
+ path_cubic.stroke = get_linewidth(param_56);
+ }
+ else
+ {
+ path_cubic.stroke = vec2(0.0);
+ }
+ path_out_ref = PathSegRef(_2435.conf.pathseg_alloc.offset + ((st.pathseg_count - 1u) * 52u));
+ param_57.offset = _2435.conf.pathseg_alloc.offset;
+ PathSegRef param_58 = path_out_ref;
+ uint param_59 = fill_mode;
+ PathCubic param_60 = path_cubic;
+ PathSeg_Cubic_write(param_57, param_58, param_59, param_60);
+ break;
+ }
+ case 4u:
+ {
+ ElementRef param_61 = this_ref;
+ FillColor fill = Element_FillColor_read(param_61);
+ anno_fill.rgba_color = fill.rgba_color;
+ if (is_stroke)
+ {
+ State param_62 = st;
+ vec2 lw = get_linewidth(param_62);
+ anno_fill.bbox = st.bbox + vec4(-lw, lw);
+ anno_fill.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z)));
+ }
+ else
+ {
+ anno_fill.bbox = st.bbox;
+ anno_fill.linewidth = 0.0;
+ }
+ out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u));
+ param_63.offset = _2435.conf.anno_alloc.offset;
+ AnnotatedRef param_64 = out_ref;
+ uint param_65 = fill_mode;
+ AnnoColor param_66 = anno_fill;
+ Annotated_Color_write(param_63, param_64, param_65, param_66);
+ break;
+ }
+ case 9u:
+ {
+ ElementRef param_67 = this_ref;
+ FillImage fill_img = Element_FillImage_read(param_67);
+ anno_img.index = fill_img.index;
+ anno_img.offset = fill_img.offset;
+ if (is_stroke)
+ {
+ State param_68 = st;
+ vec2 lw_1 = get_linewidth(param_68);
+ anno_img.bbox = st.bbox + vec4(-lw_1, lw_1);
+ anno_img.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z)));
+ }
+ else
+ {
+ anno_img.bbox = st.bbox;
+ anno_img.linewidth = 0.0;
+ }
+ out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u));
+ param_69.offset = _2435.conf.anno_alloc.offset;
+ AnnotatedRef param_70 = out_ref;
+ uint param_71 = fill_mode;
+ AnnoImage param_72 = anno_img;
+ Annotated_Image_write(param_69, param_70, param_71, param_72);
+ break;
+ }
+ case 7u:
+ {
+ ElementRef param_73 = this_ref;
+ Clip begin_clip = Element_BeginClip_read(param_73);
+ anno_begin_clip.bbox = begin_clip.bbox;
+ if (is_stroke)
+ {
+ State param_74 = st;
+ vec2 lw_2 = get_linewidth(param_74);
+ anno_begin_clip.linewidth = st.linewidth * sqrt(abs((st.mat.x * st.mat.w) - (st.mat.y * st.mat.z)));
+ }
+ else
+ {
+ anno_fill.linewidth = 0.0;
+ }
+ out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u));
+ param_75.offset = _2435.conf.anno_alloc.offset;
+ AnnotatedRef param_76 = out_ref;
+ uint param_77 = fill_mode;
+ AnnoBeginClip param_78 = anno_begin_clip;
+ Annotated_BeginClip_write(param_75, param_76, param_77, param_78);
+ break;
+ }
+ case 8u:
+ {
+ ElementRef param_79 = this_ref;
+ Clip end_clip = Element_EndClip_read(param_79);
+ AnnoEndClip anno_end_clip = AnnoEndClip(end_clip.bbox);
+ out_ref = AnnotatedRef(_2435.conf.anno_alloc.offset + ((st.path_count - 1u) * 32u));
+ param_80.offset = _2435.conf.anno_alloc.offset;
+ AnnotatedRef param_81 = out_ref;
+ AnnoEndClip param_82 = anno_end_clip;
+ Annotated_EndClip_write(param_80, param_81, param_82);
+ break;
+ }
+ case 6u:
+ {
+ TransformSeg transform = TransformSeg(st.mat, st.translate);
+ TransformSegRef trans_ref = TransformSegRef(_2435.conf.trans_alloc.offset + ((st.trans_count - 1u) * 24u));
+ param_83.offset = _2435.conf.trans_alloc.offset;
+ TransformSegRef param_84 = trans_ref;
+ TransformSeg param_85 = transform;
+ TransformSeg_write(param_83, param_84, param_85);
+ break;
+ }
+ }
+ }
+}
+
+`,
+ }
+ shader_intersect_frag = driver.ShaderSources{
+ Name: "intersect.frag",
+ Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}},
+ Textures: []driver.TextureBinding{{Name: "cover", Binding: 0}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D cover;
+
+varying highp vec2 vUV;
+
+void main()
+{
+ float cover_1 = abs(texture2D(cover, vUV).x);
+ gl_FragData[0].x = cover_1;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D cover;
+
+in highp vec2 vUV;
+layout(location = 0) out vec4 fragColor;
+
+void main()
+{
+ float cover_1 = abs(texture(cover, vUV).x);
+ fragColor.x = cover_1;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D cover;
+
+in vec2 vUV;
+out vec4 fragColor;
+
+void main()
+{
+ float cover_1 = abs(texture(cover, vUV).x);
+ fragColor.x = cover_1;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D cover;
+
+in vec2 vUV;
+out vec4 fragColor;
+
+void main()
+{
+ float cover_1 = abs(texture(cover, vUV).x);
+ fragColor.x = cover_1;
+}
+
+`,
+ HLSL: "DXBC\xe0\xe4\x03\x8c\xacVF\x82l\xe7|\xc3T\xa6'\xef\x01\x00\x00\x00\b\x03\x00\x00\x06\x00\x00\x008\x00\x00\x00\xd4\x00\x00\x00\x80\x01\x00\x00\xfc\x01\x00\x00\xa0\x02\x00\x00\xd4\x02\x00\x00Aon9\x94\x00\x00\x00\x94\x00\x00\x00\x00\x02\xff\xffl\x00\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0#\x00\x00\x02\x00\x00\x01\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x0e\x80\x00\x00\x00\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\xa4\x00\x00\x00@\x00\x00\x00)\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x006\x00\x00\x06\x12 \x10\x00\x00\x00\x00\x00\n\x00\x10\x80\x81\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xe2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x9c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00q\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00k\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_cover_sampler\x00cover\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ }
+ shader_intersect_vert = driver.ShaderSources{
+ Name: "intersect.vert",
+ Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_block.uvTransform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.subUVTransform", Type: 0x0, Size: 4, Offset: 16}},
+ Size: 32,
+ },
+ GLSL100ES: `#version 100
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+struct Block
+{
+ vec4 uvTransform;
+ vec4 subUVTransform;
+};
+
+uniform Block _block;
+
+attribute vec2 pos;
+attribute vec2 uv;
+varying vec2 vUV;
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0));
+ vec3 param_1 = vec3(pos, 1.0);
+ vec3 p = transform3x2(param, param_1);
+ gl_Position = vec4(p, 1.0);
+ m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_3 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_2, param_3);
+ vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw;
+ m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_5 = vec3(vUV, 1.0);
+ vUV = transform3x2(param_4, param_5).xy;
+ vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+layout(std140) uniform Block
+{
+ vec4 uvTransform;
+ vec4 subUVTransform;
+} _block;
+
+layout(location = 0) in vec2 pos;
+layout(location = 1) in vec2 uv;
+out vec2 vUV;
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0));
+ vec3 param_1 = vec3(pos, 1.0);
+ vec3 p = transform3x2(param, param_1);
+ gl_Position = vec4(p, 1.0);
+ m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_3 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_2, param_3);
+ vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw;
+ m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_5 = vec3(vUV, 1.0);
+ vUV = transform3x2(param_4, param_5).xy;
+ vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+struct Block
+{
+ vec4 uvTransform;
+ vec4 subUVTransform;
+};
+
+uniform Block _block;
+
+in vec2 pos;
+in vec2 uv;
+out vec2 vUV;
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0));
+ vec3 param_1 = vec3(pos, 1.0);
+ vec3 p = transform3x2(param, param_1);
+ gl_Position = vec4(p, 1.0);
+ m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_3 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_2, param_3);
+ vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw;
+ m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_5 = vec3(vUV, 1.0);
+ vUV = transform3x2(param_4, param_5).xy;
+ vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct m3x2
+{
+ vec3 r0;
+ vec3 r1;
+};
+
+layout(binding = 0, std140) uniform Block
+{
+ vec4 uvTransform;
+ vec4 subUVTransform;
+} _block;
+
+in vec2 pos;
+in vec2 uv;
+out vec2 vUV;
+
+vec3 transform3x2(m3x2 t, vec3 v)
+{
+ return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v));
+}
+
+void main()
+{
+ m3x2 param = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0));
+ vec3 param_1 = vec3(pos, 1.0);
+ vec3 p = transform3x2(param, param_1);
+ gl_Position = vec4(p, 1.0);
+ m3x2 param_2 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_3 = vec3(uv, 1.0);
+ vec3 uv3 = transform3x2(param_2, param_3);
+ vUV = (uv3.xy * _block.subUVTransform.xy) + _block.subUVTransform.zw;
+ m3x2 param_4 = m3x2(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0));
+ vec3 param_5 = vec3(vUV, 1.0);
+ vUV = transform3x2(param_4, param_5).xy;
+ vUV = (vUV * _block.uvTransform.xy) + _block.uvTransform.zw;
+}
+
+`,
+ HLSL: "DXBCxH\xc4I\xbe\x0f[|\nl\x899\xe0\xb8\xcb?\x01\x00\x00\x00\xdc\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00L\x01\x00\x00\xc4\x02\x00\x00@\x03\x00\x008\x04\x00\x00\x84\x04\x00\x00Aon9\f\x01\x00\x00\f\x01\x00\x00\x00\x02\xfe\xff\xd8\x00\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x03\x00\x0f\xa0\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x80\xbf\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x04\x00\x00\x04\x00\x00\x03\x80\x01\x00U\x90\x03\x00\xe4\xa0\x03\x00\xe1\xa0\x05\x00\x00\x03\x00\x00\x03\x80\x00\x00\xe4\x80\x03\x00\xe2\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x01\x00\x00\x02\x00\x00\x01\x80\x01\x00\x00\x90\x04\x00\x00\x04\x00\x00\x03\x80\x00\x00\xe4\x80\x02\x00\xe4\xa0\x02\x00\xee\xa0\x01\x00\x00\x02\x00\x00\x04\x80\x03\x00\x00\xa0\b\x00\x00\x03\x00\x00\b\x80\x03\x00É \x00\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x03\xe0\x00\x00\xec\x80\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x90\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\f\xc0\x03\x00\x00\xa0\xff\xff\x00\x00SHDRp\x01\x00\x00@\x00\x01\x00\\\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x01\x00\x00\x006\x00\x00\x05\"\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?6\x00\x00\x05R\x00\x10\x00\x00\x00\x00\x00V\x14\x10\x00\x01\x00\x00\x00\x0f\x00\x00\n\x82\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x002\x00\x00\v2\x00\x10\x00\x00\x00\x00\x00\xe6\n\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x01\x00\x00\x006\x00\x00\x05B\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x0f\x00\x00\n\x82\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x96\x05\x10\x00\x00\x00\x00\x002\x00\x00\v2 \x10\x00\x00\x00\x00\x00\xc6\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\x052 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x01\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\n\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xf0\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\xc6\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x02\x00\x00\x00\\\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00_block_uvTransform\x00\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_subUVTransform\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xabISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab",
+ }
+ shader_kernel4_comp = driver.ShaderSources{
+ Name: "kernel4.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 16, local_size_y = 8, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct CmdStrokeRef
+{
+ uint offset;
+};
+
+struct CmdStroke
+{
+ uint tile_ref;
+ float half_width;
+};
+
+struct CmdFillRef
+{
+ uint offset;
+};
+
+struct CmdFill
+{
+ uint tile_ref;
+ int backdrop;
+};
+
+struct CmdColorRef
+{
+ uint offset;
+};
+
+struct CmdColor
+{
+ uint rgba_color;
+};
+
+struct CmdImageRef
+{
+ uint offset;
+};
+
+struct CmdImage
+{
+ uint index;
+ ivec2 offset;
+};
+
+struct CmdAlphaRef
+{
+ uint offset;
+};
+
+struct CmdAlpha
+{
+ float alpha;
+};
+
+struct CmdJumpRef
+{
+ uint offset;
+};
+
+struct CmdJump
+{
+ uint new_ref;
+};
+
+struct CmdRef
+{
+ uint offset;
+};
+
+struct CmdTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct TileSegRef
+{
+ uint offset;
+};
+
+struct TileSeg
+{
+ vec2 origin;
+ vec2 vector;
+ float y_edge;
+ TileSegRef next;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _196;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _693;
+
+layout(binding = 3, rgba8) uniform readonly highp image2D images[1];
+layout(binding = 2, rgba8) uniform writeonly highp image2D image;
+
+Alloc new_alloc(uint offset, uint size)
+{
+ Alloc a;
+ a.offset = offset;
+ return a;
+}
+
+Alloc slice_mem(Alloc a, uint offset, uint size)
+{
+ uint param = a.offset + offset;
+ uint param_1 = size;
+ return new_alloc(param, param_1);
+}
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+uint read_mem(Alloc alloc, uint offset)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return 0u;
+ }
+ uint v = _196.memory[offset];
+ return v;
+}
+
+Alloc alloc_read(Alloc a, uint offset)
+{
+ Alloc param = a;
+ uint param_1 = offset >> uint(2);
+ Alloc alloc;
+ alloc.offset = read_mem(param, param_1);
+ return alloc;
+}
+
+CmdTag Cmd_tag(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint tag_and_flags = read_mem(param, param_1);
+ return CmdTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+CmdStroke CmdStroke_read(Alloc a, CmdStrokeRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ CmdStroke s;
+ s.tile_ref = raw0;
+ s.half_width = uintBitsToFloat(raw1);
+ return s;
+}
+
+CmdStroke Cmd_Stroke_read(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ CmdStrokeRef param_1 = CmdStrokeRef(ref.offset + 4u);
+ return CmdStroke_read(param, param_1);
+}
+
+TileSeg TileSeg_read(Alloc a, TileSegRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ Alloc param_8 = a;
+ uint param_9 = ix + 4u;
+ uint raw4 = read_mem(param_8, param_9);
+ Alloc param_10 = a;
+ uint param_11 = ix + 5u;
+ uint raw5 = read_mem(param_10, param_11);
+ TileSeg s;
+ s.origin = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1));
+ s.vector = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.y_edge = uintBitsToFloat(raw4);
+ s.next = TileSegRef(raw5);
+ return s;
+}
+
+uvec2 chunk_offset(uint i)
+{
+ return uvec2((i % 2u) * 16u, (i / 2u) * 8u);
+}
+
+CmdFill CmdFill_read(Alloc a, CmdFillRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ CmdFill s;
+ s.tile_ref = raw0;
+ s.backdrop = int(raw1);
+ return s;
+}
+
+CmdFill Cmd_Fill_read(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ CmdFillRef param_1 = CmdFillRef(ref.offset + 4u);
+ return CmdFill_read(param, param_1);
+}
+
+CmdAlpha CmdAlpha_read(Alloc a, CmdAlphaRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ CmdAlpha s;
+ s.alpha = uintBitsToFloat(raw0);
+ return s;
+}
+
+CmdAlpha Cmd_Alpha_read(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ CmdAlphaRef param_1 = CmdAlphaRef(ref.offset + 4u);
+ return CmdAlpha_read(param, param_1);
+}
+
+CmdColor CmdColor_read(Alloc a, CmdColorRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ CmdColor s;
+ s.rgba_color = raw0;
+ return s;
+}
+
+CmdColor Cmd_Color_read(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ CmdColorRef param_1 = CmdColorRef(ref.offset + 4u);
+ return CmdColor_read(param, param_1);
+}
+
+vec3 fromsRGB(vec3 srgb)
+{
+ bvec3 cutoff = greaterThanEqual(srgb, vec3(0.040449999272823333740234375));
+ vec3 below = srgb / vec3(12.9200000762939453125);
+ vec3 above = pow((srgb + vec3(0.054999999701976776123046875)) / vec3(1.05499994754791259765625), vec3(2.400000095367431640625));
+ return mix(below, above, cutoff);
+}
+
+vec4 unpacksRGB(uint srgba)
+{
+ vec4 color = unpackUnorm4x8(srgba).wzyx;
+ vec3 param = color.xyz;
+ return vec4(fromsRGB(param), color.w);
+}
+
+CmdImage CmdImage_read(Alloc a, CmdImageRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ CmdImage s;
+ s.index = raw0;
+ s.offset = ivec2(int(raw1 << uint(16)) >> 16, int(raw1) >> 16);
+ return s;
+}
+
+CmdImage Cmd_Image_read(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ CmdImageRef param_1 = CmdImageRef(ref.offset + 4u);
+ return CmdImage_read(param, param_1);
+}
+
+vec4[8] fillImage(uvec2 xy, CmdImage cmd_img)
+{
+ vec4 rgba[8];
+ for (uint i = 0u; i < 8u; i++)
+ {
+ uint param = i;
+ ivec2 uv = ivec2(xy + chunk_offset(param)) + cmd_img.offset;
+ vec4 fg_rgba = imageLoad(images[0], uv);
+ vec3 param_1 = fg_rgba.xyz;
+ vec3 _663 = fromsRGB(param_1);
+ fg_rgba = vec4(_663.x, _663.y, _663.z, fg_rgba.w);
+ rgba[i] = fg_rgba;
+ }
+ return rgba;
+}
+
+vec3 tosRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375));
+ vec3 below = vec3(12.9200000762939453125) * rgb;
+ vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875);
+ return mix(below, above, cutoff);
+}
+
+uint packsRGB(inout vec4 rgba)
+{
+ vec3 param = rgba.xyz;
+ rgba = vec4(tosRGB(param), rgba.w);
+ return packUnorm4x8(rgba.wzyx);
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _196.memory[offset] = val;
+}
+
+CmdJump CmdJump_read(Alloc a, CmdJumpRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ CmdJump s;
+ s.new_ref = raw0;
+ return s;
+}
+
+CmdJump Cmd_Jump_read(Alloc a, CmdRef ref)
+{
+ Alloc param = a;
+ CmdJumpRef param_1 = CmdJumpRef(ref.offset + 4u);
+ return CmdJump_read(param, param_1);
+}
+
+void main()
+{
+ if (_196.mem_error != 0u)
+ {
+ return;
+ }
+ uint tile_ix = (gl_WorkGroupID.y * _693.conf.width_in_tiles) + gl_WorkGroupID.x;
+ Alloc param;
+ param.offset = _693.conf.ptcl_alloc.offset;
+ uint param_1 = tile_ix * 1024u;
+ uint param_2 = 1024u;
+ Alloc cmd_alloc = slice_mem(param, param_1, param_2);
+ CmdRef cmd_ref = CmdRef(cmd_alloc.offset);
+ Alloc param_3 = cmd_alloc;
+ uint param_4 = cmd_ref.offset;
+ Alloc scratch_alloc = alloc_read(param_3, param_4);
+ cmd_ref.offset += 8u;
+ uvec2 xy_uint = uvec2(gl_LocalInvocationID.x + (32u * gl_WorkGroupID.x), gl_LocalInvocationID.y + (32u * gl_WorkGroupID.y));
+ vec2 xy = vec2(xy_uint);
+ vec4 rgba[8];
+ for (uint i = 0u; i < 8u; i++)
+ {
+ rgba[i] = vec4(0.0);
+ }
+ uint clip_depth = 0u;
+ float df[8];
+ TileSegRef tile_seg_ref;
+ float area[8];
+ uint base_ix;
+ while (true)
+ {
+ Alloc param_5 = cmd_alloc;
+ CmdRef param_6 = cmd_ref;
+ uint tag = Cmd_tag(param_5, param_6).tag;
+ if (tag == 0u)
+ {
+ break;
+ }
+ switch (tag)
+ {
+ case 2u:
+ {
+ Alloc param_7 = cmd_alloc;
+ CmdRef param_8 = cmd_ref;
+ CmdStroke stroke = Cmd_Stroke_read(param_7, param_8);
+ for (uint k = 0u; k < 8u; k++)
+ {
+ df[k] = 1000000000.0;
+ }
+ tile_seg_ref = TileSegRef(stroke.tile_ref);
+ do
+ {
+ uint param_9 = tile_seg_ref.offset;
+ uint param_10 = 24u;
+ Alloc param_11 = new_alloc(param_9, param_10);
+ TileSegRef param_12 = tile_seg_ref;
+ TileSeg seg = TileSeg_read(param_11, param_12);
+ vec2 line_vec = seg.vector;
+ for (uint k_1 = 0u; k_1 < 8u; k_1++)
+ {
+ vec2 dpos = (xy + vec2(0.5)) - seg.origin;
+ uint param_13 = k_1;
+ dpos += vec2(chunk_offset(param_13));
+ float t = clamp(dot(line_vec, dpos) / dot(line_vec, line_vec), 0.0, 1.0);
+ df[k_1] = min(df[k_1], length((line_vec * t) - dpos));
+ }
+ tile_seg_ref = seg.next;
+ } while (tile_seg_ref.offset != 0u);
+ for (uint k_2 = 0u; k_2 < 8u; k_2++)
+ {
+ area[k_2] = clamp((stroke.half_width + 0.5) - df[k_2], 0.0, 1.0);
+ }
+ cmd_ref.offset += 12u;
+ break;
+ }
+ case 1u:
+ {
+ Alloc param_14 = cmd_alloc;
+ CmdRef param_15 = cmd_ref;
+ CmdFill fill = Cmd_Fill_read(param_14, param_15);
+ for (uint k_3 = 0u; k_3 < 8u; k_3++)
+ {
+ area[k_3] = float(fill.backdrop);
+ }
+ tile_seg_ref = TileSegRef(fill.tile_ref);
+ do
+ {
+ uint param_16 = tile_seg_ref.offset;
+ uint param_17 = 24u;
+ Alloc param_18 = new_alloc(param_16, param_17);
+ TileSegRef param_19 = tile_seg_ref;
+ TileSeg seg_1 = TileSeg_read(param_18, param_19);
+ for (uint k_4 = 0u; k_4 < 8u; k_4++)
+ {
+ uint param_20 = k_4;
+ vec2 my_xy = xy + vec2(chunk_offset(param_20));
+ vec2 start = seg_1.origin - my_xy;
+ vec2 end = start + seg_1.vector;
+ vec2 window = clamp(vec2(start.y, end.y), vec2(0.0), vec2(1.0));
+ if (!(window.x == window.y))
+ {
+ vec2 t_1 = (window - vec2(start.y)) / vec2(seg_1.vector.y);
+ vec2 xs = vec2(mix(start.x, end.x, t_1.x), mix(start.x, end.x, t_1.y));
+ float xmin = min(min(xs.x, xs.y), 1.0) - 9.9999999747524270787835121154785e-07;
+ float xmax = max(xs.x, xs.y);
+ float b = min(xmax, 1.0);
+ float c = max(b, 0.0);
+ float d = max(xmin, 0.0);
+ float a = ((b + (0.5 * ((d * d) - (c * c)))) - xmin) / (xmax - xmin);
+ area[k_4] += (a * (window.x - window.y));
+ }
+ area[k_4] += (sign(seg_1.vector.x) * clamp((my_xy.y - seg_1.y_edge) + 1.0, 0.0, 1.0));
+ }
+ tile_seg_ref = seg_1.next;
+ } while (tile_seg_ref.offset != 0u);
+ for (uint k_5 = 0u; k_5 < 8u; k_5++)
+ {
+ area[k_5] = min(abs(area[k_5]), 1.0);
+ }
+ cmd_ref.offset += 12u;
+ break;
+ }
+ case 3u:
+ {
+ for (uint k_6 = 0u; k_6 < 8u; k_6++)
+ {
+ area[k_6] = 1.0;
+ }
+ cmd_ref.offset += 4u;
+ break;
+ }
+ case 4u:
+ {
+ Alloc param_21 = cmd_alloc;
+ CmdRef param_22 = cmd_ref;
+ CmdAlpha alpha = Cmd_Alpha_read(param_21, param_22);
+ for (uint k_7 = 0u; k_7 < 8u; k_7++)
+ {
+ area[k_7] = alpha.alpha;
+ }
+ cmd_ref.offset += 8u;
+ break;
+ }
+ case 5u:
+ {
+ Alloc param_23 = cmd_alloc;
+ CmdRef param_24 = cmd_ref;
+ CmdColor color = Cmd_Color_read(param_23, param_24);
+ uint param_25 = color.rgba_color;
+ vec4 fg = unpacksRGB(param_25);
+ for (uint k_8 = 0u; k_8 < 8u; k_8++)
+ {
+ vec4 fg_k = fg * area[k_8];
+ rgba[k_8] = (rgba[k_8] * (1.0 - fg_k.w)) + fg_k;
+ }
+ cmd_ref.offset += 8u;
+ break;
+ }
+ case 6u:
+ {
+ Alloc param_26 = cmd_alloc;
+ CmdRef param_27 = cmd_ref;
+ CmdImage fill_img = Cmd_Image_read(param_26, param_27);
+ uvec2 param_28 = xy_uint;
+ CmdImage param_29 = fill_img;
+ vec4 img[8] = fillImage(param_28, param_29);
+ for (uint k_9 = 0u; k_9 < 8u; k_9++)
+ {
+ vec4 fg_k_1 = img[k_9] * area[k_9];
+ rgba[k_9] = (rgba[k_9] * (1.0 - fg_k_1.w)) + fg_k_1;
+ }
+ cmd_ref.offset += 12u;
+ break;
+ }
+ case 7u:
+ {
+ base_ix = (scratch_alloc.offset >> uint(2)) + (2u * ((((clip_depth * 32u) * 32u) + gl_LocalInvocationID.x) + (32u * gl_LocalInvocationID.y)));
+ for (uint k_10 = 0u; k_10 < 8u; k_10++)
+ {
+ uint param_30 = k_10;
+ uvec2 offset = chunk_offset(param_30);
+ vec4 param_31 = vec4(rgba[k_10]);
+ uint _1286 = packsRGB(param_31);
+ uint srgb = _1286;
+ float alpha_1 = clamp(abs(area[k_10]), 0.0, 1.0);
+ Alloc param_32 = scratch_alloc;
+ uint param_33 = (base_ix + 0u) + (2u * (offset.x + (offset.y * 32u)));
+ uint param_34 = srgb;
+ write_mem(param_32, param_33, param_34);
+ Alloc param_35 = scratch_alloc;
+ uint param_36 = (base_ix + 1u) + (2u * (offset.x + (offset.y * 32u)));
+ uint param_37 = floatBitsToUint(alpha_1);
+ write_mem(param_35, param_36, param_37);
+ rgba[k_10] = vec4(0.0);
+ }
+ clip_depth++;
+ cmd_ref.offset += 4u;
+ break;
+ }
+ case 8u:
+ {
+ clip_depth--;
+ base_ix = (scratch_alloc.offset >> uint(2)) + (2u * ((((clip_depth * 32u) * 32u) + gl_LocalInvocationID.x) + (32u * gl_LocalInvocationID.y)));
+ for (uint k_11 = 0u; k_11 < 8u; k_11++)
+ {
+ uint param_38 = k_11;
+ uvec2 offset_1 = chunk_offset(param_38);
+ Alloc param_39 = scratch_alloc;
+ uint param_40 = (base_ix + 0u) + (2u * (offset_1.x + (offset_1.y * 32u)));
+ uint srgb_1 = read_mem(param_39, param_40);
+ Alloc param_41 = scratch_alloc;
+ uint param_42 = (base_ix + 1u) + (2u * (offset_1.x + (offset_1.y * 32u)));
+ uint alpha_2 = read_mem(param_41, param_42);
+ uint param_43 = srgb_1;
+ vec4 bg = unpacksRGB(param_43);
+ vec4 fg_1 = (rgba[k_11] * area[k_11]) * uintBitsToFloat(alpha_2);
+ rgba[k_11] = (bg * (1.0 - fg_1.w)) + fg_1;
+ }
+ cmd_ref.offset += 4u;
+ break;
+ }
+ case 9u:
+ {
+ Alloc param_44 = cmd_alloc;
+ CmdRef param_45 = cmd_ref;
+ cmd_ref = CmdRef(Cmd_Jump_read(param_44, param_45).new_ref);
+ cmd_alloc.offset = cmd_ref.offset;
+ break;
+ }
+ }
+ }
+ for (uint i_1 = 0u; i_1 < 8u; i_1++)
+ {
+ uint param_46 = i_1;
+ vec3 param_47 = rgba[i_1].xyz;
+ imageStore(image, ivec2(xy_uint + chunk_offset(param_46)), vec4(tosRGB(param_47), rgba[i_1].w));
+ }
+}
+
+`,
+ }
+ shader_material_frag = driver.ShaderSources{
+ Name: "material.frag",
+ Inputs: []driver.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}},
+ Textures: []driver.TextureBinding{{Name: "tex", Binding: 0}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+
+varying vec2 vUV;
+
+vec3 RGBtosRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375));
+ vec3 below = vec3(12.9200000762939453125) * rgb;
+ vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875);
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ vec4 texel = texture2D(tex, vUV);
+ vec3 param = texel.xyz;
+ vec3 _59 = RGBtosRGB(param);
+ texel = vec4(_59.x, _59.y, _59.z, texel.w);
+ gl_FragData[0] = texel;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+uniform mediump sampler2D tex;
+
+in vec2 vUV;
+layout(location = 0) out vec4 fragColor;
+
+vec3 RGBtosRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375));
+ vec3 below = vec3(12.9200000762939453125) * rgb;
+ vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875);
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ vec4 texel = texture(tex, vUV);
+ vec3 param = texel.xyz;
+ vec3 _59 = RGBtosRGB(param);
+ texel = vec4(_59.x, _59.y, _59.z, texel.w);
+ fragColor = texel;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+
+in vec2 vUV;
+out vec4 fragColor;
+
+vec3 RGBtosRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375));
+ vec3 below = vec3(12.9200000762939453125) * rgb;
+ vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875);
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ vec4 texel = texture(tex, vUV);
+ vec3 param = texel.xyz;
+ vec3 _59 = RGBtosRGB(param);
+ texel = vec4(_59.x, _59.y, _59.z, texel.w);
+ fragColor = texel;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0) uniform sampler2D tex;
+
+in vec2 vUV;
+out vec4 fragColor;
+
+vec3 RGBtosRGB(vec3 rgb)
+{
+ bvec3 cutoff = greaterThanEqual(rgb, vec3(0.003130800090730190277099609375));
+ vec3 below = vec3(12.9200000762939453125) * rgb;
+ vec3 above = (vec3(1.05499994754791259765625) * pow(rgb, vec3(0.416660010814666748046875))) - vec3(0.054999999701976776123046875);
+ return vec3(cutoff.x ? above.x : below.x, cutoff.y ? above.y : below.y, cutoff.z ? above.z : below.z);
+}
+
+void main()
+{
+ vec4 texel = texture(tex, vUV);
+ vec3 param = texel.xyz;
+ vec3 _59 = RGBtosRGB(param);
+ texel = vec4(_59.x, _59.y, _59.z, texel.w);
+ fragColor = texel;
+}
+
+`,
+ HLSL: "DXBC\x9e\x87LD\xf3\x17\n\x06\\\xb7\x98\x94\xa9PKe\x01\x00\x00\x00\xc8\x04\x00\x00\x06\x00\x00\x008\x00\x00\x00\xbc\x01\x00\x00D\x03\x00\x00\xc0\x03\x00\x00`\x04\x00\x00\x94\x04\x00\x00Aon9|\x01\x00\x00|\x01\x00\x00\x00\x02\xff\xffT\x01\x00\x00(\x00\x00\x00\x00\x00(\x00\x00\x00(\x00\x00\x00(\x00\x01\x00$\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0=\n\x87?\xaeGa\xbd\x00\x00\x00\x00\x00\x00\x00\x00Q\x00\x00\x05\x01\x00\x0f\xa0\x1c.M\xbbR\xb8NAvT\xd5>\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x03\xb0\x1f\x00\x00\x02\x00\x00\x00\x90\x00\b\x0f\xa0B\x00\x00\x03\x00\x00\x0f\x80\x00\x00\xe4\xb0\x00\b\xe4\xa0\x0f\x00\x00\x02\x01\x00\x01\x80\x00\x00\x00\x80\x0f\x00\x00\x02\x01\x00\x02\x80\x00\x00U\x80\x0f\x00\x00\x02\x01\x00\x04\x80\x00\x00\xaa\x80\x05\x00\x00\x03\x01\x00\a\x80\x01\x00\xe4\x80\x01\x00\xaa\xa0\x0e\x00\x00\x02\x02\x00\x01\x80\x01\x00\x00\x80\x0e\x00\x00\x02\x02\x00\x02\x80\x01\x00U\x80\x0e\x00\x00\x02\x02\x00\x04\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\a\x80\x02\x00\xe4\x80\x00\x00\x00\xa0\x00\x00U\xa0\x02\x00\x00\x03\x01\x00\b\x80\x00\x00\x00\x80\x01\x00\x00\xa0\x05\x00\x00\x03\x02\x00\a\x80\x00\x00\xe4\x80\x01\x00U\xa0X\x00\x00\x04\x00\x00\x01\x80\x01\x00\xff\x80\x01\x00\x00\x80\x02\x00\x00\x80\x02\x00\x00\x03\x01\x00\x01\x80\x00\x00U\x80\x01\x00\x00\xa0X\x00\x00\x04\x00\x00\x02\x80\x01\x00\x00\x80\x01\x00U\x80\x02\x00U\x80\x02\x00\x00\x03\x01\x00\x01\x80\x00\x00\xaa\x80\x01\x00\x00\xa0X\x00\x00\x04\x00\x00\x04\x80\x01\x00\x00\x80\x01\x00\xaa\x80\x02\x00\xaa\x80\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDR\x80\x01\x00\x00@\x00\x00\x00`\x00\x00\x00Z\x00\x00\x03\x00`\x10\x00\x00\x00\x00\x00X\x18\x00\x04\x00p\x10\x00\x00\x00\x00\x00UU\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x00E\x00\x00\t\xf2\x00\x10\x00\x00\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x00F~\x10\x00\x00\x00\x00\x00\x00`\x10\x00\x00\x00\x00\x00/\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00vT\xd5>vT\xd5>vT\xd5>\x00\x00\x00\x00\x19\x00\x00\x05r\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x002\x00\x00\x0fr\x00\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00\x02@\x00\x00=\n\x87?=\n\x87?=\n\x87?\x00\x00\x00\x00\x02@\x00\x00\xaeGa\xbd\xaeGa\xbd\xaeGa\xbd\x00\x00\x00\x00\x1d\x00\x00\nr\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x1c.M;\x1c.M;\x1c.M;\x00\x00\x00\x008\x00\x00\nr\x00\x10\x00\x00\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00\x02@\x00\x00R\xb8NAR\xb8NAR\xb8NA\x00\x00\x00\x006\x00\x00\x05\x82 \x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x007\x00\x00\tr \x10\x00\x00\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x01\x00\x00\x00F\x02\x10\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00\n\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00m\x00\x00\x00\\\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00i\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x04\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x01\x00\x00\x00\r\x00\x00\x00_tex_sampler\x00tex\x00Microsoft (R) HLSL Shader Compiler 10.1\x00\xab\xab\xabISGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ }
+ shader_material_vert = driver.ShaderSources{
+ Name: "material.vert",
+ Inputs: []driver.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}},
+ GLSL100ES: `#version 100
+
+varying vec2 vUV;
+attribute vec2 uv;
+attribute vec2 pos;
+
+void main()
+{
+ vUV = uv;
+ gl_Position = vec4(pos, 0.0, 1.0);
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+out vec2 vUV;
+layout(location = 1) in vec2 uv;
+layout(location = 0) in vec2 pos;
+
+void main()
+{
+ vUV = uv;
+ gl_Position = vec4(pos, 0.0, 1.0);
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+out vec2 vUV;
+in vec2 uv;
+in vec2 pos;
+
+void main()
+{
+ vUV = uv;
+ gl_Position = vec4(pos, 0.0, 1.0);
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+out vec2 vUV;
+in vec2 uv;
+in vec2 pos;
+
+void main()
+{
+ vUV = uv;
+ gl_Position = vec4(pos, 0.0, 1.0);
+}
+
+`,
+ HLSL: "DXBCg\xc0\xae\x16\xd8\xe1\xbdl~Å\xf1\xc4\xf6dV\x01\x00\x00\x00\xc4\x02\x00\x00\x06\x00\x00\x008\x00\x00\x00\xc8\x00\x00\x00X\x01\x00\x00\xd4\x01\x00\x00 \x02\x00\x00l\x02\x00\x00Aon9\x88\x00\x00\x00\x88\x00\x00\x00\x00\x02\xfe\xff`\x00\x00\x00(\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x01\x00$\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x01\x00\x0f\xa0\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x90\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\x03\xe0\x01\x00\xe4\x90\x01\x00\x00\x02\x00\x00\f\xc0\x01\x00D\xa0\xff\xff\x00\x00SHDR\x88\x00\x00\x00@\x00\x01\x00\"\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x01\x00\x00\x00\x01\x00\x00\x006\x00\x00\x052 \x10\x00\x00\x00\x00\x00F\x10\x10\x00\x01\x00\x00\x006\x00\x00\x052 \x10\x00\x01\x00\x00\x00F\x10\x10\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x01\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGND\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x008\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGNP\x00\x00\x00\x02\x00\x00\x00\b\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab",
+ }
+ shader_path_coarse_comp = driver.ShaderSources{
+ Name: "path_coarse.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct MallocResult
+{
+ Alloc alloc;
+ bool failed;
+};
+
+struct PathCubicRef
+{
+ uint offset;
+};
+
+struct PathCubic
+{
+ vec2 p0;
+ vec2 p1;
+ vec2 p2;
+ vec2 p3;
+ uint path_ix;
+ uint trans_ix;
+ vec2 stroke;
+};
+
+struct PathSegRef
+{
+ uint offset;
+};
+
+struct PathSegTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct TileRef
+{
+ uint offset;
+};
+
+struct PathRef
+{
+ uint offset;
+};
+
+struct Path
+{
+ uvec4 bbox;
+ TileRef tiles;
+};
+
+struct TileSegRef
+{
+ uint offset;
+};
+
+struct TileSeg
+{
+ vec2 origin;
+ vec2 vector;
+ float y_edge;
+ TileSegRef next;
+};
+
+struct TransformSegRef
+{
+ uint offset;
+};
+
+struct TransformSeg
+{
+ vec4 mat;
+ vec2 translate;
+};
+
+struct SubdivResult
+{
+ float val;
+ float a0;
+ float a2;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _149;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _788;
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+uint read_mem(Alloc alloc, uint offset)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return 0u;
+ }
+ uint v = _149.memory[offset];
+ return v;
+}
+
+PathSegTag PathSeg_tag(Alloc a, PathSegRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint tag_and_flags = read_mem(param, param_1);
+ return PathSegTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+PathCubic PathCubic_read(Alloc a, PathCubicRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ Alloc param_8 = a;
+ uint param_9 = ix + 4u;
+ uint raw4 = read_mem(param_8, param_9);
+ Alloc param_10 = a;
+ uint param_11 = ix + 5u;
+ uint raw5 = read_mem(param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 6u;
+ uint raw6 = read_mem(param_12, param_13);
+ Alloc param_14 = a;
+ uint param_15 = ix + 7u;
+ uint raw7 = read_mem(param_14, param_15);
+ Alloc param_16 = a;
+ uint param_17 = ix + 8u;
+ uint raw8 = read_mem(param_16, param_17);
+ Alloc param_18 = a;
+ uint param_19 = ix + 9u;
+ uint raw9 = read_mem(param_18, param_19);
+ Alloc param_20 = a;
+ uint param_21 = ix + 10u;
+ uint raw10 = read_mem(param_20, param_21);
+ Alloc param_22 = a;
+ uint param_23 = ix + 11u;
+ uint raw11 = read_mem(param_22, param_23);
+ PathCubic s;
+ s.p0 = vec2(uintBitsToFloat(raw0), uintBitsToFloat(raw1));
+ s.p1 = vec2(uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.p2 = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5));
+ s.p3 = vec2(uintBitsToFloat(raw6), uintBitsToFloat(raw7));
+ s.path_ix = raw8;
+ s.trans_ix = raw9;
+ s.stroke = vec2(uintBitsToFloat(raw10), uintBitsToFloat(raw11));
+ return s;
+}
+
+PathCubic PathSeg_Cubic_read(Alloc a, PathSegRef ref)
+{
+ Alloc param = a;
+ PathCubicRef param_1 = PathCubicRef(ref.offset + 4u);
+ return PathCubic_read(param, param_1);
+}
+
+TransformSeg TransformSeg_read(Alloc a, TransformSegRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ Alloc param_8 = a;
+ uint param_9 = ix + 4u;
+ uint raw4 = read_mem(param_8, param_9);
+ Alloc param_10 = a;
+ uint param_11 = ix + 5u;
+ uint raw5 = read_mem(param_10, param_11);
+ TransformSeg s;
+ s.mat = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ s.translate = vec2(uintBitsToFloat(raw4), uintBitsToFloat(raw5));
+ return s;
+}
+
+vec2 eval_cubic(vec2 p0, vec2 p1, vec2 p2, vec2 p3, float t)
+{
+ float mt = 1.0 - t;
+ return (p0 * ((mt * mt) * mt)) + (((p1 * ((mt * mt) * 3.0)) + (((p2 * (mt * 3.0)) + (p3 * t)) * t)) * t);
+}
+
+float approx_parabola_integral(float x)
+{
+ return x * inversesqrt(sqrt(0.3300000131130218505859375 + (0.201511204242706298828125 + ((0.25 * x) * x))));
+}
+
+SubdivResult estimate_subdiv(vec2 p0, vec2 p1, vec2 p2, float sqrt_tol)
+{
+ vec2 d01 = p1 - p0;
+ vec2 d12 = p2 - p1;
+ vec2 dd = d01 - d12;
+ float _cross = ((p2.x - p0.x) * dd.y) - ((p2.y - p0.y) * dd.x);
+ float x0 = ((d01.x * dd.x) + (d01.y * dd.y)) / _cross;
+ float x2 = ((d12.x * dd.x) + (d12.y * dd.y)) / _cross;
+ float scale = abs(_cross / (length(dd) * (x2 - x0)));
+ float param = x0;
+ float a0 = approx_parabola_integral(param);
+ float param_1 = x2;
+ float a2 = approx_parabola_integral(param_1);
+ float val = 0.0;
+ if (scale < 1000000000.0)
+ {
+ float da = abs(a2 - a0);
+ float sqrt_scale = sqrt(scale);
+ if (sign(x0) == sign(x2))
+ {
+ val = da * sqrt_scale;
+ }
+ else
+ {
+ float xmin = sqrt_tol / sqrt_scale;
+ float param_2 = xmin;
+ val = (sqrt_tol * da) / approx_parabola_integral(param_2);
+ }
+ }
+ return SubdivResult(val, a0, a2);
+}
+
+uint fill_mode_from_flags(uint flags)
+{
+ return flags & 1u;
+}
+
+Path Path_read(Alloc a, PathRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Path s;
+ s.bbox = uvec4(raw0 & 65535u, raw0 >> uint(16), raw1 & 65535u, raw1 >> uint(16));
+ s.tiles = TileRef(raw2);
+ return s;
+}
+
+Alloc new_alloc(uint offset, uint size)
+{
+ Alloc a;
+ a.offset = offset;
+ return a;
+}
+
+float approx_parabola_inv_integral(float x)
+{
+ return x * sqrt(0.61000001430511474609375 + (0.1520999968051910400390625 + ((0.25 * x) * x)));
+}
+
+vec2 eval_quad(vec2 p0, vec2 p1, vec2 p2, float t)
+{
+ float mt = 1.0 - t;
+ return (p0 * (mt * mt)) + (((p1 * (mt * 2.0)) + (p2 * t)) * t);
+}
+
+MallocResult malloc(uint size)
+{
+ MallocResult r;
+ r.failed = false;
+ uint _155 = atomicAdd(_149.mem_offset, size);
+ uint offset = _155;
+ uint param = offset;
+ uint param_1 = size;
+ r.alloc = new_alloc(param, param_1);
+ if ((offset + size) > uint(int(uint(_149.memory.length())) * 4))
+ {
+ r.failed = true;
+ uint _176 = atomicMax(_149.mem_error, 1u);
+ return r;
+ }
+ return r;
+}
+
+TileRef Tile_index(TileRef ref, uint index)
+{
+ return TileRef(ref.offset + (index * 8u));
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _149.memory[offset] = val;
+}
+
+void TileSeg_write(Alloc a, TileSegRef ref, TileSeg s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = floatBitsToUint(s.origin.x);
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = floatBitsToUint(s.origin.y);
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = floatBitsToUint(s.vector.x);
+ write_mem(param_6, param_7, param_8);
+ Alloc param_9 = a;
+ uint param_10 = ix + 3u;
+ uint param_11 = floatBitsToUint(s.vector.y);
+ write_mem(param_9, param_10, param_11);
+ Alloc param_12 = a;
+ uint param_13 = ix + 4u;
+ uint param_14 = floatBitsToUint(s.y_edge);
+ write_mem(param_12, param_13, param_14);
+ Alloc param_15 = a;
+ uint param_16 = ix + 5u;
+ uint param_17 = s.next.offset;
+ write_mem(param_15, param_16, param_17);
+}
+
+void main()
+{
+ if (_149.mem_error != 0u)
+ {
+ return;
+ }
+ uint element_ix = gl_GlobalInvocationID.x;
+ PathSegRef ref = PathSegRef(_788.conf.pathseg_alloc.offset + (element_ix * 52u));
+ PathSegTag tag = PathSegTag(0u, 0u);
+ if (element_ix < _788.conf.n_pathseg)
+ {
+ Alloc param;
+ param.offset = _788.conf.pathseg_alloc.offset;
+ PathSegRef param_1 = ref;
+ tag = PathSeg_tag(param, param_1);
+ }
+ switch (tag.tag)
+ {
+ case 1u:
+ {
+ Alloc param_2;
+ param_2.offset = _788.conf.pathseg_alloc.offset;
+ PathSegRef param_3 = ref;
+ PathCubic cubic = PathSeg_Cubic_read(param_2, param_3);
+ uint trans_ix = cubic.trans_ix;
+ if (trans_ix > 0u)
+ {
+ TransformSegRef trans_ref = TransformSegRef(_788.conf.trans_alloc.offset + ((trans_ix - 1u) * 24u));
+ Alloc param_4;
+ param_4.offset = _788.conf.trans_alloc.offset;
+ TransformSegRef param_5 = trans_ref;
+ TransformSeg trans = TransformSeg_read(param_4, param_5);
+ cubic.p0 = ((trans.mat.xy * cubic.p0.x) + (trans.mat.zw * cubic.p0.y)) + trans.translate;
+ cubic.p1 = ((trans.mat.xy * cubic.p1.x) + (trans.mat.zw * cubic.p1.y)) + trans.translate;
+ cubic.p2 = ((trans.mat.xy * cubic.p2.x) + (trans.mat.zw * cubic.p2.y)) + trans.translate;
+ cubic.p3 = ((trans.mat.xy * cubic.p3.x) + (trans.mat.zw * cubic.p3.y)) + trans.translate;
+ }
+ vec2 err_v = (((cubic.p2 - cubic.p1) * 3.0) + cubic.p0) - cubic.p3;
+ float err = (err_v.x * err_v.x) + (err_v.y * err_v.y);
+ uint n_quads = max(uint(ceil(pow(err * 3.7037036418914794921875, 0.16666667163372039794921875))), 1u);
+ float val = 0.0;
+ vec2 qp0 = cubic.p0;
+ float _step = 1.0 / float(n_quads);
+ for (uint i = 0u; i < n_quads; i++)
+ {
+ float t = float(i + 1u) * _step;
+ vec2 param_6 = cubic.p0;
+ vec2 param_7 = cubic.p1;
+ vec2 param_8 = cubic.p2;
+ vec2 param_9 = cubic.p3;
+ float param_10 = t;
+ vec2 qp2 = eval_cubic(param_6, param_7, param_8, param_9, param_10);
+ vec2 param_11 = cubic.p0;
+ vec2 param_12 = cubic.p1;
+ vec2 param_13 = cubic.p2;
+ vec2 param_14 = cubic.p3;
+ float param_15 = t - (0.5 * _step);
+ vec2 qp1 = eval_cubic(param_11, param_12, param_13, param_14, param_15);
+ qp1 = (qp1 * 2.0) - ((qp0 + qp2) * 0.5);
+ vec2 param_16 = qp0;
+ vec2 param_17 = qp1;
+ vec2 param_18 = qp2;
+ float param_19 = 0.4743416607379913330078125;
+ SubdivResult params = estimate_subdiv(param_16, param_17, param_18, param_19);
+ val += params.val;
+ qp0 = qp2;
+ }
+ uint n = max(uint(ceil((val * 0.5) / 0.4743416607379913330078125)), 1u);
+ uint param_20 = tag.flags;
+ bool is_stroke = fill_mode_from_flags(param_20) == 1u;
+ uint path_ix = cubic.path_ix;
+ Alloc param_21;
+ param_21.offset = _788.conf.tile_alloc.offset;
+ PathRef param_22 = PathRef(_788.conf.tile_alloc.offset + (path_ix * 12u));
+ Path path = Path_read(param_21, param_22);
+ uint param_23 = path.tiles.offset;
+ uint param_24 = ((path.bbox.z - path.bbox.x) * (path.bbox.w - path.bbox.y)) * 8u;
+ Alloc path_alloc = new_alloc(param_23, param_24);
+ ivec4 bbox = ivec4(path.bbox);
+ vec2 p0 = cubic.p0;
+ qp0 = cubic.p0;
+ float v_step = val / float(n);
+ int n_out = 1;
+ float val_sum = 0.0;
+ vec2 p1;
+ float _1309;
+ TileSeg tile_seg;
+ for (uint i_1 = 0u; i_1 < n_quads; i_1++)
+ {
+ float t_1 = float(i_1 + 1u) * _step;
+ vec2 param_25 = cubic.p0;
+ vec2 param_26 = cubic.p1;
+ vec2 param_27 = cubic.p2;
+ vec2 param_28 = cubic.p3;
+ float param_29 = t_1;
+ vec2 qp2_1 = eval_cubic(param_25, param_26, param_27, param_28, param_29);
+ vec2 param_30 = cubic.p0;
+ vec2 param_31 = cubic.p1;
+ vec2 param_32 = cubic.p2;
+ vec2 param_33 = cubic.p3;
+ float param_34 = t_1 - (0.5 * _step);
+ vec2 qp1_1 = eval_cubic(param_30, param_31, param_32, param_33, param_34);
+ qp1_1 = (qp1_1 * 2.0) - ((qp0 + qp2_1) * 0.5);
+ vec2 param_35 = qp0;
+ vec2 param_36 = qp1_1;
+ vec2 param_37 = qp2_1;
+ float param_38 = 0.4743416607379913330078125;
+ SubdivResult params_1 = estimate_subdiv(param_35, param_36, param_37, param_38);
+ float param_39 = params_1.a0;
+ float u0 = approx_parabola_inv_integral(param_39);
+ float param_40 = params_1.a2;
+ float u2 = approx_parabola_inv_integral(param_40);
+ float uscale = 1.0 / (u2 - u0);
+ float target = float(n_out) * v_step;
+ for (;;)
+ {
+ bool _1202 = uint(n_out) == n;
+ bool _1212;
+ if (!_1202)
+ {
+ _1212 = target < (val_sum + params_1.val);
+ }
+ else
+ {
+ _1212 = _1202;
+ }
+ if (_1212)
+ {
+ if (uint(n_out) == n)
+ {
+ p1 = cubic.p3;
+ }
+ else
+ {
+ float u = (target - val_sum) / params_1.val;
+ float a = mix(params_1.a0, params_1.a2, u);
+ float param_41 = a;
+ float au = approx_parabola_inv_integral(param_41);
+ float t_2 = (au - u0) * uscale;
+ vec2 param_42 = qp0;
+ vec2 param_43 = qp1_1;
+ vec2 param_44 = qp2_1;
+ float param_45 = t_2;
+ p1 = eval_quad(param_42, param_43, param_44, param_45);
+ }
+ float xmin = min(p0.x, p1.x) - cubic.stroke.x;
+ float xmax = max(p0.x, p1.x) + cubic.stroke.x;
+ float ymin = min(p0.y, p1.y) - cubic.stroke.y;
+ float ymax = max(p0.y, p1.y) + cubic.stroke.y;
+ float dx = p1.x - p0.x;
+ float dy = p1.y - p0.y;
+ if (abs(dy) < 9.999999717180685365747194737196e-10)
+ {
+ _1309 = 1000000000.0;
+ }
+ else
+ {
+ _1309 = dx / dy;
+ }
+ float invslope = _1309;
+ float c = (cubic.stroke.x + (abs(invslope) * (16.0 + cubic.stroke.y))) * 0.03125;
+ float b = invslope;
+ float a_1 = (p0.x - ((p0.y - 16.0) * b)) * 0.03125;
+ int x0 = int(floor(xmin * 0.03125));
+ int x1 = int(floor(xmax * 0.03125) + 1.0);
+ int y0 = int(floor(ymin * 0.03125));
+ int y1 = int(floor(ymax * 0.03125) + 1.0);
+ x0 = clamp(x0, bbox.x, bbox.z);
+ y0 = clamp(y0, bbox.y, bbox.w);
+ x1 = clamp(x1, bbox.x, bbox.z);
+ y1 = clamp(y1, bbox.y, bbox.w);
+ float xc = a_1 + (b * float(y0));
+ int stride = bbox.z - bbox.x;
+ int base = ((y0 - bbox.y) * stride) - bbox.x;
+ uint n_tile_alloc = uint((x1 - x0) * (y1 - y0));
+ uint param_46 = n_tile_alloc * 24u;
+ MallocResult _1424 = malloc(param_46);
+ MallocResult tile_alloc = _1424;
+ if (tile_alloc.failed)
+ {
+ return;
+ }
+ uint tile_offset = tile_alloc.alloc.offset;
+ int xray = int(floor(p0.x * 0.03125));
+ int last_xray = int(floor(p1.x * 0.03125));
+ if (p0.y > p1.y)
+ {
+ int tmp = xray;
+ xray = last_xray;
+ last_xray = tmp;
+ }
+ for (int y = y0; y < y1; y++)
+ {
+ float tile_y0 = float(y * 32);
+ int xbackdrop = max((xray + 1), bbox.x);
+ bool _1478 = !is_stroke;
+ bool _1488;
+ if (_1478)
+ {
+ _1488 = min(p0.y, p1.y) < tile_y0;
+ }
+ else
+ {
+ _1488 = _1478;
+ }
+ bool _1495;
+ if (_1488)
+ {
+ _1495 = xbackdrop < bbox.z;
+ }
+ else
+ {
+ _1495 = _1488;
+ }
+ if (_1495)
+ {
+ int backdrop = (p1.y < p0.y) ? 1 : (-1);
+ TileRef param_47 = path.tiles;
+ uint param_48 = uint(base + xbackdrop);
+ TileRef tile_ref = Tile_index(param_47, param_48);
+ uint tile_el = tile_ref.offset >> uint(2);
+ Alloc param_49 = path_alloc;
+ uint param_50 = tile_el + 1u;
+ if (touch_mem(param_49, param_50))
+ {
+ uint _1533 = atomicAdd(_149.memory[tile_el + 1u], uint(backdrop));
+ }
+ }
+ int next_xray = last_xray;
+ if (y < (y1 - 1))
+ {
+ float tile_y1 = float((y + 1) * 32);
+ float x_edge = mix(p0.x, p1.x, (tile_y1 - p0.y) / dy);
+ next_xray = int(floor(x_edge * 0.03125));
+ }
+ int min_xray = min(xray, next_xray);
+ int max_xray = max(xray, next_xray);
+ int xx0 = min(int(floor(xc - c)), min_xray);
+ int xx1 = max(int(ceil(xc + c)), (max_xray + 1));
+ xx0 = clamp(xx0, x0, x1);
+ xx1 = clamp(xx1, x0, x1);
+ for (int x = xx0; x < xx1; x++)
+ {
+ float tile_x0 = float(x * 32);
+ TileRef param_51 = TileRef(path.tiles.offset);
+ uint param_52 = uint(base + x);
+ TileRef tile_ref_1 = Tile_index(param_51, param_52);
+ uint tile_el_1 = tile_ref_1.offset >> uint(2);
+ uint old = 0u;
+ Alloc param_53 = path_alloc;
+ uint param_54 = tile_el_1;
+ if (touch_mem(param_53, param_54))
+ {
+ uint _1636 = atomicExchange(_149.memory[tile_el_1], tile_offset);
+ old = _1636;
+ }
+ tile_seg.origin = p0;
+ tile_seg.vector = p1 - p0;
+ float y_edge = 0.0;
+ if (!is_stroke)
+ {
+ y_edge = mix(p0.y, p1.y, (tile_x0 - p0.x) / dx);
+ if (min(p0.x, p1.x) < tile_x0)
+ {
+ vec2 p = vec2(tile_x0, y_edge);
+ if (p0.x > p1.x)
+ {
+ tile_seg.vector = p - p0;
+ }
+ else
+ {
+ tile_seg.origin = p;
+ tile_seg.vector = p1 - p;
+ }
+ if (tile_seg.vector.x == 0.0)
+ {
+ tile_seg.vector.x = sign(p1.x - p0.x) * 9.999999717180685365747194737196e-10;
+ }
+ }
+ if ((x <= min_xray) || (max_xray < x))
+ {
+ y_edge = 1000000000.0;
+ }
+ }
+ tile_seg.y_edge = y_edge;
+ tile_seg.next.offset = old;
+ Alloc param_55 = tile_alloc.alloc;
+ TileSegRef param_56 = TileSegRef(tile_offset);
+ TileSeg param_57 = tile_seg;
+ TileSeg_write(param_55, param_56, param_57);
+ tile_offset += 24u;
+ }
+ xc += b;
+ base += stride;
+ xray = next_xray;
+ }
+ n_out++;
+ target += v_step;
+ p0 = p1;
+ continue;
+ }
+ else
+ {
+ break;
+ }
+ }
+ val_sum += params_1.val;
+ qp0 = qp2_1;
+ }
+ break;
+ }
+ }
+}
+
+`,
+ }
+ shader_stencil_frag = driver.ShaderSources{
+ Name: "stencil.frag",
+ Inputs: []driver.InputLocation{{Name: "vFrom", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vCtrl", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}, {Name: "vTo", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}},
+ GLSL100ES: `#version 100
+precision mediump float;
+precision highp int;
+
+varying vec2 vTo;
+varying vec2 vFrom;
+varying vec2 vCtrl;
+
+void main()
+{
+ float dx = vTo.x - vFrom.x;
+ bool increasing = vTo.x >= vFrom.x;
+ bvec2 _35 = bvec2(increasing);
+ vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y);
+ bvec2 _41 = bvec2(increasing);
+ vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y);
+ vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5));
+ float midx = mix(extent.x, extent.y, 0.5);
+ float x0 = midx - left.x;
+ vec2 p1 = vCtrl - left;
+ vec2 v = right - vCtrl;
+ float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0)));
+ float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t);
+ vec2 d_half = mix(p1, v, vec2(t));
+ float dy = d_half.y / d_half.x;
+ float width = extent.y - extent.x;
+ dy = abs(dy * width);
+ vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy);
+ sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0));
+ float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w));
+ area *= width;
+ if (width == 0.0)
+ {
+ area = 0.0;
+ }
+ gl_FragData[0].x = area;
+}
+
+`,
+ GLSL300ES: `#version 300 es
+precision mediump float;
+precision highp int;
+
+in vec2 vTo;
+in vec2 vFrom;
+in vec2 vCtrl;
+layout(location = 0) out vec4 fragCover;
+
+void main()
+{
+ float dx = vTo.x - vFrom.x;
+ bool increasing = vTo.x >= vFrom.x;
+ bvec2 _35 = bvec2(increasing);
+ vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y);
+ bvec2 _41 = bvec2(increasing);
+ vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y);
+ vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5));
+ float midx = mix(extent.x, extent.y, 0.5);
+ float x0 = midx - left.x;
+ vec2 p1 = vCtrl - left;
+ vec2 v = right - vCtrl;
+ float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0)));
+ float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t);
+ vec2 d_half = mix(p1, v, vec2(t));
+ float dy = d_half.y / d_half.x;
+ float width = extent.y - extent.x;
+ dy = abs(dy * width);
+ vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy);
+ sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0));
+ float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w));
+ area *= width;
+ if (width == 0.0)
+ {
+ area = 0.0;
+ }
+ fragCover.x = area;
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+in vec2 vTo;
+in vec2 vFrom;
+in vec2 vCtrl;
+out vec4 fragCover;
+
+void main()
+{
+ float dx = vTo.x - vFrom.x;
+ bool increasing = vTo.x >= vFrom.x;
+ bvec2 _35 = bvec2(increasing);
+ vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y);
+ bvec2 _41 = bvec2(increasing);
+ vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y);
+ vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5));
+ float midx = mix(extent.x, extent.y, 0.5);
+ float x0 = midx - left.x;
+ vec2 p1 = vCtrl - left;
+ vec2 v = right - vCtrl;
+ float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0)));
+ float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t);
+ vec2 d_half = mix(p1, v, vec2(t));
+ float dy = d_half.y / d_half.x;
+ float width = extent.y - extent.x;
+ dy = abs(dy * width);
+ vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy);
+ sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0));
+ float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w));
+ area *= width;
+ if (width == 0.0)
+ {
+ area = 0.0;
+ }
+ fragCover.x = area;
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+in vec2 vTo;
+in vec2 vFrom;
+in vec2 vCtrl;
+out vec4 fragCover;
+
+void main()
+{
+ float dx = vTo.x - vFrom.x;
+ bool increasing = vTo.x >= vFrom.x;
+ bvec2 _35 = bvec2(increasing);
+ vec2 left = vec2(_35.x ? vFrom.x : vTo.x, _35.y ? vFrom.y : vTo.y);
+ bvec2 _41 = bvec2(increasing);
+ vec2 right = vec2(_41.x ? vTo.x : vFrom.x, _41.y ? vTo.y : vFrom.y);
+ vec2 extent = clamp(vec2(vFrom.x, vTo.x), vec2(-0.5), vec2(0.5));
+ float midx = mix(extent.x, extent.y, 0.5);
+ float x0 = midx - left.x;
+ vec2 p1 = vCtrl - left;
+ vec2 v = right - vCtrl;
+ float t = x0 / (p1.x + sqrt((p1.x * p1.x) + ((v.x - p1.x) * x0)));
+ float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t);
+ vec2 d_half = mix(p1, v, vec2(t));
+ float dy = d_half.y / d_half.x;
+ float width = extent.y - extent.x;
+ dy = abs(dy * width);
+ vec4 sides = vec4((dy * 0.5) + y, (dy * (-0.5)) + y, (0.5 - y) / dy, ((-0.5) - y) / dy);
+ sides = clamp(sides + vec4(0.5), vec4(0.0), vec4(1.0));
+ float area = 0.5 * ((((sides.z - (sides.z * sides.y)) + 1.0) - sides.x) + (sides.x * sides.w));
+ area *= width;
+ if (width == 0.0)
+ {
+ area = 0.0;
+ }
+ fragCover.x = area;
+}
+
+`,
+ HLSL: "DXBC\x94!\xb9\x13L\xba\r\x11\x8f\xc7\xce\x0eAs\xec\xe1\x01\x00\x00\x00\\\n\x00\x00\x06\x00\x00\x008\x00\x00\x00\x9c\x03\x00\x00\xfc\b\x00\x00x\t\x00\x00\xc4\t\x00\x00(\n\x00\x00Aon9\\\x03\x00\x00\\\x03\x00\x00\x00\x02\xff\xff8\x03\x00\x00$\x00\x00\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x00$\x00\x00\x02\xff\xffQ\x00\x00\x05\x00\x00\x0f\xa0\x00\x00\x00\xbf\x00\x00\x00?\x00\x00\x80?\x00\x00\x00\x00\x1f\x00\x00\x02\x00\x00\x00\x80\x00\x00\x0f\xb0\x1f\x00\x00\x02\x00\x00\x00\x80\x01\x00\x03\xb0\v\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\xb0\x00\x00\x00\xa0\v\x00\x00\x03\x00\x00\x02\x80\x01\x00\x00\xb0\x00\x00\x00\xa0\n\x00\x00\x03\x01\x00\x03\x80\x00\x00\xe4\x80\x00\x00U\xa0\x02\x00\x00\x03\x00\x00\x01\x80\x01\x00\x00\x81\x01\x00U\x80\x04\x00\x00\x04\x00\x00\x02\x80\x00\x00\x00\x80\x00\x00U\xa0\x01\x00\x00\x80\x01\x00\x00\x02\x01\x00\x03\x80\x00\x00\xe4\xb0\n\x00\x00\x03\x02\x00\x01\x80\x01\x00\x00\x80\x01\x00\x00\xb0\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x02\x00\x00\x81\v\x00\x00\x03\x03\x00\x01\x80\x01\x00\x00\xb0\x01\x00\x00\x80\x02\x00\x00\x03\x00\x00\x04\x80\x01\x00\x00\x81\x01\x00\x00\xb0X\x00\x00\x04\x03\x00\x02\x80\x00\x00\xaa\x80\x01\x00U\xb0\x01\x00U\x80X\x00\x00\x04\x02\x00\x02\x80\x00\x00\xaa\x80\x01\x00U\x80\x01\x00U\xb0\x02\x00\x00\x03\x00\x00\f\x80\x03\x00\x1b\x80\x00\x00\xe4\xb1\x02\x00\x00\x03\x01\x00\x03\x80\x02\x00\xe4\x81\x00\x00\x1b\xb0\x02\x00\x00\x03\x01\x00\x04\x80\x00\x00\xff\x80\x01\x00\x00\x81\x05\x00\x00\x03\x01\x00\x04\x80\x00\x00U\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x04\x80\x01\x00\x00\x80\x01\x00\x00\x80\x01\x00\xaa\x80\a\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x06\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x02\x00\x00\x03\x01\x00\x04\x80\x01\x00\xaa\x80\x01\x00\x00\x80\x06\x00\x00\x02\x01\x00\x04\x80\x01\x00\xaa\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x01\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x04\x80\x00\x00U\x80\x01\x00U\x80\x02\x00U\x80\x12\x00\x00\x04\x02\x00\x03\x80\x00\x00U\x80\x00\x00\x1b\x80\x01\x00\xe4\x80\x04\x00\x00\x04\x00\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x00\x00\xaa\xb0\x12\x00\x00\x04\x02\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x01\x00\xaa\x80\x06\x00\x00\x02\x00\x00\x02\x80\x02\x00\x00\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x02\x00U\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00\x00\x80\x00\x00U\x80#\x00\x00\x02\x00\x00\x02\x80\x00\x00U\x80\x04\x00\x00\x04\x01\x00\x01\x80\x00\x00U\x80\x00\x00U\xa0\x02\x00\xaa\x80\x04\x00\x00\x04\x01\x00\x02\x80\x00\x00U\x80\x00\x00\x00\xa0\x02\x00\xaa\x80\x06\x00\x00\x02\x00\x00\x02\x80\x00\x00U\x80\x02\x00\x00\x03\x00\x00\f\x80\x02\x00\xaa\x81\x00\x00\x1b\xa0\x05\x00\x00\x03\x01\x00\b\x80\x00\x00U\x80\x00\x00\xff\x80\x05\x00\x00\x03\x01\x00\x04\x80\x00\x00U\x80\x00\x00\xaa\x80\x02\x00\x00\x03\x01\x00\x1f\x80\x01\x00\xe4\x80\x00\x00U\xa0\x04\x00\x00\x04\x00\x00\x02\x80\x01\x00\xaa\x80\x01\x00U\x81\x01\x00\xaa\x80\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\xaa\xa0\x02\x00\x00\x03\x00\x00\x02\x80\x01\x00\x00\x81\x00\x00U\x80\x04\x00\x00\x04\x00\x00\x02\x80\x01\x00\x00\x80\x01\x00\xff\x80\x00\x00U\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\x80\x00\x00\x00\x80\x05\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x00\x00U\xa0X\x00\x00\x04\x00\x00\x01\x80\x00\x00\x00\x81\x00\x00\xff\xa0\x00\x00U\x80\x01\x00\x00\x02\x00\x00\x0e\x80\x00\x00\xff\xa0\x01\x00\x00\x02\x00\b\x0f\x80\x00\x00\xe4\x80\xff\xff\x00\x00SHDRX\x05\x00\x00@\x00\x00\x00V\x01\x00\x00b\x10\x00\x032\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x03\xc2\x10\x10\x00\x00\x00\x00\x00b\x10\x00\x032\x10\x10\x00\x01\x00\x00\x00e\x00\x00\x03\xf2 \x10\x00\x00\x00\x00\x00h\x00\x00\x02\x03\x00\x00\x006\x00\x00\x05\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x006\x00\x00\x05\"\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x004\x00\x00\n2\x00\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x003\x00\x00\n2\x00\x10\x00\x00\x00\x00\x00F\x00\x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\"\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00?\n\x00\x10\x00\x00\x00\x00\x003\x00\x00\a2\x00\x10\x00\x01\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x01\x00\x00\x00\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x004\x00\x00\a2\x00\x10\x00\x02\x00\x00\x00\x06\x10\x10\x00\x00\x00\x00\x00\x06\x10\x10\x00\x01\x00\x00\x00\x1d\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x007\x00\x00\tB\x00\x10\x00\x02\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x01\x00\x00\x00\x1a\x10\x10\x00\x00\x00\x00\x007\x00\x00\tB\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x00\x00\x00\x00\x1a\x10\x10\x00\x01\x00\x00\x00\x00\x00\x00\br\x00\x10\x00\x02\x00\x00\x00F\x02\x10\x00\x02\x00\x00\x00\xa6\x1b\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x00\x00\x00\x00V\t\x10\x80A\x00\x00\x00\x01\x00\x00\x00\xa6\x1e\x10\x00\x00\x00\x00\x00\x00\x00\x00\b\xb2\x00\x10\x00\x01\x00\x00\x00\xa6\x0e\x10\x80A\x00\x00\x00\x00\x00\x00\x00F\b\x10\x00\x02\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00K\x00\x00\x05\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\a\x12\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x0e\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x01\x00\x00\x002\x00\x00\t\xc2\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00V\r\x10\x00\x01\x00\x00\x00\xa6\x0e\x10\x00\x00\x00\x00\x00\x0e\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x008\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x82\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x02\x00\x00\x00:\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\b\x82\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\v2\x00\x10\x00\x01\x00\x00\x00\x06\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x002\x00\x00\r2\x00\x10\x00\x02\x00\x00\x00\xa6\n\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00\x0e\x00\x00\b\xc2\x00\x10\x00\x02\x00\x00\x00\x06\x04\x10\x00\x01\x00\x00\x00\xa6\n\x10\x80\x81\x00\x00\x00\x00\x00\x00\x00\x00 \x00\n\xf2\x00\x10\x00\x01\x00\x00\x00F\x0e\x10\x00\x02\x00\x00\x00\x02@\x00\x00\x00\x00\x00?\x00\x00\x00?\x00\x00\x00?\x00\x00\x00?2\x00\x00\n\x12\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00*\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x80A\x00\x00\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x002\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x01\x00\x00\x00:\x00\x10\x00\x01\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x18\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x008\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00?7\x00\x00\t\x12 \x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x006\x00\x00\b\xe2 \x10\x00\x00\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x01STATt\x00\x00\x00)\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xff\xff\x00\x01\x00\x00\x1c\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\\\x00\x00\x00\x03\x00\x00\x00\b\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x03\x00\x00P\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\f\x00\x00P\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN,\x00\x00\x00\x01\x00\x00\x00\b\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00SV_Target\x00\xab\xab",
+ }
+ shader_stencil_vert = driver.ShaderSources{
+ Name: "stencil.vert",
+ Inputs: []driver.InputLocation{{Name: "corner", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 1}, {Name: "maxy", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 1}, {Name: "from", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}, {Name: "ctrl", Location: 3, Semantic: "TEXCOORD", SemanticIndex: 3, Type: 0x0, Size: 2}, {Name: "to", Location: 4, Semantic: "TEXCOORD", SemanticIndex: 4, Type: 0x0, Size: 2}},
+ Uniforms: driver.UniformsReflection{
+ Blocks: []driver.UniformBlock{{Name: "Block", Binding: 0}},
+ Locations: []driver.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.pathOffset", Type: 0x0, Size: 2, Offset: 16}},
+ Size: 24,
+ },
+ GLSL100ES: `#version 100
+
+struct Block
+{
+ vec4 transform;
+ vec2 pathOffset;
+};
+
+uniform Block _block;
+
+attribute vec2 from;
+attribute vec2 ctrl;
+attribute vec2 to;
+attribute float maxy;
+attribute float corner;
+varying vec2 vFrom;
+varying vec2 vCtrl;
+varying vec2 vTo;
+
+void main()
+{
+ vec2 from_1 = from + _block.pathOffset;
+ vec2 ctrl_1 = ctrl + _block.pathOffset;
+ vec2 to_1 = to + _block.pathOffset;
+ float maxy_1 = maxy + _block.pathOffset.y;
+ float c = corner;
+ vec2 pos;
+ if (c >= 0.375)
+ {
+ c -= 0.5;
+ pos.y = maxy_1 + 1.0;
+ }
+ else
+ {
+ pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;
+ }
+ if (c >= 0.125)
+ {
+ pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;
+ }
+ else
+ {
+ pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;
+ }
+ vFrom = from_1 - pos;
+ vCtrl = ctrl_1 - pos;
+ vTo = to_1 - pos;
+ pos = (pos * _block.transform.xy) + _block.transform.zw;
+ gl_Position = vec4(pos, 1.0, 1.0);
+}
+
+`,
+ GLSL300ES: `#version 300 es
+
+layout(std140) uniform Block
+{
+ vec4 transform;
+ vec2 pathOffset;
+} _block;
+
+layout(location = 2) in vec2 from;
+layout(location = 3) in vec2 ctrl;
+layout(location = 4) in vec2 to;
+layout(location = 1) in float maxy;
+layout(location = 0) in float corner;
+out vec2 vFrom;
+out vec2 vCtrl;
+out vec2 vTo;
+
+void main()
+{
+ vec2 from_1 = from + _block.pathOffset;
+ vec2 ctrl_1 = ctrl + _block.pathOffset;
+ vec2 to_1 = to + _block.pathOffset;
+ float maxy_1 = maxy + _block.pathOffset.y;
+ float c = corner;
+ vec2 pos;
+ if (c >= 0.375)
+ {
+ c -= 0.5;
+ pos.y = maxy_1 + 1.0;
+ }
+ else
+ {
+ pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;
+ }
+ if (c >= 0.125)
+ {
+ pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;
+ }
+ else
+ {
+ pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;
+ }
+ vFrom = from_1 - pos;
+ vCtrl = ctrl_1 - pos;
+ vTo = to_1 - pos;
+ pos = (pos * _block.transform.xy) + _block.transform.zw;
+ gl_Position = vec4(pos, 1.0, 1.0);
+}
+
+`,
+ GLSL130: `#version 130
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+struct Block
+{
+ vec4 transform;
+ vec2 pathOffset;
+};
+
+uniform Block _block;
+
+in vec2 from;
+in vec2 ctrl;
+in vec2 to;
+in float maxy;
+in float corner;
+out vec2 vFrom;
+out vec2 vCtrl;
+out vec2 vTo;
+
+void main()
+{
+ vec2 from_1 = from + _block.pathOffset;
+ vec2 ctrl_1 = ctrl + _block.pathOffset;
+ vec2 to_1 = to + _block.pathOffset;
+ float maxy_1 = maxy + _block.pathOffset.y;
+ float c = corner;
+ vec2 pos;
+ if (c >= 0.375)
+ {
+ c -= 0.5;
+ pos.y = maxy_1 + 1.0;
+ }
+ else
+ {
+ pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;
+ }
+ if (c >= 0.125)
+ {
+ pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;
+ }
+ else
+ {
+ pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;
+ }
+ vFrom = from_1 - pos;
+ vCtrl = ctrl_1 - pos;
+ vTo = to_1 - pos;
+ pos = (pos * _block.transform.xy) + _block.transform.zw;
+ gl_Position = vec4(pos, 1.0, 1.0);
+}
+
+`,
+ GLSL150: `#version 150
+#ifdef GL_ARB_shading_language_420pack
+#extension GL_ARB_shading_language_420pack : require
+#endif
+
+layout(binding = 0, std140) uniform Block
+{
+ vec4 transform;
+ vec2 pathOffset;
+} _block;
+
+in vec2 from;
+in vec2 ctrl;
+in vec2 to;
+in float maxy;
+in float corner;
+out vec2 vFrom;
+out vec2 vCtrl;
+out vec2 vTo;
+
+void main()
+{
+ vec2 from_1 = from + _block.pathOffset;
+ vec2 ctrl_1 = ctrl + _block.pathOffset;
+ vec2 to_1 = to + _block.pathOffset;
+ float maxy_1 = maxy + _block.pathOffset.y;
+ float c = corner;
+ vec2 pos;
+ if (c >= 0.375)
+ {
+ c -= 0.5;
+ pos.y = maxy_1 + 1.0;
+ }
+ else
+ {
+ pos.y = min(min(from_1.y, ctrl_1.y), to_1.y) - 1.0;
+ }
+ if (c >= 0.125)
+ {
+ pos.x = max(max(from_1.x, ctrl_1.x), to_1.x) + 1.0;
+ }
+ else
+ {
+ pos.x = min(min(from_1.x, ctrl_1.x), to_1.x) - 1.0;
+ }
+ vFrom = from_1 - pos;
+ vCtrl = ctrl_1 - pos;
+ vTo = to_1 - pos;
+ pos = (pos * _block.transform.xy) + _block.transform.zw;
+ gl_Position = vec4(pos, 1.0, 1.0);
+}
+
+`,
+ HLSL: "DXBC\xa5!\xd8\x10\xb4n\x90\xe3\xd9U\xdb\xe2\xb6~I0\x01\x00\x00\x00\x10\b\x00\x00\x06\x00\x00\x008\x00\x00\x00L\x02\x00\x00t\x05\x00\x00\xf0\x05\x00\x00\xf4\x06\x00\x00\x88\a\x00\x00Aon9\f\x02\x00\x00\f\x02\x00\x00\x00\x02\xfe\xff\xd8\x01\x00\x004\x00\x00\x00\x01\x00$\x00\x00\x000\x00\x00\x000\x00\x00\x00$\x00\x01\x000\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xfe\xffQ\x00\x00\x05\x03\x00\x0f\xa0\x00\x00\xc0>\x00\x00\x80?\x00\x00\x80\xbf\x00\x00\x00\xbfQ\x00\x00\x05\x04\x00\x0f\xa0\x00\x00\x00>\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x02\x05\x00\x00\x80\x00\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x01\x80\x01\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x02\x80\x02\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x03\x80\x03\x00\x0f\x90\x1f\x00\x00\x02\x05\x00\x04\x80\x04\x00\x0f\x90\x02\x00\x00\x03\x00\x00\x01\x80\x01\x00\x00\x90\x02\x00U\xa0\x02\x00\x00\x03\x00\x00\x04\x80\x00\x00\x00\x80\x03\x00U\xa0\r\x00\x00\x03\x00\x00\x01\x80\x00\x00\x00\x90\x03\x00\x00\xa0\x01\x00\x00\x02\x01\x00\x04\x80\x00\x00\x00\x90\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00\x00\x90\x03\x00\xff\xa0\x02\x00\x00\x03\x02\x00\x03\x80\x02\x00\xe4\x90\x02\x00\xe4\xa0\x02\x00\x00\x03\x02\x00\f\x80\x03\x00\x14\x90\x02\x00\x14\xa0\n\x00\x00\x03\x03\x00\x03\x80\x02\x00\xee\x80\x02\x00\xe1\x80\x02\x00\x00\x03\x03\x00\f\x80\x04\x00D\x90\x02\x00D\xa0\n\x00\x00\x03\x03\x00\x03\x80\x03\x00\xeb\x80\x03\x00\xe4\x80\x02\x00\x00\x03\x01\x00\x03\x80\x03\x00\xe4\x80\x03\x00\xaa\xa0\x12\x00\x00\x04\x04\x00\x06\x80\x00\x00\x00\x80\x00\x00\xe4\x80\x01\x00Č\r\x00\x00\x03\x00\x00\x01\x80\x04\x00U\x80\x04\x00\x00\xa0\v\x00\x00\x03\x00\x00\x02\x80\x02\x00\xff\x80\x02\x00\x00\x80\v\x00\x00\x03\x00\x00\x02\x80\x03\x00\xaa\x80\x00\x00U\x80\x02\x00\x00\x03\x00\x00\x02\x80\x00\x00U\x80\x03\x00U\xa0\x12\x00\x00\x04\x04\x00\x01\x80\x00\x00\x00\x80\x00\x00U\x80\x01\x00U\x80\x02\x00\x00\x03\x00\x00\x0f\xe0\x02\x00\xe4\x80\x04\x00(\x81\x02\x00\x00\x03\x01\x00\x03\xe0\x03\x00\xee\x80\x04\x00\xe8\x81\x04\x00\x00\x04\x00\x00\x03\x80\x04\x00\xe8\x80\x01\x00\xe4\xa0\x01\x00\xee\xa0\x02\x00\x00\x03\x00\x00\x03\xc0\x00\x00\xe4\x80\x00\x00\xe4\xa0\x01\x00\x00\x02\x00\x00\f\xc0\x03\x00U\xa0\xff\xff\x00\x00SHDR \x03\x00\x00@\x00\x01\x00\xc8\x00\x00\x00Y\x00\x00\x04F\x8e \x00\x00\x00\x00\x00\x02\x00\x00\x00_\x00\x00\x03\x12\x10\x10\x00\x00\x00\x00\x00_\x00\x00\x03\x12\x10\x10\x00\x01\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x02\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x03\x00\x00\x00_\x00\x00\x032\x10\x10\x00\x04\x00\x00\x00e\x00\x00\x032 \x10\x00\x00\x00\x00\x00e\x00\x00\x03\xc2 \x10\x00\x00\x00\x00\x00e\x00\x00\x032 \x10\x00\x01\x00\x00\x00g\x00\x00\x04\xf2 \x10\x00\x02\x00\x00\x00\x01\x00\x00\x00h\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\b\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x01\x00\x00\x00\x1a\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\aB\x00\x10\x00\x00\x00\x00\x00\n\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?\x1d\x00\x00\a\x12\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\xc0>\x00\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\xbf6\x00\x00\x05B\x00\x10\x00\x01\x00\x00\x00\n\x10\x10\x00\x00\x00\x00\x00\x00\x00\x00\b2\x00\x10\x00\x02\x00\x00\x00F\x10\x10\x00\x02\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x02\x00\x00\x00\x06\x14\x10\x00\x03\x00\x00\x00\x06\x84 \x00\x00\x00\x00\x00\x01\x00\x00\x003\x00\x00\a2\x00\x10\x00\x03\x00\x00\x00\xb6\x0f\x10\x00\x02\x00\x00\x00\x16\x05\x10\x00\x02\x00\x00\x00\x00\x00\x00\b\xc2\x00\x10\x00\x03\x00\x00\x00\x06\x14\x10\x00\x04\x00\x00\x00\x06\x84 \x00\x00\x00\x00\x00\x01\x00\x00\x003\x00\x00\a2\x00\x10\x00\x03\x00\x00\x00\xb6\x0f\x10\x00\x03\x00\x00\x00F\x00\x10\x00\x03\x00\x00\x00\x00\x00\x00\n2\x00\x10\x00\x01\x00\x00\x00F\x00\x10\x00\x03\x00\x00\x00\x02@\x00\x00\x00\x00\x80\xbf\x00\x00\x80\xbf\x00\x00\x00\x00\x00\x00\x00\x007\x00\x00\tb\x00\x10\x00\x00\x00\x00\x00\x06\x00\x10\x00\x00\x00\x00\x00V\x06\x10\x00\x00\x00\x00\x00\xa6\b\x10\x00\x01\x00\x00\x00\x1d\x00\x00\a\"\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00>4\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x02\x00\x00\x00\n\x00\x10\x00\x02\x00\x00\x004\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00*\x00\x10\x00\x03\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\a\x82\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x80?7\x00\x00\t\x12\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x00\x00\x00\x00:\x00\x10\x00\x00\x00\x00\x00\x1a\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\b\xf2 \x10\x00\x00\x00\x00\x00\x86\b\x10\x80A\x00\x00\x00\x00\x00\x00\x00F\x0e\x10\x00\x02\x00\x00\x00\x00\x00\x00\b2 \x10\x00\x01\x00\x00\x00\x86\x00\x10\x80A\x00\x00\x00\x00\x00\x00\x00\xe6\n\x10\x00\x03\x00\x00\x002\x00\x00\v2 \x10\x00\x02\x00\x00\x00\x86\x00\x10\x00\x00\x00\x00\x00F\x80 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xe6\x8a \x00\x00\x00\x00\x00\x00\x00\x00\x006\x00\x00\b\xc2 \x10\x00\x02\x00\x00\x00\x02@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80?\x00\x00\x80?>\x00\x00\x01STATt\x00\x00\x00\x16\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00RDEF\xfc\x00\x00\x00\x01\x00\x00\x00D\x00\x00\x00\x01\x00\x00\x00\x1c\x00\x00\x00\x00\x04\xfe\xff\x00\x01\x00\x00\xd4\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00Block\x00\xab\xab<\x00\x00\x00\x02\x00\x00\x00\\\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\xb0\x00\x00\x00\x10\x00\x00\x00\b\x00\x00\x00\x02\x00\x00\x00\xc4\x00\x00\x00\x00\x00\x00\x00_block_transform\x00\xab\xab\xab\x01\x00\x03\x00\x01\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00_block_pathOffset\x00\xab\xab\x01\x00\x03\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00Microsoft (R) HLSL Shader Compiler 10.1\x00ISGN\x8c\x00\x00\x00\x05\x00\x00\x00\b\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x01\x01\x00\x00\x80\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x01\x01\x00\x00\x80\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x03\x03\x00\x00\x80\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x03\x00\x00\x00\x03\x03\x00\x00\x80\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x03\x03\x00\x00TEXCOORD\x00\xab\xab\xabOSGN\x80\x00\x00\x00\x04\x00\x00\x00\b\x00\x00\x00h\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x03\f\x00\x00h\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\f\x03\x00\x00h\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x03\f\x00\x00q\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x02\x00\x00\x00\x0f\x00\x00\x00TEXCOORD\x00SV_Position\x00\xab\xab\xab",
+ }
+ shader_tile_alloc_comp = driver.ShaderSources{
+ Name: "tile_alloc.comp",
+ GLSL310ES: `#version 310 es
+layout(local_size_x = 128, local_size_y = 1, local_size_z = 1) in;
+
+struct Alloc
+{
+ uint offset;
+};
+
+struct MallocResult
+{
+ Alloc alloc;
+ bool failed;
+};
+
+struct AnnoEndClipRef
+{
+ uint offset;
+};
+
+struct AnnoEndClip
+{
+ vec4 bbox;
+};
+
+struct AnnotatedRef
+{
+ uint offset;
+};
+
+struct AnnotatedTag
+{
+ uint tag;
+ uint flags;
+};
+
+struct PathRef
+{
+ uint offset;
+};
+
+struct TileRef
+{
+ uint offset;
+};
+
+struct Path
+{
+ uvec4 bbox;
+ TileRef tiles;
+};
+
+struct Config
+{
+ uint n_elements;
+ uint n_pathseg;
+ uint width_in_tiles;
+ uint height_in_tiles;
+ Alloc tile_alloc;
+ Alloc bin_alloc;
+ Alloc ptcl_alloc;
+ Alloc pathseg_alloc;
+ Alloc anno_alloc;
+ Alloc trans_alloc;
+};
+
+layout(binding = 0, std430) buffer Memory
+{
+ uint mem_offset;
+ uint mem_error;
+ uint memory[];
+} _96;
+
+layout(binding = 1, std430) readonly buffer ConfigBuf
+{
+ Config conf;
+} _309;
+
+shared uint sh_tile_count[128];
+shared MallocResult sh_tile_alloc;
+
+bool touch_mem(Alloc alloc, uint offset)
+{
+ return true;
+}
+
+uint read_mem(Alloc alloc, uint offset)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return 0u;
+ }
+ uint v = _96.memory[offset];
+ return v;
+}
+
+AnnotatedTag Annotated_tag(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ uint param_1 = ref.offset >> uint(2);
+ uint tag_and_flags = read_mem(param, param_1);
+ return AnnotatedTag(tag_and_flags & 65535u, tag_and_flags >> uint(16));
+}
+
+AnnoEndClip AnnoEndClip_read(Alloc a, AnnoEndClipRef ref)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint raw0 = read_mem(param, param_1);
+ Alloc param_2 = a;
+ uint param_3 = ix + 1u;
+ uint raw1 = read_mem(param_2, param_3);
+ Alloc param_4 = a;
+ uint param_5 = ix + 2u;
+ uint raw2 = read_mem(param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 3u;
+ uint raw3 = read_mem(param_6, param_7);
+ AnnoEndClip s;
+ s.bbox = vec4(uintBitsToFloat(raw0), uintBitsToFloat(raw1), uintBitsToFloat(raw2), uintBitsToFloat(raw3));
+ return s;
+}
+
+AnnoEndClip Annotated_EndClip_read(Alloc a, AnnotatedRef ref)
+{
+ Alloc param = a;
+ AnnoEndClipRef param_1 = AnnoEndClipRef(ref.offset + 4u);
+ return AnnoEndClip_read(param, param_1);
+}
+
+Alloc new_alloc(uint offset, uint size)
+{
+ Alloc a;
+ a.offset = offset;
+ return a;
+}
+
+MallocResult malloc(uint size)
+{
+ MallocResult r;
+ r.failed = false;
+ uint _102 = atomicAdd(_96.mem_offset, size);
+ uint offset = _102;
+ uint param = offset;
+ uint param_1 = size;
+ r.alloc = new_alloc(param, param_1);
+ if ((offset + size) > uint(int(uint(_96.memory.length())) * 4))
+ {
+ r.failed = true;
+ uint _123 = atomicMax(_96.mem_error, 1u);
+ return r;
+ }
+ return r;
+}
+
+Alloc slice_mem(Alloc a, uint offset, uint size)
+{
+ uint param = a.offset + offset;
+ uint param_1 = size;
+ return new_alloc(param, param_1);
+}
+
+void write_mem(Alloc alloc, uint offset, uint val)
+{
+ Alloc param = alloc;
+ uint param_1 = offset;
+ if (!touch_mem(param, param_1))
+ {
+ return;
+ }
+ _96.memory[offset] = val;
+}
+
+void Path_write(Alloc a, PathRef ref, Path s)
+{
+ uint ix = ref.offset >> uint(2);
+ Alloc param = a;
+ uint param_1 = ix + 0u;
+ uint param_2 = s.bbox.x | (s.bbox.y << uint(16));
+ write_mem(param, param_1, param_2);
+ Alloc param_3 = a;
+ uint param_4 = ix + 1u;
+ uint param_5 = s.bbox.z | (s.bbox.w << uint(16));
+ write_mem(param_3, param_4, param_5);
+ Alloc param_6 = a;
+ uint param_7 = ix + 2u;
+ uint param_8 = s.tiles.offset;
+ write_mem(param_6, param_7, param_8);
+}
+
+void main()
+{
+ if (_96.mem_error != 0u)
+ {
+ return;
+ }
+ uint th_ix = gl_LocalInvocationID.x;
+ uint element_ix = gl_GlobalInvocationID.x;
+ PathRef path_ref = PathRef(_309.conf.tile_alloc.offset + (element_ix * 12u));
+ AnnotatedRef ref = AnnotatedRef(_309.conf.anno_alloc.offset + (element_ix * 32u));
+ uint tag = 0u;
+ if (element_ix < _309.conf.n_elements)
+ {
+ Alloc param;
+ param.offset = _309.conf.anno_alloc.offset;
+ AnnotatedRef param_1 = ref;
+ tag = Annotated_tag(param, param_1).tag;
+ }
+ int x0 = 0;
+ int y0 = 0;
+ int x1 = 0;
+ int y1 = 0;
+ switch (tag)
+ {
+ case 1u:
+ case 2u:
+ case 3u:
+ case 4u:
+ {
+ Alloc param_2;
+ param_2.offset = _309.conf.anno_alloc.offset;
+ AnnotatedRef param_3 = ref;
+ AnnoEndClip clip = Annotated_EndClip_read(param_2, param_3);
+ x0 = int(floor(clip.bbox.x * 0.03125));
+ y0 = int(floor(clip.bbox.y * 0.03125));
+ x1 = int(ceil(clip.bbox.z * 0.03125));
+ y1 = int(ceil(clip.bbox.w * 0.03125));
+ break;
+ }
+ }
+ x0 = clamp(x0, 0, int(_309.conf.width_in_tiles));
+ y0 = clamp(y0, 0, int(_309.conf.height_in_tiles));
+ x1 = clamp(x1, 0, int(_309.conf.width_in_tiles));
+ y1 = clamp(y1, 0, int(_309.conf.height_in_tiles));
+ Path path;
+ path.bbox = uvec4(uint(x0), uint(y0), uint(x1), uint(y1));
+ uint tile_count = uint((x1 - x0) * (y1 - y0));
+ if (tag == 4u)
+ {
+ tile_count = 0u;
+ }
+ sh_tile_count[th_ix] = tile_count;
+ uint total_tile_count = tile_count;
+ for (uint i = 0u; i < 7u; i++)
+ {
+ barrier();
+ if (th_ix >= uint(1 << int(i)))
+ {
+ total_tile_count += sh_tile_count[th_ix - uint(1 << int(i))];
+ }
+ barrier();
+ sh_tile_count[th_ix] = total_tile_count;
+ }
+ if (th_ix == 127u)
+ {
+ uint param_4 = total_tile_count * 8u;
+ MallocResult _482 = malloc(param_4);
+ sh_tile_alloc = _482;
+ }
+ barrier();
+ MallocResult alloc_start = sh_tile_alloc;
+ if (alloc_start.failed)
+ {
+ return;
+ }
+ if (element_ix < _309.conf.n_elements)
+ {
+ uint _499;
+ if (th_ix > 0u)
+ {
+ _499 = sh_tile_count[th_ix - 1u];
+ }
+ else
+ {
+ _499 = 0u;
+ }
+ uint tile_subix = _499;
+ Alloc param_5 = alloc_start.alloc;
+ uint param_6 = 8u * tile_subix;
+ uint param_7 = 8u * tile_count;
+ Alloc tiles_alloc = slice_mem(param_5, param_6, param_7);
+ path.tiles = TileRef(tiles_alloc.offset);
+ Alloc param_8;
+ param_8.offset = _309.conf.tile_alloc.offset;
+ PathRef param_9 = path_ref;
+ Path param_10 = path;
+ Path_write(param_8, param_9, param_10);
+ }
+ uint total_count = sh_tile_count[127] * 2u;
+ uint start_ix = alloc_start.alloc.offset >> uint(2);
+ for (uint i_1 = th_ix; i_1 < total_count; i_1 += 128u)
+ {
+ Alloc param_11 = alloc_start.alloc;
+ uint param_12 = start_ix + i_1;
+ uint param_13 = 0u;
+ write_mem(param_11, param_12, param_13);
+ }
+}
+
+`,
+ }
+)
diff --git a/gio/gpu/timer.go b/gio/gpu/timer.go
new file mode 100644
index 0000000..6e0bd4a
--- /dev/null
+++ b/gio/gpu/timer.go
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gpu
+
+import (
+ "time"
+
+ "realy.lol/gio/gpu/internal/driver"
+)
+
+type timers struct {
+ backend driver.Device
+ timers []*timer
+}
+
+type timer struct {
+ Elapsed time.Duration
+ backend driver.Device
+ timer driver.Timer
+ state timerState
+}
+
+type timerState uint8
+
+const (
+ timerIdle timerState = iota
+ timerRunning
+ timerWaiting
+)
+
+func newTimers(b driver.Device) *timers {
+ return &timers{
+ backend: b,
+ }
+}
+
+func (t *timers) newTimer() *timer {
+ if t == nil {
+ return nil
+ }
+ tt := &timer{
+ backend: t.backend,
+ timer: t.backend.NewTimer(),
+ }
+ t.timers = append(t.timers, tt)
+ return tt
+}
+
+func (t *timer) begin() {
+ if t == nil || t.state != timerIdle {
+ return
+ }
+ t.timer.Begin()
+ t.state = timerRunning
+}
+
+func (t *timer) end() {
+ if t == nil || t.state != timerRunning {
+ return
+ }
+ t.timer.End()
+ t.state = timerWaiting
+}
+
+func (t *timers) ready() bool {
+ if t == nil {
+ return false
+ }
+ for _, tt := range t.timers {
+ switch tt.state {
+ case timerIdle:
+ continue
+ case timerRunning:
+ return false
+ }
+ d, ok := tt.timer.Duration()
+ if !ok {
+ return false
+ }
+ tt.state = timerIdle
+ tt.Elapsed = d
+ }
+ return t.backend.IsTimeContinuous()
+}
+
+func (t *timers) release() {
+ if t == nil {
+ return
+ }
+ for _, tt := range t.timers {
+ tt.timer.Release()
+ }
+ t.timers = nil
+}
diff --git a/gio/internal/byteslice/byteslice.go b/gio/internal/byteslice/byteslice.go
new file mode 100644
index 0000000..26ebdb2
--- /dev/null
+++ b/gio/internal/byteslice/byteslice.go
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package byteslice provides byte slice views of other Go values such as
+// slices and structs.
+package byteslice
+
+import (
+ "reflect"
+ "unsafe"
+)
+
+// Struct returns a byte slice view of a struct.
+func Struct(s interface{}) []byte {
+ v := reflect.ValueOf(s).Elem()
+ sz := int(v.Type().Size())
+ var res []byte
+ h := (*reflect.SliceHeader)(unsafe.Pointer(&res))
+ h.Data = uintptr(unsafe.Pointer(v.UnsafeAddr()))
+ h.Cap = sz
+ h.Len = sz
+ return res
+}
+
+// Uint32 returns a byte slice view of a uint32 slice.
+func Uint32(s []uint32) []byte {
+ n := len(s)
+ if n == 0 {
+ return nil
+ }
+ blen := n * int(unsafe.Sizeof(s[0]))
+ return (*[1 << 30]byte)(unsafe.Pointer(&s[0]))[:blen:blen]
+}
+
+// Slice returns a byte slice view of a slice.
+func Slice(s interface{}) []byte {
+ v := reflect.ValueOf(s)
+ first := v.Index(0)
+ sz := int(first.Type().Size())
+ var res []byte
+ h := (*reflect.SliceHeader)(unsafe.Pointer(&res))
+ h.Data = first.UnsafeAddr()
+ h.Cap = v.Cap() * sz
+ h.Len = v.Len() * sz
+ return res
+}
diff --git a/gio/internal/cocoainit/cocoa_darwin.go b/gio/internal/cocoainit/cocoa_darwin.go
new file mode 100644
index 0000000..2a34e57
--- /dev/null
+++ b/gio/internal/cocoainit/cocoa_darwin.go
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package cocoainit initializes support for multithreaded
+// programs in Cocoa.
+package cocoainit
+
+/*
+#cgo CFLAGS: -xobjective-c -fmodules -fobjc-arc
+#import
+
+static inline void activate_cocoa_multithreading() {
+ [[NSThread new] start];
+}
+#pragma GCC visibility push(hidden)
+*/
+import "C"
+
+func init() {
+ C.activate_cocoa_multithreading()
+}
diff --git a/gio/internal/d3d11/d3d11_windows.go b/gio/internal/d3d11/d3d11_windows.go
new file mode 100644
index 0000000..f33eb61
--- /dev/null
+++ b/gio/internal/d3d11/d3d11_windows.go
@@ -0,0 +1,1470 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package d3d11
+
+import (
+ "fmt"
+ "math"
+ "syscall"
+ "unsafe"
+
+ "realy.lol/gio/internal/f32color"
+
+ "golang.org/x/sys/windows"
+)
+
+type DXGI_SWAP_CHAIN_DESC struct {
+ BufferDesc DXGI_MODE_DESC
+ SampleDesc DXGI_SAMPLE_DESC
+ BufferUsage uint32
+ BufferCount uint32
+ OutputWindow windows.Handle
+ Windowed uint32
+ SwapEffect uint32
+ Flags uint32
+}
+
+type DXGI_SAMPLE_DESC struct {
+ Count uint32
+ Quality uint32
+}
+
+type DXGI_MODE_DESC struct {
+ Width uint32
+ Height uint32
+ RefreshRate DXGI_RATIONAL
+ Format uint32
+ ScanlineOrdering uint32
+ Scaling uint32
+}
+
+type DXGI_RATIONAL struct {
+ Numerator uint32
+ Denominator uint32
+}
+
+type TEXTURE2D_DESC struct {
+ Width uint32
+ Height uint32
+ MipLevels uint32
+ ArraySize uint32
+ Format uint32
+ SampleDesc DXGI_SAMPLE_DESC
+ Usage uint32
+ BindFlags uint32
+ CPUAccessFlags uint32
+ MiscFlags uint32
+}
+
+type SAMPLER_DESC struct {
+ Filter uint32
+ AddressU uint32
+ AddressV uint32
+ AddressW uint32
+ MipLODBias float32
+ MaxAnisotropy uint32
+ ComparisonFunc uint32
+ BorderColor [4]float32
+ MinLOD float32
+ MaxLOD float32
+}
+
+type SHADER_RESOURCE_VIEW_DESC_TEX2D struct {
+ SHADER_RESOURCE_VIEW_DESC
+ Texture2D TEX2D_SRV
+}
+
+type SHADER_RESOURCE_VIEW_DESC struct {
+ Format uint32
+ ViewDimension uint32
+}
+
+type TEX2D_SRV struct {
+ MostDetailedMip uint32
+ MipLevels uint32
+}
+
+type INPUT_ELEMENT_DESC struct {
+ SemanticName *byte
+ SemanticIndex uint32
+ Format uint32
+ InputSlot uint32
+ AlignedByteOffset uint32
+ InputSlotClass uint32
+ InstanceDataStepRate uint32
+}
+
+type IDXGISwapChain struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ GetPrivateData uintptr
+ GetParent uintptr
+ GetDevice uintptr
+ Present uintptr
+ GetBuffer uintptr
+ SetFullscreenState uintptr
+ GetFullscreenState uintptr
+ GetDesc uintptr
+ ResizeBuffers uintptr
+ ResizeTarget uintptr
+ GetContainingOutput uintptr
+ GetFrameStatistics uintptr
+ GetLastPresentCount uintptr
+ }
+}
+
+type Device struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ CreateBuffer uintptr
+ CreateTexture1D uintptr
+ CreateTexture2D uintptr
+ CreateTexture3D uintptr
+ CreateShaderResourceView uintptr
+ CreateUnorderedAccessView uintptr
+ CreateRenderTargetView uintptr
+ CreateDepthStencilView uintptr
+ CreateInputLayout uintptr
+ CreateVertexShader uintptr
+ CreateGeometryShader uintptr
+ CreateGeometryShaderWithStreamOutput uintptr
+ CreatePixelShader uintptr
+ CreateHullShader uintptr
+ CreateDomainShader uintptr
+ CreateComputeShader uintptr
+ CreateClassLinkage uintptr
+ CreateBlendState uintptr
+ CreateDepthStencilState uintptr
+ CreateRasterizerState uintptr
+ CreateSamplerState uintptr
+ CreateQuery uintptr
+ CreatePredicate uintptr
+ CreateCounter uintptr
+ CreateDeferredContext uintptr
+ OpenSharedResource uintptr
+ CheckFormatSupport uintptr
+ CheckMultisampleQualityLevels uintptr
+ CheckCounterInfo uintptr
+ CheckCounter uintptr
+ CheckFeatureSupport uintptr
+ GetPrivateData uintptr
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ GetFeatureLevel uintptr
+ GetCreationFlags uintptr
+ GetDeviceRemovedReason uintptr
+ GetImmediateContext uintptr
+ SetExceptionMode uintptr
+ GetExceptionMode uintptr
+ }
+}
+
+type DeviceContext struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ GetDevice uintptr
+ GetPrivateData uintptr
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ VSSetConstantBuffers uintptr
+ PSSetShaderResources uintptr
+ PSSetShader uintptr
+ PSSetSamplers uintptr
+ VSSetShader uintptr
+ DrawIndexed uintptr
+ Draw uintptr
+ Map uintptr
+ Unmap uintptr
+ PSSetConstantBuffers uintptr
+ IASetInputLayout uintptr
+ IASetVertexBuffers uintptr
+ IASetIndexBuffer uintptr
+ DrawIndexedInstanced uintptr
+ DrawInstanced uintptr
+ GSSetConstantBuffers uintptr
+ GSSetShader uintptr
+ IASetPrimitiveTopology uintptr
+ VSSetShaderResources uintptr
+ VSSetSamplers uintptr
+ Begin uintptr
+ End uintptr
+ GetData uintptr
+ SetPredication uintptr
+ GSSetShaderResources uintptr
+ GSSetSamplers uintptr
+ OMSetRenderTargets uintptr
+ OMSetRenderTargetsAndUnorderedAccessViews uintptr
+ OMSetBlendState uintptr
+ OMSetDepthStencilState uintptr
+ SOSetTargets uintptr
+ DrawAuto uintptr
+ DrawIndexedInstancedIndirect uintptr
+ DrawInstancedIndirect uintptr
+ Dispatch uintptr
+ DispatchIndirect uintptr
+ RSSetState uintptr
+ RSSetViewports uintptr
+ RSSetScissorRects uintptr
+ CopySubresourceRegion uintptr
+ CopyResource uintptr
+ UpdateSubresource uintptr
+ CopyStructureCount uintptr
+ ClearRenderTargetView uintptr
+ ClearUnorderedAccessViewUint uintptr
+ ClearUnorderedAccessViewFloat uintptr
+ ClearDepthStencilView uintptr
+ GenerateMips uintptr
+ SetResourceMinLOD uintptr
+ GetResourceMinLOD uintptr
+ ResolveSubresource uintptr
+ ExecuteCommandList uintptr
+ HSSetShaderResources uintptr
+ HSSetShader uintptr
+ HSSetSamplers uintptr
+ HSSetConstantBuffers uintptr
+ DSSetShaderResources uintptr
+ DSSetShader uintptr
+ DSSetSamplers uintptr
+ DSSetConstantBuffers uintptr
+ CSSetShaderResources uintptr
+ CSSetUnorderedAccessViews uintptr
+ CSSetShader uintptr
+ CSSetSamplers uintptr
+ CSSetConstantBuffers uintptr
+ VSGetConstantBuffers uintptr
+ PSGetShaderResources uintptr
+ PSGetShader uintptr
+ PSGetSamplers uintptr
+ VSGetShader uintptr
+ PSGetConstantBuffers uintptr
+ IAGetInputLayout uintptr
+ IAGetVertexBuffers uintptr
+ IAGetIndexBuffer uintptr
+ GSGetConstantBuffers uintptr
+ GSGetShader uintptr
+ IAGetPrimitiveTopology uintptr
+ VSGetShaderResources uintptr
+ VSGetSamplers uintptr
+ GetPredication uintptr
+ GSGetShaderResources uintptr
+ GSGetSamplers uintptr
+ OMGetRenderTargets uintptr
+ OMGetRenderTargetsAndUnorderedAccessViews uintptr
+ OMGetBlendState uintptr
+ OMGetDepthStencilState uintptr
+ SOGetTargets uintptr
+ RSGetState uintptr
+ RSGetViewports uintptr
+ RSGetScissorRects uintptr
+ HSGetShaderResources uintptr
+ HSGetShader uintptr
+ HSGetSamplers uintptr
+ HSGetConstantBuffers uintptr
+ DSGetShaderResources uintptr
+ DSGetShader uintptr
+ DSGetSamplers uintptr
+ DSGetConstantBuffers uintptr
+ CSGetShaderResources uintptr
+ CSGetUnorderedAccessViews uintptr
+ CSGetShader uintptr
+ CSGetSamplers uintptr
+ CSGetConstantBuffers uintptr
+ ClearState uintptr
+ Flush uintptr
+ GetType uintptr
+ GetContextFlags uintptr
+ FinishCommandList uintptr
+ }
+}
+
+type RenderTargetView struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type Resource struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type Texture2D struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type Buffer struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type SamplerState struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type PixelShader struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type ShaderResourceView struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type DepthStencilView struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type BlendState struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type DepthStencilState struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type VertexShader struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type RasterizerState struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type InputLayout struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ GetBufferPointer uintptr
+ GetBufferSize uintptr
+ }
+}
+
+type DEPTH_STENCIL_DESC struct {
+ DepthEnable uint32
+ DepthWriteMask uint32
+ DepthFunc uint32
+ StencilEnable uint32
+ StencilReadMask uint8
+ StencilWriteMask uint8
+ FrontFace DEPTH_STENCILOP_DESC
+ BackFace DEPTH_STENCILOP_DESC
+}
+
+type DEPTH_STENCILOP_DESC struct {
+ StencilFailOp uint32
+ StencilDepthFailOp uint32
+ StencilPassOp uint32
+ StencilFunc uint32
+}
+
+type DEPTH_STENCIL_VIEW_DESC_TEX2D struct {
+ Format uint32
+ ViewDimension uint32
+ Flags uint32
+ Texture2D TEX2D_DSV
+}
+
+type TEX2D_DSV struct {
+ MipSlice uint32
+}
+
+type BLEND_DESC struct {
+ AlphaToCoverageEnable uint32
+ IndependentBlendEnable uint32
+ RenderTarget [8]RENDER_TARGET_BLEND_DESC
+}
+
+type RENDER_TARGET_BLEND_DESC struct {
+ BlendEnable uint32
+ SrcBlend uint32
+ DestBlend uint32
+ BlendOp uint32
+ SrcBlendAlpha uint32
+ DestBlendAlpha uint32
+ BlendOpAlpha uint32
+ RenderTargetWriteMask uint8
+}
+
+type IDXGIObject struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ GetPrivateData uintptr
+ GetParent uintptr
+ }
+}
+
+type IDXGIAdapter struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ GetPrivateData uintptr
+ GetParent uintptr
+ EnumOutputs uintptr
+ GetDesc uintptr
+ CheckInterfaceSupport uintptr
+ GetDesc1 uintptr
+ }
+}
+
+type IDXGIFactory struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ GetPrivateData uintptr
+ GetParent uintptr
+ EnumAdapters uintptr
+ MakeWindowAssociation uintptr
+ GetWindowAssociation uintptr
+ CreateSwapChain uintptr
+ CreateSoftwareAdapter uintptr
+ }
+}
+
+type IDXGIDevice struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ SetPrivateData uintptr
+ SetPrivateDataInterface uintptr
+ GetPrivateData uintptr
+ GetParent uintptr
+ GetAdapter uintptr
+ CreateSurface uintptr
+ QueryResourceResidency uintptr
+ SetGPUThreadPriority uintptr
+ GetGPUThreadPriority uintptr
+ }
+}
+
+type IUnknown struct {
+ Vtbl *struct {
+ _IUnknownVTbl
+ }
+}
+
+type _IUnknownVTbl struct {
+ QueryInterface uintptr
+ AddRef uintptr
+ Release uintptr
+}
+
+type BUFFER_DESC struct {
+ ByteWidth uint32
+ Usage uint32
+ BindFlags uint32
+ CPUAccessFlags uint32
+ MiscFlags uint32
+ StructureByteStride uint32
+}
+
+type GUID struct {
+ Data1 uint32
+ Data2 uint16
+ Data3 uint16
+ Data4_0 uint8
+ Data4_1 uint8
+ Data4_2 uint8
+ Data4_3 uint8
+ Data4_4 uint8
+ Data4_5 uint8
+ Data4_6 uint8
+ Data4_7 uint8
+}
+
+type VIEWPORT struct {
+ TopLeftX float32
+ TopLeftY float32
+ Width float32
+ Height float32
+ MinDepth float32
+ MaxDepth float32
+}
+
+type SUBRESOURCE_DATA struct {
+ pSysMem *byte
+}
+
+type BOX struct {
+ Left uint32
+ Top uint32
+ Front uint32
+ Right uint32
+ Bottom uint32
+ Back uint32
+}
+
+type MAPPED_SUBRESOURCE struct {
+ PData uintptr
+ RowPitch uint32
+ DepthPitch uint32
+}
+
+type ErrorCode struct {
+ Name string
+ Code uint32
+}
+
+type RASTERIZER_DESC struct {
+ FillMode uint32
+ CullMode uint32
+ FrontCounterClockwise uint32
+ DepthBias int32
+ DepthBiasClamp float32
+ SlopeScaledDepthBias float32
+ DepthClipEnable uint32
+ ScissorEnable uint32
+ MultisampleEnable uint32
+ AntialiasedLineEnable uint32
+}
+
+var (
+ IID_Texture2D = GUID{0x6f15aaf2, 0xd208, 0x4e89, 0x9a, 0xb4, 0x48, 0x95,
+ 0x35, 0xd3, 0x4f, 0x9c}
+ IID_IDXGIDevice = GUID{0x54ec77fa, 0x1377, 0x44e6, 0x8c, 0x32, 0x88, 0xfd,
+ 0x5f, 0x44, 0xc8, 0x4c}
+ IID_IDXGIFactory = GUID{0x7b7166ec, 0x21c7, 0x44ae, 0xb2, 0x1a, 0xc9, 0xae,
+ 0x32, 0x1a, 0xe3, 0x69}
+)
+
+var (
+ d3d11 = windows.NewLazySystemDLL("d3d11.dll")
+
+ _D3D11CreateDevice = d3d11.NewProc("D3D11CreateDevice")
+ _D3D11CreateDeviceAndSwapChain = d3d11.NewProc("D3D11CreateDeviceAndSwapChain")
+)
+
+const (
+ SDK_VERSION = 7
+ DRIVER_TYPE_HARDWARE = 1
+
+ DXGI_FORMAT_UNKNOWN = 0
+ DXGI_FORMAT_R16_FLOAT = 54
+ DXGI_FORMAT_R32_FLOAT = 41
+ DXGI_FORMAT_R32G32_FLOAT = 16
+ DXGI_FORMAT_R32G32B32_FLOAT = 6
+ DXGI_FORMAT_R32G32B32A32_FLOAT = 2
+ DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29
+ DXGI_FORMAT_R16_SINT = 59
+ DXGI_FORMAT_R16G16_SINT = 38
+ DXGI_FORMAT_R16_UINT = 57
+ DXGI_FORMAT_D24_UNORM_S8_UINT = 45
+ DXGI_FORMAT_R16G16_FLOAT = 34
+ DXGI_FORMAT_R16G16B16A16_FLOAT = 10
+
+ FORMAT_SUPPORT_TEXTURE2D = 0x20
+ FORMAT_SUPPORT_RENDER_TARGET = 0x4000
+
+ DXGI_USAGE_RENDER_TARGET_OUTPUT = 1 << (1 + 4)
+
+ CPU_ACCESS_READ = 0x20000
+
+ MAP_READ = 1
+
+ DXGI_SWAP_EFFECT_DISCARD = 0
+
+ FEATURE_LEVEL_9_1 = 0x9100
+ FEATURE_LEVEL_9_3 = 0x9300
+ FEATURE_LEVEL_11_0 = 0xb000
+
+ USAGE_IMMUTABLE = 1
+ USAGE_STAGING = 3
+
+ BIND_VERTEX_BUFFER = 0x1
+ BIND_INDEX_BUFFER = 0x2
+ BIND_CONSTANT_BUFFER = 0x4
+ BIND_SHADER_RESOURCE = 0x8
+ BIND_RENDER_TARGET = 0x20
+ BIND_DEPTH_STENCIL = 0x40
+
+ PRIMITIVE_TOPOLOGY_TRIANGLELIST = 4
+ PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5
+
+ FILTER_MIN_MAG_LINEAR_MIP_POINT = 0x14
+ FILTER_MIN_MAG_MIP_POINT = 0
+
+ TEXTURE_ADDRESS_MIRROR = 2
+ TEXTURE_ADDRESS_CLAMP = 3
+ TEXTURE_ADDRESS_WRAP = 1
+
+ SRV_DIMENSION_TEXTURE2D = 4
+
+ CREATE_DEVICE_DEBUG = 0x2
+
+ FILL_SOLID = 3
+
+ CULL_NONE = 1
+
+ CLEAR_DEPTH = 0x1
+ CLEAR_STENCIL = 0x2
+
+ DSV_DIMENSION_TEXTURE2D = 3
+
+ DEPTH_WRITE_MASK_ALL = 1
+
+ COMPARISON_GREATER = 5
+ COMPARISON_GREATER_EQUAL = 7
+
+ BLEND_OP_ADD = 1
+ BLEND_ONE = 2
+ BLEND_INV_SRC_ALPHA = 6
+ BLEND_ZERO = 1
+ BLEND_DEST_COLOR = 9
+ BLEND_DEST_ALPHA = 7
+
+ COLOR_WRITE_ENABLE_ALL = 1 | 2 | 4 | 8
+
+ DXGI_STATUS_OCCLUDED = 0x087A0001
+ DXGI_ERROR_DEVICE_RESET = 0x887A0007
+ DXGI_ERROR_DEVICE_REMOVED = 0x887A0005
+ D3DDDIERR_DEVICEREMOVED = 1<<31 | 0x876<<16 | 2160
+)
+
+func CreateDevice(driverType uint32, flags uint32) (*Device, *DeviceContext,
+ uint32, error) {
+ var (
+ dev *Device
+ ctx *DeviceContext
+ featLvl uint32
+ )
+ r, _, _ := _D3D11CreateDevice.Call(
+ 0, // pAdapter
+ uintptr(driverType), // driverType
+ 0, // Software
+ uintptr(flags), // Flags
+ 0, // pFeatureLevels
+ 0, // FeatureLevels
+ SDK_VERSION, // SDKVersion
+ uintptr(unsafe.Pointer(&dev)), // ppDevice
+ uintptr(unsafe.Pointer(&featLvl)), // pFeatureLevel
+ uintptr(unsafe.Pointer(&ctx)), // ppImmediateContext
+ )
+ if r != 0 {
+ return nil, nil, 0, ErrorCode{Name: "D3D11CreateDevice",
+ Code: uint32(r)}
+ }
+ return dev, ctx, featLvl, nil
+}
+
+func CreateDeviceAndSwapChain(driverType uint32, flags uint32,
+ swapDesc *DXGI_SWAP_CHAIN_DESC) (*Device, *DeviceContext, *IDXGISwapChain,
+ uint32, error) {
+ var (
+ dev *Device
+ ctx *DeviceContext
+ swchain *IDXGISwapChain
+ featLvl uint32
+ )
+ r, _, _ := _D3D11CreateDeviceAndSwapChain.Call(
+ 0, // pAdapter
+ uintptr(driverType), // driverType
+ 0, // Software
+ uintptr(flags), // Flags
+ 0, // pFeatureLevels
+ 0, // FeatureLevels
+ SDK_VERSION, // SDKVersion
+ uintptr(unsafe.Pointer(swapDesc)), // pSwapChainDesc
+ uintptr(unsafe.Pointer(&swchain)), // ppSwapChain
+ uintptr(unsafe.Pointer(&dev)), // ppDevice
+ uintptr(unsafe.Pointer(&featLvl)), // pFeatureLevel
+ uintptr(unsafe.Pointer(&ctx)), // ppImmediateContext
+ )
+ if r != 0 {
+ return nil, nil, nil, 0, ErrorCode{Name: "D3D11CreateDeviceAndSwapChain",
+ Code: uint32(r)}
+ }
+ return dev, ctx, swchain, featLvl, nil
+}
+
+func (d *Device) CheckFormatSupport(format uint32) (uint32, error) {
+ var support uint32
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.CheckFormatSupport,
+ 3,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(format),
+ uintptr(unsafe.Pointer(&support)),
+ )
+ if r != 0 {
+ return 0, ErrorCode{Name: "DeviceCheckFormatSupport", Code: uint32(r)}
+ }
+ return support, nil
+}
+
+func (d *Device) CreateBuffer(desc *BUFFER_DESC, data []byte) (*Buffer, error) {
+ var dataDesc *SUBRESOURCE_DATA
+ if len(data) > 0 {
+ dataDesc = &SUBRESOURCE_DATA{
+ pSysMem: &data[0],
+ }
+ }
+ var buf *Buffer
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateBuffer,
+ 4,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(dataDesc)),
+ uintptr(unsafe.Pointer(&buf)),
+ 0, 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateBuffer", Code: uint32(r)}
+ }
+ return buf, nil
+}
+
+func (d *Device) CreateDepthStencilViewTEX2D(res *Resource,
+ desc *DEPTH_STENCIL_VIEW_DESC_TEX2D) (*DepthStencilView, error) {
+ var view *DepthStencilView
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateDepthStencilView,
+ 4,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(res)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&view)),
+ 0, 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateDepthStencilView",
+ Code: uint32(r)}
+ }
+ return view, nil
+}
+
+func (d *Device) CreatePixelShader(bytecode []byte) (*PixelShader, error) {
+ var shader *PixelShader
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreatePixelShader,
+ 5,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(&bytecode[0])),
+ uintptr(len(bytecode)),
+ 0, // pClassLinkage
+ uintptr(unsafe.Pointer(&shader)),
+ 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreatePixelShader", Code: uint32(r)}
+ }
+ return shader, nil
+}
+
+func (d *Device) CreateVertexShader(bytecode []byte) (*VertexShader, error) {
+ var shader *VertexShader
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateVertexShader,
+ 5,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(&bytecode[0])),
+ uintptr(len(bytecode)),
+ 0, // pClassLinkage
+ uintptr(unsafe.Pointer(&shader)),
+ 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateVertexShader", Code: uint32(r)}
+ }
+ return shader, nil
+}
+
+func (d *Device) CreateShaderResourceViewTEX2D(res *Resource,
+ desc *SHADER_RESOURCE_VIEW_DESC_TEX2D) (*ShaderResourceView, error) {
+ var resView *ShaderResourceView
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateShaderResourceView,
+ 4,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(res)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&resView)),
+ 0, 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateShaderResourceView",
+ Code: uint32(r)}
+ }
+ return resView, nil
+}
+
+func (d *Device) CreateRasterizerState(desc *RASTERIZER_DESC) (*RasterizerState,
+ error) {
+ var state *RasterizerState
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.CreateRasterizerState,
+ 3,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&state)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateRasterizerState",
+ Code: uint32(r)}
+ }
+ return state, nil
+}
+
+func (d *Device) CreateInputLayout(descs []INPUT_ELEMENT_DESC,
+ bytecode []byte) (*InputLayout, error) {
+ var pdesc *INPUT_ELEMENT_DESC
+ if len(descs) > 0 {
+ pdesc = &descs[0]
+ }
+ var layout *InputLayout
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateInputLayout,
+ 6,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(pdesc)),
+ uintptr(len(descs)),
+ uintptr(unsafe.Pointer(&bytecode[0])),
+ uintptr(len(bytecode)),
+ uintptr(unsafe.Pointer(&layout)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateInputLayout", Code: uint32(r)}
+ }
+ return layout, nil
+}
+
+func (d *Device) CreateSamplerState(desc *SAMPLER_DESC) (*SamplerState, error) {
+ var sampler *SamplerState
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.CreateSamplerState,
+ 3,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&sampler)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateSamplerState", Code: uint32(r)}
+ }
+ return sampler, nil
+}
+
+func (d *Device) CreateTexture2D(desc *TEXTURE2D_DESC) (*Texture2D, error) {
+ var tex *Texture2D
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateTexture2D,
+ 4,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(desc)),
+ 0, // pInitialData
+ uintptr(unsafe.Pointer(&tex)),
+ 0, 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "CreateTexture2D", Code: uint32(r)}
+ }
+ return tex, nil
+}
+
+func (d *Device) CreateRenderTargetView(res *Resource) (*RenderTargetView,
+ error) {
+ var target *RenderTargetView
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateRenderTargetView,
+ 4,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(res)),
+ 0, // pDesc
+ uintptr(unsafe.Pointer(&target)),
+ 0, 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateRenderTargetView",
+ Code: uint32(r)}
+ }
+ return target, nil
+}
+
+func (d *Device) CreateBlendState(desc *BLEND_DESC) (*BlendState, error) {
+ var state *BlendState
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.CreateBlendState,
+ 3,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&state)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateBlendState", Code: uint32(r)}
+ }
+ return state, nil
+}
+
+func (d *Device) CreateDepthStencilState(desc *DEPTH_STENCIL_DESC) (*DepthStencilState,
+ error) {
+ var state *DepthStencilState
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.CreateDepthStencilState,
+ 3,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&state)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "DeviceCreateDepthStencilState",
+ Code: uint32(r)}
+ }
+ return state, nil
+}
+
+func (d *Device) GetFeatureLevel() int {
+ lvl, _, _ := syscall.Syscall(
+ d.Vtbl.GetFeatureLevel,
+ 1,
+ uintptr(unsafe.Pointer(d)),
+ 0, 0,
+ )
+ return int(lvl)
+}
+
+func (d *Device) GetImmediateContext() *DeviceContext {
+ var ctx *DeviceContext
+ syscall.Syscall(
+ d.Vtbl.GetImmediateContext,
+ 2,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(&ctx)),
+ 0,
+ )
+ return ctx
+}
+
+func (s *IDXGISwapChain) GetDesc() (DXGI_SWAP_CHAIN_DESC, error) {
+ var desc DXGI_SWAP_CHAIN_DESC
+ r, _, _ := syscall.Syscall(
+ s.Vtbl.GetDesc,
+ 2,
+ uintptr(unsafe.Pointer(s)),
+ uintptr(unsafe.Pointer(&desc)),
+ 0,
+ )
+ if r != 0 {
+ return DXGI_SWAP_CHAIN_DESC{}, ErrorCode{Name: "IDXGISwapChainGetDesc",
+ Code: uint32(r)}
+ }
+ return desc, nil
+}
+
+func (s *IDXGISwapChain) ResizeBuffers(buffers, width, height, newFormat, flags uint32) error {
+ r, _, _ := syscall.Syscall6(
+ s.Vtbl.ResizeBuffers,
+ 6,
+ uintptr(unsafe.Pointer(s)),
+ uintptr(buffers),
+ uintptr(width),
+ uintptr(height),
+ uintptr(newFormat),
+ uintptr(flags),
+ )
+ if r != 0 {
+ return ErrorCode{Name: "IDXGISwapChainResizeBuffers", Code: uint32(r)}
+ }
+ return nil
+}
+
+func (s *IDXGISwapChain) Present(SyncInterval int, Flags uint32) error {
+ r, _, _ := syscall.Syscall(
+ s.Vtbl.Present,
+ 3,
+ uintptr(unsafe.Pointer(s)),
+ uintptr(SyncInterval),
+ uintptr(Flags),
+ )
+ if r != 0 {
+ return ErrorCode{Name: "IDXGISwapChainPresent", Code: uint32(r)}
+ }
+ return nil
+}
+
+func (s *IDXGISwapChain) GetBuffer(index int, riid *GUID) (*IUnknown, error) {
+ var buf *IUnknown
+ r, _, _ := syscall.Syscall6(
+ s.Vtbl.GetBuffer,
+ 4,
+ uintptr(unsafe.Pointer(s)),
+ uintptr(index),
+ uintptr(unsafe.Pointer(riid)),
+ uintptr(unsafe.Pointer(&buf)),
+ 0,
+ 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "IDXGISwapChainGetBuffer", Code: uint32(r)}
+ }
+ return buf, nil
+}
+
+func (c *DeviceContext) Unmap(resource *Resource, subResource uint32) {
+ syscall.Syscall(
+ c.Vtbl.Unmap,
+ 3,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(resource)),
+ uintptr(subResource),
+ )
+}
+
+func (c *DeviceContext) Map(resource *Resource,
+ subResource, mapType, mapFlags uint32) (MAPPED_SUBRESOURCE, error) {
+ var resMap MAPPED_SUBRESOURCE
+ r, _, _ := syscall.Syscall6(
+ c.Vtbl.Map,
+ 6,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(resource)),
+ uintptr(subResource),
+ uintptr(mapType),
+ uintptr(mapFlags),
+ uintptr(unsafe.Pointer(&resMap)),
+ )
+ if r != 0 {
+ return resMap, ErrorCode{Name: "DeviceContextMap", Code: uint32(r)}
+ }
+ return resMap, nil
+}
+
+func (c *DeviceContext) CopySubresourceRegion(dst *Resource,
+ dstSubresource, dstX, dstY, dstZ uint32, src *Resource,
+ srcSubresource uint32, srcBox *BOX) {
+ syscall.Syscall9(
+ c.Vtbl.CopySubresourceRegion,
+ 9,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(dst)),
+ uintptr(dstSubresource),
+ uintptr(dstX),
+ uintptr(dstY),
+ uintptr(dstZ),
+ uintptr(unsafe.Pointer(src)),
+ uintptr(srcSubresource),
+ uintptr(unsafe.Pointer(srcBox)),
+ )
+}
+
+func (c *DeviceContext) ClearDepthStencilView(target *DepthStencilView,
+ flags uint32, depth float32, stencil uint8) {
+ syscall.Syscall6(
+ c.Vtbl.ClearDepthStencilView,
+ 5,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(target)),
+ uintptr(flags),
+ uintptr(math.Float32bits(depth)),
+ uintptr(stencil),
+ 0,
+ )
+}
+
+func (c *DeviceContext) ClearRenderTargetView(target *RenderTargetView,
+ color *[4]float32) {
+ syscall.Syscall(
+ c.Vtbl.ClearRenderTargetView,
+ 3,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(target)),
+ uintptr(unsafe.Pointer(color)),
+ )
+}
+
+func (c *DeviceContext) RSSetViewports(viewport *VIEWPORT) {
+ syscall.Syscall(
+ c.Vtbl.RSSetViewports,
+ 3,
+ uintptr(unsafe.Pointer(c)),
+ 1, // NumViewports
+ uintptr(unsafe.Pointer(viewport)),
+ )
+}
+
+func (c *DeviceContext) VSSetShader(s *VertexShader) {
+ syscall.Syscall6(
+ c.Vtbl.VSSetShader,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(s)),
+ 0, // ppClassInstances
+ 0, // NumClassInstances
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) VSSetConstantBuffers(b *Buffer) {
+ syscall.Syscall6(
+ c.Vtbl.VSSetConstantBuffers,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ 0, // StartSlot
+ 1, // NumBuffers
+ uintptr(unsafe.Pointer(&b)),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) PSSetConstantBuffers(b *Buffer) {
+ syscall.Syscall6(
+ c.Vtbl.PSSetConstantBuffers,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ 0, // StartSlot
+ 1, // NumBuffers
+ uintptr(unsafe.Pointer(&b)),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) PSSetShaderResources(startSlot uint32,
+ s *ShaderResourceView) {
+ syscall.Syscall6(
+ c.Vtbl.PSSetShaderResources,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(startSlot),
+ 1, // NumViews
+ uintptr(unsafe.Pointer(&s)),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) PSSetSamplers(startSlot uint32, s *SamplerState) {
+ syscall.Syscall6(
+ c.Vtbl.PSSetSamplers,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(startSlot),
+ 1, // NumSamplers
+ uintptr(unsafe.Pointer(&s)),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) PSSetShader(s *PixelShader) {
+ syscall.Syscall6(
+ c.Vtbl.PSSetShader,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(s)),
+ 0, // ppClassInstances
+ 0, // NumClassInstances
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) UpdateSubresource(res *Resource, dstBox *BOX,
+ rowPitch, depthPitch uint32, data []byte) {
+ syscall.Syscall9(
+ c.Vtbl.UpdateSubresource,
+ 7,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(res)),
+ 0, // DstSubresource
+ uintptr(unsafe.Pointer(dstBox)),
+ uintptr(unsafe.Pointer(&data[0])),
+ uintptr(rowPitch),
+ uintptr(depthPitch),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) RSSetState(state *RasterizerState) {
+ syscall.Syscall(
+ c.Vtbl.RSSetState,
+ 2,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(state)),
+ 0,
+ )
+}
+
+func (c *DeviceContext) IASetInputLayout(layout *InputLayout) {
+ syscall.Syscall(
+ c.Vtbl.IASetInputLayout,
+ 2,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(layout)),
+ 0,
+ )
+}
+
+func (c *DeviceContext) IASetIndexBuffer(buf *Buffer, format, offset uint32) {
+ syscall.Syscall6(
+ c.Vtbl.IASetIndexBuffer,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(buf)),
+ uintptr(format),
+ uintptr(offset),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) IASetVertexBuffers(buf *Buffer, stride, offset uint32) {
+ syscall.Syscall6(
+ c.Vtbl.IASetVertexBuffers,
+ 6,
+ uintptr(unsafe.Pointer(c)),
+ 0, // StartSlot
+ 1, // NumBuffers,
+ uintptr(unsafe.Pointer(&buf)),
+ uintptr(unsafe.Pointer(&stride)),
+ uintptr(unsafe.Pointer(&offset)),
+ )
+}
+
+func (c *DeviceContext) IASetPrimitiveTopology(mode uint32) {
+ syscall.Syscall(
+ c.Vtbl.IASetPrimitiveTopology,
+ 2,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(mode),
+ 0,
+ )
+}
+
+func (c *DeviceContext) OMGetRenderTargets() (*RenderTargetView,
+ *DepthStencilView) {
+ var (
+ target *RenderTargetView
+ depthStencilView *DepthStencilView
+ )
+ syscall.Syscall6(
+ c.Vtbl.OMGetRenderTargets,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ 1, // NumViews
+ uintptr(unsafe.Pointer(&target)),
+ uintptr(unsafe.Pointer(&depthStencilView)),
+ 0, 0,
+ )
+ return target, depthStencilView
+}
+
+func (c *DeviceContext) OMSetRenderTargets(target *RenderTargetView,
+ depthStencil *DepthStencilView) {
+ syscall.Syscall6(
+ c.Vtbl.OMSetRenderTargets,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ 1, // NumViews
+ uintptr(unsafe.Pointer(&target)),
+ uintptr(unsafe.Pointer(depthStencil)),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) Draw(count, start uint32) {
+ syscall.Syscall(
+ c.Vtbl.Draw,
+ 3,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(count),
+ uintptr(start),
+ )
+}
+
+func (c *DeviceContext) DrawIndexed(count, start uint32, base int32) {
+ syscall.Syscall6(
+ c.Vtbl.DrawIndexed,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(count),
+ uintptr(start),
+ uintptr(base),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) OMSetBlendState(state *BlendState,
+ factor *f32color.RGBA, sampleMask uint32) {
+ syscall.Syscall6(
+ c.Vtbl.OMSetBlendState,
+ 4,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(state)),
+ uintptr(unsafe.Pointer(factor)),
+ uintptr(sampleMask),
+ 0, 0,
+ )
+}
+
+func (c *DeviceContext) OMSetDepthStencilState(state *DepthStencilState,
+ stencilRef uint32) {
+ syscall.Syscall(
+ c.Vtbl.OMSetDepthStencilState,
+ 3,
+ uintptr(unsafe.Pointer(c)),
+ uintptr(unsafe.Pointer(state)),
+ uintptr(stencilRef),
+ )
+}
+
+func (d *IDXGIObject) GetParent(guid *GUID) (*IDXGIObject, error) {
+ var parent *IDXGIObject
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.GetParent,
+ 3,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(guid)),
+ uintptr(unsafe.Pointer(&parent)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "IDXGIObjectGetParent", Code: uint32(r)}
+ }
+ return parent, nil
+}
+
+func (d *IDXGIFactory) CreateSwapChain(device *IUnknown,
+ desc *DXGI_SWAP_CHAIN_DESC) (*IDXGISwapChain, error) {
+ var swchain *IDXGISwapChain
+ r, _, _ := syscall.Syscall6(
+ d.Vtbl.CreateSwapChain,
+ 4,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(device)),
+ uintptr(unsafe.Pointer(desc)),
+ uintptr(unsafe.Pointer(&swchain)),
+ 0, 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "IDXGIFactory", Code: uint32(r)}
+ }
+ return swchain, nil
+}
+
+func (d *IDXGIDevice) GetAdapter() (*IDXGIAdapter, error) {
+ var adapter *IDXGIAdapter
+ r, _, _ := syscall.Syscall(
+ d.Vtbl.GetAdapter,
+ 2,
+ uintptr(unsafe.Pointer(d)),
+ uintptr(unsafe.Pointer(&adapter)),
+ 0,
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "IDXGIDeviceGetAdapter", Code: uint32(r)}
+ }
+ return adapter, nil
+}
+
+func IUnknownQueryInterface(obj unsafe.Pointer, queryInterfaceMethod uintptr,
+ guid *GUID) (*IUnknown, error) {
+ var ref *IUnknown
+ r, _, _ := syscall.Syscall(
+ queryInterfaceMethod,
+ 3,
+ uintptr(obj),
+ uintptr(unsafe.Pointer(guid)),
+ uintptr(unsafe.Pointer(&ref)),
+ )
+ if r != 0 {
+ return nil, ErrorCode{Name: "IUnknownQueryInterface", Code: uint32(r)}
+ }
+ return ref, nil
+}
+
+func IUnknownRelease(obj unsafe.Pointer, releaseMethod uintptr) {
+ syscall.Syscall(
+ releaseMethod,
+ 1,
+ uintptr(obj),
+ 0,
+ 0,
+ )
+}
+
+func (e ErrorCode) Error() string {
+ return fmt.Sprintf("%s: %#x", e.Name, e.Code)
+}
+
+func CreateSwapChain(dev *Device, hwnd windows.Handle) (*IDXGISwapChain,
+ error) {
+ dxgiDev, err := IUnknownQueryInterface(unsafe.Pointer(dev),
+ dev.Vtbl.QueryInterface, &IID_IDXGIDevice)
+ if err != nil {
+ return nil, fmt.Errorf("NewContext: %v", err)
+ }
+ adapter, err := (*IDXGIDevice)(unsafe.Pointer(dxgiDev)).GetAdapter()
+ IUnknownRelease(unsafe.Pointer(dxgiDev), dxgiDev.Vtbl.Release)
+ if err != nil {
+ return nil, fmt.Errorf("NewContext: %v", err)
+ }
+ dxgiFactory, err := (*IDXGIObject)(unsafe.Pointer(adapter)).GetParent(&IID_IDXGIFactory)
+ IUnknownRelease(unsafe.Pointer(adapter), adapter.Vtbl.Release)
+ if err != nil {
+ return nil, fmt.Errorf("NewContext: %v", err)
+ }
+ swchain, err := (*IDXGIFactory)(unsafe.Pointer(dxgiFactory)).CreateSwapChain(
+ (*IUnknown)(unsafe.Pointer(dev)),
+ &DXGI_SWAP_CHAIN_DESC{
+ BufferDesc: DXGI_MODE_DESC{
+ Format: DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
+ },
+ SampleDesc: DXGI_SAMPLE_DESC{
+ Count: 1,
+ },
+ BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
+ BufferCount: 1,
+ OutputWindow: hwnd,
+ Windowed: 1,
+ SwapEffect: DXGI_SWAP_EFFECT_DISCARD,
+ },
+ )
+ IUnknownRelease(unsafe.Pointer(dxgiFactory), dxgiFactory.Vtbl.Release)
+ if err != nil {
+ return nil, fmt.Errorf("NewContext: %v", err)
+ }
+ return swchain, nil
+}
+
+func CreateDepthView(d *Device,
+ width, height, depthBits int) (*DepthStencilView, error) {
+ depthTex, err := d.CreateTexture2D(&TEXTURE2D_DESC{
+ Width: uint32(width),
+ Height: uint32(height),
+ MipLevels: 1,
+ ArraySize: 1,
+ Format: DXGI_FORMAT_D24_UNORM_S8_UINT,
+ SampleDesc: DXGI_SAMPLE_DESC{
+ Count: 1,
+ Quality: 0,
+ },
+ BindFlags: BIND_DEPTH_STENCIL,
+ })
+ if err != nil {
+ return nil, err
+ }
+ depthView, err := d.CreateDepthStencilViewTEX2D(
+ (*Resource)(unsafe.Pointer(depthTex)),
+ &DEPTH_STENCIL_VIEW_DESC_TEX2D{
+ Format: DXGI_FORMAT_D24_UNORM_S8_UINT,
+ ViewDimension: DSV_DIMENSION_TEXTURE2D,
+ },
+ )
+ IUnknownRelease(unsafe.Pointer(depthTex), depthTex.Vtbl.Release)
+ return depthView, err
+}
diff --git a/gio/internal/egl/egl.go b/gio/internal/egl/egl.go
new file mode 100644
index 0000000..5a23650
--- /dev/null
+++ b/gio/internal/egl/egl.go
@@ -0,0 +1,293 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build linux || windows || freebsd || openbsd
+// +build linux windows freebsd openbsd
+
+package egl
+
+import (
+ "errors"
+ "fmt"
+ "runtime"
+ "strings"
+
+ "realy.lol/gio/gpu"
+ "realy.lol/gio/internal/gl"
+ "realy.lol/gio/internal/srgb"
+)
+
+type Context struct {
+ c *gl.Functions
+ disp _EGLDisplay
+ eglCtx *eglContext
+ eglSurf _EGLSurface
+ width, height int
+ refreshFBO bool
+ // For sRGB emulation.
+ srgbFBO *srgb.FBO
+}
+
+type eglContext struct {
+ config _EGLConfig
+ ctx _EGLContext
+ visualID int
+ srgb bool
+ surfaceless bool
+}
+
+var (
+ nilEGLDisplay _EGLDisplay
+ nilEGLSurface _EGLSurface
+ nilEGLContext _EGLContext
+ nilEGLConfig _EGLConfig
+ EGL_DEFAULT_DISPLAY NativeDisplayType
+)
+
+const (
+ _EGL_ALPHA_SIZE = 0x3021
+ _EGL_BLUE_SIZE = 0x3022
+ _EGL_CONFIG_CAVEAT = 0x3027
+ _EGL_CONTEXT_CLIENT_VERSION = 0x3098
+ _EGL_DEPTH_SIZE = 0x3025
+ _EGL_GL_COLORSPACE_KHR = 0x309d
+ _EGL_GL_COLORSPACE_SRGB_KHR = 0x3089
+ _EGL_GREEN_SIZE = 0x3023
+ _EGL_EXTENSIONS = 0x3055
+ _EGL_NATIVE_VISUAL_ID = 0x302e
+ _EGL_NONE = 0x3038
+ _EGL_OPENGL_ES2_BIT = 0x4
+ _EGL_RED_SIZE = 0x3024
+ _EGL_RENDERABLE_TYPE = 0x3040
+ _EGL_SURFACE_TYPE = 0x3033
+ _EGL_WINDOW_BIT = 0x4
+)
+
+func (c *Context) Release() {
+ if c.srgbFBO != nil {
+ c.srgbFBO.Release()
+ c.srgbFBO = nil
+ }
+ c.ReleaseSurface()
+ if c.eglCtx != nil {
+ eglDestroyContext(c.disp, c.eglCtx.ctx)
+ c.eglCtx = nil
+ }
+ c.disp = nilEGLDisplay
+}
+
+func (c *Context) Present() error {
+ if c.srgbFBO != nil {
+ c.srgbFBO.Blit()
+ }
+ if !eglSwapBuffers(c.disp, c.eglSurf) {
+ return fmt.Errorf("eglSwapBuffers failed (%x)", eglGetError())
+ }
+ if c.srgbFBO != nil {
+ c.srgbFBO.AfterPresent()
+ }
+ return nil
+}
+
+func NewContext(disp NativeDisplayType) (*Context, error) {
+ if err := loadEGL(); err != nil {
+ return nil, err
+ }
+ eglDisp := eglGetDisplay(disp)
+ // eglGetDisplay can return EGL_NO_DISPLAY yet no error
+ // (EGL_SUCCESS), in which case a default EGL display might be
+ // available.
+ if eglDisp == nilEGLDisplay {
+ eglDisp = eglGetDisplay(EGL_DEFAULT_DISPLAY)
+ }
+ if eglDisp == nilEGLDisplay {
+ return nil, fmt.Errorf("eglGetDisplay failed: 0x%x", eglGetError())
+ }
+ eglCtx, err := createContext(eglDisp)
+ if err != nil {
+ return nil, err
+ }
+ f, err := gl.NewFunctions(nil)
+ if err != nil {
+ return nil, err
+ }
+ c := &Context{
+ disp: eglDisp,
+ eglCtx: eglCtx,
+ c: f,
+ }
+ return c, nil
+}
+
+func (c *Context) API() gpu.API {
+ return gpu.OpenGL{}
+}
+
+func (c *Context) ReleaseSurface() {
+ if c.eglSurf == nilEGLSurface {
+ return
+ }
+ // Make sure any in-flight GL commands are complete.
+ c.c.Finish()
+ c.ReleaseCurrent()
+ eglDestroySurface(c.disp, c.eglSurf)
+ c.eglSurf = nilEGLSurface
+}
+
+func (c *Context) VisualID() int {
+ return c.eglCtx.visualID
+}
+
+func (c *Context) CreateSurface(win NativeWindowType, width, height int) error {
+ eglSurf, err := createSurface(c.disp, c.eglCtx, win)
+ c.eglSurf = eglSurf
+ c.width = width
+ c.height = height
+ c.refreshFBO = true
+ return err
+}
+
+func (c *Context) ReleaseCurrent() {
+ if c.disp != nilEGLDisplay {
+ eglMakeCurrent(c.disp, nilEGLSurface, nilEGLSurface, nilEGLContext)
+ }
+}
+
+func (c *Context) MakeCurrent() error {
+ if c.eglSurf == nilEGLSurface && !c.eglCtx.surfaceless {
+ return errors.New("no surface created yet EGL_KHR_surfaceless_context is not supported")
+ }
+ if !eglMakeCurrent(c.disp, c.eglSurf, c.eglSurf, c.eglCtx.ctx) {
+ return fmt.Errorf("eglMakeCurrent error 0x%x", eglGetError())
+ }
+ if c.eglCtx.srgb || c.eglSurf == nilEGLSurface {
+ return nil
+ }
+ if c.srgbFBO == nil {
+ var err error
+ c.srgbFBO, err = srgb.New(nil)
+ if err != nil {
+ c.ReleaseCurrent()
+ return err
+ }
+ }
+ if c.refreshFBO {
+ c.refreshFBO = false
+ if err := c.srgbFBO.Refresh(c.width, c.height); err != nil {
+ c.ReleaseCurrent()
+ return err
+ }
+ }
+ return nil
+}
+
+func (c *Context) EnableVSync(enable bool) {
+ if enable {
+ eglSwapInterval(c.disp, 1)
+ } else {
+ eglSwapInterval(c.disp, 0)
+ }
+}
+
+func hasExtension(exts []string, ext string) bool {
+ for _, e := range exts {
+ if ext == e {
+ return true
+ }
+ }
+ return false
+}
+
+func createContext(disp _EGLDisplay) (*eglContext, error) {
+ major, minor, ret := eglInitialize(disp)
+ if !ret {
+ return nil, fmt.Errorf("eglInitialize failed: 0x%x", eglGetError())
+ }
+ // sRGB framebuffer support on EGL 1.5 or if EGL_KHR_gl_colorspace is supported.
+ exts := strings.Split(eglQueryString(disp, _EGL_EXTENSIONS), " ")
+ srgb := major > 1 || minor >= 5 || hasExtension(exts,
+ "EGL_KHR_gl_colorspace")
+ attribs := []_EGLint{
+ _EGL_RENDERABLE_TYPE, _EGL_OPENGL_ES2_BIT,
+ _EGL_SURFACE_TYPE, _EGL_WINDOW_BIT,
+ _EGL_BLUE_SIZE, 8,
+ _EGL_GREEN_SIZE, 8,
+ _EGL_RED_SIZE, 8,
+ _EGL_CONFIG_CAVEAT, _EGL_NONE,
+ }
+ if srgb {
+ if runtime.GOOS == "linux" || runtime.GOOS == "android" {
+ // Some Mesa drivers crash if an sRGB framebuffer is requested without alpha.
+ // https://bugs.freedesktop.org/show_bug.cgi?id=107782.
+ //
+ // Also, some Android devices (Samsung S9) needs alpha for sRGB to work.
+ attribs = append(attribs, _EGL_ALPHA_SIZE, 8)
+ }
+ // Only request a depth buffer if we're going to render directly to the framebuffer.
+ attribs = append(attribs, _EGL_DEPTH_SIZE, 16)
+ }
+ attribs = append(attribs, _EGL_NONE)
+ eglCfg, ret := eglChooseConfig(disp, attribs)
+ if !ret {
+ return nil, fmt.Errorf("eglChooseConfig failed: 0x%x", eglGetError())
+ }
+ if eglCfg == nilEGLConfig {
+ supportsNoCfg := hasExtension(exts, "EGL_KHR_no_config_context")
+ if !supportsNoCfg {
+ return nil, errors.New("eglChooseConfig returned no configs")
+ }
+ }
+ var visID _EGLint
+ if eglCfg != nilEGLConfig {
+ var ok bool
+ visID, ok = eglGetConfigAttrib(disp, eglCfg, _EGL_NATIVE_VISUAL_ID)
+ if !ok {
+ return nil, errors.New("newContext: eglGetConfigAttrib for _EGL_NATIVE_VISUAL_ID failed")
+ }
+ }
+ ctxAttribs := []_EGLint{
+ _EGL_CONTEXT_CLIENT_VERSION, 3,
+ _EGL_NONE,
+ }
+ eglCtx := eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs)
+ if eglCtx == nilEGLContext {
+ // Fall back to OpenGL ES 2 and rely on extensions.
+ ctxAttribs := []_EGLint{
+ _EGL_CONTEXT_CLIENT_VERSION, 2,
+ _EGL_NONE,
+ }
+ eglCtx = eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs)
+ if eglCtx == nilEGLContext {
+ return nil, fmt.Errorf("eglCreateContext failed: 0x%x",
+ eglGetError())
+ }
+ }
+ return &eglContext{
+ config: _EGLConfig(eglCfg),
+ ctx: _EGLContext(eglCtx),
+ visualID: int(visID),
+ srgb: srgb,
+ surfaceless: hasExtension(exts, "EGL_KHR_surfaceless_context"),
+ }, nil
+}
+
+func createSurface(disp _EGLDisplay, eglCtx *eglContext,
+ win NativeWindowType) (_EGLSurface, error) {
+ var surfAttribs []_EGLint
+ if eglCtx.srgb {
+ surfAttribs = append(surfAttribs, _EGL_GL_COLORSPACE_KHR,
+ _EGL_GL_COLORSPACE_SRGB_KHR)
+ }
+ surfAttribs = append(surfAttribs, _EGL_NONE)
+ eglSurf := eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs)
+ if eglSurf == nilEGLSurface && eglCtx.srgb {
+ // Try again without sRGB
+ eglCtx.srgb = false
+ surfAttribs = []_EGLint{_EGL_NONE}
+ eglSurf = eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs)
+ }
+ if eglSurf == nilEGLSurface {
+ return nilEGLSurface, fmt.Errorf("newContext: eglCreateWindowSurface failed 0x%x (sRGB=%v)",
+ eglGetError(), eglCtx.srgb)
+ }
+ return eglSurf, nil
+}
diff --git a/gio/internal/egl/egl_unix.go b/gio/internal/egl/egl_unix.go
new file mode 100644
index 0000000..059dd55
--- /dev/null
+++ b/gio/internal/egl/egl_unix.go
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build linux freebsd openbsd
+
+package egl
+
+/*
+#cgo linux,!android pkg-config: egl
+#cgo freebsd openbsd android LDFLAGS: -lEGL
+#cgo freebsd CFLAGS: -I/usr/local/include
+#cgo freebsd LDFLAGS: -L/usr/local/lib
+#cgo openbsd CFLAGS: -I/usr/X11R6/include
+#cgo openbsd LDFLAGS: -L/usr/X11R6/lib
+#cgo CFLAGS: -DEGL_NO_X11
+
+#include
+#include
+*/
+import "C"
+
+type (
+ _EGLint = C.EGLint
+ _EGLDisplay = C.EGLDisplay
+ _EGLConfig = C.EGLConfig
+ _EGLContext = C.EGLContext
+ _EGLSurface = C.EGLSurface
+ NativeDisplayType = C.EGLNativeDisplayType
+ NativeWindowType = C.EGLNativeWindowType
+)
+
+func loadEGL() error {
+ return nil
+}
+
+func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) {
+ var cfg C.EGLConfig
+ var ncfg C.EGLint
+ if C.eglChooseConfig(disp, &attribs[0], &cfg, 1, &ncfg) != C.EGL_TRUE {
+ return nilEGLConfig, false
+ }
+ return _EGLConfig(cfg), true
+}
+
+func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, attribs []_EGLint) _EGLContext {
+ ctx := C.eglCreateContext(disp, cfg, shareCtx, &attribs[0])
+ return _EGLContext(ctx)
+}
+
+func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool {
+ return C.eglDestroySurface(disp, surf) == C.EGL_TRUE
+}
+
+func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool {
+ return C.eglDestroyContext(disp, ctx) == C.EGL_TRUE
+}
+
+func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, attr _EGLint) (_EGLint, bool) {
+ var val _EGLint
+ ret := C.eglGetConfigAttrib(disp, cfg, attr, &val)
+ return val, ret == C.EGL_TRUE
+}
+
+func eglGetError() _EGLint {
+ return C.eglGetError()
+}
+
+func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) {
+ var maj, min _EGLint
+ ret := C.eglInitialize(disp, &maj, &min)
+ return maj, min, ret == C.EGL_TRUE
+}
+
+func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, ctx _EGLContext) bool {
+ return C.eglMakeCurrent(disp, draw, read, ctx) == C.EGL_TRUE
+}
+
+func eglReleaseThread() bool {
+ return C.eglReleaseThread() == C.EGL_TRUE
+}
+
+func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool {
+ return C.eglSwapBuffers(disp, surf) == C.EGL_TRUE
+}
+
+func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool {
+ return C.eglSwapInterval(disp, interval) == C.EGL_TRUE
+}
+
+func eglTerminate(disp _EGLDisplay) bool {
+ return C.eglTerminate(disp) == C.EGL_TRUE
+}
+
+func eglQueryString(disp _EGLDisplay, name _EGLint) string {
+ return C.GoString(C.eglQueryString(disp, name))
+}
+
+func eglGetDisplay(disp NativeDisplayType) _EGLDisplay {
+ return C.eglGetDisplay(disp)
+}
+
+func eglCreateWindowSurface(disp _EGLDisplay, conf _EGLConfig, win NativeWindowType, attribs []_EGLint) _EGLSurface {
+ eglSurf := C.eglCreateWindowSurface(disp, conf, win, &attribs[0])
+ return eglSurf
+}
diff --git a/gio/internal/egl/egl_windows.go b/gio/internal/egl/egl_windows.go
new file mode 100644
index 0000000..5df5c65
--- /dev/null
+++ b/gio/internal/egl/egl_windows.go
@@ -0,0 +1,174 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package egl
+
+import (
+ "fmt"
+ "runtime"
+ "sync"
+ "unsafe"
+
+ syscall "golang.org/x/sys/windows"
+
+ "realy.lol/gio/internal/gl"
+)
+
+type (
+ _EGLint int32
+ _EGLDisplay uintptr
+ _EGLConfig uintptr
+ _EGLContext uintptr
+ _EGLSurface uintptr
+ NativeDisplayType uintptr
+ NativeWindowType uintptr
+)
+
+var (
+ libEGL = syscall.NewLazyDLL("libEGL.dll")
+ _eglChooseConfig = libEGL.NewProc("eglChooseConfig")
+ _eglCreateContext = libEGL.NewProc("eglCreateContext")
+ _eglCreateWindowSurface = libEGL.NewProc("eglCreateWindowSurface")
+ _eglDestroyContext = libEGL.NewProc("eglDestroyContext")
+ _eglDestroySurface = libEGL.NewProc("eglDestroySurface")
+ _eglGetConfigAttrib = libEGL.NewProc("eglGetConfigAttrib")
+ _eglGetDisplay = libEGL.NewProc("eglGetDisplay")
+ _eglGetError = libEGL.NewProc("eglGetError")
+ _eglInitialize = libEGL.NewProc("eglInitialize")
+ _eglMakeCurrent = libEGL.NewProc("eglMakeCurrent")
+ _eglReleaseThread = libEGL.NewProc("eglReleaseThread")
+ _eglSwapInterval = libEGL.NewProc("eglSwapInterval")
+ _eglSwapBuffers = libEGL.NewProc("eglSwapBuffers")
+ _eglTerminate = libEGL.NewProc("eglTerminate")
+ _eglQueryString = libEGL.NewProc("eglQueryString")
+)
+
+var loadOnce sync.Once
+
+func loadEGL() error {
+ var err error
+ loadOnce.Do(func() {
+ err = loadDLLs()
+ })
+ return err
+}
+
+func loadDLLs() error {
+ if err := loadDLL(libEGL, "libEGL.dll"); err != nil {
+ return err
+ }
+ if err := loadDLL(gl.LibGLESv2, "libGLESv2.dll"); err != nil {
+ return err
+ }
+ // d3dcompiler_47.dll is needed internally for shader compilation to function.
+ return loadDLL(syscall.NewLazyDLL("d3dcompiler_47.dll"),
+ "d3dcompiler_47.dll")
+}
+
+func loadDLL(dll *syscall.LazyDLL, name string) error {
+ err := dll.Load()
+ if err != nil {
+ return fmt.Errorf("egl: failed to load %s: %v", name, err)
+ }
+ return nil
+}
+
+func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) {
+ var cfg _EGLConfig
+ var ncfg _EGLint
+ a := &attribs[0]
+ r, _, _ := _eglChooseConfig.Call(uintptr(disp), uintptr(unsafe.Pointer(a)),
+ uintptr(unsafe.Pointer(&cfg)), 1, uintptr(unsafe.Pointer(&ncfg)))
+ issue34474KeepAlive(a)
+ return cfg, r != 0
+}
+
+func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext,
+ attribs []_EGLint) _EGLContext {
+ a := &attribs[0]
+ c, _, _ := _eglCreateContext.Call(uintptr(disp), uintptr(cfg),
+ uintptr(shareCtx), uintptr(unsafe.Pointer(a)))
+ issue34474KeepAlive(a)
+ return _EGLContext(c)
+}
+
+func eglCreateWindowSurface(disp _EGLDisplay, cfg _EGLConfig,
+ win NativeWindowType, attribs []_EGLint) _EGLSurface {
+ a := &attribs[0]
+ s, _, _ := _eglCreateWindowSurface.Call(uintptr(disp), uintptr(cfg),
+ uintptr(win), uintptr(unsafe.Pointer(a)))
+ issue34474KeepAlive(a)
+ return _EGLSurface(s)
+}
+
+func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool {
+ r, _, _ := _eglDestroySurface.Call(uintptr(disp), uintptr(surf))
+ return r != 0
+}
+
+func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool {
+ r, _, _ := _eglDestroyContext.Call(uintptr(disp), uintptr(ctx))
+ return r != 0
+}
+
+func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig,
+ attr _EGLint) (_EGLint, bool) {
+ var val uintptr
+ r, _, _ := _eglGetConfigAttrib.Call(uintptr(disp), uintptr(cfg),
+ uintptr(attr), uintptr(unsafe.Pointer(&val)))
+ return _EGLint(val), r != 0
+}
+
+func eglGetDisplay(disp NativeDisplayType) _EGLDisplay {
+ d, _, _ := _eglGetDisplay.Call(uintptr(disp))
+ return _EGLDisplay(d)
+}
+
+func eglGetError() _EGLint {
+ e, _, _ := _eglGetError.Call()
+ return _EGLint(e)
+}
+
+func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) {
+ var maj, min uintptr
+ r, _, _ := _eglInitialize.Call(uintptr(disp), uintptr(unsafe.Pointer(&maj)),
+ uintptr(unsafe.Pointer(&min)))
+ return _EGLint(maj), _EGLint(min), r != 0
+}
+
+func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface,
+ ctx _EGLContext) bool {
+ r, _, _ := _eglMakeCurrent.Call(uintptr(disp), uintptr(draw), uintptr(read),
+ uintptr(ctx))
+ return r != 0
+}
+
+func eglReleaseThread() bool {
+ r, _, _ := _eglReleaseThread.Call()
+ return r != 0
+}
+
+func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool {
+ r, _, _ := _eglSwapInterval.Call(uintptr(disp), uintptr(interval))
+ return r != 0
+}
+
+func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool {
+ r, _, _ := _eglSwapBuffers.Call(uintptr(disp), uintptr(surf))
+ return r != 0
+}
+
+func eglTerminate(disp _EGLDisplay) bool {
+ r, _, _ := _eglTerminate.Call(uintptr(disp))
+ return r != 0
+}
+
+func eglQueryString(disp _EGLDisplay, name _EGLint) string {
+ r, _, _ := _eglQueryString.Call(uintptr(disp), uintptr(name))
+ return syscall.BytePtrToString((*byte)(unsafe.Pointer(r)))
+}
+
+// issue34474KeepAlive calls runtime.KeepAlive as a
+// workaround for golang.org/issue/34474.
+func issue34474KeepAlive(v interface{}) {
+ runtime.KeepAlive(v)
+}
diff --git a/gio/internal/f32color/rgba.go b/gio/internal/f32color/rgba.go
new file mode 100644
index 0000000..eecf018
--- /dev/null
+++ b/gio/internal/f32color/rgba.go
@@ -0,0 +1,177 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package f32color
+
+import (
+ "image/color"
+ "math"
+)
+
+// RGBA is a 32 bit floating point linear premultiplied color space.
+type RGBA struct {
+ R, G, B, A float32
+}
+
+// Array returns rgba values in a [4]float32 array.
+func (rgba RGBA) Array() [4]float32 {
+ return [4]float32{rgba.R, rgba.G, rgba.B, rgba.A}
+}
+
+// Float32 returns r, g, b, a values.
+func (col RGBA) Float32() (r, g, b, a float32) {
+ return col.R, col.G, col.B, col.A
+}
+
+// SRGBA converts from linear to sRGB color space.
+func (col RGBA) SRGB() color.NRGBA {
+ if col.A == 0 {
+ return color.NRGBA{}
+ }
+ return color.NRGBA{
+ R: uint8(linearTosRGB(col.R/col.A)*255 + .5),
+ G: uint8(linearTosRGB(col.G/col.A)*255 + .5),
+ B: uint8(linearTosRGB(col.B/col.A)*255 + .5),
+ A: uint8(col.A*255 + .5),
+ }
+}
+
+// Luminance calculates the relative luminance of a linear RGBA color.
+// Normalized to 0 for black and 1 for white.
+//
+// See https://www.w3.org/TR/WCAG20/#relativeluminancedef for more details
+func (col RGBA) Luminance() float32 {
+ return 0.2126*col.R + 0.7152*col.G + 0.0722*col.B
+}
+
+// Opaque returns the color without alpha component.
+func (col RGBA) Opaque() RGBA {
+ col.A = 1.0
+ return col
+}
+
+// LinearFromSRGB converts from col in the sRGB colorspace to RGBA.
+func LinearFromSRGB(col color.NRGBA) RGBA {
+ af := float32(col.A) / 0xFF
+ return RGBA{
+ R: sRGBToLinear(float32(col.R)/0xff) * af,
+ G: sRGBToLinear(float32(col.G)/0xff) * af,
+ B: sRGBToLinear(float32(col.B)/0xff) * af,
+ A: af,
+ }
+}
+
+// NRGBAToRGBA converts from non-premultiplied sRGB color to premultiplied sRGB color.
+//
+// Each component in the result is `sRGBToLinear(c * alpha)`, where `c`
+// is the linear color.
+func NRGBAToRGBA(col color.NRGBA) color.RGBA {
+ if col.A == 0xFF {
+ return color.RGBA(col)
+ }
+ c := LinearFromSRGB(col)
+ return color.RGBA{
+ R: uint8(linearTosRGB(c.R)*255 + .5),
+ G: uint8(linearTosRGB(c.G)*255 + .5),
+ B: uint8(linearTosRGB(c.B)*255 + .5),
+ A: col.A,
+ }
+}
+
+// NRGBAToLinearRGBA converts from non-premultiplied sRGB color to premultiplied linear RGBA color.
+//
+// Each component in the result is `c * alpha`, where `c` is the linear color.
+func NRGBAToLinearRGBA(col color.NRGBA) color.RGBA {
+ if col.A == 0xFF {
+ return color.RGBA(col)
+ }
+ c := LinearFromSRGB(col)
+ return color.RGBA{
+ R: uint8(c.R*255 + .5),
+ G: uint8(c.G*255 + .5),
+ B: uint8(c.B*255 + .5),
+ A: col.A,
+ }
+}
+
+// RGBAToNRGBA converts from premultiplied sRGB color to non-premultiplied sRGB color.
+func RGBAToNRGBA(col color.RGBA) color.NRGBA {
+ if col.A == 0xFF {
+ return color.NRGBA(col)
+ }
+
+ linear := RGBA{
+ R: sRGBToLinear(float32(col.R) / 0xff),
+ G: sRGBToLinear(float32(col.G) / 0xff),
+ B: sRGBToLinear(float32(col.B) / 0xff),
+ A: float32(col.A) / 0xff,
+ }
+
+ return linear.SRGB()
+}
+
+// linearTosRGB transforms color value from linear to sRGB.
+func linearTosRGB(c float32) float32 {
+ // Formula from EXT_sRGB.
+ switch {
+ case c <= 0:
+ return 0
+ case 0 < c && c < 0.0031308:
+ return 12.92 * c
+ case 0.0031308 <= c && c < 1:
+ return 1.055*float32(math.Pow(float64(c), 0.41666)) - 0.055
+ }
+
+ return 1
+}
+
+// sRGBToLinear transforms color value from sRGB to linear.
+func sRGBToLinear(c float32) float32 {
+ // Formula from EXT_sRGB.
+ if c <= 0.04045 {
+ return c / 12.92
+ } else {
+ return float32(math.Pow(float64((c+0.055)/1.055), 2.4))
+ }
+}
+
+// MulAlpha applies the alpha to the color.
+func MulAlpha(c color.NRGBA, alpha uint8) color.NRGBA {
+ c.A = uint8(uint32(c.A) * uint32(alpha) / 0xFF)
+ return c
+}
+
+// Disabled blends color towards the luminance and multiplies alpha.
+// Blending towards luminance will desaturate the color.
+// Multiplying alpha blends the color together more with the background.
+func Disabled(c color.NRGBA) (d color.NRGBA) {
+ const r = 80 // blend ratio
+ lum := approxLuminance(c)
+ return color.NRGBA{
+ R: byte((int(c.R)*r + int(lum)*(256-r)) / 256),
+ G: byte((int(c.G)*r + int(lum)*(256-r)) / 256),
+ B: byte((int(c.B)*r + int(lum)*(256-r)) / 256),
+ A: byte(int(c.A) * (128 + 32) / 256),
+ }
+}
+
+// Hovered blends color towards a brighter color.
+func Hovered(c color.NRGBA) (d color.NRGBA) {
+ const r = 0x20 // lighten ratio
+ return color.NRGBA{
+ R: byte(255 - int(255-c.R)*(255-r)/256),
+ G: byte(255 - int(255-c.G)*(255-r)/256),
+ B: byte(255 - int(255-c.B)*(255-r)/256),
+ A: c.A,
+ }
+}
+
+// approxLuminance is a fast approximate version of RGBA.Luminance.
+func approxLuminance(c color.NRGBA) byte {
+ const (
+ r = 13933 // 0.2126 * 256 * 256
+ g = 46871 // 0.7152 * 256 * 256
+ b = 4732 // 0.0722 * 256 * 256
+ t = r + g + b
+ )
+ return byte((r*int(c.R) + g*int(c.G) + b*int(c.B)) / t)
+}
diff --git a/gio/internal/f32color/rgba_test.go b/gio/internal/f32color/rgba_test.go
new file mode 100644
index 0000000..ea0f871
--- /dev/null
+++ b/gio/internal/f32color/rgba_test.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package f32color
+
+import (
+ "image/color"
+ "testing"
+)
+
+func TestNRGBAToLinearRGBA_Boundary(t *testing.T) {
+ for col := 0; col <= 0xFF; col++ {
+ for alpha := 0; alpha <= 0xFF; alpha++ {
+ in := color.NRGBA{R: uint8(col), A: uint8(alpha)}
+ premul := NRGBAToLinearRGBA(in)
+ if premul.A != uint8(alpha) {
+ t.Errorf("%v: got %v expected %v", in, premul.A, alpha)
+ }
+ if premul.R > premul.A {
+ t.Errorf("%v: R=%v > A=%v", in, premul.R, premul.A)
+ }
+ }
+ }
+}
+
+func TestLinearToRGBARoundtrip(t *testing.T) {
+ for col := 0; col <= 0xFF; col++ {
+ for alpha := 0; alpha <= 0xFF; alpha++ {
+ want := color.NRGBA{R: uint8(col), A: uint8(alpha)}
+ if alpha == 0 {
+ want.R = 0
+ }
+ got := LinearFromSRGB(want).SRGB()
+ if want != got {
+ t.Errorf("got %v expected %v", got, want)
+ }
+ }
+ }
+}
diff --git a/gio/internal/fling/animation.go b/gio/internal/fling/animation.go
new file mode 100644
index 0000000..82a2b8e
--- /dev/null
+++ b/gio/internal/fling/animation.go
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package fling
+
+import (
+ "math"
+ "runtime"
+ "time"
+
+ "realy.lol/gio/unit"
+)
+
+type Animation struct {
+ // Current offset in pixels.
+ x float32
+ // Initial time.
+ t0 time.Time
+ // Initial velocity in pixels pr second.
+ v0 float32
+}
+
+var (
+ // Pixels/second.
+ minFlingVelocity = unit.Dp(50)
+ maxFlingVelocity = unit.Dp(8000)
+)
+
+const (
+ thresholdVelocity = 1
+)
+
+// Start a fling given a starting velocity. Returns whether a
+// fling was started.
+func (f *Animation) Start(c unit.Metric, now time.Time, velocity float32) bool {
+ min := float32(c.Px(minFlingVelocity))
+ v := velocity
+ if -min <= v && v <= min {
+ return false
+ }
+ max := float32(c.Px(maxFlingVelocity))
+ if v > max {
+ v = max
+ } else if v < -max {
+ v = -max
+ }
+ f.init(now, v)
+ return true
+}
+
+func (f *Animation) init(now time.Time, v0 float32) {
+ f.t0 = now
+ f.v0 = v0
+ f.x = 0
+}
+
+func (f *Animation) Active() bool {
+ return f.v0 != 0
+}
+
+// Tick computes and returns a fling distance since
+// the last time Tick was called.
+func (f *Animation) Tick(now time.Time) int {
+ if !f.Active() {
+ return 0
+ }
+ var k float32
+ if runtime.GOOS == "darwin" {
+ k = -2 // iOS
+ } else {
+ k = -4.2 // Android and default
+ }
+ t := now.Sub(f.t0)
+ // The acceleration x''(t) of a point mass with a drag
+ // force, f, proportional with velocity, x'(t), is
+ // governed by the equation
+ //
+ // x''(t) = kx'(t)
+ //
+ // Given the starting position x(0) = 0, the starting
+ // velocity x'(0) = v0, the position is then
+ // given by
+ //
+ // x(t) = v0*e^(k*t)/k - v0/k
+ //
+ ekt := float32(math.Exp(float64(k) * t.Seconds()))
+ x := f.v0*ekt/k - f.v0/k
+ dist := x - f.x
+ idist := int(dist)
+ f.x += float32(idist)
+ // Solving for the velocity x'(t) gives us
+ //
+ // x'(t) = v0*e^(k*t)
+ v := f.v0 * ekt
+ if -thresholdVelocity < v && v < thresholdVelocity {
+ f.v0 = 0
+ }
+ return idist
+}
diff --git a/gio/internal/fling/extrapolation.go b/gio/internal/fling/extrapolation.go
new file mode 100644
index 0000000..655ef84
--- /dev/null
+++ b/gio/internal/fling/extrapolation.go
@@ -0,0 +1,332 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package fling
+
+import (
+ "math"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// Extrapolation computes a 1-dimensional velocity estimate
+// for a set of timestamped points using the least squares
+// fit of a 2nd order polynomial. The same method is used
+// by Android.
+type Extrapolation struct {
+ // Index into points.
+ idx int
+ // Circular buffer of samples.
+ samples []sample
+ lastValue float32
+ // Pre-allocated cache for samples.
+ cache [historySize]sample
+
+ // Filtered values and times
+ values [historySize]float32
+ times [historySize]float32
+}
+
+type sample struct {
+ t time.Duration
+ v float32
+}
+
+type matrix struct {
+ rows, cols int
+ data []float32
+}
+
+type Estimate struct {
+ Velocity float32
+ Distance float32
+}
+
+type coefficients [degree + 1]float32
+
+const (
+ degree = 2
+ historySize = 20
+ maxAge = 100 * time.Millisecond
+ maxSampleGap = 40 * time.Millisecond
+)
+
+// SampleDelta adds a relative sample to the estimation.
+func (e *Extrapolation) SampleDelta(t time.Duration, delta float32) {
+ val := delta + e.lastValue
+ e.Sample(t, val)
+}
+
+// Sample adds an absolute sample to the estimation.
+func (e *Extrapolation) Sample(t time.Duration, val float32) {
+ e.lastValue = val
+ if e.samples == nil {
+ e.samples = e.cache[:0]
+ }
+ s := sample{
+ t: t,
+ v: val,
+ }
+ if e.idx == len(e.samples) && e.idx < cap(e.samples) {
+ e.samples = append(e.samples, s)
+ } else {
+ e.samples[e.idx] = s
+ }
+ e.idx++
+ if e.idx == cap(e.samples) {
+ e.idx = 0
+ }
+}
+
+// Velocity returns an estimate of the implied velocity and
+// distance for the points sampled, or zero if the estimation method
+// failed.
+func (e *Extrapolation) Estimate() Estimate {
+ if len(e.samples) == 0 {
+ return Estimate{}
+ }
+ values := e.values[:0]
+ times := e.times[:0]
+ first := e.get(0)
+ t := first.t
+ // Walk backwards collecting samples.
+ for i := 0; i < len(e.samples); i++ {
+ p := e.get(-i)
+ age := first.t - p.t
+ if age >= maxAge || t-p.t >= maxSampleGap {
+ // If the samples are too old or
+ // too much time passed between samples
+ // assume they're not part of the fling.
+ break
+ }
+ t = p.t
+ values = append(values, first.v-p.v)
+ times = append(times, float32((-age).Seconds()))
+ }
+ coef, ok := polyFit(times, values)
+ if !ok {
+ return Estimate{}
+ }
+ dist := values[len(values)-1] - values[0]
+ return Estimate{
+ Velocity: coef[1],
+ Distance: dist,
+ }
+}
+
+func (e *Extrapolation) get(i int) sample {
+ idx := (e.idx + i - 1 + len(e.samples)) % len(e.samples)
+ return e.samples[idx]
+}
+
+// fit computes the least squares polynomial fit for
+// the set of points in X, Y. If the fitting fails
+// because of contradicting or insufficient data,
+// fit returns false.
+func polyFit(X, Y []float32) (coefficients, bool) {
+ if len(X) != len(Y) {
+ panic("X and Y lengths differ")
+ }
+ if len(X) <= degree {
+ // Not enough points to fit a curve.
+ return coefficients{}, false
+ }
+
+ // Use a method similar to Android's VelocityTracker.cpp:
+ // https://android.googlesource.com/platform/frameworks/base/+/56a2301/libs/androidfw/VelocityTracker.cpp
+ // where all weights are 1.
+
+ // First, expand the X vector to the matrix A in column-major order.
+ A := newMatrix(degree+1, len(X))
+ for i, x := range X {
+ A.set(0, i, 1)
+ for j := 1; j < A.rows; j++ {
+ A.set(j, i, A.get(j-1, i)*x)
+ }
+ }
+
+ Q, Rt, ok := decomposeQR(A)
+ if !ok {
+ return coefficients{}, false
+ }
+ // Solve R*B = Qt*Y for B, which is then the polynomial coefficients.
+ // Since R is upper triangular, we can proceed from bottom right to
+ // upper left.
+ // https://en.wikipedia.org/wiki/Non-linear_least_squares
+ var B coefficients
+ for i := Q.rows - 1; i >= 0; i-- {
+ B[i] = dot(Q.col(i), Y)
+ for j := Q.rows - 1; j > i; j-- {
+ B[i] -= Rt.get(i, j) * B[j]
+ }
+ B[i] /= Rt.get(i, i)
+ }
+ return B, true
+}
+
+// decomposeQR computes and returns Q, Rt where Q*transpose(Rt) = A, if
+// possible. R is guaranteed to be upper triangular and only the square
+// part of Rt is returned.
+func decomposeQR(A *matrix) (*matrix, *matrix, bool) {
+ // Gram-Schmidt QR decompose A where Q*R = A.
+ // https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process
+ Q := newMatrix(A.rows, A.cols) // Column-major.
+ Rt := newMatrix(A.rows, A.rows) // R transposed, row-major.
+ for i := 0; i < Q.rows; i++ {
+ // Copy A column.
+ for j := 0; j < Q.cols; j++ {
+ Q.set(i, j, A.get(i, j))
+ }
+ // Subtract projections. Note that int the projection
+ //
+ // proju a = / u
+ //
+ // the normalized column e replaces u, where = 1:
+ //
+ // proje a = / e = e
+ for j := 0; j < i; j++ {
+ d := dot(Q.col(j), Q.col(i))
+ for k := 0; k < Q.cols; k++ {
+ Q.set(i, k, Q.get(i, k)-d*Q.get(j, k))
+ }
+ }
+ // Normalize Q columns.
+ n := norm(Q.col(i))
+ if n < 0.000001 {
+ // Degenerate data, no solution.
+ return nil, nil, false
+ }
+ invNorm := 1 / n
+ for j := 0; j < Q.cols; j++ {
+ Q.set(i, j, Q.get(i, j)*invNorm)
+ }
+ // Update Rt.
+ for j := i; j < Rt.cols; j++ {
+ Rt.set(i, j, dot(Q.col(i), A.col(j)))
+ }
+ }
+ return Q, Rt, true
+}
+
+func norm(V []float32) float32 {
+ var n float32
+ for _, v := range V {
+ n += v * v
+ }
+ return float32(math.Sqrt(float64(n)))
+}
+
+func dot(V1, V2 []float32) float32 {
+ var d float32
+ for i, v1 := range V1 {
+ d += v1 * V2[i]
+ }
+ return d
+}
+
+func newMatrix(rows, cols int) *matrix {
+ return &matrix{
+ rows: rows,
+ cols: cols,
+ data: make([]float32, rows*cols),
+ }
+}
+
+func (m *matrix) set(row, col int, v float32) {
+ if row < 0 || row >= m.rows {
+ panic("row out of range")
+ }
+ if col < 0 || col >= m.cols {
+ panic("col out of range")
+ }
+ m.data[row*m.cols+col] = v
+}
+
+func (m *matrix) get(row, col int) float32 {
+ if row < 0 || row >= m.rows {
+ panic("row out of range")
+ }
+ if col < 0 || col >= m.cols {
+ panic("col out of range")
+ }
+ return m.data[row*m.cols+col]
+}
+
+func (m *matrix) col(c int) []float32 {
+ return m.data[c*m.cols : (c+1)*m.cols]
+}
+
+func (m *matrix) approxEqual(m2 *matrix) bool {
+ if m.rows != m2.rows || m.cols != m2.cols {
+ return false
+ }
+ const epsilon = 0.00001
+ for row := 0; row < m.rows; row++ {
+ for col := 0; col < m.cols; col++ {
+ d := m2.get(row, col) - m.get(row, col)
+ if d < -epsilon || d > epsilon {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+func (m *matrix) transpose() *matrix {
+ t := &matrix{
+ rows: m.cols,
+ cols: m.rows,
+ data: make([]float32, len(m.data)),
+ }
+ for i := 0; i < m.rows; i++ {
+ for j := 0; j < m.cols; j++ {
+ t.set(j, i, m.get(i, j))
+ }
+ }
+ return t
+}
+
+func (m *matrix) mul(m2 *matrix) *matrix {
+ if m.rows != m2.cols {
+ panic("mismatched matrices")
+ }
+ mm := &matrix{
+ rows: m.rows,
+ cols: m2.cols,
+ data: make([]float32, m.rows*m2.cols),
+ }
+ for i := 0; i < mm.rows; i++ {
+ for j := 0; j < mm.cols; j++ {
+ var v float32
+ for k := 0; k < m.rows; k++ {
+ v += m.get(k, j) * m2.get(i, k)
+ }
+ mm.set(i, j, v)
+ }
+ }
+ return mm
+}
+
+func (m *matrix) String() string {
+ var b strings.Builder
+ for i := 0; i < m.rows; i++ {
+ for j := 0; j < m.cols; j++ {
+ v := m.get(i, j)
+ b.WriteString(strconv.FormatFloat(float64(v), 'g', -1, 32))
+ b.WriteString(", ")
+ }
+ b.WriteString("\n")
+ }
+ return b.String()
+}
+
+func (c coefficients) approxEqual(c2 coefficients) bool {
+ const epsilon = 0.00001
+ for i, v := range c {
+ d := v - c2[i]
+ if d < -epsilon || d > epsilon {
+ return false
+ }
+ }
+ return true
+}
diff --git a/gio/internal/fling/extrapolation_test.go b/gio/internal/fling/extrapolation_test.go
new file mode 100644
index 0000000..3f9d982
--- /dev/null
+++ b/gio/internal/fling/extrapolation_test.go
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package fling
+
+import "testing"
+
+func TestDecomposeQR(t *testing.T) {
+ A := &matrix{
+ rows: 3, cols: 3,
+ data: []float32{
+ 12, 6, -4,
+ -51, 167, 24,
+ 4, -68, -41,
+ },
+ }
+ Q, Rt, ok := decomposeQR(A)
+ if !ok {
+ t.Fatal("decomposeQR failed")
+ }
+ R := Rt.transpose()
+ QR := Q.mul(R)
+ if !A.approxEqual(QR) {
+ t.Log("A\n", A)
+ t.Log("Q\n", Q)
+ t.Log("R\n", R)
+ t.Log("QR\n", QR)
+ t.Fatal("Q*R not approximately equal to A")
+ }
+}
+
+func TestFit(t *testing.T) {
+ X := []float32{-1, 0, 1}
+ Y := []float32{2, 0, 2}
+
+ got, ok := polyFit(X, Y)
+ if !ok {
+ t.Fatal("polyFit failed")
+ }
+ want := coefficients{0, 0, 2}
+ if !got.approxEqual(want) {
+ t.Fatalf("polyFit: got %v want %v", got, want)
+ }
+}
diff --git a/gio/internal/gl/gl.go b/gio/internal/gl/gl.go
new file mode 100644
index 0000000..9696c71
--- /dev/null
+++ b/gio/internal/gl/gl.go
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gl
+
+type (
+ Attrib uint
+ Enum uint
+)
+
+const (
+ ALL_BARRIER_BITS = 0xffffffff
+ ARRAY_BUFFER = 0x8892
+ BLEND = 0xbe2
+ CLAMP_TO_EDGE = 0x812f
+ COLOR_ATTACHMENT0 = 0x8ce0
+ COLOR_BUFFER_BIT = 0x4000
+ COMPILE_STATUS = 0x8b81
+ COMPUTE_SHADER = 0x91B9
+ DEPTH_BUFFER_BIT = 0x100
+ DEPTH_ATTACHMENT = 0x8d00
+ DEPTH_COMPONENT16 = 0x81a5
+ DEPTH_COMPONENT24 = 0x81A6
+ DEPTH_COMPONENT32F = 0x8CAC
+ DEPTH_TEST = 0xb71
+ DRAW_FRAMEBUFFER = 0x8CA9
+ DST_COLOR = 0x306
+ DYNAMIC_DRAW = 0x88E8
+ DYNAMIC_READ = 0x88E9
+ ELEMENT_ARRAY_BUFFER = 0x8893
+ EXTENSIONS = 0x1f03
+ FALSE = 0
+ FLOAT = 0x1406
+ FRAGMENT_SHADER = 0x8b30
+ FRAMEBUFFER = 0x8d40
+ FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING = 0x8210
+ FRAMEBUFFER_BINDING = 0x8ca6
+ FRAMEBUFFER_COMPLETE = 0x8cd5
+ HALF_FLOAT = 0x140b
+ HALF_FLOAT_OES = 0x8d61
+ INFO_LOG_LENGTH = 0x8B84
+ INVALID_INDEX = ^uint(0)
+ GREATER = 0x204
+ GEQUAL = 0x206
+ LINEAR = 0x2601
+ LINK_STATUS = 0x8b82
+ LUMINANCE = 0x1909
+ MAP_READ_BIT = 0x0001
+ MAX_TEXTURE_SIZE = 0xd33
+ NEAREST = 0x2600
+ NO_ERROR = 0x0
+ NUM_EXTENSIONS = 0x821D
+ ONE = 0x1
+ ONE_MINUS_SRC_ALPHA = 0x303
+ PROGRAM_BINARY_LENGTH = 0x8741
+ QUERY_RESULT = 0x8866
+ QUERY_RESULT_AVAILABLE = 0x8867
+ R16F = 0x822d
+ R8 = 0x8229
+ READ_FRAMEBUFFER = 0x8ca8
+ READ_ONLY = 0x88B8
+ READ_WRITE = 0x88BA
+ RED = 0x1903
+ RENDERER = 0x1F01
+ RENDERBUFFER = 0x8d41
+ RENDERBUFFER_BINDING = 0x8ca7
+ RENDERBUFFER_HEIGHT = 0x8d43
+ RENDERBUFFER_WIDTH = 0x8d42
+ RGB = 0x1907
+ RGBA = 0x1908
+ RGBA8 = 0x8058
+ SHADER_STORAGE_BUFFER = 0x90D2
+ SHORT = 0x1402
+ SRGB = 0x8c40
+ SRGB_ALPHA_EXT = 0x8c42
+ SRGB8 = 0x8c41
+ SRGB8_ALPHA8 = 0x8c43
+ STATIC_DRAW = 0x88e4
+ STENCIL_BUFFER_BIT = 0x00000400
+ TEXTURE_2D = 0xde1
+ TEXTURE_MAG_FILTER = 0x2800
+ TEXTURE_MIN_FILTER = 0x2801
+ TEXTURE_WRAP_S = 0x2802
+ TEXTURE_WRAP_T = 0x2803
+ TEXTURE0 = 0x84c0
+ TEXTURE1 = 0x84c1
+ TRIANGLE_STRIP = 0x5
+ TRIANGLES = 0x4
+ TRUE = 1
+ UNIFORM_BUFFER = 0x8A11
+ UNPACK_ALIGNMENT = 0xcf5
+ UNSIGNED_BYTE = 0x1401
+ UNSIGNED_SHORT = 0x1403
+ VERSION = 0x1f02
+ VERTEX_SHADER = 0x8b31
+ WRITE_ONLY = 0x88B9
+ ZERO = 0x0
+
+ // EXT_disjoint_timer_query
+ TIME_ELAPSED_EXT = 0x88BF
+ GPU_DISJOINT_EXT = 0x8FBB
+)
+
+var _ interface {
+ ActiveTexture(texture Enum)
+ AttachShader(p Program, s Shader)
+ BeginQuery(target Enum, query Query)
+ BindAttribLocation(p Program, a Attrib, name string)
+ BindBuffer(target Enum, b Buffer)
+ BindBufferBase(target Enum, index int, buffer Buffer)
+ BindFramebuffer(target Enum, fb Framebuffer)
+ BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum)
+ BindRenderbuffer(target Enum, fb Renderbuffer)
+ BindTexture(target Enum, t Texture)
+ BlendEquation(mode Enum)
+ BlendFunc(sfactor, dfactor Enum)
+ BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum)
+ BufferData(target Enum, size int, usage Enum)
+ BufferSubData(target Enum, offset int, src []byte)
+ CheckFramebufferStatus(target Enum) Enum
+ Clear(mask Enum)
+ ClearColor(red, green, blue, alpha float32)
+ ClearDepthf(d float32)
+ CompileShader(s Shader)
+ CreateBuffer() Buffer
+ CreateFramebuffer() Framebuffer
+ CreateProgram() Program
+ CreateQuery() Query
+ CreateRenderbuffer() Renderbuffer
+ CreateShader(ty Enum) Shader
+ CreateTexture() Texture
+ DeleteBuffer(v Buffer)
+ DeleteFramebuffer(v Framebuffer)
+ DeleteProgram(p Program)
+ DeleteQuery(query Query)
+ DeleteRenderbuffer(r Renderbuffer)
+ DeleteShader(s Shader)
+ DeleteTexture(v Texture)
+ DepthFunc(f Enum)
+ DepthMask(mask bool)
+ DisableVertexAttribArray(a Attrib)
+ Disable(cap Enum)
+ DispatchCompute(x, y, z int)
+ DrawArrays(mode Enum, first, count int)
+ DrawElements(mode Enum, count int, ty Enum, offset int)
+ Enable(cap Enum)
+ EnableVertexAttribArray(a Attrib)
+ EndQuery(target Enum)
+ FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int)
+ FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer)
+ GetBinding(pname Enum) Object
+ GetError() Enum
+ GetInteger(pname Enum) int
+ GetProgrami(p Program, pname Enum) int
+ GetProgramInfoLog(p Program) string
+ GetQueryObjectuiv(query Query, pname Enum) uint
+ GetShaderi(s Shader, pname Enum) int
+ GetShaderInfoLog(s Shader) string
+ GetString(pname Enum) string
+ GetUniformBlockIndex(p Program, name string) uint
+ GetUniformLocation(p Program, name string) Uniform
+ InvalidateFramebuffer(target, attachment Enum)
+ LinkProgram(p Program)
+ MapBufferRange(target Enum, offset, length int, access Enum) []byte
+ MemoryBarrier(barriers Enum)
+ ReadPixels(x, y, width, height int, format, ty Enum, data []byte)
+ RenderbufferStorage(target, internalformat Enum, width, height int)
+ ShaderSource(s Shader, src string)
+ TexImage2D(target Enum, level int, internalFormat Enum, width, height int, format, ty Enum)
+ TexParameteri(target, pname Enum, param int)
+ TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int)
+ TexSubImage2D(target Enum, level, xoff, yoff int, width, height int, format, ty Enum, data []byte)
+ UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint)
+ Uniform1f(dst Uniform, v float32)
+ Uniform1i(dst Uniform, v int)
+ Uniform2f(dst Uniform, v0, v1 float32)
+ Uniform3f(dst Uniform, v0, v1, v2 float32)
+ Uniform4f(dst Uniform, v0, v1, v2, v3 float32)
+ UseProgram(p Program)
+ UnmapBuffer(target Enum) bool
+ VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int)
+ Viewport(x, y, width, height int)
+} = (*Functions)(nil)
diff --git a/gio/internal/gl/gl_js.go b/gio/internal/gl/gl_js.go
new file mode 100644
index 0000000..13890a7
--- /dev/null
+++ b/gio/internal/gl/gl_js.go
@@ -0,0 +1,381 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gl
+
+import (
+ "errors"
+ "strings"
+ "syscall/js"
+)
+
+type Functions struct {
+ Ctx js.Value
+ EXT_disjoint_timer_query js.Value
+ EXT_disjoint_timer_query_webgl2 js.Value
+
+ // Cached reference to the Uint8Array JS type.
+ uint8Array js.Value
+
+ // Cached JS arrays.
+ arrayBuf js.Value
+ int32Buf js.Value
+}
+
+type Context js.Value
+
+func NewFunctions(ctx Context) (*Functions, error) {
+ f := &Functions{
+ Ctx: js.Value(ctx),
+ uint8Array: js.Global().Get("Uint8Array"),
+ }
+ if err := f.Init(); err != nil {
+ return nil, err
+ }
+ return f, nil
+}
+
+func (f *Functions) Init() error {
+ webgl2Class := js.Global().Get("WebGL2RenderingContext")
+ iswebgl2 := !webgl2Class.IsUndefined() && f.Ctx.InstanceOf(webgl2Class)
+ if !iswebgl2 {
+ f.EXT_disjoint_timer_query = f.getExtension("EXT_disjoint_timer_query")
+ if f.getExtension("OES_texture_half_float").IsNull() && f.getExtension("OES_texture_float").IsNull() {
+ return errors.New("gl: no support for neither OES_texture_half_float nor OES_texture_float")
+ }
+ if f.getExtension("EXT_sRGB").IsNull() {
+ return errors.New("gl: EXT_sRGB not supported")
+ }
+ } else {
+ // WebGL2 extensions.
+ f.EXT_disjoint_timer_query_webgl2 = f.getExtension("EXT_disjoint_timer_query_webgl2")
+ if f.getExtension("EXT_color_buffer_half_float").IsNull() && f.getExtension("EXT_color_buffer_float").IsNull() {
+ return errors.New("gl: no support for neither EXT_color_buffer_half_float nor EXT_color_buffer_float")
+ }
+ }
+ return nil
+}
+
+func (f *Functions) getExtension(name string) js.Value {
+ return f.Ctx.Call("getExtension", name)
+}
+
+func (f *Functions) ActiveTexture(t Enum) {
+ f.Ctx.Call("activeTexture", int(t))
+}
+func (f *Functions) AttachShader(p Program, s Shader) {
+ f.Ctx.Call("attachShader", js.Value(p), js.Value(s))
+}
+func (f *Functions) BeginQuery(target Enum, query Query) {
+ if !f.EXT_disjoint_timer_query_webgl2.IsNull() {
+ f.Ctx.Call("beginQuery", int(target), js.Value(query))
+ } else {
+ f.EXT_disjoint_timer_query.Call("beginQueryEXT", int(target), js.Value(query))
+ }
+}
+func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) {
+ f.Ctx.Call("bindAttribLocation", js.Value(p), int(a), name)
+}
+func (f *Functions) BindBuffer(target Enum, b Buffer) {
+ f.Ctx.Call("bindBuffer", int(target), js.Value(b))
+}
+func (f *Functions) BindBufferBase(target Enum, index int, b Buffer) {
+ f.Ctx.Call("bindBufferBase", int(target), index, js.Value(b))
+}
+func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) {
+ f.Ctx.Call("bindFramebuffer", int(target), js.Value(fb))
+}
+func (f *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) {
+ f.Ctx.Call("bindRenderbuffer", int(target), js.Value(rb))
+}
+func (f *Functions) BindTexture(target Enum, t Texture) {
+ f.Ctx.Call("bindTexture", int(target), js.Value(t))
+}
+func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) {
+ panic("not implemented")
+}
+func (f *Functions) BlendEquation(mode Enum) {
+ f.Ctx.Call("blendEquation", int(mode))
+}
+func (f *Functions) BlendFunc(sfactor, dfactor Enum) {
+ f.Ctx.Call("blendFunc", int(sfactor), int(dfactor))
+}
+func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) {
+ panic("not implemented")
+}
+func (f *Functions) BufferData(target Enum, size int, usage Enum) {
+ f.Ctx.Call("bufferData", int(target), size, int(usage))
+}
+func (f *Functions) BufferSubData(target Enum, offset int, src []byte) {
+ f.Ctx.Call("bufferSubData", int(target), offset, f.byteArrayOf(src))
+}
+func (f *Functions) CheckFramebufferStatus(target Enum) Enum {
+ return Enum(f.Ctx.Call("checkFramebufferStatus", int(target)).Int())
+}
+func (f *Functions) Clear(mask Enum) {
+ f.Ctx.Call("clear", int(mask))
+}
+func (f *Functions) ClearColor(red, green, blue, alpha float32) {
+ f.Ctx.Call("clearColor", red, green, blue, alpha)
+}
+func (f *Functions) ClearDepthf(d float32) {
+ f.Ctx.Call("clearDepth", d)
+}
+func (f *Functions) CompileShader(s Shader) {
+ f.Ctx.Call("compileShader", js.Value(s))
+}
+func (f *Functions) CreateBuffer() Buffer {
+ return Buffer(f.Ctx.Call("createBuffer"))
+}
+func (f *Functions) CreateFramebuffer() Framebuffer {
+ return Framebuffer(f.Ctx.Call("createFramebuffer"))
+}
+func (f *Functions) CreateProgram() Program {
+ return Program(f.Ctx.Call("createProgram"))
+}
+func (f *Functions) CreateQuery() Query {
+ return Query(f.Ctx.Call("createQuery"))
+}
+func (f *Functions) CreateRenderbuffer() Renderbuffer {
+ return Renderbuffer(f.Ctx.Call("createRenderbuffer"))
+}
+func (f *Functions) CreateShader(ty Enum) Shader {
+ return Shader(f.Ctx.Call("createShader", int(ty)))
+}
+func (f *Functions) CreateTexture() Texture {
+ return Texture(f.Ctx.Call("createTexture"))
+}
+func (f *Functions) DeleteBuffer(v Buffer) {
+ f.Ctx.Call("deleteBuffer", js.Value(v))
+}
+func (f *Functions) DeleteFramebuffer(v Framebuffer) {
+ f.Ctx.Call("deleteFramebuffer", js.Value(v))
+}
+func (f *Functions) DeleteProgram(p Program) {
+ f.Ctx.Call("deleteProgram", js.Value(p))
+}
+func (f *Functions) DeleteQuery(query Query) {
+ if !f.EXT_disjoint_timer_query_webgl2.IsNull() {
+ f.Ctx.Call("deleteQuery", js.Value(query))
+ } else {
+ f.EXT_disjoint_timer_query.Call("deleteQueryEXT", js.Value(query))
+ }
+}
+func (f *Functions) DeleteShader(s Shader) {
+ f.Ctx.Call("deleteShader", js.Value(s))
+}
+func (f *Functions) DeleteRenderbuffer(v Renderbuffer) {
+ f.Ctx.Call("deleteRenderbuffer", js.Value(v))
+}
+func (f *Functions) DeleteTexture(v Texture) {
+ f.Ctx.Call("deleteTexture", js.Value(v))
+}
+func (f *Functions) DepthFunc(fn Enum) {
+ f.Ctx.Call("depthFunc", int(fn))
+}
+func (f *Functions) DepthMask(mask bool) {
+ f.Ctx.Call("depthMask", mask)
+}
+func (f *Functions) DisableVertexAttribArray(a Attrib) {
+ f.Ctx.Call("disableVertexAttribArray", int(a))
+}
+func (f *Functions) Disable(cap Enum) {
+ f.Ctx.Call("disable", int(cap))
+}
+func (f *Functions) DrawArrays(mode Enum, first, count int) {
+ f.Ctx.Call("drawArrays", int(mode), first, count)
+}
+func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) {
+ f.Ctx.Call("drawElements", int(mode), count, int(ty), offset)
+}
+func (f *Functions) DispatchCompute(x, y, z int) {
+ panic("not implemented")
+}
+func (f *Functions) Enable(cap Enum) {
+ f.Ctx.Call("enable", int(cap))
+}
+func (f *Functions) EnableVertexAttribArray(a Attrib) {
+ f.Ctx.Call("enableVertexAttribArray", int(a))
+}
+func (f *Functions) EndQuery(target Enum) {
+ if !f.EXT_disjoint_timer_query_webgl2.IsNull() {
+ f.Ctx.Call("endQuery", int(target))
+ } else {
+ f.EXT_disjoint_timer_query.Call("endQueryEXT", int(target))
+ }
+}
+func (f *Functions) Finish() {
+ f.Ctx.Call("finish")
+}
+func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) {
+ f.Ctx.Call("framebufferRenderbuffer", int(target), int(attachment), int(renderbuffertarget), js.Value(renderbuffer))
+}
+func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) {
+ f.Ctx.Call("framebufferTexture2D", int(target), int(attachment), int(texTarget), js.Value(t), level)
+}
+func (f *Functions) GetError() Enum {
+ // Avoid slow getError calls. See gio#179.
+ return 0
+}
+func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int {
+ return paramVal(f.Ctx.Call("getRenderbufferParameteri", int(pname)))
+}
+func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int {
+ return paramVal(f.Ctx.Call("getFramebufferAttachmentParameter", int(target), int(attachment), int(pname)))
+}
+func (f *Functions) GetBinding(pname Enum) Object {
+ return Object(f.Ctx.Call("getParameter", int(pname)))
+}
+func (f *Functions) GetInteger(pname Enum) int {
+ return paramVal(f.Ctx.Call("getParameter", int(pname)))
+}
+func (f *Functions) GetProgrami(p Program, pname Enum) int {
+ return paramVal(f.Ctx.Call("getProgramParameter", js.Value(p), int(pname)))
+}
+func (f *Functions) GetProgramInfoLog(p Program) string {
+ return f.Ctx.Call("getProgramInfoLog", js.Value(p)).String()
+}
+func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint {
+ if !f.EXT_disjoint_timer_query_webgl2.IsNull() {
+ return uint(paramVal(f.Ctx.Call("getQueryParameter", js.Value(query), int(pname))))
+ } else {
+ return uint(paramVal(f.EXT_disjoint_timer_query.Call("getQueryObjectEXT", js.Value(query), int(pname))))
+ }
+}
+func (f *Functions) GetShaderi(s Shader, pname Enum) int {
+ return paramVal(f.Ctx.Call("getShaderParameter", js.Value(s), int(pname)))
+}
+func (f *Functions) GetShaderInfoLog(s Shader) string {
+ return f.Ctx.Call("getShaderInfoLog", js.Value(s)).String()
+}
+func (f *Functions) GetString(pname Enum) string {
+ switch pname {
+ case EXTENSIONS:
+ extsjs := f.Ctx.Call("getSupportedExtensions")
+ var exts []string
+ for i := 0; i < extsjs.Length(); i++ {
+ exts = append(exts, "GL_"+extsjs.Index(i).String())
+ }
+ return strings.Join(exts, " ")
+ default:
+ return f.Ctx.Call("getParameter", int(pname)).String()
+ }
+}
+func (f *Functions) GetUniformBlockIndex(p Program, name string) uint {
+ return uint(paramVal(f.Ctx.Call("getUniformBlockIndex", js.Value(p), name)))
+}
+func (f *Functions) GetUniformLocation(p Program, name string) Uniform {
+ return Uniform(f.Ctx.Call("getUniformLocation", js.Value(p), name))
+}
+func (f *Functions) InvalidateFramebuffer(target, attachment Enum) {
+ fn := f.Ctx.Get("invalidateFramebuffer")
+ if !fn.IsUndefined() {
+ if f.int32Buf.IsUndefined() {
+ f.int32Buf = js.Global().Get("Int32Array").New(1)
+ }
+ f.int32Buf.SetIndex(0, int32(attachment))
+ f.Ctx.Call("invalidateFramebuffer", int(target), f.int32Buf)
+ }
+}
+func (f *Functions) LinkProgram(p Program) {
+ f.Ctx.Call("linkProgram", js.Value(p))
+}
+func (f *Functions) PixelStorei(pname Enum, param int32) {
+ f.Ctx.Call("pixelStorei", int(pname), param)
+}
+func (f *Functions) MemoryBarrier(barriers Enum) {
+ panic("not implemented")
+}
+func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte {
+ panic("not implemented")
+}
+func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) {
+ f.Ctx.Call("renderbufferStorage", int(target), int(internalformat), width, height)
+}
+func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) {
+ ba := f.byteArrayOf(data)
+ f.Ctx.Call("readPixels", x, y, width, height, int(format), int(ty), ba)
+ js.CopyBytesToGo(data, ba)
+}
+func (f *Functions) Scissor(x, y, width, height int32) {
+ f.Ctx.Call("scissor", x, y, width, height)
+}
+func (f *Functions) ShaderSource(s Shader, src string) {
+ f.Ctx.Call("shaderSource", js.Value(s), src)
+}
+func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width, height int, format, ty Enum) {
+ f.Ctx.Call("texImage2D", int(target), int(level), int(internalFormat), int(width), int(height), 0, int(format), int(ty), nil)
+}
+func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) {
+ f.Ctx.Call("texStorage2D", int(target), levels, int(internalFormat), width, height)
+}
+func (f *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) {
+ f.Ctx.Call("texSubImage2D", int(target), level, x, y, width, height, int(format), int(ty), f.byteArrayOf(data))
+}
+func (f *Functions) TexParameteri(target, pname Enum, param int) {
+ f.Ctx.Call("texParameteri", int(target), int(pname), int(param))
+}
+func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) {
+ f.Ctx.Call("uniformBlockBinding", js.Value(p), int(uniformBlockIndex), int(uniformBlockBinding))
+}
+func (f *Functions) Uniform1f(dst Uniform, v float32) {
+ f.Ctx.Call("uniform1f", js.Value(dst), v)
+}
+func (f *Functions) Uniform1i(dst Uniform, v int) {
+ f.Ctx.Call("uniform1i", js.Value(dst), v)
+}
+func (f *Functions) Uniform2f(dst Uniform, v0, v1 float32) {
+ f.Ctx.Call("uniform2f", js.Value(dst), v0, v1)
+}
+func (f *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) {
+ f.Ctx.Call("uniform3f", js.Value(dst), v0, v1, v2)
+}
+func (f *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) {
+ f.Ctx.Call("uniform4f", js.Value(dst), v0, v1, v2, v3)
+}
+func (f *Functions) UseProgram(p Program) {
+ f.Ctx.Call("useProgram", js.Value(p))
+}
+func (f *Functions) UnmapBuffer(target Enum) bool {
+ panic("not implemented")
+}
+func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) {
+ f.Ctx.Call("vertexAttribPointer", int(dst), size, int(ty), normalized, stride, offset)
+}
+func (f *Functions) Viewport(x, y, width, height int) {
+ f.Ctx.Call("viewport", x, y, width, height)
+}
+
+func (f *Functions) byteArrayOf(data []byte) js.Value {
+ if len(data) == 0 {
+ return js.Null()
+ }
+ f.resizeByteBuffer(len(data))
+ ba := f.uint8Array.New(f.arrayBuf, int(0), int(len(data)))
+ js.CopyBytesToJS(ba, data)
+ return ba
+}
+
+func (f *Functions) resizeByteBuffer(n int) {
+ if n == 0 {
+ return
+ }
+ if !f.arrayBuf.IsUndefined() && f.arrayBuf.Length() >= n {
+ return
+ }
+ f.arrayBuf = js.Global().Get("ArrayBuffer").New(n)
+}
+
+func paramVal(v js.Value) int {
+ switch v.Type() {
+ case js.TypeBoolean:
+ if b := v.Bool(); b {
+ return 1
+ } else {
+ return 0
+ }
+ case js.TypeNumber:
+ return v.Int()
+ default:
+ panic("unknown parameter type")
+ }
+}
diff --git a/gio/internal/gl/gl_unix.go b/gio/internal/gl/gl_unix.go
new file mode 100644
index 0000000..a1d017a
--- /dev/null
+++ b/gio/internal/gl/gl_unix.go
@@ -0,0 +1,635 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build darwin linux freebsd openbsd
+
+package gl
+
+import (
+ "runtime"
+ "strings"
+ "unsafe"
+)
+
+/*
+#cgo CFLAGS: -Werror
+#cgo linux,!android pkg-config: glesv2
+#cgo linux freebsd LDFLAGS: -ldl
+#cgo freebsd openbsd android LDFLAGS: -lGLESv2
+#cgo freebsd CFLAGS: -I/usr/local/include
+#cgo freebsd LDFLAGS: -L/usr/local/lib
+#cgo openbsd CFLAGS: -I/usr/X11R6/include
+#cgo openbsd LDFLAGS: -L/usr/X11R6/lib
+#cgo darwin,!ios CFLAGS: -DGL_SILENCE_DEPRECATION
+#cgo darwin,!ios LDFLAGS: -framework OpenGL
+#cgo darwin,ios CFLAGS: -DGLES_SILENCE_DEPRECATION
+#cgo darwin,ios LDFLAGS: -framework OpenGLES
+
+#include
+#define __USE_GNU
+#include
+
+#ifdef __APPLE__
+ #include "TargetConditionals.h"
+ #if TARGET_OS_IPHONE
+ #include
+ #else
+ #include
+ #endif
+#else
+#include
+#include
+#endif
+
+static void (*_glBindBufferBase)(GLenum target, GLuint index, GLuint buffer);
+static GLuint (*_glGetUniformBlockIndex)(GLuint program, const GLchar *uniformBlockName);
+static void (*_glUniformBlockBinding)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);
+static void (*_glInvalidateFramebuffer)(GLenum target, GLsizei numAttachments, const GLenum *attachments);
+
+static void (*_glBeginQuery)(GLenum target, GLuint id);
+static void (*_glDeleteQueries)(GLsizei n, const GLuint *ids);
+static void (*_glEndQuery)(GLenum target);
+static void (*_glGenQueries)(GLsizei n, GLuint *ids);
+static void (*_glGetProgramBinary)(GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary);
+static void (*_glGetQueryObjectuiv)(GLuint id, GLenum pname, GLuint *params);
+static const GLubyte* (*_glGetStringi)(GLenum name, GLuint index);
+static void (*_glMemoryBarrier)(GLbitfield barriers);
+static void (*_glDispatchCompute)(GLuint x, GLuint y, GLuint z);
+static void* (*_glMapBufferRange)(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);
+static GLboolean (*_glUnmapBuffer)(GLenum target);
+static void (*_glBindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format);
+static void (*_glTexStorage2D)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
+static void (*_glBlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
+
+// The pointer-free version of glVertexAttribPointer, to avoid the Cgo pointer checks.
+__attribute__ ((visibility ("hidden"))) void gio_glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, uintptr_t offset) {
+ glVertexAttribPointer(index, size, type, normalized, stride, (const GLvoid *)offset);
+}
+
+// The pointer-free version of glDrawElements, to avoid the Cgo pointer checks.
+__attribute__ ((visibility ("hidden"))) void gio_glDrawElements(GLenum mode, GLsizei count, GLenum type, const uintptr_t offset) {
+ glDrawElements(mode, count, type, (const GLvoid *)offset);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glBindBufferBase(GLenum target, GLuint index, GLuint buffer) {
+ _glBindBufferBase(target, index, buffer);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glUniformBlockBinding(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding) {
+ _glUniformBlockBinding(program, uniformBlockIndex, uniformBlockBinding);
+}
+
+__attribute__ ((visibility ("hidden"))) GLuint gio_glGetUniformBlockIndex(GLuint program, const GLchar *uniformBlockName) {
+ return _glGetUniformBlockIndex(program, uniformBlockName);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glInvalidateFramebuffer(GLenum target, GLenum attachment) {
+ // Framebuffer invalidation is just a hint and can safely be ignored.
+ if (_glInvalidateFramebuffer != NULL) {
+ _glInvalidateFramebuffer(target, 1, &attachment);
+ }
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glBeginQuery(GLenum target, GLenum attachment) {
+ _glBeginQuery(target, attachment);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glDeleteQueries(GLsizei n, const GLuint *ids) {
+ _glDeleteQueries(n, ids);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glEndQuery(GLenum target) {
+ _glEndQuery(target);
+}
+
+__attribute__ ((visibility ("hidden"))) const GLubyte* gio_glGetStringi(GLenum name, GLuint index) {
+ if (_glGetStringi == NULL) {
+ return NULL;
+ }
+ return _glGetStringi(name, index);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glGenQueries(GLsizei n, GLuint *ids) {
+ _glGenQueries(n, ids);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glGetProgramBinary(GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary) {
+ _glGetProgramBinary(program, bufsize, length, binaryFormat, binary);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glGetQueryObjectuiv(GLuint id, GLenum pname, GLuint *params) {
+ _glGetQueryObjectuiv(id, pname, params);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glMemoryBarrier(GLbitfield barriers) {
+ _glMemoryBarrier(barriers);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glDispatchCompute(GLuint x, GLuint y, GLuint z) {
+ _glDispatchCompute(x, y, z);
+}
+
+__attribute__ ((visibility ("hidden"))) void *gio_glMapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) {
+ return _glMapBufferRange(target, offset, length, access);
+}
+
+__attribute__ ((visibility ("hidden"))) GLboolean gio_glUnmapBuffer(GLenum target) {
+ return _glUnmapBuffer(target);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format) {
+ _glBindImageTexture(unit, texture, level, layered, layer, access, format);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glTexStorage2D(GLenum target, GLsizei levels, GLenum internalFormat, GLsizei width, GLsizei height) {
+ _glTexStorage2D(target, levels, internalFormat, width, height);
+}
+
+__attribute__ ((visibility ("hidden"))) void gio_glBlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
+ _glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter);
+}
+
+__attribute__((constructor)) static void gio_loadGLFunctions() {
+ // Load libGLESv3 if available.
+ dlopen("libGLESv3.so", RTLD_NOW | RTLD_GLOBAL);
+
+ _glBindBufferBase = dlsym(RTLD_DEFAULT, "glBindBufferBase");
+ _glGetUniformBlockIndex = dlsym(RTLD_DEFAULT, "glGetUniformBlockIndex");
+ _glUniformBlockBinding = dlsym(RTLD_DEFAULT, "glUniformBlockBinding");
+ _glInvalidateFramebuffer = dlsym(RTLD_DEFAULT, "glInvalidateFramebuffer");
+ _glGetStringi = dlsym(RTLD_DEFAULT, "glGetStringi");
+ // Fall back to EXT_invalidate_framebuffer if available.
+ if (_glInvalidateFramebuffer == NULL) {
+ _glInvalidateFramebuffer = dlsym(RTLD_DEFAULT, "glDiscardFramebufferEXT");
+ }
+
+ _glBeginQuery = dlsym(RTLD_DEFAULT, "glBeginQuery");
+ if (_glBeginQuery == NULL)
+ _glBeginQuery = dlsym(RTLD_DEFAULT, "glBeginQueryEXT");
+ _glDeleteQueries = dlsym(RTLD_DEFAULT, "glDeleteQueries");
+ if (_glDeleteQueries == NULL)
+ _glDeleteQueries = dlsym(RTLD_DEFAULT, "glDeleteQueriesEXT");
+ _glEndQuery = dlsym(RTLD_DEFAULT, "glEndQuery");
+ if (_glEndQuery == NULL)
+ _glEndQuery = dlsym(RTLD_DEFAULT, "glEndQueryEXT");
+ _glGenQueries = dlsym(RTLD_DEFAULT, "glGenQueries");
+ if (_glGenQueries == NULL)
+ _glGenQueries = dlsym(RTLD_DEFAULT, "glGenQueriesEXT");
+ _glGetQueryObjectuiv = dlsym(RTLD_DEFAULT, "glGetQueryObjectuiv");
+ if (_glGetQueryObjectuiv == NULL)
+ _glGetQueryObjectuiv = dlsym(RTLD_DEFAULT, "glGetQueryObjectuivEXT");
+
+ _glMemoryBarrier = dlsym(RTLD_DEFAULT, "glMemoryBarrier");
+ _glDispatchCompute = dlsym(RTLD_DEFAULT, "glDispatchCompute");
+ _glMapBufferRange = dlsym(RTLD_DEFAULT, "glMapBufferRange");
+ _glUnmapBuffer = dlsym(RTLD_DEFAULT, "glUnmapBuffer");
+ _glBindImageTexture = dlsym(RTLD_DEFAULT, "glBindImageTexture");
+ _glTexStorage2D = dlsym(RTLD_DEFAULT, "glTexStorage2D");
+ _glBlitFramebuffer = dlsym(RTLD_DEFAULT, "glBlitFramebuffer");
+ _glGetProgramBinary = dlsym(RTLD_DEFAULT, "glGetProgramBinary");
+}
+*/
+import "C"
+
+type Context interface{}
+
+type Functions struct {
+ // Query caches.
+ uints [100]C.GLuint
+ ints [100]C.GLint
+}
+
+func NewFunctions(ctx Context) (*Functions, error) {
+ if ctx != nil {
+ panic("non-nil context")
+ }
+ return new(Functions), nil
+}
+
+func (f *Functions) ActiveTexture(texture Enum) {
+ C.glActiveTexture(C.GLenum(texture))
+}
+
+func (f *Functions) AttachShader(p Program, s Shader) {
+ C.glAttachShader(C.GLuint(p.V), C.GLuint(s.V))
+}
+
+func (f *Functions) BeginQuery(target Enum, query Query) {
+ C.gio_glBeginQuery(C.GLenum(target), C.GLenum(query.V))
+}
+
+func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) {
+ cname := C.CString(name)
+ defer C.free(unsafe.Pointer(cname))
+ C.glBindAttribLocation(C.GLuint(p.V), C.GLuint(a), cname)
+}
+
+func (f *Functions) BindBufferBase(target Enum, index int, b Buffer) {
+ C.gio_glBindBufferBase(C.GLenum(target), C.GLuint(index), C.GLuint(b.V))
+}
+
+func (f *Functions) BindBuffer(target Enum, b Buffer) {
+ C.glBindBuffer(C.GLenum(target), C.GLuint(b.V))
+}
+
+func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) {
+ C.glBindFramebuffer(C.GLenum(target), C.GLuint(fb.V))
+}
+
+func (f *Functions) BindRenderbuffer(target Enum, fb Renderbuffer) {
+ C.glBindRenderbuffer(C.GLenum(target), C.GLuint(fb.V))
+}
+
+func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) {
+ l := C.GLboolean(C.GL_FALSE)
+ if layered {
+ l = C.GL_TRUE
+ }
+ C.gio_glBindImageTexture(C.GLuint(unit), C.GLuint(t.V), C.GLint(level), l, C.GLint(layer), C.GLenum(access), C.GLenum(format))
+}
+
+func (f *Functions) BindTexture(target Enum, t Texture) {
+ C.glBindTexture(C.GLenum(target), C.GLuint(t.V))
+}
+
+func (f *Functions) BlendEquation(mode Enum) {
+ C.glBlendEquation(C.GLenum(mode))
+}
+
+func (f *Functions) BlendFunc(sfactor, dfactor Enum) {
+ C.glBlendFunc(C.GLenum(sfactor), C.GLenum(dfactor))
+}
+
+func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) {
+ C.gio_glBlitFramebuffer(
+ C.GLint(sx0), C.GLint(sy0), C.GLint(sx1), C.GLint(sy1),
+ C.GLint(dx0), C.GLint(dy0), C.GLint(dx1), C.GLint(dy1),
+ C.GLenum(mask), C.GLenum(filter),
+ )
+}
+
+func (f *Functions) BufferData(target Enum, size int, usage Enum) {
+ C.glBufferData(C.GLenum(target), C.GLsizeiptr(size), nil, C.GLenum(usage))
+}
+
+func (f *Functions) BufferSubData(target Enum, offset int, src []byte) {
+ var p unsafe.Pointer
+ if len(src) > 0 {
+ p = unsafe.Pointer(&src[0])
+ }
+ C.glBufferSubData(C.GLenum(target), C.GLintptr(offset), C.GLsizeiptr(len(src)), p)
+}
+
+func (f *Functions) CheckFramebufferStatus(target Enum) Enum {
+ return Enum(C.glCheckFramebufferStatus(C.GLenum(target)))
+}
+
+func (f *Functions) Clear(mask Enum) {
+ C.glClear(C.GLbitfield(mask))
+}
+
+func (f *Functions) ClearColor(red float32, green float32, blue float32, alpha float32) {
+ C.glClearColor(C.GLfloat(red), C.GLfloat(green), C.GLfloat(blue), C.GLfloat(alpha))
+}
+
+func (f *Functions) ClearDepthf(d float32) {
+ C.glClearDepthf(C.GLfloat(d))
+}
+
+func (f *Functions) CompileShader(s Shader) {
+ C.glCompileShader(C.GLuint(s.V))
+}
+
+func (f *Functions) CreateBuffer() Buffer {
+ C.glGenBuffers(1, &f.uints[0])
+ return Buffer{uint(f.uints[0])}
+}
+
+func (f *Functions) CreateFramebuffer() Framebuffer {
+ C.glGenFramebuffers(1, &f.uints[0])
+ return Framebuffer{uint(f.uints[0])}
+}
+
+func (f *Functions) CreateProgram() Program {
+ return Program{uint(C.glCreateProgram())}
+}
+
+func (f *Functions) CreateQuery() Query {
+ C.gio_glGenQueries(1, &f.uints[0])
+ return Query{uint(f.uints[0])}
+}
+
+func (f *Functions) CreateRenderbuffer() Renderbuffer {
+ C.glGenRenderbuffers(1, &f.uints[0])
+ return Renderbuffer{uint(f.uints[0])}
+}
+
+func (f *Functions) CreateShader(ty Enum) Shader {
+ return Shader{uint(C.glCreateShader(C.GLenum(ty)))}
+}
+
+func (f *Functions) CreateTexture() Texture {
+ C.glGenTextures(1, &f.uints[0])
+ return Texture{uint(f.uints[0])}
+}
+
+func (f *Functions) DeleteBuffer(v Buffer) {
+ f.uints[0] = C.GLuint(v.V)
+ C.glDeleteBuffers(1, &f.uints[0])
+}
+
+func (f *Functions) DeleteFramebuffer(v Framebuffer) {
+ f.uints[0] = C.GLuint(v.V)
+ C.glDeleteFramebuffers(1, &f.uints[0])
+}
+
+func (f *Functions) DeleteProgram(p Program) {
+ C.glDeleteProgram(C.GLuint(p.V))
+}
+
+func (f *Functions) DeleteQuery(query Query) {
+ f.uints[0] = C.GLuint(query.V)
+ C.gio_glDeleteQueries(1, &f.uints[0])
+}
+
+func (f *Functions) DeleteRenderbuffer(v Renderbuffer) {
+ f.uints[0] = C.GLuint(v.V)
+ C.glDeleteRenderbuffers(1, &f.uints[0])
+}
+
+func (f *Functions) DeleteShader(s Shader) {
+ C.glDeleteShader(C.GLuint(s.V))
+}
+
+func (f *Functions) DeleteTexture(v Texture) {
+ f.uints[0] = C.GLuint(v.V)
+ C.glDeleteTextures(1, &f.uints[0])
+}
+
+func (f *Functions) DepthFunc(v Enum) {
+ C.glDepthFunc(C.GLenum(v))
+}
+
+func (f *Functions) DepthMask(mask bool) {
+ m := C.GLboolean(C.GL_FALSE)
+ if mask {
+ m = C.GLboolean(C.GL_TRUE)
+ }
+ C.glDepthMask(m)
+}
+
+func (f *Functions) DisableVertexAttribArray(a Attrib) {
+ C.glDisableVertexAttribArray(C.GLuint(a))
+}
+
+func (f *Functions) Disable(cap Enum) {
+ C.glDisable(C.GLenum(cap))
+}
+
+func (f *Functions) DrawArrays(mode Enum, first int, count int) {
+ C.glDrawArrays(C.GLenum(mode), C.GLint(first), C.GLsizei(count))
+}
+
+func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) {
+ C.gio_glDrawElements(C.GLenum(mode), C.GLsizei(count), C.GLenum(ty), C.uintptr_t(offset))
+}
+
+func (f *Functions) DispatchCompute(x, y, z int) {
+ C.gio_glDispatchCompute(C.GLuint(x), C.GLuint(y), C.GLuint(z))
+}
+
+func (f *Functions) Enable(cap Enum) {
+ C.glEnable(C.GLenum(cap))
+}
+
+func (f *Functions) EndQuery(target Enum) {
+ C.gio_glEndQuery(C.GLenum(target))
+}
+
+func (f *Functions) EnableVertexAttribArray(a Attrib) {
+ C.glEnableVertexAttribArray(C.GLuint(a))
+}
+
+func (f *Functions) Finish() {
+ C.glFinish()
+}
+
+func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) {
+ C.glFramebufferRenderbuffer(C.GLenum(target), C.GLenum(attachment), C.GLenum(renderbuffertarget), C.GLuint(renderbuffer.V))
+}
+
+func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) {
+ C.glFramebufferTexture2D(C.GLenum(target), C.GLenum(attachment), C.GLenum(texTarget), C.GLuint(t.V), C.GLint(level))
+}
+
+func (c *Functions) GetBinding(pname Enum) Object {
+ return Object{uint(c.GetInteger(pname))}
+}
+
+func (f *Functions) GetError() Enum {
+ return Enum(C.glGetError())
+}
+
+func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int {
+ C.glGetRenderbufferParameteriv(C.GLenum(target), C.GLenum(pname), &f.ints[0])
+ return int(f.ints[0])
+}
+
+func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int {
+ C.glGetFramebufferAttachmentParameteriv(C.GLenum(target), C.GLenum(attachment), C.GLenum(pname), &f.ints[0])
+ return int(f.ints[0])
+}
+
+func (f *Functions) GetInteger(pname Enum) int {
+ C.glGetIntegerv(C.GLenum(pname), &f.ints[0])
+ return int(f.ints[0])
+}
+
+func (f *Functions) GetProgrami(p Program, pname Enum) int {
+ C.glGetProgramiv(C.GLuint(p.V), C.GLenum(pname), &f.ints[0])
+ return int(f.ints[0])
+}
+
+func (f *Functions) GetProgramBinary(p Program) []byte {
+ sz := f.GetProgrami(p, PROGRAM_BINARY_LENGTH)
+ if sz == 0 {
+ return nil
+ }
+ buf := make([]byte, sz)
+ var format C.GLenum
+ C.gio_glGetProgramBinary(C.GLuint(p.V), C.GLsizei(sz), nil, &format, unsafe.Pointer(&buf[0]))
+ return buf
+}
+
+func (f *Functions) GetProgramInfoLog(p Program) string {
+ n := f.GetProgrami(p, INFO_LOG_LENGTH)
+ buf := make([]byte, n)
+ C.glGetProgramInfoLog(C.GLuint(p.V), C.GLsizei(len(buf)), nil, (*C.GLchar)(unsafe.Pointer(&buf[0])))
+ return string(buf)
+}
+
+func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint {
+ C.gio_glGetQueryObjectuiv(C.GLuint(query.V), C.GLenum(pname), &f.uints[0])
+ return uint(f.uints[0])
+}
+
+func (f *Functions) GetShaderi(s Shader, pname Enum) int {
+ C.glGetShaderiv(C.GLuint(s.V), C.GLenum(pname), &f.ints[0])
+ return int(f.ints[0])
+}
+
+func (f *Functions) GetShaderInfoLog(s Shader) string {
+ n := f.GetShaderi(s, INFO_LOG_LENGTH)
+ buf := make([]byte, n)
+ C.glGetShaderInfoLog(C.GLuint(s.V), C.GLsizei(len(buf)), nil, (*C.GLchar)(unsafe.Pointer(&buf[0])))
+ return string(buf)
+}
+
+func (f *Functions) GetStringi(pname Enum, index int) string {
+ str := C.gio_glGetStringi(C.GLenum(pname), C.GLuint(index))
+ if str == nil {
+ return ""
+ }
+ return C.GoString((*C.char)(unsafe.Pointer(str)))
+}
+
+func (f *Functions) GetString(pname Enum) string {
+ switch {
+ case runtime.GOOS == "darwin" && pname == EXTENSIONS:
+ // macOS OpenGL 3 core profile doesn't support glGetString(GL_EXTENSIONS).
+ // Use glGetStringi(GL_EXTENSIONS, ).
+ var exts []string
+ nexts := f.GetInteger(NUM_EXTENSIONS)
+ for i := 0; i < nexts; i++ {
+ ext := f.GetStringi(EXTENSIONS, i)
+ exts = append(exts, ext)
+ }
+ return strings.Join(exts, " ")
+ default:
+ str := C.glGetString(C.GLenum(pname))
+ return C.GoString((*C.char)(unsafe.Pointer(str)))
+ }
+}
+
+func (f *Functions) GetUniformBlockIndex(p Program, name string) uint {
+ cname := C.CString(name)
+ defer C.free(unsafe.Pointer(cname))
+ return uint(C.gio_glGetUniformBlockIndex(C.GLuint(p.V), cname))
+}
+
+func (f *Functions) GetUniformLocation(p Program, name string) Uniform {
+ cname := C.CString(name)
+ defer C.free(unsafe.Pointer(cname))
+ return Uniform{int(C.glGetUniformLocation(C.GLuint(p.V), cname))}
+}
+
+func (f *Functions) InvalidateFramebuffer(target, attachment Enum) {
+ C.gio_glInvalidateFramebuffer(C.GLenum(target), C.GLenum(attachment))
+}
+
+func (f *Functions) LinkProgram(p Program) {
+ C.glLinkProgram(C.GLuint(p.V))
+}
+
+func (f *Functions) PixelStorei(pname Enum, param int32) {
+ C.glPixelStorei(C.GLenum(pname), C.GLint(param))
+}
+
+func (f *Functions) MemoryBarrier(barriers Enum) {
+ C.gio_glMemoryBarrier(C.GLbitfield(barriers))
+}
+
+func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte {
+ p := C.gio_glMapBufferRange(C.GLenum(target), C.GLintptr(offset), C.GLsizeiptr(length), C.GLbitfield(access))
+ if p == nil {
+ return nil
+ }
+ return (*[1 << 30]byte)(p)[:length:length]
+}
+
+func (f *Functions) Scissor(x, y, width, height int32) {
+ C.glScissor(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height))
+}
+
+func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) {
+ var p unsafe.Pointer
+ if len(data) > 0 {
+ p = unsafe.Pointer(&data[0])
+ }
+ C.glReadPixels(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p)
+}
+
+func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) {
+ C.glRenderbufferStorage(C.GLenum(target), C.GLenum(internalformat), C.GLsizei(width), C.GLsizei(height))
+}
+
+func (f *Functions) ShaderSource(s Shader, src string) {
+ csrc := C.CString(src)
+ defer C.free(unsafe.Pointer(csrc))
+ strlen := C.GLint(len(src))
+ C.glShaderSource(C.GLuint(s.V), 1, &csrc, &strlen)
+}
+
+func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width int, height int, format Enum, ty Enum) {
+ C.glTexImage2D(C.GLenum(target), C.GLint(level), C.GLint(internalFormat), C.GLsizei(width), C.GLsizei(height), 0, C.GLenum(format), C.GLenum(ty), nil)
+}
+
+func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) {
+ C.gio_glTexStorage2D(C.GLenum(target), C.GLsizei(levels), C.GLenum(internalFormat), C.GLsizei(width), C.GLsizei(height))
+}
+
+func (f *Functions) TexSubImage2D(target Enum, level int, x int, y int, width int, height int, format Enum, ty Enum, data []byte) {
+ var p unsafe.Pointer
+ if len(data) > 0 {
+ p = unsafe.Pointer(&data[0])
+ }
+ C.glTexSubImage2D(C.GLenum(target), C.GLint(level), C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p)
+}
+
+func (f *Functions) TexParameteri(target, pname Enum, param int) {
+ C.glTexParameteri(C.GLenum(target), C.GLenum(pname), C.GLint(param))
+}
+
+func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) {
+ C.gio_glUniformBlockBinding(C.GLuint(p.V), C.GLuint(uniformBlockIndex), C.GLuint(uniformBlockBinding))
+}
+
+func (f *Functions) Uniform1f(dst Uniform, v float32) {
+ C.glUniform1f(C.GLint(dst.V), C.GLfloat(v))
+}
+
+func (f *Functions) Uniform1i(dst Uniform, v int) {
+ C.glUniform1i(C.GLint(dst.V), C.GLint(v))
+}
+
+func (f *Functions) Uniform2f(dst Uniform, v0 float32, v1 float32) {
+ C.glUniform2f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1))
+}
+
+func (f *Functions) Uniform3f(dst Uniform, v0 float32, v1 float32, v2 float32) {
+ C.glUniform3f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2))
+}
+
+func (f *Functions) Uniform4f(dst Uniform, v0 float32, v1 float32, v2 float32, v3 float32) {
+ C.glUniform4f(C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2), C.GLfloat(v3))
+}
+
+func (f *Functions) UseProgram(p Program) {
+ C.glUseProgram(C.GLuint(p.V))
+}
+
+func (f *Functions) UnmapBuffer(target Enum) bool {
+ r := C.gio_glUnmapBuffer(C.GLenum(target))
+ return r == C.GL_TRUE
+}
+
+func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride int, offset int) {
+ var n C.GLboolean = C.GL_FALSE
+ if normalized {
+ n = C.GL_TRUE
+ }
+ C.gio_glVertexAttribPointer(C.GLuint(dst), C.GLint(size), C.GLenum(ty), n, C.GLsizei(stride), C.uintptr_t(offset))
+}
+
+func (f *Functions) Viewport(x int, y int, width int, height int) {
+ C.glViewport(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height))
+}
diff --git a/gio/internal/gl/gl_windows.go b/gio/internal/gl/gl_windows.go
new file mode 100644
index 0000000..099c82b
--- /dev/null
+++ b/gio/internal/gl/gl_windows.go
@@ -0,0 +1,430 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gl
+
+import (
+ "math"
+ "runtime"
+ "syscall"
+ "unsafe"
+
+ "golang.org/x/sys/windows"
+)
+
+var (
+ LibGLESv2 = windows.NewLazyDLL("libGLESv2.dll")
+ _glActiveTexture = LibGLESv2.NewProc("glActiveTexture")
+ _glAttachShader = LibGLESv2.NewProc("glAttachShader")
+ _glBeginQuery = LibGLESv2.NewProc("glBeginQuery")
+ _glBindAttribLocation = LibGLESv2.NewProc("glBindAttribLocation")
+ _glBindBuffer = LibGLESv2.NewProc("glBindBuffer")
+ _glBindBufferBase = LibGLESv2.NewProc("glBindBufferBase")
+ _glBindFramebuffer = LibGLESv2.NewProc("glBindFramebuffer")
+ _glBindRenderbuffer = LibGLESv2.NewProc("glBindRenderbuffer")
+ _glBindTexture = LibGLESv2.NewProc("glBindTexture")
+ _glBlendEquation = LibGLESv2.NewProc("glBlendEquation")
+ _glBlendFunc = LibGLESv2.NewProc("glBlendFunc")
+ _glBufferData = LibGLESv2.NewProc("glBufferData")
+ _glBufferSubData = LibGLESv2.NewProc("glBufferSubData")
+ _glCheckFramebufferStatus = LibGLESv2.NewProc("glCheckFramebufferStatus")
+ _glClear = LibGLESv2.NewProc("glClear")
+ _glClearColor = LibGLESv2.NewProc("glClearColor")
+ _glClearDepthf = LibGLESv2.NewProc("glClearDepthf")
+ _glDeleteQueries = LibGLESv2.NewProc("glDeleteQueries")
+ _glCompileShader = LibGLESv2.NewProc("glCompileShader")
+ _glGenBuffers = LibGLESv2.NewProc("glGenBuffers")
+ _glGenFramebuffers = LibGLESv2.NewProc("glGenFramebuffers")
+ _glGetUniformBlockIndex = LibGLESv2.NewProc("glGetUniformBlockIndex")
+ _glCreateProgram = LibGLESv2.NewProc("glCreateProgram")
+ _glGenRenderbuffers = LibGLESv2.NewProc("glGenRenderbuffers")
+ _glCreateShader = LibGLESv2.NewProc("glCreateShader")
+ _glGenTextures = LibGLESv2.NewProc("glGenTextures")
+ _glDeleteBuffers = LibGLESv2.NewProc("glDeleteBuffers")
+ _glDeleteFramebuffers = LibGLESv2.NewProc("glDeleteFramebuffers")
+ _glDeleteProgram = LibGLESv2.NewProc("glDeleteProgram")
+ _glDeleteShader = LibGLESv2.NewProc("glDeleteShader")
+ _glDeleteRenderbuffers = LibGLESv2.NewProc("glDeleteRenderbuffers")
+ _glDeleteTextures = LibGLESv2.NewProc("glDeleteTextures")
+ _glDepthFunc = LibGLESv2.NewProc("glDepthFunc")
+ _glDepthMask = LibGLESv2.NewProc("glDepthMask")
+ _glDisableVertexAttribArray = LibGLESv2.NewProc("glDisableVertexAttribArray")
+ _glDisable = LibGLESv2.NewProc("glDisable")
+ _glDrawArrays = LibGLESv2.NewProc("glDrawArrays")
+ _glDrawElements = LibGLESv2.NewProc("glDrawElements")
+ _glEnable = LibGLESv2.NewProc("glEnable")
+ _glEnableVertexAttribArray = LibGLESv2.NewProc("glEnableVertexAttribArray")
+ _glEndQuery = LibGLESv2.NewProc("glEndQuery")
+ _glFinish = LibGLESv2.NewProc("glFinish")
+ _glFramebufferRenderbuffer = LibGLESv2.NewProc("glFramebufferRenderbuffer")
+ _glFramebufferTexture2D = LibGLESv2.NewProc("glFramebufferTexture2D")
+ _glGenQueries = LibGLESv2.NewProc("glGenQueries")
+ _glGetError = LibGLESv2.NewProc("glGetError")
+ _glGetRenderbufferParameteri = LibGLESv2.NewProc("glGetRenderbufferParameteri")
+ _glGetFramebufferAttachmentParameteri = LibGLESv2.NewProc("glGetFramebufferAttachmentParameteri")
+ _glGetIntegerv = LibGLESv2.NewProc("glGetIntegerv")
+ _glGetProgramiv = LibGLESv2.NewProc("glGetProgramiv")
+ _glGetProgramInfoLog = LibGLESv2.NewProc("glGetProgramInfoLog")
+ _glGetQueryObjectuiv = LibGLESv2.NewProc("glGetQueryObjectuiv")
+ _glGetShaderiv = LibGLESv2.NewProc("glGetShaderiv")
+ _glGetShaderInfoLog = LibGLESv2.NewProc("glGetShaderInfoLog")
+ _glGetString = LibGLESv2.NewProc("glGetString")
+ _glGetUniformLocation = LibGLESv2.NewProc("glGetUniformLocation")
+ _glInvalidateFramebuffer = LibGLESv2.NewProc("glInvalidateFramebuffer")
+ _glLinkProgram = LibGLESv2.NewProc("glLinkProgram")
+ _glPixelStorei = LibGLESv2.NewProc("glPixelStorei")
+ _glReadPixels = LibGLESv2.NewProc("glReadPixels")
+ _glRenderbufferStorage = LibGLESv2.NewProc("glRenderbufferStorage")
+ _glScissor = LibGLESv2.NewProc("glScissor")
+ _glShaderSource = LibGLESv2.NewProc("glShaderSource")
+ _glTexImage2D = LibGLESv2.NewProc("glTexImage2D")
+ _glTexStorage2D = LibGLESv2.NewProc("glTexStorage2D")
+ _glTexSubImage2D = LibGLESv2.NewProc("glTexSubImage2D")
+ _glTexParameteri = LibGLESv2.NewProc("glTexParameteri")
+ _glUniformBlockBinding = LibGLESv2.NewProc("glUniformBlockBinding")
+ _glUniform1f = LibGLESv2.NewProc("glUniform1f")
+ _glUniform1i = LibGLESv2.NewProc("glUniform1i")
+ _glUniform2f = LibGLESv2.NewProc("glUniform2f")
+ _glUniform3f = LibGLESv2.NewProc("glUniform3f")
+ _glUniform4f = LibGLESv2.NewProc("glUniform4f")
+ _glUseProgram = LibGLESv2.NewProc("glUseProgram")
+ _glVertexAttribPointer = LibGLESv2.NewProc("glVertexAttribPointer")
+ _glViewport = LibGLESv2.NewProc("glViewport")
+)
+
+type Functions struct {
+ // Query caches.
+ int32s [100]int32
+}
+
+type Context interface{}
+
+func NewFunctions(ctx Context) (*Functions, error) {
+ if ctx != nil {
+ panic("non-nil context")
+ }
+ return new(Functions), nil
+}
+
+func (c *Functions) ActiveTexture(t Enum) {
+ syscall.Syscall(_glActiveTexture.Addr(), 1, uintptr(t), 0, 0)
+}
+func (c *Functions) AttachShader(p Program, s Shader) {
+ syscall.Syscall(_glAttachShader.Addr(), 2, uintptr(p.V), uintptr(s.V), 0)
+}
+func (f *Functions) BeginQuery(target Enum, query Query) {
+ syscall.Syscall(_glBeginQuery.Addr(), 2, uintptr(target), uintptr(query.V), 0)
+}
+func (c *Functions) BindAttribLocation(p Program, a Attrib, name string) {
+ cname := cString(name)
+ c0 := &cname[0]
+ syscall.Syscall(_glBindAttribLocation.Addr(), 3, uintptr(p.V), uintptr(a), uintptr(unsafe.Pointer(c0)))
+ issue34474KeepAlive(c)
+}
+func (c *Functions) BindBuffer(target Enum, b Buffer) {
+ syscall.Syscall(_glBindBuffer.Addr(), 2, uintptr(target), uintptr(b.V), 0)
+}
+func (c *Functions) BindBufferBase(target Enum, index int, b Buffer) {
+ syscall.Syscall(_glBindBufferBase.Addr(), 3, uintptr(target), uintptr(index), uintptr(b.V))
+}
+func (c *Functions) BindFramebuffer(target Enum, fb Framebuffer) {
+ syscall.Syscall(_glBindFramebuffer.Addr(), 2, uintptr(target), uintptr(fb.V), 0)
+}
+func (c *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) {
+ syscall.Syscall(_glBindRenderbuffer.Addr(), 2, uintptr(target), uintptr(rb.V), 0)
+}
+func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) {
+ panic("not implemented")
+}
+func (c *Functions) BindTexture(target Enum, t Texture) {
+ syscall.Syscall(_glBindTexture.Addr(), 2, uintptr(target), uintptr(t.V), 0)
+}
+func (c *Functions) BlendEquation(mode Enum) {
+ syscall.Syscall(_glBlendEquation.Addr(), 1, uintptr(mode), 0, 0)
+}
+func (c *Functions) BlendFunc(sfactor, dfactor Enum) {
+ syscall.Syscall(_glBlendFunc.Addr(), 2, uintptr(sfactor), uintptr(dfactor), 0)
+}
+func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) {
+ panic("not implemented")
+}
+func (c *Functions) BufferData(target Enum, size int, usage Enum) {
+ syscall.Syscall6(_glBufferData.Addr(), 4, uintptr(target), uintptr(size), 0, uintptr(usage), 0, 0)
+}
+func (f *Functions) BufferSubData(target Enum, offset int, src []byte) {
+ if n := len(src); n > 0 {
+ s0 := &src[0]
+ syscall.Syscall6(_glBufferSubData.Addr(), 4, uintptr(target), uintptr(offset), uintptr(n), uintptr(unsafe.Pointer(s0)), 0, 0)
+ issue34474KeepAlive(s0)
+ }
+}
+func (c *Functions) CheckFramebufferStatus(target Enum) Enum {
+ s, _, _ := syscall.Syscall(_glCheckFramebufferStatus.Addr(), 1, uintptr(target), 0, 0)
+ return Enum(s)
+}
+func (c *Functions) Clear(mask Enum) {
+ syscall.Syscall(_glClear.Addr(), 1, uintptr(mask), 0, 0)
+}
+func (c *Functions) ClearColor(red, green, blue, alpha float32) {
+ syscall.Syscall6(_glClearColor.Addr(), 4, uintptr(math.Float32bits(red)), uintptr(math.Float32bits(green)), uintptr(math.Float32bits(blue)), uintptr(math.Float32bits(alpha)), 0, 0)
+}
+func (c *Functions) ClearDepthf(d float32) {
+ syscall.Syscall(_glClearDepthf.Addr(), 1, uintptr(math.Float32bits(d)), 0, 0)
+}
+func (c *Functions) CompileShader(s Shader) {
+ syscall.Syscall(_glCompileShader.Addr(), 1, uintptr(s.V), 0, 0)
+}
+func (c *Functions) CreateBuffer() Buffer {
+ var buf uintptr
+ syscall.Syscall(_glGenBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&buf)), 0)
+ return Buffer{uint(buf)}
+}
+func (c *Functions) CreateFramebuffer() Framebuffer {
+ var fb uintptr
+ syscall.Syscall(_glGenFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&fb)), 0)
+ return Framebuffer{uint(fb)}
+}
+func (c *Functions) CreateProgram() Program {
+ p, _, _ := syscall.Syscall(_glCreateProgram.Addr(), 0, 0, 0, 0)
+ return Program{uint(p)}
+}
+func (f *Functions) CreateQuery() Query {
+ var q uintptr
+ syscall.Syscall(_glGenQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&q)), 0)
+ return Query{uint(q)}
+}
+func (c *Functions) CreateRenderbuffer() Renderbuffer {
+ var rb uintptr
+ syscall.Syscall(_glGenRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&rb)), 0)
+ return Renderbuffer{uint(rb)}
+}
+func (c *Functions) CreateShader(ty Enum) Shader {
+ s, _, _ := syscall.Syscall(_glCreateShader.Addr(), 1, uintptr(ty), 0, 0)
+ return Shader{uint(s)}
+}
+func (c *Functions) CreateTexture() Texture {
+ var t uintptr
+ syscall.Syscall(_glGenTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&t)), 0)
+ return Texture{uint(t)}
+}
+func (c *Functions) DeleteBuffer(v Buffer) {
+ syscall.Syscall(_glDeleteBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v)), 0)
+}
+func (c *Functions) DeleteFramebuffer(v Framebuffer) {
+ syscall.Syscall(_glDeleteFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0)
+}
+func (c *Functions) DeleteProgram(p Program) {
+ syscall.Syscall(_glDeleteProgram.Addr(), 1, uintptr(p.V), 0, 0)
+}
+func (f *Functions) DeleteQuery(query Query) {
+ syscall.Syscall(_glDeleteQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&query.V)), 0)
+}
+func (c *Functions) DeleteShader(s Shader) {
+ syscall.Syscall(_glDeleteShader.Addr(), 1, uintptr(s.V), 0, 0)
+}
+func (c *Functions) DeleteRenderbuffer(v Renderbuffer) {
+ syscall.Syscall(_glDeleteRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0)
+}
+func (c *Functions) DeleteTexture(v Texture) {
+ syscall.Syscall(_glDeleteTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0)
+}
+func (c *Functions) DepthFunc(f Enum) {
+ syscall.Syscall(_glDepthFunc.Addr(), 1, uintptr(f), 0, 0)
+}
+func (c *Functions) DepthMask(mask bool) {
+ var m uintptr
+ if mask {
+ m = 1
+ }
+ syscall.Syscall(_glDepthMask.Addr(), 1, m, 0, 0)
+}
+func (c *Functions) DisableVertexAttribArray(a Attrib) {
+ syscall.Syscall(_glDisableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0)
+}
+func (c *Functions) Disable(cap Enum) {
+ syscall.Syscall(_glDisable.Addr(), 1, uintptr(cap), 0, 0)
+}
+func (c *Functions) DrawArrays(mode Enum, first, count int) {
+ syscall.Syscall(_glDrawArrays.Addr(), 3, uintptr(mode), uintptr(first), uintptr(count))
+}
+func (c *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) {
+ syscall.Syscall6(_glDrawElements.Addr(), 4, uintptr(mode), uintptr(count), uintptr(ty), uintptr(offset), 0, 0)
+}
+func (f *Functions) DispatchCompute(x, y, z int) {
+ panic("not implemented")
+}
+func (c *Functions) Enable(cap Enum) {
+ syscall.Syscall(_glEnable.Addr(), 1, uintptr(cap), 0, 0)
+}
+func (c *Functions) EnableVertexAttribArray(a Attrib) {
+ syscall.Syscall(_glEnableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0)
+}
+func (f *Functions) EndQuery(target Enum) {
+ syscall.Syscall(_glEndQuery.Addr(), 1, uintptr(target), 0, 0)
+}
+func (c *Functions) Finish() {
+ syscall.Syscall(_glFinish.Addr(), 0, 0, 0, 0)
+}
+func (c *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) {
+ syscall.Syscall6(_glFramebufferRenderbuffer.Addr(), 4, uintptr(target), uintptr(attachment), uintptr(renderbuffertarget), uintptr(renderbuffer.V), 0, 0)
+}
+func (c *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) {
+ syscall.Syscall6(_glFramebufferTexture2D.Addr(), 5, uintptr(target), uintptr(attachment), uintptr(texTarget), uintptr(t.V), uintptr(level), 0)
+}
+func (f *Functions) GetUniformBlockIndex(p Program, name string) uint {
+ cname := cString(name)
+ c0 := &cname[0]
+ u, _, _ := syscall.Syscall(_glGetUniformBlockIndex.Addr(), 2, uintptr(p.V), uintptr(unsafe.Pointer(c0)), 0)
+ issue34474KeepAlive(c0)
+ return uint(u)
+}
+func (c *Functions) GetBinding(pname Enum) Object {
+ return Object{uint(c.GetInteger(pname))}
+}
+func (c *Functions) GetError() Enum {
+ e, _, _ := syscall.Syscall(_glGetError.Addr(), 0, 0, 0, 0)
+ return Enum(e)
+}
+func (c *Functions) GetRenderbufferParameteri(target, pname Enum) int {
+ p, _, _ := syscall.Syscall(_glGetRenderbufferParameteri.Addr(), 2, uintptr(target), uintptr(pname), 0)
+ return int(p)
+}
+func (c *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int {
+ p, _, _ := syscall.Syscall(_glGetFramebufferAttachmentParameteri.Addr(), 3, uintptr(target), uintptr(attachment), uintptr(pname))
+ return int(p)
+}
+func (c *Functions) GetInteger(pname Enum) int {
+ syscall.Syscall(_glGetIntegerv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])), 0)
+ return int(c.int32s[0])
+}
+func (c *Functions) GetProgrami(p Program, pname Enum) int {
+ syscall.Syscall(_glGetProgramiv.Addr(), 3, uintptr(p.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])))
+ return int(c.int32s[0])
+}
+func (c *Functions) GetProgramInfoLog(p Program) string {
+ n := c.GetProgrami(p, INFO_LOG_LENGTH)
+ buf := make([]byte, n)
+ syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0)
+ return string(buf)
+}
+func (c *Functions) GetQueryObjectuiv(query Query, pname Enum) uint {
+ syscall.Syscall(_glGetQueryObjectuiv.Addr(), 3, uintptr(query.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])))
+ return uint(c.int32s[0])
+}
+func (c *Functions) GetShaderi(s Shader, pname Enum) int {
+ syscall.Syscall(_glGetShaderiv.Addr(), 3, uintptr(s.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])))
+ return int(c.int32s[0])
+}
+func (c *Functions) GetShaderInfoLog(s Shader) string {
+ n := c.GetShaderi(s, INFO_LOG_LENGTH)
+ buf := make([]byte, n)
+ syscall.Syscall6(_glGetShaderInfoLog.Addr(), 4, uintptr(s.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0)
+ return string(buf)
+}
+func (c *Functions) GetString(pname Enum) string {
+ s, _, _ := syscall.Syscall(_glGetString.Addr(), 1, uintptr(pname), 0, 0)
+ return windows.BytePtrToString((*byte)(unsafe.Pointer(s)))
+}
+func (c *Functions) GetUniformLocation(p Program, name string) Uniform {
+ cname := cString(name)
+ c0 := &cname[0]
+ u, _, _ := syscall.Syscall(_glGetUniformLocation.Addr(), 2, uintptr(p.V), uintptr(unsafe.Pointer(c0)), 0)
+ issue34474KeepAlive(c0)
+ return Uniform{int(u)}
+}
+func (c *Functions) InvalidateFramebuffer(target, attachment Enum) {
+ addr := _glInvalidateFramebuffer.Addr()
+ if addr == 0 {
+ // InvalidateFramebuffer is just a hint. Skip it if not supported.
+ return
+ }
+ syscall.Syscall(addr, 3, uintptr(target), 1, uintptr(unsafe.Pointer(&attachment)))
+}
+func (c *Functions) LinkProgram(p Program) {
+ syscall.Syscall(_glLinkProgram.Addr(), 1, uintptr(p.V), 0, 0)
+}
+func (c *Functions) PixelStorei(pname Enum, param int32) {
+ syscall.Syscall(_glPixelStorei.Addr(), 2, uintptr(pname), uintptr(param), 0)
+}
+func (f *Functions) MemoryBarrier(barriers Enum) {
+ panic("not implemented")
+}
+func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte {
+ panic("not implemented")
+}
+func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) {
+ d0 := &data[0]
+ syscall.Syscall9(_glReadPixels.Addr(), 7, uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(d0)), 0, 0)
+ issue34474KeepAlive(d0)
+}
+func (c *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) {
+ syscall.Syscall6(_glRenderbufferStorage.Addr(), 4, uintptr(target), uintptr(internalformat), uintptr(width), uintptr(height), 0, 0)
+}
+func (c *Functions) Scissor(x, y, width, height int32) {
+ syscall.Syscall6(_glScissor.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0)
+}
+func (c *Functions) ShaderSource(s Shader, src string) {
+ var n uintptr = uintptr(len(src))
+ psrc := &src
+ syscall.Syscall6(_glShaderSource.Addr(), 4, uintptr(s.V), 1, uintptr(unsafe.Pointer(psrc)), uintptr(unsafe.Pointer(&n)), 0, 0)
+ issue34474KeepAlive(psrc)
+}
+func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width int, height int, format Enum, ty Enum) {
+ syscall.Syscall9(_glTexImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(internalFormat), uintptr(width), uintptr(height), 0, uintptr(format), uintptr(ty), 0)
+}
+func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) {
+ syscall.Syscall6(_glTexStorage2D.Addr(), 5, uintptr(target), uintptr(levels), uintptr(internalFormat), uintptr(width), uintptr(height), 0)
+}
+func (c *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) {
+ d0 := &data[0]
+ syscall.Syscall9(_glTexSubImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(d0)))
+ issue34474KeepAlive(d0)
+}
+func (c *Functions) TexParameteri(target, pname Enum, param int) {
+ syscall.Syscall(_glTexParameteri.Addr(), 3, uintptr(target), uintptr(pname), uintptr(param))
+}
+func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) {
+ syscall.Syscall(_glUniformBlockBinding.Addr(), 3, uintptr(p.V), uintptr(uniformBlockIndex), uintptr(uniformBlockBinding))
+}
+func (c *Functions) Uniform1f(dst Uniform, v float32) {
+ syscall.Syscall(_glUniform1f.Addr(), 2, uintptr(dst.V), uintptr(math.Float32bits(v)), 0)
+}
+func (c *Functions) Uniform1i(dst Uniform, v int) {
+ syscall.Syscall(_glUniform1i.Addr(), 2, uintptr(dst.V), uintptr(v), 0)
+}
+func (c *Functions) Uniform2f(dst Uniform, v0, v1 float32) {
+ syscall.Syscall(_glUniform2f.Addr(), 3, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)))
+}
+func (c *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) {
+ syscall.Syscall6(_glUniform3f.Addr(), 4, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), 0, 0)
+}
+func (c *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) {
+ syscall.Syscall6(_glUniform4f.Addr(), 5, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), uintptr(math.Float32bits(v3)), 0)
+}
+func (c *Functions) UseProgram(p Program) {
+ syscall.Syscall(_glUseProgram.Addr(), 1, uintptr(p.V), 0, 0)
+}
+func (f *Functions) UnmapBuffer(target Enum) bool {
+ panic("not implemented")
+}
+func (c *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) {
+ var norm uintptr
+ if normalized {
+ norm = 1
+ }
+ syscall.Syscall6(_glVertexAttribPointer.Addr(), 6, uintptr(dst), uintptr(size), uintptr(ty), norm, uintptr(stride), uintptr(offset))
+}
+func (c *Functions) Viewport(x, y, width, height int) {
+ syscall.Syscall6(_glViewport.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0)
+}
+
+func cString(s string) []byte {
+ b := make([]byte, len(s)+1)
+ copy(b, s)
+ return b
+}
+
+// issue34474KeepAlive calls runtime.KeepAlive as a
+// workaround for golang.org/issue/34474.
+func issue34474KeepAlive(v interface{}) {
+ runtime.KeepAlive(v)
+}
diff --git a/gio/internal/gl/types.go b/gio/internal/gl/types.go
new file mode 100644
index 0000000..45db3be
--- /dev/null
+++ b/gio/internal/gl/types.go
@@ -0,0 +1,27 @@
+// +build !js
+
+package gl
+
+type (
+ Buffer struct{ V uint }
+ Framebuffer struct{ V uint }
+ Program struct{ V uint }
+ Renderbuffer struct{ V uint }
+ Shader struct{ V uint }
+ Texture struct{ V uint }
+ Query struct{ V uint }
+ Uniform struct{ V int }
+ Object struct{ V uint }
+)
+
+func (u Uniform) Valid() bool {
+ return u.V != -1
+}
+
+func (p Program) Valid() bool {
+ return p.V != 0
+}
+
+func (s Shader) Valid() bool {
+ return s.V != 0
+}
diff --git a/gio/internal/gl/types_js.go b/gio/internal/gl/types_js.go
new file mode 100644
index 0000000..584c2af
--- /dev/null
+++ b/gio/internal/gl/types_js.go
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gl
+
+import "syscall/js"
+
+type (
+ Buffer js.Value
+ Framebuffer js.Value
+ Program js.Value
+ Renderbuffer js.Value
+ Shader js.Value
+ Texture js.Value
+ Query js.Value
+ Uniform js.Value
+ Object js.Value
+)
+
+func (p Program) Valid() bool {
+ return !js.Value(p).IsUndefined() && !js.Value(p).IsNull()
+}
+
+func (s Shader) Valid() bool {
+ return !js.Value(s).IsUndefined() && !js.Value(s).IsNull()
+}
+
+func (u Uniform) Valid() bool {
+ return !js.Value(u).IsUndefined() && !js.Value(u).IsNull()
+}
diff --git a/gio/internal/gl/util.go b/gio/internal/gl/util.go
new file mode 100644
index 0000000..3d5b44b
--- /dev/null
+++ b/gio/internal/gl/util.go
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package gl
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+)
+
+func CreateProgram(ctx *Functions, vsSrc, fsSrc string, attribs []string) (Program, error) {
+ vs, err := createShader(ctx, VERTEX_SHADER, vsSrc)
+ if err != nil {
+ return Program{}, err
+ }
+ defer ctx.DeleteShader(vs)
+ fs, err := createShader(ctx, FRAGMENT_SHADER, fsSrc)
+ if err != nil {
+ return Program{}, err
+ }
+ defer ctx.DeleteShader(fs)
+ prog := ctx.CreateProgram()
+ if !prog.Valid() {
+ return Program{}, errors.New("glCreateProgram failed")
+ }
+ ctx.AttachShader(prog, vs)
+ ctx.AttachShader(prog, fs)
+ for i, a := range attribs {
+ ctx.BindAttribLocation(prog, Attrib(i), a)
+ }
+ ctx.LinkProgram(prog)
+ if ctx.GetProgrami(prog, LINK_STATUS) == 0 {
+ log := ctx.GetProgramInfoLog(prog)
+ ctx.DeleteProgram(prog)
+ return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log))
+ }
+ return prog, nil
+}
+
+func CreateComputeProgram(ctx *Functions, src string) (Program, error) {
+ cs, err := createShader(ctx, COMPUTE_SHADER, src)
+ if err != nil {
+ return Program{}, err
+ }
+ defer ctx.DeleteShader(cs)
+ prog := ctx.CreateProgram()
+ if !prog.Valid() {
+ return Program{}, errors.New("glCreateProgram failed")
+ }
+ ctx.AttachShader(prog, cs)
+ ctx.LinkProgram(prog)
+ if ctx.GetProgrami(prog, LINK_STATUS) == 0 {
+ log := ctx.GetProgramInfoLog(prog)
+ ctx.DeleteProgram(prog)
+ return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log))
+ }
+ return prog, nil
+}
+
+func createShader(ctx *Functions, typ Enum, src string) (Shader, error) {
+ sh := ctx.CreateShader(typ)
+ if !sh.Valid() {
+ return Shader{}, errors.New("glCreateShader failed")
+ }
+ ctx.ShaderSource(sh, src)
+ ctx.CompileShader(sh)
+ if ctx.GetShaderi(sh, COMPILE_STATUS) == 0 {
+ log := ctx.GetShaderInfoLog(sh)
+ ctx.DeleteShader(sh)
+ return Shader{}, fmt.Errorf("shader compilation failed: %s", strings.TrimSpace(log))
+ }
+ return sh, nil
+}
+
+func ParseGLVersion(glVer string) (version [2]int, gles bool, err error) {
+ var ver [2]int
+ if _, err := fmt.Sscanf(glVer, "OpenGL ES %d.%d", &ver[0], &ver[1]); err == nil {
+ return ver, true, nil
+ } else if _, err := fmt.Sscanf(glVer, "WebGL %d.%d", &ver[0], &ver[1]); err == nil {
+ // WebGL major version v corresponds to OpenGL ES version v + 1
+ ver[0]++
+ return ver, true, nil
+ } else if _, err := fmt.Sscanf(glVer, "%d.%d", &ver[0], &ver[1]); err == nil {
+ return ver, false, nil
+ }
+ return ver, false, fmt.Errorf("failed to parse OpenGL ES version (%s)", glVer)
+}
diff --git a/gio/internal/opconst/ops.go b/gio/internal/opconst/ops.go
new file mode 100644
index 0000000..db9dd8d
--- /dev/null
+++ b/gio/internal/opconst/ops.go
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package opconst
+
+type OpType byte
+
+// Start at a high number for easier debugging.
+const firstOpIndex = 200
+
+const (
+ TypeMacro OpType = iota + firstOpIndex
+ TypeCall
+ TypeDefer
+ TypeTransform
+ TypeInvalidate
+ TypeImage
+ TypePaint
+ TypeColor
+ TypeLinearGradient
+ TypeArea
+ TypePointerInput
+ TypePass
+ TypeClipboardRead
+ TypeClipboardWrite
+ TypeKeyInput
+ TypeKeyFocus
+ TypeKeySoftKeyboard
+ TypeSave
+ TypeLoad
+ TypeAux
+ TypeClip
+ TypeProfile
+ TypeCursor
+ TypePath
+ TypeStroke
+)
+
+const (
+ TypeMacroLen = 1 + 4 + 4
+ TypeCallLen = 1 + 4 + 4
+ TypeDeferLen = 1
+ TypeTransformLen = 1 + 4*6
+ TypeRedrawLen = 1 + 8
+ TypeImageLen = 1
+ TypePaintLen = 1
+ TypeColorLen = 1 + 4
+ TypeLinearGradientLen = 1 + 8*2 + 4*2
+ TypeAreaLen = 1 + 1 + 4*4
+ TypePointerInputLen = 1 + 1 + 1 + 2*4 + 2*4
+ TypePassLen = 1 + 1
+ TypeClipboardReadLen = 1
+ TypeClipboardWriteLen = 1
+ TypeKeyInputLen = 1
+ TypeKeyFocusLen = 1
+ TypeKeySoftKeyboardLen = 1 + 1
+ TypeSaveLen = 1 + 4
+ TypeLoadLen = 1 + 1 + 4
+ TypeAuxLen = 1
+ TypeClipLen = 1 + 4*4 + 1
+ TypeProfileLen = 1
+ TypeCursorLen = 1 + 1
+ TypePathLen = 1
+ TypeStrokeLen = 1 + 4
+)
+
+// StateMask is a bitmask of state types a load operation
+// should restore.
+type StateMask uint8
+
+const (
+ TransformState StateMask = 1 << iota
+
+ AllState = ^StateMask(0)
+)
+
+// InitialStateID is the ID for saving and loading
+// the initial operation state.
+const InitialStateID = 0
+
+func (t OpType) Size() int {
+ return [...]int{
+ TypeMacroLen,
+ TypeCallLen,
+ TypeDeferLen,
+ TypeTransformLen,
+ TypeRedrawLen,
+ TypeImageLen,
+ TypePaintLen,
+ TypeColorLen,
+ TypeLinearGradientLen,
+ TypeAreaLen,
+ TypePointerInputLen,
+ TypePassLen,
+ TypeClipboardReadLen,
+ TypeClipboardWriteLen,
+ TypeKeyInputLen,
+ TypeKeyFocusLen,
+ TypeKeySoftKeyboardLen,
+ TypeSaveLen,
+ TypeLoadLen,
+ TypeAuxLen,
+ TypeClipLen,
+ TypeProfileLen,
+ TypeCursorLen,
+ TypePathLen,
+ TypeStrokeLen,
+ }[t-firstOpIndex]
+}
+
+func (t OpType) NumRefs() int {
+ switch t {
+ case TypeKeyInput, TypeKeyFocus, TypePointerInput, TypeProfile, TypeCall, TypeClipboardRead, TypeClipboardWrite, TypeCursor:
+ return 1
+ case TypeImage:
+ return 2
+ default:
+ return 0
+ }
+}
diff --git a/gio/internal/ops/ops.go b/gio/internal/ops/ops.go
new file mode 100644
index 0000000..a25839f
--- /dev/null
+++ b/gio/internal/ops/ops.go
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package ops
+
+import (
+ "encoding/binary"
+ "math"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/byteslice"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/scene"
+)
+
+func DecodeCommand(d []byte) scene.Command {
+ var cmd scene.Command
+ copy(byteslice.Uint32(cmd[:]), d)
+ return cmd
+}
+
+func EncodeCommand(out []byte, cmd scene.Command) {
+ copy(out, byteslice.Uint32(cmd[:]))
+}
+
+func DecodeTransform(data []byte) (t f32.Affine2D) {
+ if opconst.OpType(data[0]) != opconst.TypeTransform {
+ panic("invalid op")
+ }
+ data = data[1:]
+ data = data[:4*6]
+
+ bo := binary.LittleEndian
+ a := math.Float32frombits(bo.Uint32(data))
+ b := math.Float32frombits(bo.Uint32(data[4*1:]))
+ c := math.Float32frombits(bo.Uint32(data[4*2:]))
+ d := math.Float32frombits(bo.Uint32(data[4*3:]))
+ e := math.Float32frombits(bo.Uint32(data[4*4:]))
+ f := math.Float32frombits(bo.Uint32(data[4*5:]))
+ return f32.NewAffine2D(a, b, c, d, e, f)
+}
+
+// DecodeSave decodes the state id of a save op.
+func DecodeSave(data []byte) int {
+ if opconst.OpType(data[0]) != opconst.TypeSave {
+ panic("invalid op")
+ }
+ bo := binary.LittleEndian
+ return int(bo.Uint32(data[1:]))
+}
+
+// DecodeLoad decodes the state id and mask of a load op.
+func DecodeLoad(data []byte) (int, opconst.StateMask) {
+ if opconst.OpType(data[0]) != opconst.TypeLoad {
+ panic("invalid op")
+ }
+ bo := binary.LittleEndian
+ return int(bo.Uint32(data[2:])), opconst.StateMask(data[1])
+}
diff --git a/gio/internal/ops/reader.go b/gio/internal/ops/reader.go
new file mode 100644
index 0000000..8465446
--- /dev/null
+++ b/gio/internal/ops/reader.go
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package ops
+
+import (
+ "encoding/binary"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/op"
+)
+
+// Reader parses an ops list.
+type Reader struct {
+ pc PC
+ stack []macro
+ ops *op.Ops
+ deferOps op.Ops
+ deferDone bool
+}
+
+// EncodedOp represents an encoded op returned by
+// Reader.
+type EncodedOp struct {
+ Key Key
+ Data []byte
+ Refs []interface{}
+}
+
+// Key is a unique key for a given op.
+type Key struct {
+ ops *op.Ops
+ pc int
+ version int
+ sx, hx, sy, hy float32
+}
+
+// Shadow of op.MacroOp.
+type macroOp struct {
+ ops *op.Ops
+ pc PC
+}
+
+// PC is an instruction counter for an operation list.
+type PC struct {
+ data int
+ refs int
+}
+
+type macro struct {
+ ops *op.Ops
+ retPC PC
+ endPC PC
+}
+
+type opMacroDef struct {
+ endpc PC
+}
+
+// Reset start reading from the beginning of ops.
+func (r *Reader) Reset(ops *op.Ops) {
+ r.ResetAt(ops, PC{})
+}
+
+// ResetAt is like Reset, except it starts reading from pc.
+func (r *Reader) ResetAt(ops *op.Ops, pc PC) {
+ r.stack = r.stack[:0]
+ r.deferOps.Reset()
+ r.deferDone = false
+ r.pc = pc
+ r.ops = ops
+}
+
+// NewPC returns a PC representing the current instruction counter of
+// ops.
+func NewPC(ops *op.Ops) PC {
+ return PC{
+ data: len(ops.Data()),
+ refs: len(ops.Refs()),
+ }
+}
+
+func (k Key) SetTransform(t f32.Affine2D) Key {
+ sx, hx, _, hy, sy, _ := t.Elems()
+ k.sx = sx
+ k.hx = hx
+ k.hy = hy
+ k.sy = sy
+ return k
+}
+
+func (r *Reader) Decode() (EncodedOp, bool) {
+ if r.ops == nil {
+ return EncodedOp{}, false
+ }
+ deferring := false
+ for {
+ if len(r.stack) > 0 {
+ b := r.stack[len(r.stack)-1]
+ if r.pc == b.endPC {
+ r.ops = b.ops
+ r.pc = b.retPC
+ r.stack = r.stack[:len(r.stack)-1]
+ continue
+ }
+ }
+ data := r.ops.Data()
+ data = data[r.pc.data:]
+ refs := r.ops.Refs()
+ if len(data) == 0 {
+ if r.deferDone {
+ return EncodedOp{}, false
+ }
+ r.deferDone = true
+ // Execute deferred macros.
+ r.ops = &r.deferOps
+ r.pc = PC{}
+ continue
+ }
+ key := Key{ops: r.ops, pc: r.pc.data, version: r.ops.Version()}
+ t := opconst.OpType(data[0])
+ n := t.Size()
+ nrefs := t.NumRefs()
+ data = data[:n]
+ refs = refs[r.pc.refs:]
+ refs = refs[:nrefs]
+ switch t {
+ case opconst.TypeDefer:
+ deferring = true
+ r.pc.data += n
+ r.pc.refs += nrefs
+ continue
+ case opconst.TypeAux:
+ // An Aux operations is always wrapped in a macro, and
+ // its length is the remaining space.
+ block := r.stack[len(r.stack)-1]
+ n += block.endPC.data - r.pc.data - opconst.TypeAuxLen
+ data = data[:n]
+ case opconst.TypeCall:
+ if deferring {
+ deferring = false
+ // Copy macro for deferred execution.
+ if t.NumRefs() != 1 {
+ panic("internal error: unexpected number of macro refs")
+ }
+ deferData := r.deferOps.Write1(t.Size(), refs[0])
+ copy(deferData, data)
+ continue
+ }
+ var op macroOp
+ op.decode(data, refs)
+ macroData := op.ops.Data()[op.pc.data:]
+ if opconst.OpType(macroData[0]) != opconst.TypeMacro {
+ panic("invalid macro reference")
+ }
+ var opDef opMacroDef
+ opDef.decode(macroData[:opconst.TypeMacro.Size()])
+ retPC := r.pc
+ retPC.data += n
+ retPC.refs += nrefs
+ r.stack = append(r.stack, macro{
+ ops: r.ops,
+ retPC: retPC,
+ endPC: opDef.endpc,
+ })
+ r.ops = op.ops
+ r.pc = op.pc
+ r.pc.data += opconst.TypeMacro.Size()
+ r.pc.refs += opconst.TypeMacro.NumRefs()
+ continue
+ case opconst.TypeMacro:
+ var op opMacroDef
+ op.decode(data)
+ r.pc = op.endpc
+ continue
+ }
+ r.pc.data += n
+ r.pc.refs += nrefs
+ return EncodedOp{Key: key, Data: data, Refs: refs}, true
+ }
+}
+
+func (op *opMacroDef) decode(data []byte) {
+ if opconst.OpType(data[0]) != opconst.TypeMacro {
+ panic("invalid op")
+ }
+ bo := binary.LittleEndian
+ data = data[:9]
+ dataIdx := int(int32(bo.Uint32(data[1:])))
+ refsIdx := int(int32(bo.Uint32(data[5:])))
+ *op = opMacroDef{
+ endpc: PC{
+ data: dataIdx,
+ refs: refsIdx,
+ },
+ }
+}
+
+func (m *macroOp) decode(data []byte, refs []interface{}) {
+ if opconst.OpType(data[0]) != opconst.TypeCall {
+ panic("invalid op")
+ }
+ data = data[:9]
+ bo := binary.LittleEndian
+ dataIdx := int(int32(bo.Uint32(data[1:])))
+ refsIdx := int(int32(bo.Uint32(data[5:])))
+ *m = macroOp{
+ ops: refs[0].(*op.Ops),
+ pc: PC{
+ data: dataIdx,
+ refs: refsIdx,
+ },
+ }
+}
diff --git a/gio/internal/scene/scene.go b/gio/internal/scene/scene.go
new file mode 100644
index 0000000..8761a13
--- /dev/null
+++ b/gio/internal/scene/scene.go
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package scene encodes and decodes graphics commands in the format used by the
+// compute renderer.
+package scene
+
+import (
+ "fmt"
+ "image/color"
+ "math"
+ "unsafe"
+
+ "realy.lol/gio/f32"
+)
+
+type Op uint32
+
+type Command [sceneElemSize / 4]uint32
+
+// GPU commands from scene.h
+const (
+ OpNop Op = iota
+ OpLine
+ OpQuad
+ OpCubic
+ OpFillColor
+ OpLineWidth
+ OpTransform
+ OpBeginClip
+ OpEndClip
+ OpFillImage
+ OpSetFillMode
+)
+
+// FillModes, from setup.h.
+type FillMode uint32
+
+const (
+ FillModeNonzero = 0
+ FillModeStroke = 1
+)
+
+const CommandSize = int(unsafe.Sizeof(Command{}))
+
+const sceneElemSize = 36
+
+func (c Command) Op() Op {
+ return Op(c[0])
+}
+
+func (c Command) String() string {
+ switch Op(c[0]) {
+ case OpNop:
+ return "nop"
+ case OpLine:
+ from, to := DecodeLine(c)
+ return fmt.Sprintf("line(%v, %v)", from, to)
+ case OpQuad:
+ from, ctrl, to := DecodeQuad(c)
+ return fmt.Sprintf("quad(%v, %v, %v)", from, ctrl, to)
+ case OpCubic:
+ from, ctrl0, ctrl1, to := DecodeCubic(c)
+ return fmt.Sprintf("cubic(%v, %v, %v, %v)", from, ctrl0, ctrl1, to)
+ case OpFillColor:
+ return "fillcolor"
+ case OpLineWidth:
+ return "linewidth"
+ case OpTransform:
+ t := f32.NewAffine2D(
+ math.Float32frombits(c[1]),
+ math.Float32frombits(c[3]),
+ math.Float32frombits(c[5]),
+ math.Float32frombits(c[2]),
+ math.Float32frombits(c[4]),
+ math.Float32frombits(c[6]),
+ )
+ return fmt.Sprintf("transform (%v)", t)
+ case OpBeginClip:
+ bounds := f32.Rectangle{
+ Min: f32.Pt(math.Float32frombits(c[1]), math.Float32frombits(c[2])),
+ Max: f32.Pt(math.Float32frombits(c[3]), math.Float32frombits(c[4])),
+ }
+ return fmt.Sprintf("beginclip (%v)", bounds)
+ case OpEndClip:
+ bounds := f32.Rectangle{
+ Min: f32.Pt(math.Float32frombits(c[1]), math.Float32frombits(c[2])),
+ Max: f32.Pt(math.Float32frombits(c[3]), math.Float32frombits(c[4])),
+ }
+ return fmt.Sprintf("endclip (%v)", bounds)
+ case OpFillImage:
+ return "fillimage"
+ case OpSetFillMode:
+ return "setfillmode"
+ default:
+ panic("unreachable")
+ }
+}
+
+func Line(start, end f32.Point) Command {
+ return Command{
+ 0: uint32(OpLine),
+ 1: math.Float32bits(start.X),
+ 2: math.Float32bits(start.Y),
+ 3: math.Float32bits(end.X),
+ 4: math.Float32bits(end.Y),
+ }
+}
+
+func Cubic(start, ctrl0, ctrl1, end f32.Point) Command {
+ return Command{
+ 0: uint32(OpCubic),
+ 1: math.Float32bits(start.X),
+ 2: math.Float32bits(start.Y),
+ 3: math.Float32bits(ctrl0.X),
+ 4: math.Float32bits(ctrl0.Y),
+ 5: math.Float32bits(ctrl1.X),
+ 6: math.Float32bits(ctrl1.Y),
+ 7: math.Float32bits(end.X),
+ 8: math.Float32bits(end.Y),
+ }
+}
+
+func Quad(start, ctrl, end f32.Point) Command {
+ return Command{
+ 0: uint32(OpQuad),
+ 1: math.Float32bits(start.X),
+ 2: math.Float32bits(start.Y),
+ 3: math.Float32bits(ctrl.X),
+ 4: math.Float32bits(ctrl.Y),
+ 5: math.Float32bits(end.X),
+ 6: math.Float32bits(end.Y),
+ }
+}
+
+func Transform(m f32.Affine2D) Command {
+ sx, hx, ox, hy, sy, oy := m.Elems()
+ return Command{
+ 0: uint32(OpTransform),
+ 1: math.Float32bits(sx),
+ 2: math.Float32bits(hy),
+ 3: math.Float32bits(hx),
+ 4: math.Float32bits(sy),
+ 5: math.Float32bits(ox),
+ 6: math.Float32bits(oy),
+ }
+}
+
+func SetLineWidth(width float32) Command {
+ return Command{
+ 0: uint32(OpLineWidth),
+ 1: math.Float32bits(width),
+ }
+}
+
+func BeginClip(bbox f32.Rectangle) Command {
+ return Command{
+ 0: uint32(OpBeginClip),
+ 1: math.Float32bits(bbox.Min.X),
+ 2: math.Float32bits(bbox.Min.Y),
+ 3: math.Float32bits(bbox.Max.X),
+ 4: math.Float32bits(bbox.Max.Y),
+ }
+}
+
+func EndClip(bbox f32.Rectangle) Command {
+ return Command{
+ 0: uint32(OpEndClip),
+ 1: math.Float32bits(bbox.Min.X),
+ 2: math.Float32bits(bbox.Min.Y),
+ 3: math.Float32bits(bbox.Max.X),
+ 4: math.Float32bits(bbox.Max.Y),
+ }
+}
+
+func FillColor(col color.RGBA) Command {
+ return Command{
+ 0: uint32(OpFillColor),
+ 1: uint32(col.R)<<24 | uint32(col.G)<<16 | uint32(col.B)<<8 | uint32(col.A),
+ }
+}
+
+func FillImage(index int) Command {
+ return Command{
+ 0: uint32(OpFillImage),
+ 1: uint32(index),
+ }
+}
+
+func SetFillMode(mode FillMode) Command {
+ return Command{
+ 0: uint32(OpSetFillMode),
+ 1: uint32(mode),
+ }
+}
+
+func DecodeLine(cmd Command) (from, to f32.Point) {
+ if cmd[0] != uint32(OpLine) {
+ panic("invalid command")
+ }
+ from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2]))
+ to = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4]))
+ return
+}
+
+func DecodeQuad(cmd Command) (from, ctrl, to f32.Point) {
+ if cmd[0] != uint32(OpQuad) {
+ panic("invalid command")
+ }
+ from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2]))
+ ctrl = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4]))
+ to = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6]))
+ return
+}
+
+func DecodeCubic(cmd Command) (from, ctrl0, ctrl1, to f32.Point) {
+ if cmd[0] != uint32(OpCubic) {
+ panic("invalid command")
+ }
+ from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2]))
+ ctrl0 = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4]))
+ ctrl1 = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6]))
+ to = f32.Pt(math.Float32frombits(cmd[7]), math.Float32frombits(cmd[8]))
+ return
+}
diff --git a/gio/internal/srgb/srgb.go b/gio/internal/srgb/srgb.go
new file mode 100644
index 0000000..1cd67cf
--- /dev/null
+++ b/gio/internal/srgb/srgb.go
@@ -0,0 +1,206 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package srgb
+
+import (
+ "fmt"
+ "runtime"
+ "strings"
+
+ "realy.lol/gio/internal/byteslice"
+ "realy.lol/gio/internal/gl"
+)
+
+// FBO implements an intermediate sRGB FBO
+// for gamma-correct rendering on platforms without
+// sRGB enabled native framebuffers.
+type FBO struct {
+ c *gl.Functions
+ width, height int
+ frameBuffer gl.Framebuffer
+ depthBuffer gl.Renderbuffer
+ colorTex gl.Texture
+ blitted bool
+ quad gl.Buffer
+ prog gl.Program
+ gl3 bool
+}
+
+func New(ctx gl.Context) (*FBO, error) {
+ f, err := gl.NewFunctions(ctx)
+ if err != nil {
+ return nil, err
+ }
+ var gl3 bool
+ glVer := f.GetString(gl.VERSION)
+ ver, _, err := gl.ParseGLVersion(glVer)
+ if err != nil {
+ return nil, err
+ }
+ if ver[0] >= 3 {
+ gl3 = true
+ } else {
+ exts := f.GetString(gl.EXTENSIONS)
+ if !strings.Contains(exts, "EXT_sRGB") {
+ return nil, fmt.Errorf("no support for OpenGL ES 3 nor EXT_sRGB")
+ }
+ }
+ s := &FBO{
+ c: f,
+ gl3: gl3,
+ frameBuffer: f.CreateFramebuffer(),
+ colorTex: f.CreateTexture(),
+ depthBuffer: f.CreateRenderbuffer(),
+ }
+ f.BindTexture(gl.TEXTURE_2D, s.colorTex)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
+ f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
+ return s, nil
+}
+
+func (s *FBO) Blit() {
+ if !s.blitted {
+ prog, err := gl.CreateProgram(s.c, blitVSrc, blitFSrc,
+ []string{"pos", "uv"})
+ if err != nil {
+ panic(err)
+ }
+ s.prog = prog
+ s.c.UseProgram(prog)
+ s.c.Uniform1i(s.c.GetUniformLocation(prog, "tex"), 0)
+ s.quad = s.c.CreateBuffer()
+ s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad)
+ coords := byteslice.Slice([]float32{
+ -1, +1, 0, 1,
+ +1, +1, 1, 1,
+ -1, -1, 0, 0,
+ +1, -1, 1, 0,
+ })
+ s.c.BufferData(gl.ARRAY_BUFFER, len(coords), gl.STATIC_DRAW)
+ s.c.BufferSubData(gl.ARRAY_BUFFER, 0, coords)
+ s.blitted = true
+ }
+ s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{})
+ s.c.UseProgram(s.prog)
+ s.c.BindTexture(gl.TEXTURE_2D, s.colorTex)
+ s.c.BindBuffer(gl.ARRAY_BUFFER, s.quad)
+ s.c.VertexAttribPointer(0 /* pos */, 2, gl.FLOAT, false, 4*4, 0)
+ s.c.VertexAttribPointer(1 /* uv */, 2, gl.FLOAT, false, 4*4, 4*2)
+ s.c.EnableVertexAttribArray(0)
+ s.c.EnableVertexAttribArray(1)
+ s.c.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)
+ s.c.BindTexture(gl.TEXTURE_2D, gl.Texture{})
+ s.c.DisableVertexAttribArray(0)
+ s.c.DisableVertexAttribArray(1)
+ s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer)
+ s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0)
+ s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT)
+ // The Android emulator requires framebuffer 0 bound at eglSwapBuffer time.
+ // Bind the sRGB framebuffer again in afterPresent.
+ s.c.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{})
+}
+
+func (s *FBO) AfterPresent() {
+ s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer)
+}
+
+func (s *FBO) Refresh(w, h int) error {
+ s.width, s.height = w, h
+ if w == 0 || h == 0 {
+ return nil
+ }
+ s.c.BindTexture(gl.TEXTURE_2D, s.colorTex)
+ if s.gl3 {
+ s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB8_ALPHA8, w, h, gl.RGBA,
+ gl.UNSIGNED_BYTE)
+ } else /* EXT_sRGB */ {
+ s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.SRGB_ALPHA_EXT, w, h,
+ gl.SRGB_ALPHA_EXT, gl.UNSIGNED_BYTE)
+ }
+ currentRB := gl.Renderbuffer(s.c.GetBinding(gl.RENDERBUFFER_BINDING))
+ s.c.BindRenderbuffer(gl.RENDERBUFFER, s.depthBuffer)
+ s.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h)
+ s.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB)
+ s.c.BindFramebuffer(gl.FRAMEBUFFER, s.frameBuffer)
+ s.c.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
+ gl.TEXTURE_2D, s.colorTex, 0)
+ s.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT,
+ gl.RENDERBUFFER, s.depthBuffer)
+ if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
+ return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x error: %x",
+ s.width, s.height, st, s.c.GetError())
+ }
+
+ if runtime.GOOS == "js" {
+ // With macOS Safari, rendering to and then reading from a SRGB8_ALPHA8
+ // texture result in twice gamma corrected colors. Using a plain RGBA
+ // texture seems to work.
+ s.c.ClearColor(.5, .5, .5, 1.0)
+ s.c.Clear(gl.COLOR_BUFFER_BIT)
+ var pixel [4]byte
+ s.c.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel[:])
+ if pixel[0] == 128 { // Correct sRGB color value is ~188
+ s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, gl.RGBA,
+ gl.UNSIGNED_BYTE)
+ if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE {
+ return fmt.Errorf("fallback RGBA framebuffer incomplete (%dx%d), status: %#x error: %x",
+ s.width, s.height, st, s.c.GetError())
+ }
+ }
+ }
+
+ return nil
+}
+
+func (s *FBO) Release() {
+ s.c.DeleteFramebuffer(s.frameBuffer)
+ s.c.DeleteTexture(s.colorTex)
+ s.c.DeleteRenderbuffer(s.depthBuffer)
+ if s.blitted {
+ s.c.DeleteBuffer(s.quad)
+ s.c.DeleteProgram(s.prog)
+ }
+ s.c = nil
+}
+
+const (
+ blitVSrc = `
+#version 100
+
+precision highp float;
+
+attribute vec2 pos;
+attribute vec2 uv;
+
+varying vec2 vUV;
+
+void main() {
+ gl_Position = vec4(pos, 0, 1);
+ vUV = uv;
+}
+`
+ blitFSrc = `
+#version 100
+
+precision mediump float;
+
+uniform sampler2D tex;
+varying vec2 vUV;
+
+vec3 gamma(vec3 rgb) {
+ vec3 exp = vec3(1.055)*pow(rgb, vec3(0.41666)) - vec3(0.055);
+ vec3 lin = rgb * vec3(12.92);
+ bvec3 cut = lessThan(rgb, vec3(0.0031308));
+ return vec3(cut.r ? lin.r : exp.r, cut.g ? lin.g : exp.g, cut.b ? lin.b : exp.b);
+}
+
+void main() {
+ vec4 col = texture2D(tex, vUV);
+ vec3 rgb = col.rgb;
+ rgb = gamma(rgb);
+ gl_FragColor = vec4(rgb, col.a);
+}
+`
+)
diff --git a/gio/internal/stroke/dash.go b/gio/internal/stroke/dash.go
new file mode 100644
index 0000000..c57a032
--- /dev/null
+++ b/gio/internal/stroke/dash.go
@@ -0,0 +1,401 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// The algorithms to compute dashes have been extracted, adapted from
+// (and used as a reference implementation):
+// - github.com/tdewolff/canvas (Licensed under MIT)
+
+package stroke
+
+import (
+ "math"
+ "sort"
+
+ "realy.lol/gio/f32"
+)
+
+type DashOp struct {
+ Phase float32
+ Dashes []float32
+}
+
+func IsSolidLine(sty DashOp) bool {
+ return sty.Phase == 0 && len(sty.Dashes) == 0
+}
+
+func (qs StrokeQuads) dash(sty DashOp) StrokeQuads {
+ sty = dashCanonical(sty)
+
+ switch {
+ case len(sty.Dashes) == 0:
+ return qs
+ case len(sty.Dashes) == 1 && sty.Dashes[0] == 0.0:
+ return StrokeQuads{}
+ }
+
+ if len(sty.Dashes)%2 == 1 {
+ // If the dash pattern is of uneven length, dash and space lengths
+ // alternate. The following duplicates the pattern so that uneven
+ // indices are always spaces.
+ sty.Dashes = append(sty.Dashes, sty.Dashes...)
+ }
+
+ var (
+ i0, pos0 = dashStart(sty)
+ out StrokeQuads
+
+ contour uint32 = 1
+ )
+
+ for _, ps := range qs.split() {
+ var (
+ i = i0
+ pos = pos0
+ t []float64
+ length = ps.len()
+ )
+ for pos+sty.Dashes[i] < length {
+ pos += sty.Dashes[i]
+ if 0.0 < pos {
+ t = append(t, float64(pos))
+ }
+ i++
+ if i == len(sty.Dashes) {
+ i = 0
+ }
+ }
+
+ j0 := 0
+ endsInDash := i%2 == 0
+ if len(t)%2 == 1 && endsInDash || len(t)%2 == 0 && !endsInDash {
+ j0 = 1
+ }
+
+ var (
+ qd StrokeQuads
+ pd = ps.splitAt(&contour, t...)
+ )
+ for j := j0; j < len(pd)-1; j += 2 {
+ qd = qd.append(pd[j])
+ }
+ if endsInDash {
+ if ps.closed() {
+ qd = pd[len(pd)-1].append(qd)
+ } else {
+ qd = qd.append(pd[len(pd)-1])
+ }
+ }
+ out = out.append(qd)
+ contour++
+ }
+ return out
+}
+
+func dashCanonical(sty DashOp) DashOp {
+ var (
+ o = sty
+ ds = o.Dashes
+ )
+
+ if len(sty.Dashes) == 0 {
+ return sty
+ }
+
+ // Remove zeros except first and last.
+ for i := 1; i < len(ds)-1; i++ {
+ if f32Eq(ds[i], 0.0) {
+ ds[i-1] += ds[i+1]
+ ds = append(ds[:i], ds[i+2:]...)
+ i--
+ }
+ }
+
+ // Remove first zero, collapse with second and last.
+ if f32Eq(ds[0], 0.0) {
+ if len(ds) < 3 {
+ return DashOp{
+ Phase: 0.0,
+ Dashes: []float32{0.0},
+ }
+ }
+ o.Phase -= ds[1]
+ ds[len(ds)-1] += ds[1]
+ ds = ds[2:]
+ }
+
+ // Remove last zero, collapse with fist and second to last.
+ if f32Eq(ds[len(ds)-1], 0.0) {
+ if len(ds) < 3 {
+ return DashOp{}
+ }
+ o.Phase += ds[len(ds)-2]
+ ds[0] += ds[len(ds)-2]
+ ds = ds[:len(ds)-2]
+ }
+
+ // If there are zeros or negatives, don't draw dashes.
+ for i := 0; i < len(ds); i++ {
+ if ds[i] < 0.0 || f32Eq(ds[i], 0.0) {
+ return DashOp{
+ Phase: 0.0,
+ Dashes: []float32{0.0},
+ }
+ }
+ }
+
+ // Remove repeated patterns.
+loop:
+ for len(ds)%2 == 0 {
+ mid := len(ds) / 2
+ for i := 0; i < mid; i++ {
+ if !f32Eq(ds[i], ds[mid+i]) {
+ break loop
+ }
+ }
+ ds = ds[:mid]
+ }
+ return o
+}
+
+func dashStart(sty DashOp) (int, float32) {
+ i0 := 0 // i0 is the index into dashes.
+ for sty.Dashes[i0] <= sty.Phase {
+ sty.Phase -= sty.Dashes[i0]
+ i0++
+ if i0 == len(sty.Dashes) {
+ i0 = 0
+ }
+ }
+ // pos0 may be negative if the offset lands halfway into dash.
+ pos0 := -sty.Phase
+ if sty.Phase < 0.0 {
+ var sum float32
+ for _, d := range sty.Dashes {
+ sum += d
+ }
+ pos0 = -(sum + sty.Phase) // handle negative offsets
+ }
+ return i0, pos0
+}
+
+func (qs StrokeQuads) len() float32 {
+ var sum float32
+ for i := range qs {
+ q := qs[i].Quad
+ sum += quadBezierLen(q.From, q.Ctrl, q.To)
+ }
+ return sum
+}
+
+// splitAt splits the path into separate paths at the specified intervals
+// along the path.
+// splitAt updates the provided contour counter as it splits the segments.
+func (qs StrokeQuads) splitAt(contour *uint32, ts ...float64) []StrokeQuads {
+ if len(ts) == 0 {
+ qs.setContour(*contour)
+ return []StrokeQuads{qs}
+ }
+
+ sort.Float64s(ts)
+ if ts[0] == 0 {
+ ts = ts[1:]
+ }
+
+ var (
+ j int // index into ts
+ t float64 // current position along curve
+ )
+
+ var oo []StrokeQuads
+ var oi StrokeQuads
+ push := func() {
+ oo = append(oo, oi)
+ oi = nil
+ }
+
+ for _, ps := range qs.split() {
+ for _, q := range ps {
+ if j == len(ts) {
+ oi = append(oi, q)
+ continue
+ }
+ speed := func(t float64) float64 {
+ return float64(lenPt(quadBezierD1(q.Quad.From, q.Quad.Ctrl,
+ q.Quad.To, float32(t))))
+ }
+ invL, dt := invSpeedPolynomialChebyshevApprox(20, gaussLegendre7,
+ speed, 0, 1)
+
+ var (
+ t0 float64
+ r0 = q.Quad.From
+ r1 = q.Quad.Ctrl
+ r2 = q.Quad.To
+
+ // from keeps track of the start of the 'running' segment.
+ from = r0
+ )
+ for j < len(ts) && t < ts[j] && ts[j] <= t+dt {
+ tj := invL(ts[j] - t)
+ tsub := (tj - t0) / (1.0 - t0)
+ t0 = tj
+
+ var q1 f32.Point
+ _, q1, _, r0, r1, r2 = quadBezierSplit(r0, r1, r2,
+ float32(tsub))
+
+ oi = append(oi, StrokeQuad{
+ Contour: *contour,
+ Quad: QuadSegment{
+ From: from,
+ Ctrl: q1,
+ To: r0,
+ },
+ })
+ push()
+ (*contour)++
+
+ from = r0
+ j++
+ }
+ if !f64Eq(t0, 1) {
+ if len(oi) > 0 {
+ r0 = oi.pen()
+ }
+ oi = append(oi, StrokeQuad{
+ Contour: *contour,
+ Quad: QuadSegment{
+ From: r0,
+ Ctrl: r1,
+ To: r2,
+ },
+ })
+ }
+ t += dt
+ }
+ }
+ if len(oi) > 0 {
+ push()
+ (*contour)++
+ }
+
+ return oo
+}
+
+func f32Eq(a, b float32) bool {
+ const epsilon = 1e-10
+ return math.Abs(float64(a-b)) < epsilon
+}
+
+func f64Eq(a, b float64) bool {
+ const epsilon = 1e-10
+ return math.Abs(a-b) < epsilon
+}
+
+func invSpeedPolynomialChebyshevApprox(N int, gaussLegendre gaussLegendreFunc,
+ fp func(float64) float64, tmin, tmax float64) (func(float64) float64,
+ float64) {
+ // The TODOs below are copied verbatim from tdewolff/canvas:
+ //
+ // TODO: find better way to determine N. For Arc 10 seems fine, for some
+ // Quads 10 is too low, for Cube depending on inflection points is
+ // maybe not the best indicator
+ //
+ // TODO: track efficiency, how many times is fp called?
+ // Does a look-up table make more sense?
+ fLength := func(t float64) float64 {
+ return math.Abs(gaussLegendre(fp, tmin, t))
+ }
+ totalLength := fLength(tmax)
+ t := func(L float64) float64 {
+ return bisectionMethod(fLength, L, tmin, tmax)
+ }
+ return polynomialChebyshevApprox(N, t, 0.0, totalLength, tmin,
+ tmax), totalLength
+}
+
+func polynomialChebyshevApprox(N int, f func(float64) float64,
+ xmin, xmax, ymin, ymax float64) func(float64) float64 {
+ var (
+ invN = 1.0 / float64(N)
+ fs = make([]float64, N)
+ )
+ for k := 0; k < N; k++ {
+ u := math.Cos(math.Pi * (float64(k+1) - 0.5) * invN)
+ fs[k] = f(xmin + 0.5*(xmax-xmin)*(u+1))
+ }
+
+ c := make([]float64, N)
+ for j := 0; j < N; j++ {
+ var a float64
+ for k := 0; k < N; k++ {
+ a += fs[k] * math.Cos(float64(j)*math.Pi*(float64(k+1)-0.5)/float64(N))
+ }
+ c[j] = 2 * invN * a
+ }
+
+ if ymax < ymin {
+ ymin, ymax = ymax, ymin
+ }
+ return func(x float64) float64 {
+ x = math.Min(xmax, math.Max(xmin, x))
+ u := (x-xmin)/(xmax-xmin)*2 - 1
+ var a float64
+ for j := 0; j < N; j++ {
+ a += c[j] * math.Cos(float64(j)*math.Acos(u))
+ }
+ y := -0.5*c[0] + a
+ if !math.IsNaN(ymin) && !math.IsNaN(ymax) {
+ y = math.Min(ymax, math.Max(ymin, y))
+ }
+ return y
+ }
+}
+
+// bisectionMethod finds the value x for which f(x) = y in the interval x
+// in [xmin, xmax] using the bisection method.
+func bisectionMethod(f func(float64) float64, y, xmin, xmax float64) float64 {
+ const (
+ maxIter = 100
+ tolerance = 0.001 // 0.1%
+ )
+
+ var (
+ n = 0
+ x float64
+ tolX = math.Abs(xmax-xmin) * tolerance
+ tolY = math.Abs(f(xmax)-f(xmin)) * tolerance
+ )
+ for {
+ x = 0.5 * (xmin + xmax)
+ if n >= maxIter {
+ return x
+ }
+
+ dy := f(x) - y
+ switch {
+ case math.Abs(dy) < tolY, math.Abs(0.5*(xmax-xmin)) < tolX:
+ return x
+ case dy > 0:
+ xmax = x
+ default:
+ xmin = x
+ }
+ n++
+ }
+}
+
+type gaussLegendreFunc func(func(float64) float64, float64, float64) float64
+
+// Gauss-Legendre quadrature integration from a to b with n=7
+func gaussLegendre7(f func(float64) float64, a, b float64) float64 {
+ c := 0.5 * (b - a)
+ d := 0.5 * (a + b)
+ Qd1 := f(-0.949108*c + d)
+ Qd2 := f(-0.741531*c + d)
+ Qd3 := f(-0.405845*c + d)
+ Qd4 := f(d)
+ Qd5 := f(0.405845*c + d)
+ Qd6 := f(0.741531*c + d)
+ Qd7 := f(0.949108*c + d)
+ return c * (0.129485*(Qd1+Qd7) + 0.279705*(Qd2+Qd6) + 0.381830*(Qd3+Qd5) + 0.417959*Qd4)
+}
diff --git a/gio/internal/stroke/stroke.go b/gio/internal/stroke/stroke.go
new file mode 100644
index 0000000..b88a432
--- /dev/null
+++ b/gio/internal/stroke/stroke.go
@@ -0,0 +1,902 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Most of the algorithms to compute strokes and their offsets have been
+// extracted, adapted from (and used as a reference implementation):
+// - github.com/tdewolff/canvas (Licensed under MIT)
+//
+// These algorithms have been implemented from:
+// Fast, precise flattening of cubic BĆ©zier path and offset curves
+// Thomas F. Hain, et al.
+//
+// An electronic version is available at:
+// https://seant23.files.wordpress.com/2010/11/fastpreciseflatteningofbeziercurve.pdf
+//
+// Possible improvements (in term of speed and/or accuracy) on these
+// algorithms are:
+//
+// - Polar Stroking: New Theory and Methods for Stroking Paths,
+// M. Kilgard
+// https://arxiv.org/pdf/2007.00308.pdf
+//
+// - https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html
+// R. Levien
+
+// Package stroke implements conversion of strokes to filled outlines. It is used as a
+// fallback for stroke configurations not natively supported by the renderer.
+package stroke
+
+import (
+ "encoding/binary"
+ "math"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/internal/scene"
+)
+
+// The following are copies of types from op/clip to avoid a circular import of
+// that package.
+// TODO: when the old renderer is gone, this package can be merged with
+// op/clip, eliminating the duplicate types.
+type StrokeStyle struct {
+ Width float32
+ Miter float32
+ Cap StrokeCap
+ Join StrokeJoin
+}
+
+type StrokeCap uint8
+
+const (
+ RoundCap StrokeCap = iota
+ FlatCap
+ SquareCap
+)
+
+type StrokeJoin uint8
+
+const (
+ RoundJoin StrokeJoin = iota
+ BevelJoin
+)
+
+// strokeTolerance is used to reconcile rounding errors arising
+// when splitting quads into smaller and smaller segments to approximate
+// them into straight lines, and when joining back segments.
+//
+// The magic value of 0.01 was found by striking a compromise between
+// aesthetic looking (curves did look like curves, even after linearization)
+// and speed.
+const strokeTolerance = 0.01
+
+type QuadSegment struct {
+ From, Ctrl, To f32.Point
+}
+
+type StrokeQuad struct {
+ Contour uint32
+ Quad QuadSegment
+}
+
+type strokeState struct {
+ p0, p1 f32.Point // p0 is the start point, p1 the end point.
+ n0, n1 f32.Point // n0 is the normal vector at the start point, n1 at the end point.
+ r0, r1 float32 // r0 is the curvature at the start point, r1 at the end point.
+ ctl f32.Point // ctl is the control point of the quadratic BĆ©zier segment.
+}
+
+type StrokeQuads []StrokeQuad
+
+func (qs *StrokeQuads) setContour(n uint32) {
+ for i := range *qs {
+ (*qs)[i].Contour = n
+ }
+}
+
+func (qs *StrokeQuads) pen() f32.Point {
+ return (*qs)[len(*qs)-1].Quad.To
+}
+
+func (qs *StrokeQuads) closed() bool {
+ beg := (*qs)[0].Quad.From
+ end := (*qs)[len(*qs)-1].Quad.To
+ return f32Eq(beg.X, end.X) && f32Eq(beg.Y, end.Y)
+}
+
+func (qs *StrokeQuads) lineTo(pt f32.Point) {
+ end := qs.pen()
+ *qs = append(*qs, StrokeQuad{
+ Quad: QuadSegment{
+ From: end,
+ Ctrl: end.Add(pt).Mul(0.5),
+ To: pt,
+ },
+ })
+}
+
+func (qs *StrokeQuads) arc(f1, f2 f32.Point, angle float32) {
+ const segments = 16
+ pen := qs.pen()
+ m := ArcTransform(pen, f1.Add(pen), f2.Add(pen), angle, segments)
+ for i := 0; i < segments; i++ {
+ p0 := qs.pen()
+ p1 := m.Transform(p0)
+ p2 := m.Transform(p1)
+ ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
+ *qs = append(*qs, StrokeQuad{
+ Quad: QuadSegment{
+ From: p0, Ctrl: ctl, To: p2,
+ },
+ })
+ }
+}
+
+// split splits a slice of quads into slices of quads grouped
+// by contours (ie: splitted at move-to boundaries).
+func (qs StrokeQuads) split() []StrokeQuads {
+ if len(qs) == 0 {
+ return nil
+ }
+
+ var (
+ c uint32
+ o []StrokeQuads
+ i = len(o)
+ )
+ for _, q := range qs {
+ if q.Contour != c {
+ c = q.Contour
+ i = len(o)
+ o = append(o, StrokeQuads{})
+ }
+ o[i] = append(o[i], q)
+ }
+
+ return o
+}
+
+func (qs StrokeQuads) stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads {
+ if !IsSolidLine(dashes) {
+ qs = qs.dash(dashes)
+ }
+
+ var (
+ o StrokeQuads
+ hw = 0.5 * stroke.Width
+ )
+
+ for _, ps := range qs.split() {
+ rhs, lhs := ps.offset(hw, stroke)
+ switch lhs {
+ case nil:
+ o = o.append(rhs)
+ default:
+ // Closed path.
+ // Inner path should go opposite direction to cancel outer path.
+ switch {
+ case ps.ccw():
+ lhs = lhs.reverse()
+ o = o.append(rhs)
+ o = o.append(lhs)
+ default:
+ rhs = rhs.reverse()
+ o = o.append(lhs)
+ o = o.append(rhs)
+ }
+ }
+ }
+
+ return o
+}
+
+// offset returns the right-hand and left-hand sides of the path, offset by
+// the half-width hw.
+// The stroke handles how segments are joined and ends are capped.
+func (qs StrokeQuads) offset(hw float32,
+ stroke StrokeStyle) (rhs, lhs StrokeQuads) {
+ var (
+ states []strokeState
+ beg = qs[0].Quad.From
+ end = qs[len(qs)-1].Quad.To
+ closed = beg == end
+ )
+ for i := range qs {
+ q := qs[i].Quad
+
+ var (
+ n0 = strokePathNorm(q.From, q.Ctrl, q.To, 0, hw)
+ n1 = strokePathNorm(q.From, q.Ctrl, q.To, 1, hw)
+ r0 = strokePathCurv(q.From, q.Ctrl, q.To, 0)
+ r1 = strokePathCurv(q.From, q.Ctrl, q.To, 1)
+ )
+ states = append(states, strokeState{
+ p0: q.From,
+ p1: q.To,
+ n0: n0,
+ n1: n1,
+ r0: r0,
+ r1: r1,
+ ctl: q.Ctrl,
+ })
+ }
+
+ for i, state := range states {
+ rhs = rhs.append(strokeQuadBezier(state, +hw, strokeTolerance))
+ lhs = lhs.append(strokeQuadBezier(state, -hw, strokeTolerance))
+
+ // join the current and next segments
+ if hasNext := i+1 < len(states); hasNext || closed {
+ var next strokeState
+ switch {
+ case hasNext:
+ next = states[i+1]
+ case closed:
+ next = states[0]
+ }
+ if state.n1 != next.n0 {
+ strokePathJoin(stroke, &rhs, &lhs, hw, state.p1, state.n1,
+ next.n0, state.r1, next.r0)
+ }
+ }
+ }
+
+ if closed {
+ rhs.close()
+ lhs.close()
+ return rhs, lhs
+ }
+
+ qbeg := &states[0]
+ qend := &states[len(states)-1]
+
+ // Default to counter-clockwise direction.
+ lhs = lhs.reverse()
+ strokePathCap(stroke, &rhs, hw, qend.p1, qend.n1)
+
+ rhs = rhs.append(lhs)
+ strokePathCap(stroke, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1))
+
+ rhs.close()
+
+ return rhs, nil
+}
+
+func (qs *StrokeQuads) close() {
+ p0 := (*qs)[len(*qs)-1].Quad.To
+ p1 := (*qs)[0].Quad.From
+
+ if p1 == p0 {
+ return
+ }
+
+ *qs = append(*qs, StrokeQuad{
+ Quad: QuadSegment{
+ From: p0,
+ Ctrl: p0.Add(p1).Mul(0.5),
+ To: p1,
+ },
+ })
+}
+
+// ccw returns whether the path is counter-clockwise.
+func (qs StrokeQuads) ccw() bool {
+ // Use the Shoelace formula:
+ // https://en.wikipedia.org/wiki/Shoelace_formula
+ var area float32
+ for _, ps := range qs.split() {
+ for i := 1; i < len(ps); i++ {
+ pi := ps[i].Quad.To
+ pj := ps[i-1].Quad.To
+ area += (pi.X - pj.X) * (pi.Y + pj.Y)
+ }
+ }
+ return area <= 0.0
+}
+
+func (qs StrokeQuads) reverse() StrokeQuads {
+ if len(qs) == 0 {
+ return nil
+ }
+
+ ps := make(StrokeQuads, 0, len(qs))
+ for i := range qs {
+ q := qs[len(qs)-1-i]
+ q.Quad.To, q.Quad.From = q.Quad.From, q.Quad.To
+ ps = append(ps, q)
+ }
+
+ return ps
+}
+
+func (qs StrokeQuads) append(ps StrokeQuads) StrokeQuads {
+ switch {
+ case len(ps) == 0:
+ return qs
+ case len(qs) == 0:
+ return ps
+ }
+
+ // Consolidate quads and smooth out rounding errors.
+ // We need to also check for the strokeTolerance to correctly handle
+ // join/cap points or on-purpose disjoint quads.
+ p0 := qs[len(qs)-1].Quad.To
+ p1 := ps[0].Quad.From
+ if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance {
+ qs = append(qs, StrokeQuad{
+ Quad: QuadSegment{
+ From: p0,
+ Ctrl: p0.Add(p1).Mul(0.5),
+ To: p1,
+ },
+ })
+ }
+ return append(qs, ps...)
+}
+
+func (q QuadSegment) Transform(t f32.Affine2D) QuadSegment {
+ q.From = t.Transform(q.From)
+ q.Ctrl = t.Transform(q.Ctrl)
+ q.To = t.Transform(q.To)
+ return q
+}
+
+// strokePathNorm returns the normal vector at t.
+func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
+ switch t {
+ case 0:
+ n := p1.Sub(p0)
+ if n.X == 0 && n.Y == 0 {
+ return f32.Point{}
+ }
+ n = rot90CW(n)
+ return normPt(n, d)
+ case 1:
+ n := p2.Sub(p1)
+ if n.X == 0 && n.Y == 0 {
+ return f32.Point{}
+ }
+ n = rot90CW(n)
+ return normPt(n, d)
+ }
+ panic("impossible")
+}
+
+func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) }
+func rot90CCW(p f32.Point) f32.Point { return f32.Pt(-p.Y, +p.X) }
+
+// cosPt returns the cosine of the opening angle between p and q.
+func cosPt(p, q f32.Point) float32 {
+ np := math.Hypot(float64(p.X), float64(p.Y))
+ nq := math.Hypot(float64(q.X), float64(q.Y))
+ return dotPt(p, q) / float32(np*nq)
+}
+
+func normPt(p f32.Point, l float32) f32.Point {
+ d := math.Hypot(float64(p.X), float64(p.Y))
+ l64 := float64(l)
+ if math.Abs(d-l64) < 1e-10 {
+ return f32.Point{}
+ }
+ n := float32(l64 / d)
+ return f32.Point{X: p.X * n, Y: p.Y * n}
+}
+
+func lenPt(p f32.Point) float32 {
+ return float32(math.Hypot(float64(p.X), float64(p.Y)))
+}
+
+func dotPt(p, q f32.Point) float32 {
+ return p.X*q.X + p.Y*q.Y
+}
+
+func perpDot(p, q f32.Point) float32 {
+ return p.X*q.Y - p.Y*q.X
+}
+
+// strokePathCurv returns the curvature at t, along the quadratic BĆ©zier
+// curve defined by the triplet (beg, ctl, end).
+func strokePathCurv(beg, ctl, end f32.Point, t float32) float32 {
+ var (
+ d1p = quadBezierD1(beg, ctl, end, t)
+ d2p = quadBezierD2(beg, ctl, end, t)
+
+ // Negative when bending right, ie: the curve is CW at this point.
+ a = float64(perpDot(d1p, d2p))
+ )
+
+ // We check early that the segment isn't too line-like and
+ // save a costly call to math.Pow that will be discarded by dividing
+ // with a too small 'a'.
+ if math.Abs(a) < 1e-10 {
+ return float32(math.NaN())
+ }
+ return float32(math.Pow(float64(d1p.X*d1p.X+d1p.Y*d1p.Y), 1.5) / a)
+}
+
+// quadBezierSample returns the point on the BĆ©zier curve at t.
+// B(t) = (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2
+func quadBezierSample(p0, p1, p2 f32.Point, t float32) f32.Point {
+ t1 := 1 - t
+ c0 := t1 * t1
+ c1 := 2 * t1 * t
+ c2 := t * t
+
+ o := p0.Mul(c0)
+ o = o.Add(p1.Mul(c1))
+ o = o.Add(p2.Mul(c2))
+ return o
+}
+
+// quadBezierD1 returns the first derivative of the BĆ©zier curve with respect to t.
+// B'(t) = 2(1-t)(P1 - P0) + 2t(P2 - P1)
+func quadBezierD1(p0, p1, p2 f32.Point, t float32) f32.Point {
+ p10 := p1.Sub(p0).Mul(2 * (1 - t))
+ p21 := p2.Sub(p1).Mul(2 * t)
+
+ return p10.Add(p21)
+}
+
+// quadBezierD2 returns the second derivative of the BĆ©zier curve with respect to t:
+// B''(t) = 2(P2 - 2P1 + P0)
+func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point {
+ p := p2.Sub(p1.Mul(2)).Add(p0)
+ return p.Mul(2)
+}
+
+// quadBezierLen returns the length of the BĆ©zier curve.
+// See:
+// https://malczak.linuxpl.com/blog/quadratic-bezier-curve-length/
+func quadBezierLen(p0, p1, p2 f32.Point) float32 {
+ a := p0.Sub(p1.Mul(2)).Add(p2)
+ b := p1.Mul(2).Sub(p0.Mul(2))
+ A := float64(4 * dotPt(a, a))
+ B := float64(4 * dotPt(a, b))
+ C := float64(dotPt(b, b))
+ if f64Eq(A, 0.0) {
+ // p1 is in the middle between p0 and p2,
+ // so it is a straight line from p0 to p2.
+ return lenPt(p2.Sub(p0))
+ }
+
+ Sabc := 2 * math.Sqrt(A+B+C)
+ A2 := math.Sqrt(A)
+ A32 := 2 * A * A2
+ C2 := 2 * math.Sqrt(C)
+ BA := B / A2
+ return float32((A32*Sabc + A2*B*(Sabc-C2) + (4*C*A-B*B)*math.Log((2*A2+BA+Sabc)/(BA+C2))) / (4 * A32))
+}
+
+func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads {
+ // Gio strokes are only quadratic BĆ©zier curves, w/o any inflection point.
+ // So we just have to flatten them.
+ var qs StrokeQuads
+ return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness)
+}
+
+// flattenQuadBezier splits a BĆ©zier quadratic curve into linear sub-segments,
+// themselves also encoded as BĆ©zier (degenerate, flat) quadratic curves.
+func flattenQuadBezier(qs StrokeQuads, p0, p1, p2 f32.Point,
+ d, flatness float32) StrokeQuads {
+ var (
+ t float32
+ flat64 = float64(flatness)
+ )
+ for t < 1 {
+ s2 := float64((p2.X-p0.X)*(p1.Y-p0.Y) - (p2.Y-p0.Y)*(p1.X-p0.X))
+ den := math.Hypot(float64(p1.X-p0.X), float64(p1.Y-p0.Y))
+ if s2*den == 0.0 {
+ break
+ }
+
+ s2 /= den
+ t = 2.0 * float32(math.Sqrt(flat64/3.0/math.Abs(s2)))
+ if t >= 1.0 {
+ break
+ }
+ var q0, q1, q2 f32.Point
+ q0, q1, q2, p0, p1, p2 = quadBezierSplit(p0, p1, p2, t)
+ qs.addLine(q0, q1, q2, 0, d)
+ }
+ qs.addLine(p0, p1, p2, 1, d)
+ return qs
+}
+
+func (qs *StrokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) {
+
+ switch i := len(*qs); i {
+ case 0:
+ p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d))
+ default:
+ // Address possible rounding errors and use previous point.
+ p0 = (*qs)[i-1].Quad.To
+ }
+
+ p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d))
+
+ *qs = append(*qs,
+ StrokeQuad{
+ Quad: QuadSegment{
+ From: p0,
+ Ctrl: p0.Add(p1).Mul(0.5),
+ To: p1,
+ },
+ },
+ )
+}
+
+// quadInterp returns the interpolated point at t.
+func quadInterp(p, q f32.Point, t float32) f32.Point {
+ return f32.Pt(
+ (1-t)*p.X+t*q.X,
+ (1-t)*p.Y+t*q.Y,
+ )
+}
+
+// quadBezierSplit returns the pair of triplets (from,ctrl,to) BĆ©zier curve,
+// split before (resp. after) the provided parametric t value.
+func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point,
+ f32.Point, f32.Point, f32.Point, f32.Point) {
+
+ var (
+ b0 = p0
+ b1 = quadInterp(p0, p1, t)
+ b2 = quadBezierSample(p0, p1, p2, t)
+
+ a0 = b2
+ a1 = quadInterp(p1, p2, t)
+ a2 = p2
+ )
+
+ return b0, b1, b2, a0, a1, a2
+}
+
+// strokePathJoin joins the two paths rhs and lhs, according to the provided
+// stroke operation.
+func strokePathJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32,
+ pivot, n0, n1 f32.Point, r0, r1 float32) {
+ if stroke.Miter > 0 {
+ strokePathMiterJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1)
+ return
+ }
+ switch stroke.Join {
+ case BevelJoin:
+ strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
+ case RoundJoin:
+ strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
+ default:
+ panic("impossible")
+ }
+}
+
+func strokePathBevelJoin(rhs, lhs *StrokeQuads, hw float32,
+ pivot, n0, n1 f32.Point, r0, r1 float32) {
+
+ rp := pivot.Add(n1)
+ lp := pivot.Sub(n1)
+
+ rhs.lineTo(rp)
+ lhs.lineTo(lp)
+}
+
+func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32,
+ pivot, n0, n1 f32.Point, r0, r1 float32) {
+ rp := pivot.Add(n1)
+ lp := pivot.Sub(n1)
+ cw := dotPt(rot90CW(n0), n1) >= 0.0
+ switch {
+ case cw:
+ // Path bends to the right, ie. CW (or 180 degree turn).
+ c := pivot.Sub(lhs.pen())
+ angle := -math.Acos(float64(cosPt(n0, n1)))
+ lhs.arc(c, c, float32(angle))
+ lhs.lineTo(lp) // Add a line to accommodate for rounding errors.
+ rhs.lineTo(rp)
+ default:
+ // Path bends to the left, ie. CCW.
+ angle := math.Acos(float64(cosPt(n0, n1)))
+ c := pivot.Sub(rhs.pen())
+ rhs.arc(c, c, float32(angle))
+ rhs.lineTo(rp) // Add a line to accommodate for rounding errors.
+ lhs.lineTo(lp)
+ }
+}
+
+func strokePathMiterJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32,
+ pivot, n0, n1 f32.Point, r0, r1 float32) {
+ if n0 == n1.Mul(-1) {
+ strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
+ return
+ }
+
+ // This is to handle nearly linear joints that would be clipped otherwise.
+ limit := math.Max(float64(stroke.Miter), 1.001)
+
+ cw := dotPt(rot90CW(n0), n1) >= 0.0
+ if cw {
+ // hw is used to calculate |R|.
+ // When running CW, n0 and n1 point the other way,
+ // so the sign of r0 and r1 is negated.
+ hw = -hw
+ }
+ hw64 := float64(hw)
+
+ cos := math.Sqrt(0.5 * (1 + float64(cosPt(n0, n1))))
+ d := hw64 / cos
+ if math.Abs(limit*hw64) < math.Abs(d) {
+ stroke.Miter = 0 // Set miter to zero to disable the miter joint.
+ strokePathJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1)
+ return
+ }
+ mid := pivot.Add(normPt(n0.Add(n1), float32(d)))
+
+ rp := pivot.Add(n1)
+ lp := pivot.Sub(n1)
+ switch {
+ case cw:
+ // Path bends to the right, ie. CW.
+ lhs.lineTo(mid)
+ default:
+ // Path bends to the left, ie. CCW.
+ rhs.lineTo(mid)
+ }
+ rhs.lineTo(rp)
+ lhs.lineTo(lp)
+}
+
+// strokePathCap caps the provided path qs, according to the provided stroke operation.
+func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32,
+ pivot, n0 f32.Point) {
+ switch stroke.Cap {
+ case FlatCap:
+ strokePathFlatCap(qs, hw, pivot, n0)
+ case SquareCap:
+ strokePathSquareCap(qs, hw, pivot, n0)
+ case RoundCap:
+ strokePathRoundCap(qs, hw, pivot, n0)
+ default:
+ panic("impossible")
+ }
+}
+
+// strokePathFlatCap caps the start or end of a path with a flat cap.
+func strokePathFlatCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
+ end := pivot.Sub(n0)
+ qs.lineTo(end)
+}
+
+// strokePathSquareCap caps the start or end of a path with a square cap.
+func strokePathSquareCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
+ var (
+ e = pivot.Add(rot90CCW(n0))
+ corner1 = e.Add(n0)
+ corner2 = e.Sub(n0)
+ end = pivot.Sub(n0)
+ )
+
+ qs.lineTo(corner1)
+ qs.lineTo(corner2)
+ qs.lineTo(end)
+}
+
+// strokePathRoundCap caps the start or end of a path with a round cap.
+func strokePathRoundCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
+ c := pivot.Sub(qs.pen())
+ qs.arc(c, c, math.Pi)
+}
+
+// ArcTransform computes a transformation that can be used for generating quadratic bƩzier
+// curve approximations for an arc.
+//
+// The math is extracted from the following paper:
+// "Drawing an elliptical arc using polylines, quadratic or
+// cubic Bezier curves", L. Maisonobe
+// An electronic version may be found at:
+// http://spaceroots.org/documents/ellipse/elliptical-arc.pdf
+func ArcTransform(p, f1, f2 f32.Point, angle float32,
+ segments int) f32.Affine2D {
+ c := f32.Point{
+ X: 0.5 * (f1.X + f2.X),
+ Y: 0.5 * (f1.Y + f2.Y),
+ }
+
+ // semi-major axis: 2a = |PF1| + |PF2|
+ a := 0.5 * (dist(f1, p) + dist(f2, p))
+
+ // semi-minor axis: c^2 = a^2+b^2 (c: focal distance)
+ f := dist(f1, c)
+ b := math.Sqrt(a*a - f*f)
+
+ var rx, ry, alpha, start float64
+ switch {
+ case a > b:
+ rx = a
+ ry = b
+ default:
+ rx = b
+ ry = a
+ }
+
+ var x float64
+ switch {
+ case f1 == c || f2 == c:
+ // degenerate case of a circle.
+ alpha = 0
+ default:
+ switch {
+ case f1.X > c.X:
+ x = float64(f1.X - c.X)
+ alpha = math.Acos(x / f)
+ case f1.X < c.X:
+ x = float64(f2.X - c.X)
+ alpha = math.Acos(x / f)
+ case f1.X == c.X:
+ // special case of a "vertical" ellipse.
+ alpha = math.Pi / 2
+ if f1.Y < c.Y {
+ alpha = -alpha
+ }
+ }
+ }
+
+ start = math.Acos(float64(p.X-c.X) / dist(c, p))
+ if c.Y > p.Y {
+ start = -start
+ }
+ start -= alpha
+
+ var (
+ Īø = angle / float32(segments)
+ ref f32.Affine2D // transform from absolute frame to ellipse-based one
+ rot f32.Affine2D // rotation matrix for each segment
+ inv f32.Affine2D // transform from ellipse-based frame to absolute one
+ )
+ ref = ref.Offset(f32.Point{}.Sub(c))
+ ref = ref.Rotate(f32.Point{}, float32(-alpha))
+ ref = ref.Scale(f32.Point{}, f32.Point{
+ X: float32(1 / rx),
+ Y: float32(1 / ry),
+ })
+ inv = ref.Invert()
+ rot = rot.Rotate(f32.Point{}, float32(0.5*Īø))
+
+ // Instead of invoking math.Sincos for every segment, compute a rotation
+ // matrix once and apply for each segment.
+ // Before applying the rotation matrix rot, transform the coordinates
+ // to a frame centered to the ellipse (and warped into a unit circle), then rotate.
+ // Finally, transform back into the original frame.
+ return inv.Mul(rot).Mul(ref)
+}
+
+func dist(p1, p2 f32.Point) float64 {
+ var (
+ x1 = float64(p1.X)
+ y1 = float64(p1.Y)
+ x2 = float64(p2.X)
+ y2 = float64(p2.Y)
+ dx = x2 - x1
+ dy = y2 - y1
+ )
+ return math.Hypot(dx, dy)
+}
+
+func StrokePathCommands(style StrokeStyle, dashes DashOp,
+ scene []byte) StrokeQuads {
+ quads := decodeToStrokeQuads(scene)
+ return quads.stroke(style, dashes)
+}
+
+// decodeToStrokeQuads decodes scene commands to quads ready to stroke.
+func decodeToStrokeQuads(pathData []byte) StrokeQuads {
+ quads := make(StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4))
+ for len(pathData) >= scene.CommandSize+4 {
+ contour := binary.LittleEndian.Uint32(pathData)
+ cmd := ops.DecodeCommand(pathData[4:])
+ switch cmd.Op() {
+ case scene.OpLine:
+ var q QuadSegment
+ q.From, q.To = scene.DecodeLine(cmd)
+ q.Ctrl = q.From.Add(q.To).Mul(.5)
+ quad := StrokeQuad{
+ Contour: contour,
+ Quad: q,
+ }
+ quads = append(quads, quad)
+ case scene.OpQuad:
+ var q QuadSegment
+ q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
+ quad := StrokeQuad{
+ Contour: contour,
+ Quad: q,
+ }
+ quads = append(quads, quad)
+ case scene.OpCubic:
+ for _, q := range SplitCubic(scene.DecodeCubic(cmd)) {
+ quad := StrokeQuad{
+ Contour: contour,
+ Quad: q,
+ }
+ quads = append(quads, quad)
+ }
+ default:
+ panic("unsupported scene command")
+ }
+ pathData = pathData[scene.CommandSize+4:]
+ }
+ return quads
+}
+
+func SplitCubic(from, ctrl0, ctrl1, to f32.Point) []QuadSegment {
+ quads := make([]QuadSegment, 0, 10)
+ // Set the maximum distance proportionally to the longest side
+ // of the bounding rectangle.
+ hull := f32.Rectangle{
+ Min: from,
+ Max: ctrl0,
+ }.Canon().Add(ctrl1).Add(to)
+ l := hull.Dx()
+ if h := hull.Dy(); h > l {
+ l = h
+ }
+ approxCubeTo(&quads, 0, l*0.001, from, ctrl0, ctrl1, to)
+ return quads
+}
+
+// approxCubeTo approximates a cubic BĆ©zier by a series of quadratic
+// curves.
+func approxCubeTo(quads *[]QuadSegment, splits int, maxDist float32,
+ from, ctrl0, ctrl1, to f32.Point) int {
+ // The idea is from
+ // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html
+ // where a quadratic approximates a cubic by eliminating its tĀ³ term
+ // from its polynomial expression anchored at the starting point:
+ //
+ // P(t) = pen + 3t(ctrl0 - pen) + 3tĀ²(ctrl1 - 2ctrl0 + pen) + tĀ³(to - 3ctrl1 + 3ctrl0 - pen)
+ //
+ // The control point for the new quadratic Q1 that shares starting point, pen, with P is
+ //
+ // C1 = (3ctrl0 - pen)/2
+ //
+ // The reverse cubic anchored at the end point has the polynomial
+ //
+ // P'(t) = to + 3t(ctrl1 - to) + 3tĀ²(ctrl0 - 2ctrl1 + to) + tĀ³(pen - 3ctrl0 + 3ctrl1 - to)
+ //
+ // The corresponding quadratic Q2 that shares the end point, to, with P has control
+ // point
+ //
+ // C2 = (3ctrl1 - to)/2
+ //
+ // The combined quadratic BĆ©zier, Q, shares both start and end points with its cubic
+ // and use the midpoint between the two curves Q1 and Q2 as control point:
+ //
+ // C = (3ctrl0 - pen + 3ctrl1 - to)/4
+ c := ctrl0.Mul(3).Sub(from).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0)
+ const maxSplits = 32
+ if splits >= maxSplits {
+ *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to})
+ return splits
+ }
+ // The maximum distance between the cubic P and its approximation Q given t
+ // can be shown to be
+ //
+ // d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen|
+ //
+ // To save a square root, compare dĀ² with the squared tolerance.
+ v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(from)
+ d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36)
+ if d2 <= maxDist*maxDist {
+ *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to})
+ return splits
+ }
+ // De Casteljau split the curve and approximate the halves.
+ t := float32(0.5)
+ c0 := from.Add(ctrl0.Sub(from).Mul(t))
+ c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t))
+ c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t))
+ c01 := c0.Add(c1.Sub(c0).Mul(t))
+ c12 := c1.Add(c2.Sub(c1).Mul(t))
+ c0112 := c01.Add(c12.Sub(c01).Mul(t))
+ splits++
+ splits = approxCubeTo(quads, splits, maxDist, from, c0, c01, c0112)
+ splits = approxCubeTo(quads, splits, maxDist, c0112, c12, c2, to)
+ return splits
+}
diff --git a/gio/io/clipboard/clipboard.go b/gio/io/clipboard/clipboard.go
new file mode 100644
index 0000000..3d1e64c
--- /dev/null
+++ b/gio/io/clipboard/clipboard.go
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package clipboard
+
+import (
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/op"
+)
+
+// Event is generated when the clipboard content is requested.
+type Event struct {
+ Text string
+}
+
+// ReadOp requests the text of the clipboard, delivered to
+// the current handler through an Event.
+type ReadOp struct {
+ Tag event.Tag
+}
+
+// WriteOp copies Text to the clipboard.
+type WriteOp struct {
+ Text string
+}
+
+func (h ReadOp) Add(o *op.Ops) {
+ data := o.Write1(opconst.TypeClipboardReadLen, h.Tag)
+ data[0] = byte(opconst.TypeClipboardRead)
+}
+
+func (h WriteOp) Add(o *op.Ops) {
+ data := o.Write1(opconst.TypeClipboardWriteLen, &h.Text)
+ data[0] = byte(opconst.TypeClipboardWrite)
+}
+
+func (Event) ImplementsEvent() {}
diff --git a/gio/io/event/event.go b/gio/io/event/event.go
new file mode 100644
index 0000000..998dccb
--- /dev/null
+++ b/gio/io/event/event.go
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package event contains the types for event handling.
+
+The Queue interface is the protocol for receiving external events.
+
+For example:
+
+ var queue event.Queue = ...
+
+ for _, e := range queue.Events(h) {
+ switch e.(type) {
+ ...
+ }
+ }
+
+In general, handlers must be declared before events become
+available. Other packages such as pointer and key provide
+the means for declaring handlers for specific event types.
+
+The following example declares a handler ready for key input:
+
+ import realy.lol/gio/io/key
+
+ ops := new(op.Ops)
+ var h *Handler = ...
+ key.InputOp{Tag: h}.Add(ops)
+
+*/
+package event
+
+// Queue maps an event handler key to the events
+// available to the handler.
+type Queue interface {
+ // Events returns the available events for an
+ // event handler tag.
+ Events(t Tag) []Event
+}
+
+// Tag is the stable identifier for an event handler.
+// For a handler h, the tag is typically &h.
+type Tag interface{}
+
+// Event is the marker interface for events.
+type Event interface {
+ ImplementsEvent()
+}
diff --git a/gio/io/key/key.go b/gio/io/key/key.go
new file mode 100644
index 0000000..913dbb2
--- /dev/null
+++ b/gio/io/key/key.go
@@ -0,0 +1,182 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package key implements key and text events and operations.
+
+The InputOp operations is used for declaring key input handlers. Use
+an implementation of the Queue interface from package ui to receive
+events.
+*/
+package key
+
+import (
+ "fmt"
+ "strings"
+
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/op"
+)
+
+// InputOp declares a handler ready for key events.
+// Key events are in general only delivered to the
+// focused key handler.
+type InputOp struct {
+ Tag event.Tag
+}
+
+// SoftKeyboardOp shows or hide the on-screen keyboard, if available.
+// It replaces any previous SoftKeyboardOp.
+type SoftKeyboardOp struct {
+ Show bool
+}
+
+// FocusOp sets or clears the keyboard focus. It replaces any previous
+// FocusOp in the same frame.
+type FocusOp struct {
+ // Tag is the new focus. The focus is cleared if Tag is nil, or if Tag
+ // has no InputOp in the same frame.
+ Tag event.Tag
+}
+
+// A FocusEvent is generated when a handler gains or loses
+// focus.
+type FocusEvent struct {
+ Focus bool
+}
+
+// An Event is generated when a key is pressed. For text input
+// use EditEvent.
+type Event struct {
+ // Name of the key. For letters, the upper case form is used, via
+ // unicode.ToUpper. The shift modifier is taken into account, all other
+ // modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1"
+ // combinations both give the Name "!" with the US keyboard layout.
+ Name string
+ // Modifiers is the set of active modifiers when the key was pressed.
+ Modifiers Modifiers
+ // State is the state of the key when the event was fired.
+ State State
+}
+
+// An EditEvent is generated when text is input.
+type EditEvent struct {
+ Text string
+}
+
+// State is the state of a key during an event.
+type State uint8
+
+const (
+ // Press is the state of a pressed key.
+ Press State = iota
+ // Release is the state of a key that has been released.
+ //
+ // Note: release events are only implemented on the following platforms:
+ // macOS, Linux, Windows, WebAssembly.
+ Release
+)
+
+// Modifiers
+type Modifiers uint32
+
+const (
+ // ModCtrl is the ctrl modifier key.
+ ModCtrl Modifiers = 1 << iota
+ // ModCommand is the command modifier key
+ // found on Apple keyboards.
+ ModCommand
+ // ModShift is the shift modifier key.
+ ModShift
+ // ModAlt is the alt modifier key, or the option
+ // key on Apple keyboards.
+ ModAlt
+ // ModSuper is the "logo" modifier key, often
+ // represented by a Windows logo.
+ ModSuper
+)
+
+const (
+ // Names for special keys.
+ NameLeftArrow = "ā"
+ NameRightArrow = "ā"
+ NameUpArrow = "ā"
+ NameDownArrow = "ā"
+ NameReturn = "ā"
+ NameEnter = "ā¤"
+ NameEscape = "ā"
+ NameHome = "ā±"
+ NameEnd = "ā²"
+ NameDeleteBackward = "ā«"
+ NameDeleteForward = "ā¦"
+ NamePageUp = "ā"
+ NamePageDown = "ā"
+ NameTab = "ā„"
+ NameSpace = "Space"
+)
+
+// Contain reports whether m contains all modifiers
+// in m2.
+func (m Modifiers) Contain(m2 Modifiers) bool {
+ return m&m2 == m2
+}
+
+func (h InputOp) Add(o *op.Ops) {
+ if h.Tag == nil {
+ panic("Tag must be non-nil")
+ }
+ data := o.Write1(opconst.TypeKeyInputLen, h.Tag)
+ data[0] = byte(opconst.TypeKeyInput)
+}
+
+func (h SoftKeyboardOp) Add(o *op.Ops) {
+ data := o.Write(opconst.TypeKeySoftKeyboardLen)
+ data[0] = byte(opconst.TypeKeySoftKeyboard)
+ if h.Show {
+ data[1] = 1
+ }
+}
+
+func (h FocusOp) Add(o *op.Ops) {
+ data := o.Write1(opconst.TypeKeyFocusLen, h.Tag)
+ data[0] = byte(opconst.TypeKeyFocus)
+}
+
+func (EditEvent) ImplementsEvent() {}
+func (Event) ImplementsEvent() {}
+func (FocusEvent) ImplementsEvent() {}
+
+func (e Event) String() string {
+ return fmt.Sprintf("%v %v %v}", e.Name, e.Modifiers, e.State)
+}
+
+func (m Modifiers) String() string {
+ var strs []string
+ if m.Contain(ModCtrl) {
+ strs = append(strs, "ModCtrl")
+ }
+ if m.Contain(ModCommand) {
+ strs = append(strs, "ModCommand")
+ }
+ if m.Contain(ModShift) {
+ strs = append(strs, "ModShift")
+ }
+ if m.Contain(ModAlt) {
+ strs = append(strs, "ModAlt")
+ }
+ if m.Contain(ModSuper) {
+ strs = append(strs, "ModSuper")
+ }
+ return strings.Join(strs, "|")
+}
+
+func (s State) String() string {
+ switch s {
+ case Press:
+ return "Press"
+ case Release:
+ return "Release"
+ default:
+ panic("invalid State")
+ }
+}
diff --git a/gio/io/key/mod.go b/gio/io/key/mod.go
new file mode 100644
index 0000000..c5db56c
--- /dev/null
+++ b/gio/io/key/mod.go
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// +build !darwin
+
+package key
+
+// ModShortcut is the platform's shortcut modifier, usually the Ctrl
+// key. On Apple platforms it is the Cmd key.
+const ModShortcut = ModCtrl
diff --git a/gio/io/key/mod_darwin.go b/gio/io/key/mod_darwin.go
new file mode 100644
index 0000000..c0f1437
--- /dev/null
+++ b/gio/io/key/mod_darwin.go
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package key
+
+// ModShortcut is the platform's shortcut modifier, usually the Ctrl
+// key. On Apple platforms it is the Cmd key.
+const ModShortcut = ModCommand
diff --git a/gio/io/pointer/doc.go b/gio/io/pointer/doc.go
new file mode 100644
index 0000000..7243b94
--- /dev/null
+++ b/gio/io/pointer/doc.go
@@ -0,0 +1,131 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package pointer implements pointer events and operations.
+A pointer is either a mouse controlled cursor or a touch
+object such as a finger.
+
+The InputOp operation is used to declare a handler ready for pointer
+events. Use an event.Queue to receive events.
+
+Types
+
+Only events that match a specified list of types are delivered to a handler.
+
+For example, to receive Press, Drag, and Release events (but not Move, Enter,
+Leave, or Scroll):
+
+ var ops op.Ops
+ var h *Handler = ...
+
+ pointer.InputOp{
+ Tag: h,
+ Types: pointer.Press | pointer.Drag | pointer.Release,
+ }.Add(ops)
+
+Cancel events are always delivered.
+
+Areas
+
+The area operations are used for specifying the area where
+subsequent InputOp are active.
+
+For example, to set up a rectangular hit area:
+
+ r := image.Rectangle{...}
+ pointer.Rect(r).Add(ops)
+ pointer.InputOp{Tag: h}.Add(ops)
+
+Note that areas compound: the effective area of multiple area
+operations is the intersection of the areas.
+
+Matching events
+
+StackOp operations and input handlers form an implicit tree.
+Each stack operation is a node, and each input handler is associated
+with the most recent node.
+
+For example:
+
+ ops := new(op.Ops)
+ var stack op.StackOp
+ var h1, h2 *Handler
+
+ state := op.Save(ops)
+ pointer.InputOp{Tag: h1}.Add(Ops)
+ state.Load()
+
+ state = op.Save(ops)
+ pointer.InputOp{Tag: h2}.Add(ops)
+ state.Load()
+
+implies a tree of two inner nodes, each with one pointer handler.
+
+When determining which handlers match an Event, only handlers whose
+areas contain the event position are considered. The matching
+proceeds as follows.
+
+First, the foremost matching handler is included. If the handler
+has pass-through enabled, this step is repeated.
+
+Then, all matching handlers from the current node and all parent
+nodes are included.
+
+In the example above, all events will go to h2 only even though both
+handlers have the same area (the entire screen).
+
+Pass-through
+
+The PassOp operations controls the pass-through setting. A handler's
+pass-through setting is recorded along with the InputOp.
+
+Pass-through handlers are useful for overlay widgets such as a hidden
+side drawer. When the user touches the side, both the (transparent)
+drawer handle and the interface below should receive pointer events.
+
+Disambiguation
+
+When more than one handler matches a pointer event, the event queue
+follows a set of rules for distributing the event.
+
+As long as the pointer has not received a Press event, all
+matching handlers receive all events.
+
+When a pointer is pressed, the set of matching handlers is
+recorded. The set is not updated according to the pointer position
+and hit areas. Rather, handlers stay in the matching set until they
+no longer appear in a InputOp or when another handler in the set
+grabs the pointer.
+
+A handler can exclude all other handler from its matching sets
+by setting the Grab flag in its InputOp. The Grab flag is sticky
+and stays in effect until the handler no longer appears in any
+matching sets.
+
+The losing handlers are notified by a Cancel event.
+
+For multiple grabbing handlers, the foremost handler wins.
+
+Priorities
+
+Handlers know their position in a matching set of a pointer through
+event priorities. The Shared priority is for matching sets with
+multiple handlers; the Grabbed priority indicate exclusive access.
+
+Priorities are useful for deferred gesture matching.
+
+Consider a scrollable list of clickable elements. When the user touches an
+element, it is unknown whether the gesture is a click on the element
+or a drag (scroll) of the list. While the click handler might light up
+the element in anticipation of a click, the scrolling handler does not
+scroll on finger movements with lower than Grabbed priority.
+
+Should the user release the finger, the click handler registers a click.
+
+However, if the finger moves beyond a threshold, the scrolling handler
+determines that the gesture is a drag and sets its Grab flag. The
+click handler receives a Cancel (removing the highlight) and further
+movements for the scroll handler has priority Grabbed, scrolling the
+list.
+*/
+package pointer
diff --git a/gio/io/pointer/pointer.go b/gio/io/pointer/pointer.go
new file mode 100644
index 0000000..f3aafae
--- /dev/null
+++ b/gio/io/pointer/pointer.go
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package pointer
+
+import (
+ "encoding/binary"
+ "fmt"
+ "image"
+ "strings"
+ "time"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/op"
+)
+
+// Event is a pointer event.
+type Event struct {
+ Type Type
+ Source Source
+ // PointerID is the id for the pointer and can be used
+ // to track a particular pointer from Press to
+ // Release or Cancel.
+ PointerID ID
+ // Priority is the priority of the receiving handler
+ // for this event.
+ Priority Priority
+ // Time is when the event was received. The
+ // timestamp is relative to an undefined base.
+ Time time.Duration
+ // Buttons are the set of pressed mouse buttons for this event.
+ Buttons Buttons
+ // Position is the position of the event, relative to
+ // the current transformation, as set by op.TransformOp.
+ Position f32.Point
+ // Scroll is the scroll amount, if any.
+ Scroll f32.Point
+ // Modifiers is the set of active modifiers when
+ // the mouse button was pressed.
+ Modifiers key.Modifiers
+}
+
+// AreaOp updates the hit area to the intersection of the current
+// hit area and the area. The area is transformed before applying
+// it.
+type AreaOp struct {
+ kind areaKind
+ rect image.Rectangle
+}
+
+// CursorNameOp sets the cursor for the current area.
+type CursorNameOp struct {
+ Name CursorName
+}
+
+// InputOp declares an input handler ready for pointer
+// events.
+type InputOp struct {
+ Tag event.Tag
+ // Grab, if set, request that the handler get
+ // Grabbed priority.
+ Grab bool
+ // Types is a bitwise-or of event types to receive.
+ Types Type
+ // ScrollBounds describe the maximum scrollable distances in both
+ // axes. Specifically, any Event e delivered to Tag will satisfy
+ //
+ // ScrollBounds.Min.X <= e.Scroll.X <= ScrollBounds.Max.X (horizontal axis)
+ // ScrollBounds.Min.Y <= e.Scroll.Y <= ScrollBounds.Max.Y (vertical axis)
+ ScrollBounds image.Rectangle
+}
+
+// PassOp sets the pass-through mode.
+type PassOp struct {
+ Pass bool
+}
+
+type ID uint16
+
+// Type of an Event.
+type Type uint8
+
+// Priority of an Event.
+type Priority uint8
+
+// Source of an Event.
+type Source uint8
+
+// Buttons is a set of mouse buttons
+type Buttons uint8
+
+// CursorName is the name of a cursor.
+type CursorName string
+
+// Must match app/internal/input.areaKind
+type areaKind uint8
+
+const (
+ // CursorDefault is the default cursor.
+ CursorDefault CursorName = ""
+ // CursorText is the cursor for text.
+ CursorText CursorName = "text"
+ // CursorPointer is the cursor for a link.
+ CursorPointer CursorName = "pointer"
+ // CursorCrossHair is the cursor for precise location.
+ CursorCrossHair CursorName = "crosshair"
+ // CursorColResize is the cursor for vertical resize.
+ CursorColResize CursorName = "col-resize"
+ // CursorRowResize is the cursor for horizontal resize.
+ CursorRowResize CursorName = "row-resize"
+ // CursorGrab is the cursor for moving object in any direction.
+ CursorGrab CursorName = "grab"
+ // CursorNone hides the cursor. To show it again, use any other cursor.
+ CursorNone CursorName = "none"
+)
+
+const (
+ // A Cancel event is generated when the current gesture is
+ // interrupted by other handlers or the system.
+ Cancel Type = (1 << iota) >> 1
+ // Press of a pointer.
+ Press
+ // Release of a pointer.
+ Release
+ // Move of a pointer.
+ Move
+ // Drag of a pointer.
+ Drag
+ // Pointer enters an area watching for pointer input
+ Enter
+ // Pointer leaves an area watching for pointer input
+ Leave
+ // Scroll of a pointer.
+ Scroll
+)
+
+const (
+ // Mouse generated event.
+ Mouse Source = iota
+ // Touch generated event.
+ Touch
+)
+
+const (
+ // Shared priority is for handlers that
+ // are part of a matching set larger than 1.
+ Shared Priority = iota
+ // Foremost priority is like Shared, but the
+ // handler is the foremost of the matching set.
+ Foremost
+ // Grabbed is used for matching sets of size 1.
+ Grabbed
+)
+
+const (
+ // ButtonPrimary is the primary button, usually the left button for a
+ // right-handed user.
+ ButtonPrimary Buttons = 1 << iota
+ // ButtonSecondary is the secondary button, usually the right button for a
+ // right-handed user.
+ ButtonSecondary
+ // ButtonTertiary is the tertiary button, usually the middle button.
+ ButtonTertiary
+)
+
+const (
+ areaRect areaKind = iota
+ areaEllipse
+)
+
+// Rect constructs a rectangular hit area.
+func Rect(size image.Rectangle) AreaOp {
+ return AreaOp{
+ kind: areaRect,
+ rect: size,
+ }
+}
+
+// Ellipse constructs an ellipsoid hit area.
+func Ellipse(size image.Rectangle) AreaOp {
+ return AreaOp{
+ kind: areaEllipse,
+ rect: size,
+ }
+}
+
+func (op AreaOp) Add(o *op.Ops) {
+ data := o.Write(opconst.TypeAreaLen)
+ data[0] = byte(opconst.TypeArea)
+ data[1] = byte(op.kind)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[2:], uint32(op.rect.Min.X))
+ bo.PutUint32(data[6:], uint32(op.rect.Min.Y))
+ bo.PutUint32(data[10:], uint32(op.rect.Max.X))
+ bo.PutUint32(data[14:], uint32(op.rect.Max.Y))
+}
+
+func (op CursorNameOp) Add(o *op.Ops) {
+ data := o.Write1(opconst.TypeCursorLen, op.Name)
+ data[0] = byte(opconst.TypeCursor)
+}
+
+// Add panics if the scroll range does not contain zero.
+func (op InputOp) Add(o *op.Ops) {
+ if op.Tag == nil {
+ panic("Tag must be non-nil")
+ }
+ if b := op.ScrollBounds; b.Min.X > 0 || b.Max.X < 0 || b.Min.Y > 0 || b.Max.Y < 0 {
+ panic(fmt.Errorf("invalid scroll range value %v", b))
+ }
+ data := o.Write1(opconst.TypePointerInputLen, op.Tag)
+ data[0] = byte(opconst.TypePointerInput)
+ if op.Grab {
+ data[1] = 1
+ }
+ data[2] = byte(op.Types)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[3:], uint32(op.ScrollBounds.Min.X))
+ bo.PutUint32(data[7:], uint32(op.ScrollBounds.Min.Y))
+ bo.PutUint32(data[11:], uint32(op.ScrollBounds.Max.X))
+ bo.PutUint32(data[15:], uint32(op.ScrollBounds.Max.Y))
+}
+
+func (op PassOp) Add(o *op.Ops) {
+ data := o.Write(opconst.TypePassLen)
+ data[0] = byte(opconst.TypePass)
+ if op.Pass {
+ data[1] = 1
+ }
+}
+
+func (t Type) String() string {
+ switch t {
+ case Press:
+ return "Press"
+ case Release:
+ return "Release"
+ case Cancel:
+ return "Cancel"
+ case Move:
+ return "Move"
+ case Drag:
+ return "Drag"
+ case Enter:
+ return "Enter"
+ case Leave:
+ return "Leave"
+ case Scroll:
+ return "Scroll"
+ default:
+ panic("unknown Type")
+ }
+}
+
+func (p Priority) String() string {
+ switch p {
+ case Shared:
+ return "Shared"
+ case Foremost:
+ return "Foremost"
+ case Grabbed:
+ return "Grabbed"
+ default:
+ panic("unknown priority")
+ }
+}
+
+func (s Source) String() string {
+ switch s {
+ case Mouse:
+ return "Mouse"
+ case Touch:
+ return "Touch"
+ default:
+ panic("unknown source")
+ }
+}
+
+// Contain reports whether the set b contains
+// all of the buttons.
+func (b Buttons) Contain(buttons Buttons) bool {
+ return b&buttons == buttons
+}
+
+func (b Buttons) String() string {
+ var strs []string
+ if b.Contain(ButtonPrimary) {
+ strs = append(strs, "ButtonPrimary")
+ }
+ if b.Contain(ButtonSecondary) {
+ strs = append(strs, "ButtonSecondary")
+ }
+ if b.Contain(ButtonTertiary) {
+ strs = append(strs, "ButtonTertiary")
+ }
+ return strings.Join(strs, "|")
+}
+
+func (c CursorName) String() string {
+ if c == CursorDefault {
+ return "default"
+ }
+ return string(c)
+}
+
+func (Event) ImplementsEvent() {}
diff --git a/gio/io/profile/profile.go b/gio/io/profile/profile.go
new file mode 100644
index 0000000..58be154
--- /dev/null
+++ b/gio/io/profile/profile.go
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package profiles provides access to rendering
+// profiles.
+package profile
+
+import (
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/op"
+)
+
+// Op registers a handler for receiving
+// Events.
+type Op struct {
+ Tag event.Tag
+}
+
+// Event contains profile data from a single
+// rendered frame.
+type Event struct {
+ // Timings. Very likely to change.
+ Timings string
+}
+
+func (p Op) Add(o *op.Ops) {
+ data := o.Write1(opconst.TypeProfileLen, p.Tag)
+ data[0] = byte(opconst.TypeProfile)
+}
+
+func (p Event) ImplementsEvent() {}
diff --git a/gio/io/router/clipboard.go b/gio/io/router/clipboard.go
new file mode 100644
index 0000000..122c9bc
--- /dev/null
+++ b/gio/io/router/clipboard.go
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package router
+
+import (
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/io/event"
+)
+
+type clipboardQueue struct {
+ receivers map[event.Tag]struct{}
+ // request avoid read clipboard every frame while waiting.
+ requested bool
+ text *string
+ reader ops.Reader
+}
+
+// WriteClipboard returns the most recent text to be copied
+// to the clipboard, if any.
+func (q *clipboardQueue) WriteClipboard() (string, bool) {
+ if q.text == nil {
+ return "", false
+ }
+ text := *q.text
+ q.text = nil
+ return text, true
+}
+
+// ReadClipboard reports if any new handler is waiting
+// to read the clipboard.
+func (q *clipboardQueue) ReadClipboard() bool {
+ if len(q.receivers) <= 0 || q.requested {
+ return false
+ }
+ q.requested = true
+ return true
+}
+
+func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) {
+ for r := range q.receivers {
+ events.Add(r, e)
+ delete(q.receivers, r)
+ }
+}
+
+func (q *clipboardQueue) ProcessWriteClipboard(d []byte, refs []interface{}) {
+ if opconst.OpType(d[0]) != opconst.TypeClipboardWrite {
+ panic("invalid op")
+ }
+ q.text = refs[0].(*string)
+}
+
+func (q *clipboardQueue) ProcessReadClipboard(d []byte, refs []interface{}) {
+ if opconst.OpType(d[0]) != opconst.TypeClipboardRead {
+ panic("invalid op")
+ }
+ if q.receivers == nil {
+ q.receivers = make(map[event.Tag]struct{})
+ }
+ tag := refs[0].(event.Tag)
+ if _, ok := q.receivers[tag]; !ok {
+ q.receivers[tag] = struct{}{}
+ q.requested = false
+ }
+}
diff --git a/gio/io/router/clipboard_test.go b/gio/io/router/clipboard_test.go
new file mode 100644
index 0000000..ac5ebe7
--- /dev/null
+++ b/gio/io/router/clipboard_test.go
@@ -0,0 +1,155 @@
+package router
+
+import (
+ "testing"
+
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/op"
+)
+
+func TestClipboardDuplicateEvent(t *testing.T) {
+ ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
+
+ // Both must receive the event once
+ clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
+ clipboard.ReadOp{Tag: &handler[1]}.Add(ops)
+
+ router.Frame(ops)
+ event := clipboard.Event{Text: "Test"}
+ router.Queue(event)
+ assertClipboardReadOp(t, router, 0)
+ assertClipboardEvent(t, router.Events(&handler[0]), true)
+ assertClipboardEvent(t, router.Events(&handler[1]), true)
+ ops.Reset()
+
+ // No ReadOp
+
+ router.Frame(ops)
+ assertClipboardReadOp(t, router, 0)
+ assertClipboardEvent(t, router.Events(&handler[0]), false)
+ assertClipboardEvent(t, router.Events(&handler[1]), false)
+ ops.Reset()
+
+ clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
+
+ router.Frame(ops)
+ // No ClipboardEvent sent
+ assertClipboardReadOp(t, router, 1)
+ assertClipboardEvent(t, router.Events(&handler[0]), false)
+ assertClipboardEvent(t, router.Events(&handler[1]), false)
+ ops.Reset()
+}
+
+func TestQueueProcessReadClipboard(t *testing.T) {
+ ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
+ ops.Reset()
+
+ // Request read
+ clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
+
+ router.Frame(ops)
+ assertClipboardReadOp(t, router, 1)
+ ops.Reset()
+
+ for i := 0; i < 3; i++ {
+ // No ReadOp
+ // One receiver must still wait for response
+
+ router.Frame(ops)
+ assertClipboardReadOpDuplicated(t, router, 1)
+ ops.Reset()
+ }
+
+ router.Frame(ops)
+ // Send the clipboard event
+ event := clipboard.Event{Text: "Text 2"}
+ router.Queue(event)
+ assertClipboardReadOp(t, router, 0)
+ assertClipboardEvent(t, router.Events(&handler[0]), true)
+ ops.Reset()
+
+ // No ReadOp
+ // There's no receiver waiting
+
+ router.Frame(ops)
+ assertClipboardReadOp(t, router, 0)
+ assertClipboardEvent(t, router.Events(&handler[0]), false)
+ ops.Reset()
+}
+
+func TestQueueProcessWriteClipboard(t *testing.T) {
+ ops, router := new(op.Ops), new(Router)
+ ops.Reset()
+
+ clipboard.WriteOp{Text: "Write 1"}.Add(ops)
+
+ router.Frame(ops)
+ assertClipboardWriteOp(t, router, "Write 1")
+ ops.Reset()
+
+ // No WriteOp
+
+ router.Frame(ops)
+ assertClipboardWriteOp(t, router, "")
+ ops.Reset()
+
+ clipboard.WriteOp{Text: "Write 2"}.Add(ops)
+
+ router.Frame(ops)
+ assertClipboardReadOp(t, router, 0)
+ assertClipboardWriteOp(t, router, "Write 2")
+ ops.Reset()
+}
+
+func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) {
+ t.Helper()
+ var evtClipboard int
+ for _, e := range events {
+ switch e.(type) {
+ case clipboard.Event:
+ evtClipboard++
+ }
+ }
+ if evtClipboard <= 0 && expected {
+ t.Error("expected to receive some event")
+ }
+ if evtClipboard > 0 && !expected {
+ t.Error("unexpected event received")
+ }
+}
+
+func assertClipboardReadOp(t *testing.T, router *Router, expected int) {
+ t.Helper()
+ if len(router.cqueue.receivers) != expected {
+ t.Error("unexpected number of receivers")
+ }
+ if router.cqueue.ReadClipboard() != (expected > 0) {
+ t.Error("missing requests")
+ }
+}
+
+func assertClipboardReadOpDuplicated(t *testing.T, router *Router,
+ expected int) {
+ t.Helper()
+ if len(router.cqueue.receivers) != expected {
+ t.Error("receivers removed")
+ }
+ if router.cqueue.ReadClipboard() != false {
+ t.Error("duplicated requests")
+ }
+}
+
+func assertClipboardWriteOp(t *testing.T, router *Router, expected string) {
+ t.Helper()
+ if (router.cqueue.text != nil) != (expected != "") {
+ t.Error("text not defined")
+ }
+ text, ok := router.cqueue.WriteClipboard()
+ if ok != (expected != "") {
+ t.Error("duplicated requests")
+ }
+ if text != expected {
+ t.Errorf("got text %s, expected %s", text, expected)
+ }
+}
diff --git a/gio/io/router/key.go b/gio/io/router/key.go
new file mode 100644
index 0000000..0fe946e
--- /dev/null
+++ b/gio/io/router/key.go
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package router
+
+import (
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/op"
+)
+
+type TextInputState uint8
+
+type keyQueue struct {
+ focus event.Tag
+ handlers map[event.Tag]*keyHandler
+ reader ops.Reader
+ state TextInputState
+}
+
+type keyHandler struct {
+ // visible will be true if the InputOp is present
+ // in the current frame.
+ visible bool
+ new bool
+}
+
+const (
+ TextInputKeep TextInputState = iota
+ TextInputClose
+ TextInputOpen
+)
+
+// InputState returns the last text input state as
+// determined in Frame.
+func (q *keyQueue) InputState() TextInputState {
+ return q.state
+}
+
+func (q *keyQueue) Frame(root *op.Ops, events *handlerEvents) {
+ if q.handlers == nil {
+ q.handlers = make(map[event.Tag]*keyHandler)
+ }
+ for _, h := range q.handlers {
+ h.visible, h.new = false, false
+ }
+ q.reader.Reset(root)
+
+ focus, changed, state := q.resolveFocus(events)
+ for k, h := range q.handlers {
+ if !h.visible {
+ delete(q.handlers, k)
+ if q.focus == k {
+ // Remove the focus from the handler that is no longer visible.
+ q.focus = nil
+ state = TextInputClose
+ }
+ } else if h.new && k != focus {
+ // Reset the handler on (each) first appearance, but don't trigger redraw.
+ events.AddNoRedraw(k, key.FocusEvent{Focus: false})
+ }
+ }
+ if changed && focus != nil {
+ if _, exists := q.handlers[focus]; !exists {
+ focus = nil
+ }
+ }
+ if changed && focus != q.focus {
+ if q.focus != nil {
+ events.Add(q.focus, key.FocusEvent{Focus: false})
+ }
+ q.focus = focus
+ if q.focus != nil {
+ events.Add(q.focus, key.FocusEvent{Focus: true})
+ } else {
+ state = TextInputClose
+ }
+ }
+ q.state = state
+}
+
+func (q *keyQueue) Push(e event.Event, events *handlerEvents) {
+ if q.focus != nil {
+ events.Add(q.focus, e)
+ }
+}
+
+func (q *keyQueue) resolveFocus(events *handlerEvents) (focus event.Tag,
+ changed bool, state TextInputState) {
+ for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
+ switch opconst.OpType(encOp.Data[0]) {
+ case opconst.TypeKeyFocus:
+ op := decodeFocusOp(encOp.Data, encOp.Refs)
+ changed = true
+ focus = op.Tag
+ case opconst.TypeKeySoftKeyboard:
+ op := decodeSoftKeyboardOp(encOp.Data, encOp.Refs)
+ if op.Show {
+ state = TextInputOpen
+ } else {
+ state = TextInputClose
+ }
+ case opconst.TypeKeyInput:
+ op := decodeKeyInputOp(encOp.Data, encOp.Refs)
+ h, ok := q.handlers[op.Tag]
+ if !ok {
+ h = &keyHandler{new: true}
+ q.handlers[op.Tag] = h
+ }
+ h.visible = true
+ }
+ }
+ return
+}
+
+func decodeKeyInputOp(d []byte, refs []interface{}) key.InputOp {
+ if opconst.OpType(d[0]) != opconst.TypeKeyInput {
+ panic("invalid op")
+ }
+ return key.InputOp{
+ Tag: refs[0].(event.Tag),
+ }
+}
+
+func decodeSoftKeyboardOp(d []byte, refs []interface{}) key.SoftKeyboardOp {
+ if opconst.OpType(d[0]) != opconst.TypeKeySoftKeyboard {
+ panic("invalid op")
+ }
+ return key.SoftKeyboardOp{
+ Show: d[1] != 0,
+ }
+}
+
+func decodeFocusOp(d []byte, refs []interface{}) key.FocusOp {
+ if opconst.OpType(d[0]) != opconst.TypeKeyFocus {
+ panic("invalid op")
+ }
+ return key.FocusOp{
+ Tag: refs[0],
+ }
+}
diff --git a/gio/io/router/key_test.go b/gio/io/router/key_test.go
new file mode 100644
index 0000000..59176df
--- /dev/null
+++ b/gio/io/router/key_test.go
@@ -0,0 +1,322 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package router
+
+import (
+ "reflect"
+ "testing"
+
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/op"
+)
+
+func TestKeyWakeup(t *testing.T) {
+ handler := new(int)
+ var ops op.Ops
+ key.InputOp{Tag: handler}.Add(&ops)
+
+ var r Router
+ // Test that merely adding a handler doesn't trigger redraw.
+ r.Frame(&ops)
+ if _, wake := r.WakeupTime(); wake {
+ t.Errorf("adding key.InputOp triggered a redraw")
+ }
+ // However, adding a handler queues a Focus(false) event.
+ if evts := r.Events(handler); len(evts) != 1 {
+ t.Errorf("no Focus event for newly registered key.InputOp")
+ }
+ // Verify that r.Events does trigger a redraw.
+ r.Frame(&ops)
+ if _, wake := r.WakeupTime(); !wake {
+ t.Errorf("key.FocusEvent event didn't trigger a redraw")
+ }
+}
+
+func TestKeyMultiples(t *testing.T) {
+ handlers := make([]int, 3)
+ ops := new(op.Ops)
+ r := new(Router)
+
+ key.SoftKeyboardOp{Show: true}.Add(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ key.FocusOp{Tag: &handlers[2]}.Add(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+
+ // The last one must be focused:
+ key.InputOp{Tag: &handlers[2]}.Add(ops)
+
+ r.Frame(ops)
+
+ assertKeyEvent(t, r.Events(&handlers[0]), false)
+ assertKeyEvent(t, r.Events(&handlers[1]), false)
+ assertKeyEvent(t, r.Events(&handlers[2]), true)
+ assertFocus(t, r, &handlers[2])
+ assertKeyboard(t, r, TextInputOpen)
+}
+
+func TestKeyStacked(t *testing.T) {
+ handlers := make([]int, 4)
+ ops := new(op.Ops)
+ r := new(Router)
+
+ s := op.Save(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ key.FocusOp{Tag: nil}.Add(ops)
+ s.Load()
+ s = op.Save(ops)
+ key.SoftKeyboardOp{Show: false}.Add(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ key.FocusOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[2]}.Add(ops)
+ key.SoftKeyboardOp{Show: true}.Add(ops)
+ s.Load()
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[3]}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEvent(t, r.Events(&handlers[0]), false)
+ assertKeyEvent(t, r.Events(&handlers[1]), true)
+ assertKeyEvent(t, r.Events(&handlers[2]), false)
+ assertKeyEvent(t, r.Events(&handlers[3]), false)
+ assertFocus(t, r, &handlers[1])
+ assertKeyboard(t, r, TextInputOpen)
+}
+
+func TestKeySoftKeyboardNoFocus(t *testing.T) {
+ ops := new(op.Ops)
+ r := new(Router)
+
+ // It's possible to open the keyboard
+ // without any active focus:
+ key.SoftKeyboardOp{Show: true}.Add(ops)
+
+ r.Frame(ops)
+
+ assertFocus(t, r, nil)
+ assertKeyboard(t, r, TextInputOpen)
+}
+
+func TestKeyRemoveFocus(t *testing.T) {
+ handlers := make([]int, 2)
+ ops := new(op.Ops)
+ r := new(Router)
+
+ // New InputOp with Focus and Keyboard:
+ s := op.Save(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ key.FocusOp{Tag: &handlers[0]}.Add(ops)
+ key.SoftKeyboardOp{Show: true}.Add(ops)
+ s.Load()
+
+ // New InputOp without any focus:
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ // Add some key events:
+ event := event.Event(key.Event{Name: key.NameTab,
+ Modifiers: key.ModShortcut, State: key.Press})
+ r.Queue(event)
+
+ assertKeyEvent(t, r.Events(&handlers[0]), true, event)
+ assertKeyEvent(t, r.Events(&handlers[1]), false)
+ assertFocus(t, r, &handlers[0])
+ assertKeyboard(t, r, TextInputOpen)
+
+ ops.Reset()
+
+ // Will get the focus removed:
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ s.Load()
+
+ // Unchanged:
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+
+ // Remove focus by focusing on a tag that don't exist.
+ s = op.Save(ops)
+ key.FocusOp{Tag: new(int)}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEventUnexpected(t, r.Events(&handlers[1]))
+ assertFocus(t, r, nil)
+ assertKeyboard(t, r, TextInputClose)
+
+ ops.Reset()
+
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ s.Load()
+
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEventUnexpected(t, r.Events(&handlers[0]))
+ assertKeyEventUnexpected(t, r.Events(&handlers[1]))
+ assertFocus(t, r, nil)
+ assertKeyboard(t, r, TextInputKeep)
+
+ ops.Reset()
+
+ // Set focus to InputOp which already
+ // exists in the previous frame:
+ s = op.Save(ops)
+ key.FocusOp{Tag: &handlers[0]}.Add(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ key.SoftKeyboardOp{Show: true}.Add(ops)
+ s.Load()
+
+ // Remove focus.
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ key.FocusOp{Tag: nil}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEventUnexpected(t, r.Events(&handlers[1]))
+ assertFocus(t, r, nil)
+ assertKeyboard(t, r, TextInputOpen)
+}
+
+func TestKeyFocusedInvisible(t *testing.T) {
+ handlers := make([]int, 2)
+ ops := new(op.Ops)
+ r := new(Router)
+
+ // Set new InputOp with focus:
+ s := op.Save(ops)
+ key.FocusOp{Tag: &handlers[0]}.Add(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ key.SoftKeyboardOp{Show: true}.Add(ops)
+ s.Load()
+
+ // Set new InputOp without focus:
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEvent(t, r.Events(&handlers[0]), true)
+ assertKeyEvent(t, r.Events(&handlers[1]), false)
+ assertFocus(t, r, &handlers[0])
+ assertKeyboard(t, r, TextInputOpen)
+
+ ops.Reset()
+
+ //
+ // Removed first (focused) element!
+ //
+
+ // Unchanged:
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEventUnexpected(t, r.Events(&handlers[0]))
+ assertKeyEventUnexpected(t, r.Events(&handlers[1]))
+ assertFocus(t, r, nil)
+ assertKeyboard(t, r, TextInputClose)
+
+ ops.Reset()
+
+ // Respawn the first element:
+ // It must receive one `Event{Focus: false}`.
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[0]}.Add(ops)
+ s.Load()
+
+ // Unchanged
+ s = op.Save(ops)
+ key.InputOp{Tag: &handlers[1]}.Add(ops)
+ s.Load()
+
+ r.Frame(ops)
+
+ assertKeyEvent(t, r.Events(&handlers[0]), false)
+ assertKeyEventUnexpected(t, r.Events(&handlers[1]))
+ assertFocus(t, r, nil)
+ assertKeyboard(t, r, TextInputKeep)
+
+}
+
+func assertKeyEvent(t *testing.T, events []event.Event, expected bool,
+ expectedInputs ...event.Event) {
+ t.Helper()
+ var evtFocus int
+ var evtKeyPress int
+ for _, e := range events {
+ switch ev := e.(type) {
+ case key.FocusEvent:
+ if ev.Focus != expected {
+ t.Errorf("focus is expected to be %v, got %v", expected,
+ ev.Focus)
+ }
+ evtFocus++
+ case key.Event, key.EditEvent:
+ if len(expectedInputs) <= evtKeyPress {
+ t.Errorf("unexpected key events")
+ }
+ if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) {
+ t.Errorf("expected %v events, got %v",
+ expectedInputs[evtKeyPress], ev)
+ }
+ evtKeyPress++
+ }
+ }
+ if evtFocus <= 0 {
+ t.Errorf("expected focus event")
+ }
+ if evtFocus > 1 {
+ t.Errorf("expected single focus event")
+ }
+ if evtKeyPress != len(expectedInputs) {
+ t.Errorf("expected key events")
+ }
+}
+
+func assertKeyEventUnexpected(t *testing.T, events []event.Event) {
+ t.Helper()
+ var evtFocus int
+ for _, e := range events {
+ switch e.(type) {
+ case key.FocusEvent:
+ evtFocus++
+ }
+ }
+ if evtFocus > 1 {
+ t.Errorf("unexpected focus event")
+ }
+}
+
+func assertFocus(t *testing.T, router *Router, expected event.Tag) {
+ t.Helper()
+ if router.kqueue.focus != expected {
+ t.Errorf("expected %v to be focused, got %v", expected,
+ router.kqueue.focus)
+ }
+}
+
+func assertKeyboard(t *testing.T, router *Router, expected TextInputState) {
+ t.Helper()
+ if router.kqueue.state != expected {
+ t.Errorf("expected %v keyboard, got %v", expected, router.kqueue.state)
+ }
+}
diff --git a/gio/io/router/pointer.go b/gio/io/router/pointer.go
new file mode 100644
index 0000000..588657c
--- /dev/null
+++ b/gio/io/router/pointer.go
@@ -0,0 +1,515 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package router
+
+import (
+ "encoding/binary"
+ "image"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/op"
+)
+
+type pointerQueue struct {
+ hitTree []hitNode
+ areas []areaNode
+ cursors []cursorNode
+ cursor pointer.CursorName
+ handlers map[event.Tag]*pointerHandler
+ pointers []pointerInfo
+ reader ops.Reader
+
+ // states holds the storage for save/restore ops.
+ states []collectState
+ scratch []event.Tag
+}
+
+type hitNode struct {
+ next int
+ area int
+ // Pass tracks the most recent PassOp mode.
+ pass bool
+
+ // For handler nodes.
+ tag event.Tag
+}
+
+type cursorNode struct {
+ name pointer.CursorName
+ area int
+}
+
+type pointerInfo struct {
+ id pointer.ID
+ pressed bool
+ handlers []event.Tag
+ // last tracks the last pointer event received,
+ // used while processing frame events.
+ last pointer.Event
+
+ // entered tracks the tags that contain the pointer.
+ entered []event.Tag
+}
+
+type pointerHandler struct {
+ area int
+ active bool
+ wantsGrab bool
+ types pointer.Type
+ // min and max horizontal/vertical scroll
+ scrollRange image.Rectangle
+}
+
+type areaOp struct {
+ kind areaKind
+ rect f32.Rectangle
+}
+
+type areaNode struct {
+ trans f32.Affine2D
+ next int
+ area areaOp
+}
+
+type areaKind uint8
+
+// collectState represents the state for collectHandlers
+type collectState struct {
+ t f32.Affine2D
+ area int
+ node int
+ pass bool
+}
+
+const (
+ areaRect areaKind = iota
+ areaEllipse
+)
+
+func (q *pointerQueue) save(id int, state collectState) {
+ if extra := id - len(q.states) + 1; extra > 0 {
+ q.states = append(q.states, make([]collectState, extra)...)
+ }
+ q.states[id] = state
+}
+
+func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents) {
+ state := collectState{
+ area: -1,
+ node: -1,
+ }
+ q.save(opconst.InitialStateID, state)
+ for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
+ switch opconst.OpType(encOp.Data[0]) {
+ case opconst.TypeSave:
+ id := ops.DecodeSave(encOp.Data)
+ q.save(id, state)
+ case opconst.TypeLoad:
+ id, mask := ops.DecodeLoad(encOp.Data)
+ s := q.states[id]
+ if mask&opconst.TransformState != 0 {
+ state.t = s.t
+ }
+ if mask&^opconst.TransformState != 0 {
+ state = s
+ }
+ case opconst.TypePass:
+ state.pass = encOp.Data[1] != 0
+ case opconst.TypeArea:
+ var op areaOp
+ op.Decode(encOp.Data)
+ q.areas = append(q.areas,
+ areaNode{trans: state.t, next: state.area, area: op})
+ state.area = len(q.areas) - 1
+ q.hitTree = append(q.hitTree, hitNode{
+ next: state.node,
+ area: state.area,
+ pass: state.pass,
+ })
+ state.node = len(q.hitTree) - 1
+ case opconst.TypeTransform:
+ dop := ops.DecodeTransform(encOp.Data)
+ state.t = state.t.Mul(dop)
+ case opconst.TypePointerInput:
+ op := pointer.InputOp{
+ Tag: encOp.Refs[0].(event.Tag),
+ Grab: encOp.Data[1] != 0,
+ Types: pointer.Type(encOp.Data[2]),
+ }
+ q.hitTree = append(q.hitTree, hitNode{
+ next: state.node,
+ area: state.area,
+ pass: state.pass,
+ tag: op.Tag,
+ })
+ state.node = len(q.hitTree) - 1
+ h, ok := q.handlers[op.Tag]
+ if !ok {
+ h = new(pointerHandler)
+ q.handlers[op.Tag] = h
+ // Cancel handlers on (each) first appearance, but don't
+ // trigger redraw.
+ events.AddNoRedraw(op.Tag, pointer.Event{Type: pointer.Cancel})
+ }
+ h.active = true
+ h.area = state.area
+ h.wantsGrab = h.wantsGrab || op.Grab
+ h.types = h.types | op.Types
+ bo := binary.LittleEndian.Uint32
+ h.scrollRange = image.Rectangle{
+ Min: image.Point{
+ X: int(int32(bo(encOp.Data[3:]))),
+ Y: int(int32(bo(encOp.Data[7:]))),
+ },
+ Max: image.Point{
+ X: int(int32(bo(encOp.Data[11:]))),
+ Y: int(int32(bo(encOp.Data[15:]))),
+ },
+ }
+ case opconst.TypeCursor:
+ q.cursors = append(q.cursors, cursorNode{
+ name: encOp.Refs[0].(pointer.CursorName),
+ area: len(q.areas) - 1,
+ })
+ }
+ }
+}
+
+func (q *pointerQueue) opHit(handlers *[]event.Tag, pos f32.Point) {
+ // Track whether we're passing through hits.
+ pass := true
+ idx := len(q.hitTree) - 1
+ for idx >= 0 {
+ n := &q.hitTree[idx]
+ if !q.hit(n.area, pos) {
+ idx--
+ continue
+ }
+ pass = pass && n.pass
+ if pass {
+ idx--
+ } else {
+ idx = n.next
+ }
+ if n.tag != nil {
+ if _, exists := q.handlers[n.tag]; exists {
+ *handlers = append(*handlers, n.tag)
+ }
+ }
+ }
+}
+
+func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point {
+ if areaIdx == -1 {
+ return p
+ }
+ return q.areas[areaIdx].trans.Invert().Transform(p)
+}
+
+func (q *pointerQueue) hit(areaIdx int, p f32.Point) bool {
+ for areaIdx != -1 {
+ a := &q.areas[areaIdx]
+ p := a.trans.Invert().Transform(p)
+ if !a.area.Hit(p) {
+ return false
+ }
+ areaIdx = a.next
+ }
+ return true
+}
+
+func (q *pointerQueue) reset() {
+ if q.handlers == nil {
+ q.handlers = make(map[event.Tag]*pointerHandler)
+ }
+}
+
+func (q *pointerQueue) Frame(root *op.Ops, events *handlerEvents) {
+ q.reset()
+ for _, h := range q.handlers {
+ // Reset handler.
+ h.active = false
+ h.wantsGrab = false
+ h.types = 0
+ }
+ q.hitTree = q.hitTree[:0]
+ q.areas = q.areas[:0]
+ q.cursors = q.cursors[:0]
+ q.reader.Reset(root)
+ q.collectHandlers(&q.reader, events)
+ for k, h := range q.handlers {
+ if !h.active {
+ q.dropHandlers(events, k)
+ delete(q.handlers, k)
+ }
+ if h.wantsGrab {
+ for _, p := range q.pointers {
+ if !p.pressed {
+ continue
+ }
+ for i, k2 := range p.handlers {
+ if k2 == k {
+ // Drop other handlers that lost their grab.
+ dropped := make([]event.Tag, 0, len(p.handlers)-1)
+ dropped = append(dropped, p.handlers[:i]...)
+ dropped = append(dropped, p.handlers[i+1:]...)
+ cancelHandlers(events, dropped...)
+ q.dropHandlers(events, dropped...)
+ break
+ }
+ }
+ }
+ }
+ }
+ for i := range q.pointers {
+ p := &q.pointers[i]
+ q.deliverEnterLeaveEvents(p, events, p.last)
+ }
+}
+
+func cancelHandlers(events *handlerEvents, tags ...event.Tag) {
+ for _, k := range tags {
+ events.Add(k, pointer.Event{Type: pointer.Cancel})
+ }
+}
+
+func (q *pointerQueue) dropHandlers(events *handlerEvents, tags ...event.Tag) {
+ for _, k := range tags {
+ for i := range q.pointers {
+ p := &q.pointers[i]
+ for i := len(p.handlers) - 1; i >= 0; i-- {
+ if p.handlers[i] == k {
+ p.handlers = append(p.handlers[:i], p.handlers[i+1:]...)
+ }
+ }
+ for i := len(p.entered) - 1; i >= 0; i-- {
+ if p.entered[i] == k {
+ p.entered = append(p.entered[:i], p.entered[i+1:]...)
+ }
+ }
+ }
+ }
+}
+
+func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
+ q.reset()
+ if e.Type == pointer.Cancel {
+ q.pointers = q.pointers[:0]
+ for k := range q.handlers {
+ cancelHandlers(events, k)
+ q.dropHandlers(events, k)
+ }
+ return
+ }
+ pidx := -1
+ for i, p := range q.pointers {
+ if p.id == e.PointerID {
+ pidx = i
+ break
+ }
+ }
+ if pidx == -1 {
+ q.pointers = append(q.pointers, pointerInfo{id: e.PointerID})
+ pidx = len(q.pointers) - 1
+ }
+ p := &q.pointers[pidx]
+ p.last = e
+
+ if e.Type == pointer.Move && p.pressed {
+ e.Type = pointer.Drag
+ }
+
+ if e.Type == pointer.Release {
+ q.deliverEvent(p, events, e)
+ p.pressed = false
+ }
+ q.deliverEnterLeaveEvents(p, events, e)
+
+ if !p.pressed {
+ p.handlers = append(p.handlers[:0], q.scratch...)
+ }
+ if e.Type == pointer.Press {
+ p.pressed = true
+ }
+ switch e.Type {
+ case pointer.Release:
+ case pointer.Scroll:
+ q.deliverScrollEvent(p, events, e)
+ default:
+ q.deliverEvent(p, events, e)
+ }
+ if !p.pressed && len(p.entered) == 0 {
+ // No longer need to track pointer.
+ q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...)
+ }
+}
+
+func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents,
+ e pointer.Event) {
+ foremost := true
+ if p.pressed && len(p.handlers) == 1 {
+ e.Priority = pointer.Grabbed
+ foremost = false
+ }
+ for _, k := range p.handlers {
+ h := q.handlers[k]
+ if e.Type&h.types == 0 {
+ continue
+ }
+ e := e
+ if foremost {
+ foremost = false
+ e.Priority = pointer.Foremost
+ }
+ e.Position = q.invTransform(h.area, e.Position)
+ events.Add(k, e)
+ }
+}
+
+func (q *pointerQueue) deliverScrollEvent(p *pointerInfo, events *handlerEvents,
+ e pointer.Event) {
+ foremost := true
+ if p.pressed && len(p.handlers) == 1 {
+ e.Priority = pointer.Grabbed
+ foremost = false
+ }
+ var sx, sy = e.Scroll.X, e.Scroll.Y
+ for _, k := range p.handlers {
+ if sx == 0 && sy == 0 {
+ return
+ }
+ h := q.handlers[k]
+ // Distribute the scroll to the handler based on its ScrollRange.
+ sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X,
+ h.scrollRange.Max.X)
+ sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y,
+ h.scrollRange.Max.Y)
+ e := e
+ if foremost {
+ foremost = false
+ e.Priority = pointer.Foremost
+ }
+ e.Position = q.invTransform(h.area, e.Position)
+ events.Add(k, e)
+ }
+}
+
+func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo,
+ events *handlerEvents, e pointer.Event) {
+ q.scratch = q.scratch[:0]
+ q.opHit(&q.scratch, e.Position)
+ if p.pressed {
+ // Filter out non-participating handlers.
+ for i := len(q.scratch) - 1; i >= 0; i-- {
+ if _, found := searchTag(p.handlers, q.scratch[i]); !found {
+ q.scratch = append(q.scratch[:i], q.scratch[i+1:]...)
+ }
+ }
+ }
+ hits := q.scratch
+ if e.Source != pointer.Mouse && !p.pressed && e.Type != pointer.Press {
+ // Consider non-mouse pointers leaving when they're released.
+ hits = nil
+ }
+ // Deliver Leave events.
+ for _, k := range p.entered {
+ if _, found := searchTag(hits, k); found {
+ continue
+ }
+ h := q.handlers[k]
+ e.Type = pointer.Leave
+
+ if e.Type&h.types != 0 {
+ e.Position = q.invTransform(h.area, e.Position)
+ events.Add(k, e)
+ }
+ }
+ // Deliver Enter events and update cursor.
+ q.cursor = pointer.CursorDefault
+ for _, k := range hits {
+ h := q.handlers[k]
+ for i := len(q.cursors) - 1; i >= 0; i-- {
+ if c := q.cursors[i]; c.area == h.area {
+ q.cursor = c.name
+ break
+ }
+ }
+ if _, found := searchTag(p.entered, k); found {
+ continue
+ }
+ e.Type = pointer.Enter
+
+ if e.Type&h.types != 0 {
+ e.Position = q.invTransform(h.area, e.Position)
+ events.Add(k, e)
+ }
+ }
+ p.entered = append(p.entered[:0], hits...)
+}
+
+func searchTag(tags []event.Tag, tag event.Tag) (int, bool) {
+ for i, t := range tags {
+ if t == tag {
+ return i, true
+ }
+ }
+ return 0, false
+}
+
+func opDecodeFloat32(d []byte) float32 {
+ return float32(int32(binary.LittleEndian.Uint32(d)))
+}
+
+func (op *areaOp) Decode(d []byte) {
+ if opconst.OpType(d[0]) != opconst.TypeArea {
+ panic("invalid op")
+ }
+ rect := f32.Rectangle{
+ Min: f32.Point{
+ X: opDecodeFloat32(d[2:]),
+ Y: opDecodeFloat32(d[6:]),
+ },
+ Max: f32.Point{
+ X: opDecodeFloat32(d[10:]),
+ Y: opDecodeFloat32(d[14:]),
+ },
+ }
+ *op = areaOp{
+ kind: areaKind(d[1]),
+ rect: rect,
+ }
+}
+
+func (op *areaOp) Hit(pos f32.Point) bool {
+ pos = pos.Sub(op.rect.Min)
+ size := op.rect.Size()
+ switch op.kind {
+ case areaRect:
+ return 0 <= pos.X && pos.X < size.X &&
+ 0 <= pos.Y && pos.Y < size.Y
+ case areaEllipse:
+ rx := size.X / 2
+ ry := size.Y / 2
+ xh := pos.X - rx
+ yk := pos.Y - ry
+ // The ellipse function works in all cases because
+ // 0/0 is not <= 1.
+ return (xh*xh)/(rx*rx)+(yk*yk)/(ry*ry) <= 1
+ default:
+ panic("invalid area kind")
+ }
+}
+
+func setScrollEvent(scroll float32, min, max int) (left, scrolled float32) {
+ if v := float32(max); scroll > v {
+ return scroll - v, v
+ }
+ if v := float32(min); scroll < v {
+ return scroll - v, v
+ }
+ return 0, scroll
+}
diff --git a/gio/io/router/pointer_test.go b/gio/io/router/pointer_test.go
new file mode 100644
index 0000000..5a28d0e
--- /dev/null
+++ b/gio/io/router/pointer_test.go
@@ -0,0 +1,787 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package router
+
+import (
+ "fmt"
+ "image"
+ "reflect"
+ "testing"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/op"
+)
+
+func TestPointerWakeup(t *testing.T) {
+ handler := new(int)
+ var ops op.Ops
+ addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100))
+
+ var r Router
+ // Test that merely adding a handler doesn't trigger redraw.
+ r.Frame(&ops)
+ if _, wake := r.WakeupTime(); wake {
+ t.Errorf("adding pointer.InputOp triggered a redraw")
+ }
+ // However, adding a handler queues a Cancel event.
+ assertEventSequence(t, r.Events(handler), pointer.Cancel)
+ // Verify that r.Events does trigger a redraw.
+ r.Frame(&ops)
+ if _, wake := r.WakeupTime(); !wake {
+ t.Errorf("pointer.Cancel event didn't trigger a redraw")
+ }
+}
+
+func TestPointerDrag(t *testing.T) {
+ handler := new(int)
+ var ops op.Ops
+ addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100))
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ // Press.
+ pointer.Event{
+ Type: pointer.Press,
+ Position: f32.Pt(50, 50),
+ },
+ // Move outside the area.
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(150, 150),
+ },
+ )
+ assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter,
+ pointer.Press, pointer.Leave, pointer.Drag)
+}
+
+func TestPointerDragNegative(t *testing.T) {
+ handler := new(int)
+ var ops op.Ops
+ addPointerHandler(&ops, handler, image.Rect(-100, -100, 0, 0))
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ // Press.
+ pointer.Event{
+ Type: pointer.Press,
+ Position: f32.Pt(-50, -50),
+ },
+ // Move outside the area.
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(-150, -150),
+ },
+ )
+ assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter,
+ pointer.Press, pointer.Leave, pointer.Drag)
+}
+
+func TestPointerGrab(t *testing.T) {
+ handler1 := new(int)
+ handler2 := new(int)
+ handler3 := new(int)
+ var ops op.Ops
+
+ types := pointer.Press | pointer.Release
+
+ pointer.InputOp{Tag: handler1, Types: types, Grab: true}.Add(&ops)
+ pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
+ pointer.InputOp{Tag: handler3, Types: types}.Add(&ops)
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Press,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Press)
+ assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Press)
+ assertEventSequence(t, r.Events(handler3), pointer.Cancel, pointer.Press)
+ r.Frame(&ops)
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Release,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Release)
+ assertEventSequence(t, r.Events(handler2), pointer.Cancel)
+ assertEventSequence(t, r.Events(handler3), pointer.Cancel)
+}
+
+func TestPointerMove(t *testing.T) {
+ handler1 := new(int)
+ handler2 := new(int)
+ var ops op.Ops
+
+ types := pointer.Move | pointer.Enter | pointer.Leave
+
+ // Handler 1 area: (0, 0) - (100, 100)
+ pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops)
+ pointer.InputOp{Tag: handler1, Types: types}.Add(&ops)
+ // Handler 2 area: (50, 50) - (100, 100) (areas intersect).
+ pointer.Rect(image.Rect(50, 50, 200, 200)).Add(&ops)
+ pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ // Hit both handlers.
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(50, 50),
+ },
+ // Hit handler 1.
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(49, 50),
+ },
+ // Hit no handlers.
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(100, 50),
+ },
+ pointer.Event{
+ Type: pointer.Cancel,
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter,
+ pointer.Move, pointer.Move, pointer.Leave, pointer.Cancel)
+ assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter,
+ pointer.Move, pointer.Leave, pointer.Cancel)
+}
+
+func TestPointerTypes(t *testing.T) {
+ handler := new(int)
+ var ops op.Ops
+ pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops)
+ pointer.InputOp{
+ Tag: handler,
+ Types: pointer.Press | pointer.Release,
+ }.Add(&ops)
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Press,
+ Position: f32.Pt(50, 50),
+ },
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(150, 150),
+ },
+ pointer.Event{
+ Type: pointer.Release,
+ Position: f32.Pt(150, 150),
+ },
+ )
+ assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Press,
+ pointer.Release)
+}
+
+func TestPointerPriority(t *testing.T) {
+ handler1 := new(int)
+ handler2 := new(int)
+ handler3 := new(int)
+ var ops op.Ops
+
+ st := op.Save(&ops)
+ pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops)
+ pointer.InputOp{
+ Tag: handler1,
+ Types: pointer.Scroll,
+ ScrollBounds: image.Rectangle{Max: image.Point{X: 100}},
+ }.Add(&ops)
+
+ pointer.Rect(image.Rect(0, 0, 100, 50)).Add(&ops)
+ pointer.InputOp{
+ Tag: handler2,
+ Types: pointer.Scroll,
+ ScrollBounds: image.Rectangle{Max: image.Point{X: 20}},
+ }.Add(&ops)
+ st.Load()
+
+ pointer.Rect(image.Rect(0, 100, 100, 200)).Add(&ops)
+ pointer.InputOp{
+ Tag: handler3,
+ Types: pointer.Scroll,
+ ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}},
+ }.Add(&ops)
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ // Hit handler 1 and 2.
+ pointer.Event{
+ Type: pointer.Scroll,
+ Position: f32.Pt(50, 25),
+ Scroll: f32.Pt(50, 0),
+ },
+ // Hit handler 1.
+ pointer.Event{
+ Type: pointer.Scroll,
+ Position: f32.Pt(50, 75),
+ Scroll: f32.Pt(50, 50),
+ },
+ // Hit handler 3.
+ pointer.Event{
+ Type: pointer.Scroll,
+ Position: f32.Pt(50, 150),
+ Scroll: f32.Pt(-30, -30),
+ },
+ // Hit no handlers.
+ pointer.Event{
+ Type: pointer.Scroll,
+ Position: f32.Pt(50, 225),
+ },
+ )
+
+ hev1 := r.Events(handler1)
+ hev2 := r.Events(handler2)
+ hev3 := r.Events(handler3)
+ assertEventSequence(t, hev1, pointer.Cancel, pointer.Scroll, pointer.Scroll)
+ assertEventSequence(t, hev2, pointer.Cancel, pointer.Scroll)
+ assertEventSequence(t, hev3, pointer.Cancel, pointer.Scroll)
+ assertEventPriorities(t, hev1, pointer.Shared, pointer.Shared,
+ pointer.Foremost)
+ assertEventPriorities(t, hev2, pointer.Shared, pointer.Foremost)
+ assertEventPriorities(t, hev3, pointer.Shared, pointer.Foremost)
+ assertScrollEvent(t, hev1[1], f32.Pt(30, 0))
+ assertScrollEvent(t, hev2[1], f32.Pt(20, 0))
+ assertScrollEvent(t, hev1[2], f32.Pt(50, 0))
+ assertScrollEvent(t, hev3[1], f32.Pt(-20, -30))
+}
+
+func TestPointerEnterLeave(t *testing.T) {
+ handler1 := new(int)
+ handler2 := new(int)
+ var ops op.Ops
+
+ // Handler 1 area: (0, 0) - (100, 100)
+ addPointerHandler(&ops, handler1, image.Rect(0, 0, 100, 100))
+
+ // Handler 2 area: (50, 50) - (200, 200) (areas overlap).
+ addPointerHandler(&ops, handler2, image.Rect(50, 50, 200, 200))
+
+ var r Router
+ r.Frame(&ops)
+ // Hit both handlers.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ // First event for a handler is always a Cancel.
+ // Only handler2 should receive the enter/move events because it is on top
+ // and handler1 is not an ancestor in the hit tree.
+ assertEventSequence(t, r.Events(handler1), pointer.Cancel)
+ assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter,
+ pointer.Move)
+
+ // Leave the second area by moving into the first.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(45, 45),
+ },
+ )
+ // The cursor leaves handler2 and enters handler1.
+ assertEventSequence(t, r.Events(handler1), pointer.Enter, pointer.Move)
+ assertEventSequence(t, r.Events(handler2), pointer.Leave)
+
+ // Move, but stay within the same hit area.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(40, 40),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Move)
+ assertEventSequence(t, r.Events(handler2))
+
+ // Move outside of both inputs.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(300, 300),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Leave)
+ assertEventSequence(t, r.Events(handler2))
+
+ // Check that a Press event generates Enter Events.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Press,
+ Position: f32.Pt(125, 125),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1))
+ assertEventSequence(t, r.Events(handler2), pointer.Enter, pointer.Press)
+
+ // Check that a drag only affects the participating handlers.
+ r.Queue(
+ // Leave
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(25, 25),
+ },
+ // Enter
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1))
+ assertEventSequence(t, r.Events(handler2), pointer.Leave, pointer.Drag,
+ pointer.Enter, pointer.Drag)
+
+ // Check that a Release event generates Enter/Leave Events.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Release,
+ Position: f32.Pt(25,
+ 25),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Enter)
+ // The second handler gets the release event because the press started inside it.
+ assertEventSequence(t, r.Events(handler2), pointer.Release, pointer.Leave)
+
+}
+
+func TestMultipleAreas(t *testing.T) {
+ handler := new(int)
+
+ var ops op.Ops
+
+ addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100))
+ st := op.Save(&ops)
+ pointer.Rect(image.Rect(50, 50, 200, 200)).Add(&ops)
+ // Second area has no Types set, yet should receive events because
+ // Types for the same handles are or-ed together.
+ pointer.InputOp{Tag: handler}.Add(&ops)
+ st.Load()
+
+ var r Router
+ r.Frame(&ops)
+ // Hit first area, then second area, then both.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(25, 25),
+ },
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(150, 150),
+ },
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ assertEventSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter,
+ pointer.Move, pointer.Move, pointer.Move)
+}
+
+func TestPointerEnterLeaveNested(t *testing.T) {
+ handler1 := new(int)
+ handler2 := new(int)
+ var ops op.Ops
+
+ types := pointer.Press | pointer.Move | pointer.Release | pointer.Enter | pointer.Leave
+
+ // Handler 1 area: (0, 0) - (100, 100)
+ pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops)
+ pointer.InputOp{Tag: handler1, Types: types}.Add(&ops)
+
+ // Handler 2 area: (25, 25) - (75, 75) (nested within first).
+ pointer.Rect(image.Rect(25, 25, 75, 75)).Add(&ops)
+ pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
+
+ var r Router
+ r.Frame(&ops)
+ // Hit both handlers.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ // First event for a handler is always a Cancel.
+ // Both handlers should receive the Enter and Move events because handler2 is a child of handler1.
+ assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter,
+ pointer.Move)
+ assertEventSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter,
+ pointer.Move)
+
+ // Leave the second area by moving into the first.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(20, 20),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Move)
+ assertEventSequence(t, r.Events(handler2), pointer.Leave)
+
+ // Move, but stay within the same hit area.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(10, 10),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Move)
+ assertEventSequence(t, r.Events(handler2))
+
+ // Move outside of both inputs.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(200, 200),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Leave)
+ assertEventSequence(t, r.Events(handler2))
+
+ // Check that a Press event generates Enter Events.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Press,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Enter, pointer.Press)
+ assertEventSequence(t, r.Events(handler2), pointer.Enter, pointer.Press)
+
+ // Check that a Release event generates Enter/Leave Events.
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Release,
+ Position: f32.Pt(20, 20),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Release)
+ assertEventSequence(t, r.Events(handler2), pointer.Release, pointer.Leave)
+}
+
+func TestPointerActiveInputDisappears(t *testing.T) {
+ handler1 := new(int)
+ var ops op.Ops
+ var r Router
+
+ // Draw handler.
+ ops.Reset()
+ addPointerHandler(&ops, handler1, image.Rect(0, 0, 100, 100))
+ r.Frame(&ops)
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(25, 25),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter,
+ pointer.Move)
+
+ // Re-render with handler missing.
+ ops.Reset()
+ r.Frame(&ops)
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(25, 25),
+ },
+ )
+ assertEventSequence(t, r.Events(handler1))
+}
+
+func TestMultitouch(t *testing.T) {
+ var ops op.Ops
+
+ // Add two separate handlers.
+ h1, h2 := new(int), new(int)
+ addPointerHandler(&ops, h1, image.Rect(0, 0, 100, 100))
+ addPointerHandler(&ops, h2, image.Rect(0, 100, 100, 200))
+
+ h1pt, h2pt := f32.Pt(0, 0), f32.Pt(0, 100)
+ var p1, p2 pointer.ID = 0, 1
+
+ var r Router
+ r.Frame(&ops)
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Press,
+ Position: h1pt,
+ PointerID: p1,
+ },
+ )
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Press,
+ Position: h2pt,
+ PointerID: p2,
+ },
+ )
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Release,
+ Position: h2pt,
+ PointerID: p2,
+ },
+ )
+ assertEventSequence(t, r.Events(h1), pointer.Cancel, pointer.Enter,
+ pointer.Press)
+ assertEventSequence(t, r.Events(h2), pointer.Cancel, pointer.Enter,
+ pointer.Press, pointer.Release)
+}
+
+func TestCursorNameOp(t *testing.T) {
+ ops := new(op.Ops)
+ var r Router
+ var h, h2 int
+ var widget2 func()
+ widget := func() {
+ // This is the area where the cursor is changed to CursorPointer.
+ pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops)
+ // The cursor is checked and changed upon cursor movement.
+ pointer.InputOp{Tag: &h}.Add(ops)
+ pointer.CursorNameOp{Name: pointer.CursorPointer}.Add(ops)
+ if widget2 != nil {
+ widget2()
+ }
+ }
+ // Register the handlers.
+ widget()
+ // No cursor change as the mouse has not moved yet.
+ if got, want := r.Cursor(), pointer.CursorDefault; got != want {
+ t.Errorf("got %q; want %q", got, want)
+ }
+
+ _at := func(x, y float32) pointer.Event {
+ return pointer.Event{
+ Type: pointer.Move,
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Position: f32.Pt(x, y),
+ }
+ }
+ for _, tc := range []struct {
+ label string
+ event interface{}
+ want pointer.CursorName
+ }{
+ {label: "move inside",
+ event: _at(50, 50),
+ want: pointer.CursorPointer,
+ },
+ {label: "move outside",
+ event: _at(200, 200),
+ want: pointer.CursorDefault,
+ },
+ {label: "move back inside",
+ event: _at(50, 50),
+ want: pointer.CursorPointer,
+ },
+ {label: "send key events while inside",
+ event: []event.Event{
+ key.Event{Name: "A", State: key.Press},
+ key.Event{Name: "A", State: key.Release},
+ },
+ want: pointer.CursorPointer,
+ },
+ {label: "send key events while outside",
+ event: []event.Event{
+ _at(200, 200),
+ key.Event{Name: "A", State: key.Press},
+ key.Event{Name: "A", State: key.Release},
+ },
+ want: pointer.CursorDefault,
+ },
+ {label: "add new input on top while inside",
+ event: func() []event.Event {
+ widget2 = func() {
+ pointer.InputOp{Tag: &h2}.Add(ops)
+ pointer.CursorNameOp{Name: pointer.CursorCrossHair}.Add(ops)
+ }
+ return []event.Event{
+ _at(50, 50),
+ key.Event{
+ Name: "A",
+ State: key.Press,
+ },
+ }
+ },
+ want: pointer.CursorCrossHair,
+ },
+ {label: "remove input on top while inside",
+ event: func() []event.Event {
+ widget2 = nil
+ return []event.Event{
+ _at(50, 50),
+ key.Event{
+ Name: "A",
+ State: key.Press,
+ },
+ }
+ },
+ want: pointer.CursorPointer,
+ },
+ } {
+ t.Run(tc.label, func(t *testing.T) {
+ ops.Reset()
+ widget()
+ r.Frame(ops)
+ switch ev := tc.event.(type) {
+ case event.Event:
+ r.Queue(ev)
+ case []event.Event:
+ r.Queue(ev...)
+ case func() event.Event:
+ r.Queue(ev())
+ case func() []event.Event:
+ r.Queue(ev()...)
+ default:
+ panic(fmt.Sprintf("unkown event %T", ev))
+ }
+ widget()
+ r.Frame(ops)
+ // The cursor should now have been changed if the mouse moved over the declared area.
+ if got, want := r.Cursor(), tc.want; got != want {
+ t.Errorf("got %q; want %q", got, want)
+ }
+ })
+ }
+}
+
+// addPointerHandler adds a pointer.InputOp for the tag in a
+// rectangular area.
+func addPointerHandler(ops *op.Ops, tag event.Tag, area image.Rectangle) {
+ defer op.Save(ops).Load()
+ pointer.Rect(area).Add(ops)
+ pointer.InputOp{
+ Tag: tag,
+ Types: pointer.Press | pointer.Release | pointer.Move | pointer.Drag | pointer.Enter | pointer.Leave,
+ }.Add(ops)
+}
+
+// pointerTypes converts a sequence of event.Event to their pointer.Types. It assumes
+// that all input events are of underlying type pointer.Event, and thus will
+// panic if some are not.
+func pointerTypes(events []event.Event) []pointer.Type {
+ var types []pointer.Type
+ for _, e := range events {
+ if e, ok := e.(pointer.Event); ok {
+ types = append(types, e.Type)
+ }
+ }
+ return types
+}
+
+// assertEventSequence checks that the provided events match the expected pointer event types
+// in the provided order.
+func assertEventSequence(t *testing.T, events []event.Event,
+ expected ...pointer.Type) {
+ t.Helper()
+ got := pointerTypes(events)
+ if !reflect.DeepEqual(got, expected) {
+ t.Errorf("expected %v events, got %v", expected, got)
+ }
+}
+
+// assertEventPriorities checks that the pointer.Event priorities of events match prios.
+func assertEventPriorities(t *testing.T, events []event.Event,
+ prios ...pointer.Priority) {
+ t.Helper()
+ var got []pointer.Priority
+ for _, e := range events {
+ if e, ok := e.(pointer.Event); ok {
+ got = append(got, e.Priority)
+ }
+ }
+ if !reflect.DeepEqual(got, prios) {
+ t.Errorf("expected priorities %v, got %v", prios, got)
+ }
+}
+
+// assertScrollEvent checks that the event scrolling amount matches the supplied value.
+func assertScrollEvent(t *testing.T, ev event.Event, scroll f32.Point) {
+ t.Helper()
+ if got, want := ev.(pointer.Event).Scroll, scroll; got != want {
+ t.Errorf("got %v; want %v", got, want)
+ }
+}
+
+func BenchmarkRouterAdd(b *testing.B) {
+ // Set this to the number of overlapping handlers that you want to
+ // evaluate performance for. Typical values for the example applications
+ // are 1-3, though checking highers values helps evaluate performance for
+ // more complex applications.
+ const startingHandlerCount = 3
+ const maxHandlerCount = 100
+ for i := startingHandlerCount; i < maxHandlerCount; i *= 3 {
+ handlerCount := i
+ b.Run(fmt.Sprintf("%d-handlers", i), func(b *testing.B) {
+ handlers := make([]event.Tag, handlerCount)
+ for i := 0; i < handlerCount; i++ {
+ h := new(int)
+ *h = i
+ handlers[i] = h
+ }
+ var ops op.Ops
+
+ for i := range handlers {
+ pointer.Rect(image.Rectangle{
+ Max: image.Point{
+ X: 100,
+ Y: 100,
+ },
+ }).Add(&ops)
+ pointer.InputOp{
+ Tag: handlers[i],
+ Types: pointer.Move,
+ }.Add(&ops)
+ }
+ var r Router
+ r.Frame(&ops)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ r.Queue(
+ pointer.Event{
+ Type: pointer.Move,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ }
+ })
+ }
+}
+
+var benchAreaOp areaOp
+
+func BenchmarkAreaOp_Decode(b *testing.B) {
+ ops := new(op.Ops)
+ pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops)
+ for i := 0; i < b.N; i++ {
+ benchAreaOp.Decode(ops.Data())
+ }
+}
+
+func BenchmarkAreaOp_Hit(b *testing.B) {
+ ops := new(op.Ops)
+ pointer.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Add(ops)
+ benchAreaOp.Decode(ops.Data())
+ for i := 0; i < b.N; i++ {
+ benchAreaOp.Hit(f32.Pt(50, 50))
+ }
+}
diff --git a/gio/io/router/router.go b/gio/io/router/router.go
new file mode 100644
index 0000000..f7e251b
--- /dev/null
+++ b/gio/io/router/router.go
@@ -0,0 +1,222 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package router implements Router, a event.Queue implementation
+that that disambiguates and routes events to handlers declared
+in operation lists.
+
+Router is used by app.Window and is otherwise only useful for
+using Gio with external window implementations.
+*/
+package router
+
+import (
+ "encoding/binary"
+ "time"
+
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/profile"
+ "realy.lol/gio/op"
+)
+
+// Router is a Queue implementation that routes events
+// to handlers declared in operation lists.
+type Router struct {
+ pqueue pointerQueue
+ kqueue keyQueue
+ cqueue clipboardQueue
+
+ handlers handlerEvents
+
+ reader ops.Reader
+
+ // InvalidateOp summary.
+ wakeup bool
+ wakeupTime time.Time
+
+ // ProfileOp summary.
+ profHandlers map[event.Tag]struct{}
+ profile profile.Event
+}
+
+type handlerEvents struct {
+ handlers map[event.Tag][]event.Event
+ hadEvents bool
+}
+
+// Events returns the available events for the handler key.
+func (q *Router) Events(k event.Tag) []event.Event {
+ events := q.handlers.Events(k)
+ if _, isprof := q.profHandlers[k]; isprof {
+ delete(q.profHandlers, k)
+ events = append(events, q.profile)
+ }
+ return events
+}
+
+// Frame replaces the declared handlers from the supplied
+// operation list. The text input state, wakeup time and whether
+// there are active profile handlers is also saved.
+func (q *Router) Frame(ops *op.Ops) {
+ q.handlers.Clear()
+ q.wakeup = false
+ for k := range q.profHandlers {
+ delete(q.profHandlers, k)
+ }
+ q.reader.Reset(ops)
+ q.collect()
+
+ q.pqueue.Frame(ops, &q.handlers)
+ q.kqueue.Frame(ops, &q.handlers)
+ if q.handlers.HadEvents() {
+ q.wakeup = true
+ q.wakeupTime = time.Time{}
+ }
+}
+
+// Queue an event and report whether at least one handler had an event queued.
+func (q *Router) Queue(events ...event.Event) bool {
+ for _, e := range events {
+ switch e := e.(type) {
+ case profile.Event:
+ q.profile = e
+ case pointer.Event:
+ q.pqueue.Push(e, &q.handlers)
+ case key.EditEvent, key.Event, key.FocusEvent:
+ q.kqueue.Push(e, &q.handlers)
+ case clipboard.Event:
+ q.cqueue.Push(e, &q.handlers)
+ }
+ }
+ return q.handlers.HadEvents()
+}
+
+// TextInputState returns the input state from the most recent
+// call to Frame.
+func (q *Router) TextInputState() TextInputState {
+ return q.kqueue.InputState()
+}
+
+// WriteClipboard returns the most recent text to be copied
+// to the clipboard, if any.
+func (q *Router) WriteClipboard() (string, bool) {
+ return q.cqueue.WriteClipboard()
+}
+
+// ReadClipboard reports if any new handler is waiting
+// to read the clipboard.
+func (q *Router) ReadClipboard() bool {
+ return q.cqueue.ReadClipboard()
+}
+
+// Cursor returns the last cursor set.
+func (q *Router) Cursor() pointer.CursorName {
+ return q.pqueue.cursor
+}
+
+func (q *Router) collect() {
+ for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
+ switch opconst.OpType(encOp.Data[0]) {
+ case opconst.TypeInvalidate:
+ op := decodeInvalidateOp(encOp.Data)
+ if !q.wakeup || op.At.Before(q.wakeupTime) {
+ q.wakeup = true
+ q.wakeupTime = op.At
+ }
+ case opconst.TypeProfile:
+ op := decodeProfileOp(encOp.Data, encOp.Refs)
+ if q.profHandlers == nil {
+ q.profHandlers = make(map[event.Tag]struct{})
+ }
+ q.profHandlers[op.Tag] = struct{}{}
+ case opconst.TypeClipboardRead:
+ q.cqueue.ProcessReadClipboard(encOp.Data, encOp.Refs)
+ case opconst.TypeClipboardWrite:
+ q.cqueue.ProcessWriteClipboard(encOp.Data, encOp.Refs)
+ }
+ }
+}
+
+// Profiling reports whether there was profile handlers in the
+// most recent Frame call.
+func (q *Router) Profiling() bool {
+ return len(q.profHandlers) > 0
+}
+
+// WakeupTime returns the most recent time for doing another frame,
+// as determined from the last call to Frame.
+func (q *Router) WakeupTime() (time.Time, bool) {
+ return q.wakeupTime, q.wakeup
+}
+
+func (h *handlerEvents) init() {
+ if h.handlers == nil {
+ h.handlers = make(map[event.Tag][]event.Event)
+ }
+}
+
+func (h *handlerEvents) AddNoRedraw(k event.Tag, e event.Event) {
+ h.init()
+ h.handlers[k] = append(h.handlers[k], e)
+}
+
+func (h *handlerEvents) Add(k event.Tag, e event.Event) {
+ h.AddNoRedraw(k, e)
+ h.hadEvents = true
+}
+
+func (h *handlerEvents) HadEvents() bool {
+ u := h.hadEvents
+ h.hadEvents = false
+ return u
+}
+
+func (h *handlerEvents) Events(k event.Tag) []event.Event {
+ if events, ok := h.handlers[k]; ok {
+ h.handlers[k] = h.handlers[k][:0]
+ // Schedule another frame if we delivered events to the user
+ // to flush half-updated state. This is important when an
+ // event changes UI state that has already been laid out. In
+ // the worst case, we waste a frame, increasing power usage.
+ //
+ // Gio is expected to grow the ability to construct
+ // frame-to-frame differences and only render to changed
+ // areas. In that case, the waste of a spurious frame should
+ // be minimal.
+ h.hadEvents = h.hadEvents || len(events) > 0
+ return events
+ }
+ return nil
+}
+
+func (h *handlerEvents) Clear() {
+ for k := range h.handlers {
+ delete(h.handlers, k)
+ }
+}
+
+func decodeProfileOp(d []byte, refs []interface{}) profile.Op {
+ if opconst.OpType(d[0]) != opconst.TypeProfile {
+ panic("invalid op")
+ }
+ return profile.Op{
+ Tag: refs[0].(event.Tag),
+ }
+}
+
+func decodeInvalidateOp(d []byte) op.InvalidateOp {
+ bo := binary.LittleEndian
+ if opconst.OpType(d[0]) != opconst.TypeInvalidate {
+ panic("invalid op")
+ }
+ var o op.InvalidateOp
+ if nanos := bo.Uint64(d[1:]); nanos > 0 {
+ o.At = time.Unix(0, int64(nanos))
+ }
+ return o
+}
diff --git a/gio/io/system/system.go b/gio/io/system/system.go
new file mode 100644
index 0000000..14e4dd7
--- /dev/null
+++ b/gio/io/system/system.go
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package system contains events usually handled at the top-level
+// program level.
+package system
+
+import (
+ "image"
+ "time"
+
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+)
+
+// A FrameEvent requests a new frame in the form of a list of
+// operations that describes what to display and how to handle
+// input.
+type FrameEvent struct {
+ // Now is the current animation. Use Now instead of time.Now to
+ // synchronize animation and to avoid the time.Now call overhead.
+ Now time.Time
+ // Metric converts device independent dp and sp to device pixels.
+ Metric unit.Metric
+ // Size is the dimensions of the window.
+ Size image.Point
+ // Insets is the insets to apply.
+ Insets Insets
+ // Frame is the callback to supply the list of
+ // operations to complete the FrameEvent.
+ //
+ // Note that the operation list and the operations themselves
+ // may not be mutated until another FrameEvent is received from
+ // the same event source.
+ // That means that calls to frame.Reset and changes to referenced
+ // data such as ImageOp backing images should happen between
+ // receiving a FrameEvent and calling Frame.
+ //
+ // Example:
+ //
+ // var w *app.Window
+ // var frame *op.Ops
+ // for e := range w.Events() {
+ // if e, ok := e.(system.FrameEvent); ok {
+ // // Call frame.Reset and manipulate images for ImageOps
+ // // here.
+ // e.Frame(frame)
+ // }
+ // }
+ Frame func(frame *op.Ops)
+ // Queue supplies the events for event handlers.
+ Queue event.Queue
+}
+
+// DestroyEvent is the last event sent through
+// a window event channel.
+type DestroyEvent struct {
+ // Err is nil for normal window closures. If a
+ // window is prematurely closed, Err is the cause.
+ Err error
+}
+
+// Insets is the space taken up by
+// system decoration such as translucent
+// system bars and software keyboards.
+type Insets struct {
+ Top, Bottom, Left, Right unit.Value
+}
+
+// A StageEvent is generated whenever the stage of a
+// Window changes.
+type StageEvent struct {
+ Stage Stage
+}
+
+// CommandEvent is a system event. Unlike most other events, CommandEvent is
+// delivered as a pointer to allow Cancel to suppress it.
+type CommandEvent struct {
+ Type CommandType
+ // Cancel suppress the default action of the command.
+ Cancel bool
+}
+
+// Stage of a Window.
+type Stage uint8
+
+// CommandType is the type of a CommandEvent.
+type CommandType uint8
+
+const (
+ // StagePaused is the Stage for inactive Windows.
+ // Inactive Windows don't receive FrameEvents.
+ StagePaused Stage = iota
+ // StateRunning is for active Windows.
+ StageRunning
+)
+
+const (
+ // CommandBack is the command for a back action
+ // such as the Android back button.
+ CommandBack CommandType = iota
+)
+
+func (l Stage) String() string {
+ switch l {
+ case StagePaused:
+ return "StagePaused"
+ case StageRunning:
+ return "StageRunning"
+ default:
+ panic("unexpected Stage value")
+ }
+}
+
+func (FrameEvent) ImplementsEvent() {}
+func (StageEvent) ImplementsEvent() {}
+func (*CommandEvent) ImplementsEvent() {}
+func (DestroyEvent) ImplementsEvent() {}
diff --git a/gio/layout/alloc_test.go b/gio/layout/alloc_test.go
new file mode 100644
index 0000000..1df19e9
--- /dev/null
+++ b/gio/layout/alloc_test.go
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build !race
+// +build !race
+
+package layout
+
+import (
+ "image"
+ "testing"
+
+ "realy.lol/gio/op"
+)
+
+func TestStackAllocs(t *testing.T) {
+ var ops op.Ops
+ allocs := testing.AllocsPerRun(1, func() {
+ ops.Reset()
+ gtx := Context{
+ Ops: &ops,
+ }
+ Stack{}.Layout(gtx,
+ Stacked(func(gtx Context) Dimensions {
+ return Dimensions{Size: image.Point{X: 50, Y: 50}}
+ }),
+ )
+ })
+ if allocs != 0 {
+ t.Errorf("expected no allocs, got %f", allocs)
+ }
+}
+
+func TestFlexAllocs(t *testing.T) {
+ var ops op.Ops
+ allocs := testing.AllocsPerRun(1, func() {
+ ops.Reset()
+ gtx := Context{
+ Ops: &ops,
+ }
+ Flex{}.Layout(gtx,
+ Rigid(func(gtx Context) Dimensions {
+ return Dimensions{Size: image.Point{X: 50, Y: 50}}
+ }),
+ )
+ })
+ if allocs != 0 {
+ t.Errorf("expected no allocs, got %f", allocs)
+ }
+}
diff --git a/gio/layout/context.go b/gio/layout/context.go
new file mode 100644
index 0000000..4f8d2c8
--- /dev/null
+++ b/gio/layout/context.go
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "time"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/system"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+)
+
+// Context carries the state needed by almost all layouts and widgets.
+// A zero value Context never returns events, map units to pixels
+// with a scale of 1.0, and returns the zero time from Now.
+type Context struct {
+ // Constraints track the constraints for the active widget or
+ // layout.
+ Constraints Constraints
+
+ Metric unit.Metric
+ // By convention, a nil Queue is a signal to widgets to draw themselves
+ // in a disabled state.
+ Queue event.Queue
+ // Now is the animation time.
+ Now time.Time
+
+ *op.Ops
+}
+
+// NewContext is a shorthand for
+//
+// Context{
+// Ops: ops,
+// Now: e.Now,
+// Queue: e.Queue,
+// Config: e.Config,
+// Constraints: Exact(e.Size),
+// }
+//
+// NewContext calls ops.Reset and adjusts ops for e.Insets.
+func NewContext(ops *op.Ops, e system.FrameEvent) Context {
+ ops.Reset()
+
+ size := e.Size
+
+ if e.Insets != (system.Insets{}) {
+ left := e.Metric.Px(e.Insets.Left)
+ top := e.Metric.Px(e.Insets.Top)
+ op.Offset(f32.Point{
+ X: float32(left),
+ Y: float32(top),
+ }).Add(ops)
+
+ size.X -= left + e.Metric.Px(e.Insets.Right)
+ size.Y -= top + e.Metric.Px(e.Insets.Bottom)
+ }
+
+ return Context{
+ Ops: ops,
+ Now: e.Now,
+ Queue: e.Queue,
+ Metric: e.Metric,
+ Constraints: Exact(size),
+ }
+}
+
+// Px maps the value to pixels.
+func (c Context) Px(v unit.Value) int {
+ return c.Metric.Px(v)
+}
+
+// Events returns the events available for the key. If no
+// queue is configured, Events returns nil.
+func (c Context) Events(k event.Tag) []event.Event {
+ if c.Queue == nil {
+ return nil
+ }
+ return c.Queue.Events(k)
+}
+
+// Disabled returns a copy of this context with a nil Queue,
+// blocking events to widgets using it.
+//
+// By convention, a nil Queue is a signal to widgets to draw themselves
+// in a disabled state.
+func (c Context) Disabled() Context {
+ c.Queue = nil
+ return c
+}
diff --git a/gio/layout/doc.go b/gio/layout/doc.go
new file mode 100644
index 0000000..3824084
--- /dev/null
+++ b/gio/layout/doc.go
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package layout implements layouts common to GUI programs.
+
+Constraints and dimensions
+
+Constraints and dimensions form the interface between layouts and
+interface child elements. This package operates on Widgets, functions
+that compute Dimensions from a a set of constraints for acceptable
+widths and heights. Both the constraints and dimensions are maintained
+in an implicit Context to keep the Widget declaration short.
+
+For example, to add space above a widget:
+
+ var gtx layout.Context
+
+ // Configure a top inset.
+ inset := layout.Inset{Top: unit.Dp(8), ...}
+ // Use the inset to lay out a widget.
+ inset.Layout(gtx, func() {
+ // Lay out widget and determine its size given the constraints
+ // in gtx.Constraints.
+ ...
+ return layout.Dimensions{...}
+ })
+
+Note that the example does not generate any garbage even though the
+Inset is transient. Layouts that don't accept user input are designed
+to not escape to the heap during their use.
+
+Layout operations are recursive: a child in a layout operation can
+itself be another layout. That way, complex user interfaces can
+be created from a few generic layouts.
+
+This example both aligns and insets a child:
+
+ inset := layout.Inset{...}
+ inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ align := layout.Alignment(...)
+ return align.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return widget.Layout(gtx, ...)
+ })
+ })
+
+More complex layouts such as Stack and Flex lay out multiple children,
+and stateful layouts such as List accept user input.
+
+*/
+package layout
diff --git a/gio/layout/example_test.go b/gio/layout/example_test.go
new file mode 100644
index 0000000..9636c8d
--- /dev/null
+++ b/gio/layout/example_test.go
@@ -0,0 +1,137 @@
+package layout_test
+
+import (
+ "fmt"
+ "image"
+
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+)
+
+func ExampleInset() {
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ // Loose constraints with no minimal size.
+ Constraints: layout.Constraints{
+ Max: image.Point{X: 100, Y: 100},
+ },
+ }
+
+ // Inset all edges by 10.
+ inset := layout.UniformInset(unit.Dp(10))
+ dims := inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ // Lay out a 50x50 sized widget.
+ dims := layoutWidget(gtx, 50, 50)
+ fmt.Println(dims.Size)
+ return dims
+ })
+
+ fmt.Println(dims.Size)
+
+ // Output:
+ // (50,50)
+ // (70,70)
+}
+
+func ExampleDirection() {
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ // Rigid constraints with both minimum and maximum set.
+ Constraints: layout.Exact(image.Point{X: 100, Y: 100}),
+ }
+
+ dims := layout.Center.Layout(gtx,
+ func(gtx layout.Context) layout.Dimensions {
+ // Lay out a 50x50 sized widget.
+ dims := layoutWidget(gtx, 50, 50)
+ fmt.Println(dims.Size)
+ return dims
+ })
+
+ fmt.Println(dims.Size)
+
+ // Output:
+ // (50,50)
+ // (100,100)
+}
+
+func ExampleFlex() {
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ // Rigid constraints with both minimum and maximum set.
+ Constraints: layout.Exact(image.Point{X: 100, Y: 100}),
+ }
+
+ layout.Flex{WeightSum: 2}.Layout(gtx,
+ // Rigid 10x10 widget.
+ layout.Rigid(func(gtx layout.Context) layout.Dimensions {
+ fmt.Printf("Rigid: %v\n", gtx.Constraints)
+ return layoutWidget(gtx, 10, 10)
+ }),
+ // Child with 50% space allowance.
+ layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
+ fmt.Printf("50%%: %v\n", gtx.Constraints)
+ return layoutWidget(gtx, 10, 10)
+ }),
+ )
+
+ // Output:
+ // Rigid: {(0,100) (100,100)}
+ // 50%: {(45,100) (45,100)}
+}
+
+func ExampleStack() {
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Constraints{
+ Max: image.Point{X: 100, Y: 100},
+ },
+ }
+
+ layout.Stack{}.Layout(gtx,
+ // Force widget to the same size as the second.
+ layout.Expanded(func(gtx layout.Context) layout.Dimensions {
+ fmt.Printf("Expand: %v\n", gtx.Constraints)
+ return layoutWidget(gtx, 10, 10)
+ }),
+ // Rigid 50x50 widget.
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ return layoutWidget(gtx, 50, 50)
+ }),
+ )
+
+ // Output:
+ // Expand: {(50,50) (100,100)}
+}
+
+func ExampleList() {
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ // Rigid constraints with both minimum and maximum set.
+ Constraints: layout.Exact(image.Point{X: 100, Y: 100}),
+ }
+
+ // The list is 1e6 elements, but only 5 fit the constraints.
+ const listLen = 1e6
+
+ var list layout.List
+ list.Layout(gtx, listLen,
+ func(gtx layout.Context, i int) layout.Dimensions {
+ return layoutWidget(gtx, 20, 20)
+ })
+
+ fmt.Println(list.Position.Count)
+
+ // Output:
+ // 5
+}
+
+func layoutWidget(ctx layout.Context, width, height int) layout.Dimensions {
+ return layout.Dimensions{
+ Size: image.Point{
+ X: width,
+ Y: height,
+ },
+ }
+}
diff --git a/gio/layout/flex.go b/gio/layout/flex.go
new file mode 100644
index 0000000..50d936d
--- /dev/null
+++ b/gio/layout/flex.go
@@ -0,0 +1,241 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "image"
+
+ "realy.lol/gio/op"
+)
+
+// Flex lays out child elements along an axis,
+// according to alignment and weights.
+type Flex struct {
+ // Axis is the main axis, either Horizontal or Vertical.
+ Axis Axis
+ // Spacing controls the distribution of space left after
+ // layout.
+ Spacing Spacing
+ // Alignment is the alignment in the cross axis.
+ Alignment Alignment
+ // WeightSum is the sum of weights used for the weighted
+ // size of Flexed children. If WeightSum is zero, the sum
+ // of all Flexed weights is used.
+ WeightSum float32
+}
+
+// FlexChild is the descriptor for a Flex child.
+type FlexChild struct {
+ flex bool
+ weight float32
+
+ widget Widget
+
+ // Scratch space.
+ call op.CallOp
+ dims Dimensions
+}
+
+// Spacing determine the spacing mode for a Flex.
+type Spacing uint8
+
+const (
+ // SpaceEnd leaves space at the end.
+ SpaceEnd Spacing = iota
+ // SpaceStart leaves space at the start.
+ SpaceStart
+ // SpaceSides shares space between the start and end.
+ SpaceSides
+ // SpaceAround distributes space evenly between children,
+ // with half as much space at the start and end.
+ SpaceAround
+ // SpaceBetween distributes space evenly between children,
+ // leaving no space at the start and end.
+ SpaceBetween
+ // SpaceEvenly distributes space evenly between children and
+ // at the start and end.
+ SpaceEvenly
+)
+
+// Rigid returns a Flex child with a maximal constraint of the
+// remaining space.
+func Rigid(widget Widget) FlexChild {
+ return FlexChild{
+ widget: widget,
+ }
+}
+
+// Flexed returns a Flex child forced to take up weight fraction of the
+// space left over from Rigid children. The fraction is weight
+// divided by either the weight sum of all Flexed children or the Flex
+// WeightSum if non zero.
+func Flexed(weight float32, widget Widget) FlexChild {
+ return FlexChild{
+ flex: true,
+ weight: weight,
+ widget: widget,
+ }
+}
+
+// Layout a list of children. The position of the children are
+// determined by the specified order, but Rigid children are laid out
+// before Flexed children.
+func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions {
+ size := 0
+ cs := gtx.Constraints
+ mainMin, mainMax := f.Axis.mainConstraint(cs)
+ crossMin, crossMax := f.Axis.crossConstraint(cs)
+ remaining := mainMax
+ var totalWeight float32
+ cgtx := gtx
+ // Lay out Rigid children.
+ for i, child := range children {
+ if child.flex {
+ totalWeight += child.weight
+ continue
+ }
+ macro := op.Record(gtx.Ops)
+ cgtx.Constraints = f.Axis.constraints(0, remaining, crossMin, crossMax)
+ dims := child.widget(cgtx)
+ c := macro.Stop()
+ sz := f.Axis.Convert(dims.Size).X
+ size += sz
+ remaining -= sz
+ if remaining < 0 {
+ remaining = 0
+ }
+ children[i].call = c
+ children[i].dims = dims
+ }
+ if w := f.WeightSum; w != 0 {
+ totalWeight = w
+ }
+ // fraction is the rounding error from a Flex weighting.
+ var fraction float32
+ flexTotal := remaining
+ // Lay out Flexed children.
+ for i, child := range children {
+ if !child.flex {
+ continue
+ }
+ var flexSize int
+ if remaining > 0 && totalWeight > 0 {
+ // Apply weight and add any leftover fraction from a
+ // previous Flexed.
+ childSize := float32(flexTotal) * child.weight / totalWeight
+ flexSize = int(childSize + fraction + .5)
+ fraction = childSize - float32(flexSize)
+ if flexSize > remaining {
+ flexSize = remaining
+ }
+ }
+ macro := op.Record(gtx.Ops)
+ cgtx.Constraints = f.Axis.constraints(flexSize, flexSize, crossMin,
+ crossMax)
+ dims := child.widget(cgtx)
+ c := macro.Stop()
+ sz := f.Axis.Convert(dims.Size).X
+ size += sz
+ remaining -= sz
+ if remaining < 0 {
+ remaining = 0
+ }
+ children[i].call = c
+ children[i].dims = dims
+ }
+ var maxCross int
+ var maxBaseline int
+ for _, child := range children {
+ if c := f.Axis.Convert(child.dims.Size).Y; c > maxCross {
+ maxCross = c
+ }
+ if b := child.dims.Size.Y - child.dims.Baseline; b > maxBaseline {
+ maxBaseline = b
+ }
+ }
+ var space int
+ if mainMin > size {
+ space = mainMin - size
+ }
+ var mainSize int
+ switch f.Spacing {
+ case SpaceSides:
+ mainSize += space / 2
+ case SpaceStart:
+ mainSize += space
+ case SpaceEvenly:
+ mainSize += space / (1 + len(children))
+ case SpaceAround:
+ if len(children) > 0 {
+ mainSize += space / (len(children) * 2)
+ }
+ }
+ for i, child := range children {
+ dims := child.dims
+ b := dims.Size.Y - dims.Baseline
+ var cross int
+ switch f.Alignment {
+ case End:
+ cross = maxCross - f.Axis.Convert(dims.Size).Y
+ case Middle:
+ cross = (maxCross - f.Axis.Convert(dims.Size).Y) / 2
+ case Baseline:
+ if f.Axis == Horizontal {
+ cross = maxBaseline - b
+ }
+ }
+ stack := op.Save(gtx.Ops)
+ pt := f.Axis.Convert(image.Pt(mainSize, cross))
+ op.Offset(FPt(pt)).Add(gtx.Ops)
+ child.call.Add(gtx.Ops)
+ stack.Load()
+ mainSize += f.Axis.Convert(dims.Size).X
+ if i < len(children)-1 {
+ switch f.Spacing {
+ case SpaceEvenly:
+ mainSize += space / (1 + len(children))
+ case SpaceAround:
+ if len(children) > 0 {
+ mainSize += space / len(children)
+ }
+ case SpaceBetween:
+ if len(children) > 1 {
+ mainSize += space / (len(children) - 1)
+ }
+ }
+ }
+ }
+ switch f.Spacing {
+ case SpaceSides:
+ mainSize += space / 2
+ case SpaceEnd:
+ mainSize += space
+ case SpaceEvenly:
+ mainSize += space / (1 + len(children))
+ case SpaceAround:
+ if len(children) > 0 {
+ mainSize += space / (len(children) * 2)
+ }
+ }
+ sz := f.Axis.Convert(image.Pt(mainSize, maxCross))
+ return Dimensions{Size: sz, Baseline: sz.Y - maxBaseline}
+}
+
+func (s Spacing) String() string {
+ switch s {
+ case SpaceEnd:
+ return "SpaceEnd"
+ case SpaceStart:
+ return "SpaceStart"
+ case SpaceSides:
+ return "SpaceSides"
+ case SpaceAround:
+ return "SpaceAround"
+ case SpaceBetween:
+ return "SpaceAround"
+ case SpaceEvenly:
+ return "SpaceEvenly"
+ default:
+ panic("unreachable")
+ }
+}
diff --git a/gio/layout/layout.go b/gio/layout/layout.go
new file mode 100644
index 0000000..6a4bdd2
--- /dev/null
+++ b/gio/layout/layout.go
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "image"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+)
+
+// Constraints represent the minimum and maximum size of a widget.
+//
+// A widget does not have to treat its constraints as "hard". For
+// example, if it's passed a constraint with a minimum size that's
+// smaller than its actual minimum size, it should return its minimum
+// size dimensions instead. Parent widgets should deal appropriately
+// with child widgets that return dimensions that do not fit their
+// constraints (for example, by clipping).
+type Constraints struct {
+ Min, Max image.Point
+}
+
+// Dimensions are the resolved size and baseline for a widget.
+//
+// Baseline is the distance from the bottom of a widget to the baseline of
+// any text it contains (or 0). The purpose is to be able to align text
+// that span multiple widgets.
+type Dimensions struct {
+ Size image.Point
+ Baseline int
+}
+
+// Axis is the Horizontal or Vertical direction.
+type Axis uint8
+
+// Alignment is the mutual alignment of a list of widgets.
+type Alignment uint8
+
+// Direction is the alignment of widgets relative to a containing
+// space.
+type Direction uint8
+
+// Widget is a function scope for drawing, processing events and
+// computing dimensions for a user interface element.
+type Widget func(gtx Context) Dimensions
+
+const (
+ Start Alignment = iota
+ End
+ Middle
+ Baseline
+)
+
+const (
+ NW Direction = iota
+ N
+ NE
+ E
+ SE
+ S
+ SW
+ W
+ Center
+)
+
+const (
+ Horizontal Axis = iota
+ Vertical
+)
+
+// Exact returns the Constraints with the minimum and maximum size
+// set to size.
+func Exact(size image.Point) Constraints {
+ return Constraints{
+ Min: size, Max: size,
+ }
+}
+
+// FPt converts an point to a f32.Point.
+func FPt(p image.Point) f32.Point {
+ return f32.Point{
+ X: float32(p.X), Y: float32(p.Y),
+ }
+}
+
+// FRect converts a rectangle to a f32.Rectangle.
+func FRect(r image.Rectangle) f32.Rectangle {
+ return f32.Rectangle{
+ Min: FPt(r.Min), Max: FPt(r.Max),
+ }
+}
+
+// Constrain a size so each dimension is in the range [min;max].
+func (c Constraints) Constrain(size image.Point) image.Point {
+ if min := c.Min.X; size.X < min {
+ size.X = min
+ }
+ if min := c.Min.Y; size.Y < min {
+ size.Y = min
+ }
+ if max := c.Max.X; size.X > max {
+ size.X = max
+ }
+ if max := c.Max.Y; size.Y > max {
+ size.Y = max
+ }
+ return size
+}
+
+// Inset adds space around a widget by decreasing its maximum
+// constraints. The minimum constraints will be adjusted to ensure
+// they do not exceed the maximum.
+type Inset struct {
+ Top, Right, Bottom, Left unit.Value
+}
+
+// Layout a widget.
+func (in Inset) Layout(gtx Context, w Widget) Dimensions {
+ top := gtx.Px(in.Top)
+ right := gtx.Px(in.Right)
+ bottom := gtx.Px(in.Bottom)
+ left := gtx.Px(in.Left)
+ mcs := gtx.Constraints
+ mcs.Max.X -= left + right
+ if mcs.Max.X < 0 {
+ left = 0
+ right = 0
+ mcs.Max.X = 0
+ }
+ if mcs.Min.X > mcs.Max.X {
+ mcs.Min.X = mcs.Max.X
+ }
+ mcs.Max.Y -= top + bottom
+ if mcs.Max.Y < 0 {
+ bottom = 0
+ top = 0
+ mcs.Max.Y = 0
+ }
+ if mcs.Min.Y > mcs.Max.Y {
+ mcs.Min.Y = mcs.Max.Y
+ }
+ stack := op.Save(gtx.Ops)
+ op.Offset(FPt(image.Point{X: left, Y: top})).Add(gtx.Ops)
+ gtx.Constraints = mcs
+ dims := w(gtx)
+ stack.Load()
+ return Dimensions{
+ Size: dims.Size.Add(image.Point{X: right + left, Y: top + bottom}),
+ Baseline: dims.Baseline + bottom,
+ }
+}
+
+// UniformInset returns an Inset with a single inset applied to all
+// edges.
+func UniformInset(v unit.Value) Inset {
+ return Inset{Top: v, Right: v, Bottom: v, Left: v}
+}
+
+// Layout a widget according to the direction.
+// The widget is called with the context constraints minimum cleared.
+func (d Direction) Layout(gtx Context, w Widget) Dimensions {
+ macro := op.Record(gtx.Ops)
+ cs := gtx.Constraints
+ gtx.Constraints.Min = image.Point{}
+ dims := w(gtx)
+ call := macro.Stop()
+ sz := dims.Size
+ if sz.X < cs.Min.X {
+ sz.X = cs.Min.X
+ }
+ if sz.Y < cs.Min.Y {
+ sz.Y = cs.Min.Y
+ }
+
+ defer op.Save(gtx.Ops).Load()
+ p := d.Position(dims.Size, sz)
+ op.Offset(FPt(p)).Add(gtx.Ops)
+ call.Add(gtx.Ops)
+
+ return Dimensions{
+ Size: sz,
+ Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y,
+ }
+}
+
+// Position calculates widget position according to the direction.
+func (d Direction) Position(widget, bounds image.Point) image.Point {
+ var p image.Point
+
+ switch d {
+ case N, S, Center:
+ p.X = (bounds.X - widget.X) / 2
+ case NE, SE, E:
+ p.X = bounds.X - widget.X
+ }
+
+ switch d {
+ case W, Center, E:
+ p.Y = (bounds.Y - widget.Y) / 2
+ case SW, S, SE:
+ p.Y = bounds.Y - widget.Y
+ }
+
+ return p
+}
+
+// Spacer adds space between widgets.
+type Spacer struct {
+ Width, Height unit.Value
+}
+
+func (s Spacer) Layout(gtx Context) Dimensions {
+ return Dimensions{
+ Size: image.Point{
+ X: gtx.Px(s.Width),
+ Y: gtx.Px(s.Height),
+ },
+ }
+}
+
+func (a Alignment) String() string {
+ switch a {
+ case Start:
+ return "Start"
+ case End:
+ return "End"
+ case Middle:
+ return "Middle"
+ case Baseline:
+ return "Baseline"
+ default:
+ panic("unreachable")
+ }
+}
+
+// Convert a point in (x, y) coordinates to (main, cross) coordinates,
+// or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged
+// for the horizontal axis, or (y, x) for the vertical axis.
+func (a Axis) Convert(pt image.Point) image.Point {
+ if a == Horizontal {
+ return pt
+ }
+ return image.Pt(pt.Y, pt.X)
+}
+
+// mainConstraint returns the min and max main constraints for axis a.
+func (a Axis) mainConstraint(cs Constraints) (int, int) {
+ if a == Horizontal {
+ return cs.Min.X, cs.Max.X
+ }
+ return cs.Min.Y, cs.Max.Y
+}
+
+// crossConstraint returns the min and max cross constraints for axis a.
+func (a Axis) crossConstraint(cs Constraints) (int, int) {
+ if a == Horizontal {
+ return cs.Min.Y, cs.Max.Y
+ }
+ return cs.Min.X, cs.Max.X
+}
+
+// constraints returns the constraints for axis a.
+func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints {
+ if a == Horizontal {
+ return Constraints{Min: image.Pt(mainMin, crossMin),
+ Max: image.Pt(mainMax, crossMax)}
+ }
+ return Constraints{Min: image.Pt(crossMin, mainMin),
+ Max: image.Pt(crossMax, mainMax)}
+}
+
+func (a Axis) String() string {
+ switch a {
+ case Horizontal:
+ return "Horizontal"
+ case Vertical:
+ return "Vertical"
+ default:
+ panic("unreachable")
+ }
+}
+
+func (d Direction) String() string {
+ switch d {
+ case NW:
+ return "NW"
+ case N:
+ return "N"
+ case NE:
+ return "NE"
+ case E:
+ return "E"
+ case SE:
+ return "SE"
+ case S:
+ return "S"
+ case SW:
+ return "SW"
+ case W:
+ return "W"
+ case Center:
+ return "Center"
+ default:
+ panic("unreachable")
+ }
+}
diff --git a/gio/layout/layout_test.go b/gio/layout/layout_test.go
new file mode 100644
index 0000000..b04863c
--- /dev/null
+++ b/gio/layout/layout_test.go
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "image"
+ "testing"
+
+ "realy.lol/gio/op"
+)
+
+func TestStack(t *testing.T) {
+ gtx := Context{
+ Ops: new(op.Ops),
+ Constraints: Constraints{
+ Max: image.Pt(100, 100),
+ },
+ }
+ exp := image.Point{X: 60, Y: 70}
+ dims := Stack{Alignment: Center}.Layout(gtx,
+ Expanded(func(gtx Context) Dimensions {
+ return Dimensions{Size: exp}
+ }),
+ Stacked(func(gtx Context) Dimensions {
+ return Dimensions{Size: image.Point{X: 50, Y: 50}}
+ }),
+ )
+ if got := dims.Size; got != exp {
+ t.Errorf("Stack ignored Expanded size, got %v expected %v", got, exp)
+ }
+}
diff --git a/gio/layout/list.go b/gio/layout/list.go
new file mode 100644
index 0000000..45c884e
--- /dev/null
+++ b/gio/layout/list.go
@@ -0,0 +1,309 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "image"
+
+ "realy.lol/gio/gesture"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+)
+
+type scrollChild struct {
+ size image.Point
+ call op.CallOp
+}
+
+// List displays a subsection of a potentially infinitely
+// large underlying list. List accepts user input to scroll
+// the subsection.
+type List struct {
+ Axis Axis
+ // ScrollToEnd instructs the list to stay scrolled to the far end position
+ // once reached. A List with ScrollToEnd == true and Position.BeforeEnd ==
+ // false draws its content with the last item at the bottom of the list
+ // area.
+ ScrollToEnd bool
+ // Alignment is the cross axis alignment of list elements.
+ Alignment Alignment
+
+ cs Constraints
+ scroll gesture.Scroll
+ scrollDelta int
+
+ // Position is updated during Layout. To save the list scroll position,
+ // just save Position after Layout finishes. To scroll the list
+ // programmatically, update Position (e.g. restore it from a saved value)
+ // before calling Layout.
+ Position Position
+
+ len int
+
+ // maxSize is the total size of visible children.
+ maxSize int
+ children []scrollChild
+ dir iterationDir
+}
+
+// ListElement is a function that computes the dimensions of
+// a list element.
+type ListElement func(gtx Context, index int) Dimensions
+
+type iterationDir uint8
+
+// Position is a List scroll offset represented as an offset from the top edge
+// of a child element.
+type Position struct {
+ // BeforeEnd tracks whether the List position is before the very end. We
+ // use "before end" instead of "at end" so that the zero value of a
+ // Position struct is useful.
+ //
+ // When laying out a list, if ScrollToEnd is true and BeforeEnd is false,
+ // then First and Offset are ignored, and the list is drawn with the last
+ // item at the bottom. If ScrollToEnd is false then BeforeEnd is ignored.
+ BeforeEnd bool
+ // First is the index of the first visible child.
+ First int
+ // Offset is the distance in pixels from the top edge to the child at index
+ // First.
+ Offset int
+ // OffsetLast is the signed distance in pixels from the bottom edge to the
+ // bottom edge of the child at index First+Count.
+ OffsetLast int
+ // Count is the number of visible children.
+ Count int
+}
+
+const (
+ iterateNone iterationDir = iota
+ iterateForward
+ iterateBackward
+)
+
+const inf = 1e6
+
+// init prepares the list for iterating through its children with next.
+func (l *List) init(gtx Context, len int) {
+ if l.more() {
+ panic("unfinished child")
+ }
+ l.cs = gtx.Constraints
+ l.maxSize = 0
+ l.children = l.children[:0]
+ l.len = len
+ l.update(gtx)
+ if l.scrollToEnd() || l.Position.First > len {
+ l.Position.Offset = 0
+ l.Position.First = len
+ }
+}
+
+// Layout the List.
+func (l *List) Layout(gtx Context, len int, w ListElement) Dimensions {
+ l.init(gtx, len)
+ crossMin, crossMax := l.Axis.crossConstraint(gtx.Constraints)
+ gtx.Constraints = l.Axis.constraints(0, inf, crossMin, crossMax)
+ macro := op.Record(gtx.Ops)
+ for l.next(); l.more(); l.next() {
+ child := op.Record(gtx.Ops)
+ dims := w(gtx, l.index())
+ call := child.Stop()
+ l.end(dims, call)
+ }
+ return l.layout(gtx.Ops, macro)
+}
+
+func (l *List) scrollToEnd() bool {
+ return l.ScrollToEnd && !l.Position.BeforeEnd
+}
+
+// Dragging reports whether the List is being dragged.
+func (l *List) Dragging() bool {
+ return l.scroll.State() == gesture.StateDragging
+}
+
+func (l *List) update(gtx Context) {
+ d := l.scroll.Scroll(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis))
+ l.scrollDelta = d
+ l.Position.Offset += d
+}
+
+// next advances to the next child.
+func (l *List) next() {
+ l.dir = l.nextDir()
+ // The user scroll offset is applied after scrolling to
+ // list end.
+ if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 {
+ l.Position.BeforeEnd = true
+ l.Position.Offset += l.scrollDelta
+ l.dir = l.nextDir()
+ }
+}
+
+// index is current child's position in the underlying list.
+func (l *List) index() int {
+ switch l.dir {
+ case iterateBackward:
+ return l.Position.First - 1
+ case iterateForward:
+ return l.Position.First + len(l.children)
+ default:
+ panic("Index called before Next")
+ }
+}
+
+// more reports whether more children are needed.
+func (l *List) more() bool {
+ return l.dir != iterateNone
+}
+
+func (l *List) nextDir() iterationDir {
+ _, vsize := l.Axis.mainConstraint(l.cs)
+ last := l.Position.First + len(l.children)
+ // Clamp offset.
+ if l.maxSize-l.Position.Offset < vsize && last == l.len {
+ l.Position.Offset = l.maxSize - vsize
+ }
+ if l.Position.Offset < 0 && l.Position.First == 0 {
+ l.Position.Offset = 0
+ }
+ switch {
+ case len(l.children) == l.len:
+ return iterateNone
+ case l.maxSize-l.Position.Offset < vsize:
+ return iterateForward
+ case l.Position.Offset < 0:
+ return iterateBackward
+ }
+ return iterateNone
+}
+
+// End the current child by specifying its dimensions.
+func (l *List) end(dims Dimensions, call op.CallOp) {
+ child := scrollChild{dims.Size, call}
+ mainSize := l.Axis.Convert(child.size).X
+ l.maxSize += mainSize
+ switch l.dir {
+ case iterateForward:
+ l.children = append(l.children, child)
+ case iterateBackward:
+ l.children = append(l.children, scrollChild{})
+ copy(l.children[1:], l.children)
+ l.children[0] = child
+ l.Position.First--
+ l.Position.Offset += mainSize
+ default:
+ panic("call Next before End")
+ }
+ l.dir = iterateNone
+}
+
+// Layout the List and return its dimensions.
+func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
+ if l.more() {
+ panic("unfinished child")
+ }
+ mainMin, mainMax := l.Axis.mainConstraint(l.cs)
+ children := l.children
+ // Skip invisible children
+ for len(children) > 0 {
+ sz := children[0].size
+ mainSize := l.Axis.Convert(sz).X
+ if l.Position.Offset < mainSize {
+ // First child is partially visible.
+ break
+ }
+ l.Position.First++
+ l.Position.Offset -= mainSize
+ children = children[1:]
+ }
+ size := -l.Position.Offset
+ var maxCross int
+ for i, child := range children {
+ sz := l.Axis.Convert(child.size)
+ if c := sz.Y; c > maxCross {
+ maxCross = c
+ }
+ size += sz.X
+ if size >= mainMax {
+ children = children[:i+1]
+ break
+ }
+ }
+ l.Position.Count = len(children)
+ l.Position.OffsetLast = mainMax - size
+ pos := -l.Position.Offset
+ // ScrollToEnd lists are end aligned.
+ if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 {
+ pos += space
+ }
+ for _, child := range children {
+ sz := l.Axis.Convert(child.size)
+ var cross int
+ switch l.Alignment {
+ case End:
+ cross = maxCross - sz.Y
+ case Middle:
+ cross = (maxCross - sz.Y) / 2
+ }
+ childSize := sz.X
+ max := childSize + pos
+ if max > mainMax {
+ max = mainMax
+ }
+ min := pos
+ if min < 0 {
+ min = 0
+ }
+ r := image.Rectangle{
+ Min: l.Axis.Convert(image.Pt(min, -inf)),
+ Max: l.Axis.Convert(image.Pt(max, inf)),
+ }
+ stack := op.Save(ops)
+ clip.Rect(r).Add(ops)
+ pt := l.Axis.Convert(image.Pt(pos, cross))
+ op.Offset(FPt(pt)).Add(ops)
+ child.call.Add(ops)
+ stack.Load()
+ pos += childSize
+ }
+ atStart := l.Position.First == 0 && l.Position.Offset <= 0
+ atEnd := l.Position.First+len(children) == l.len && mainMax >= pos
+ if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 {
+ l.scroll.Stop()
+ }
+ l.Position.BeforeEnd = !atEnd
+ if pos < mainMin {
+ pos = mainMin
+ }
+ if pos > mainMax {
+ pos = mainMax
+ }
+ dims := l.Axis.Convert(image.Pt(pos, maxCross))
+ call := macro.Stop()
+ defer op.Save(ops).Load()
+ pointer.Rect(image.Rectangle{Max: dims}).Add(ops)
+
+ var min, max int
+ if o := l.Position.Offset; o > 0 {
+ // Use the size of the invisible part as scroll boundary.
+ min = -o
+ } else if l.Position.First > 0 {
+ min = -inf
+ }
+ if o := l.Position.OffsetLast; o < 0 {
+ max = -o
+ } else if l.Position.First+l.Position.Count < l.len {
+ max = inf
+ }
+ scrollRange := image.Rectangle{
+ Min: l.Axis.Convert(image.Pt(min, 0)),
+ Max: l.Axis.Convert(image.Pt(max, 0)),
+ }
+ l.scroll.Add(ops, scrollRange)
+
+ call.Add(ops)
+ return Dimensions{Size: dims}
+}
diff --git a/gio/layout/list_test.go b/gio/layout/list_test.go
new file mode 100644
index 0000000..6a026b3
--- /dev/null
+++ b/gio/layout/list_test.go
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "image"
+ "testing"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/router"
+ "realy.lol/gio/op"
+)
+
+func TestListPosition(t *testing.T) {
+ _s := func(e ...event.Event) []event.Event { return e }
+ r := new(router.Router)
+ gtx := Context{
+ Ops: new(op.Ops),
+ Constraints: Constraints{
+ Max: image.Pt(20, 10),
+ },
+ Queue: r,
+ }
+ el := func(gtx Context, idx int) Dimensions {
+ return Dimensions{Size: image.Pt(10, 10)}
+ }
+ for _, tc := range []struct {
+ label string
+ num int
+ scroll []event.Event
+ first int
+ count int
+ offset int
+ last int
+ }{
+ {label: "no item", last: 20},
+ {label: "1 visible 0 hidden", num: 1, count: 1, last: 10},
+ {label: "2 visible 0 hidden", num: 2, count: 2},
+ {label: "2 visible 1 hidden", num: 3, count: 2},
+ {label: "3 visible 0 hidden small scroll", num: 3, count: 3, offset: 5,
+ last: -5,
+ scroll: _s(
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Press,
+ Position: f32.Pt(0, 0),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Type: pointer.Scroll,
+ Scroll: f32.Pt(5, 0),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Release,
+ Position: f32.Pt(5, 0),
+ },
+ )},
+ {label: "3 visible 0 hidden small scroll 2", num: 3, count: 3,
+ offset: 3, last: -7,
+ scroll: _s(
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Press,
+ Position: f32.Pt(0, 0),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Type: pointer.Scroll,
+ Scroll: f32.Pt(3, 0),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Release,
+ Position: f32.Pt(5, 0),
+ },
+ )},
+ {label: "2 visible 1 hidden large scroll", num: 3, count: 2, first: 1,
+ scroll: _s(
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Press,
+ Position: f32.Pt(0, 0),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Type: pointer.Scroll,
+ Scroll: f32.Pt(10, 0),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Release,
+ Position: f32.Pt(15, 0),
+ },
+ )},
+ } {
+ t.Run(tc.label, func(t *testing.T) {
+ gtx.Ops.Reset()
+
+ var list List
+ // Initialize the list.
+ list.Layout(gtx, tc.num, el)
+ // Generate the scroll events.
+ r.Frame(gtx.Ops)
+ r.Queue(tc.scroll...)
+ // Let the list process the events.
+ list.Layout(gtx, tc.num, el)
+
+ pos := list.Position
+ if got, want := pos.First, tc.first; got != want {
+ t.Errorf("List: invalid first position: got %v; want %v", got,
+ want)
+ }
+ if got, want := pos.Count, tc.count; got != want {
+ t.Errorf("List: invalid number of visible children: got %v; want %v",
+ got, want)
+ }
+ if got, want := pos.Offset, tc.offset; got != want {
+ t.Errorf("List: invalid first visible offset: got %v; want %v",
+ got, want)
+ }
+ if got, want := pos.OffsetLast, tc.last; got != want {
+ t.Errorf("List: invalid last visible offset: got %v; want %v",
+ got, want)
+ }
+ })
+ }
+}
diff --git a/gio/layout/stack.go b/gio/layout/stack.go
new file mode 100644
index 0000000..f46a091
--- /dev/null
+++ b/gio/layout/stack.go
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package layout
+
+import (
+ "image"
+
+ "realy.lol/gio/op"
+)
+
+// Stack lays out child elements on top of each other,
+// according to an alignment direction.
+type Stack struct {
+ // Alignment is the direction to align children
+ // smaller than the available space.
+ Alignment Direction
+}
+
+// StackChild represents a child for a Stack layout.
+type StackChild struct {
+ expanded bool
+ widget Widget
+
+ // Scratch space.
+ call op.CallOp
+ dims Dimensions
+}
+
+// Stacked returns a Stack child that is laid out with no minimum
+// constraints and the maximum constraints passed to Stack.Layout.
+func Stacked(w Widget) StackChild {
+ return StackChild{
+ widget: w,
+ }
+}
+
+// Expanded returns a Stack child with the minimum constraints set
+// to the largest Stacked child. The maximum constraints are set to
+// the same as passed to Stack.Layout.
+func Expanded(w Widget) StackChild {
+ return StackChild{
+ expanded: true,
+ widget: w,
+ }
+}
+
+// Layout a stack of children. The position of the children are
+// determined by the specified order, but Stacked children are laid out
+// before Expanded children.
+func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions {
+ var maxSZ image.Point
+ // First lay out Stacked children.
+ cgtx := gtx
+ cgtx.Constraints.Min = image.Point{}
+ for i, w := range children {
+ if w.expanded {
+ continue
+ }
+ macro := op.Record(gtx.Ops)
+ dims := w.widget(cgtx)
+ call := macro.Stop()
+ if w := dims.Size.X; w > maxSZ.X {
+ maxSZ.X = w
+ }
+ if h := dims.Size.Y; h > maxSZ.Y {
+ maxSZ.Y = h
+ }
+ children[i].call = call
+ children[i].dims = dims
+ }
+ // Then lay out Expanded children.
+ for i, w := range children {
+ if !w.expanded {
+ continue
+ }
+ macro := op.Record(gtx.Ops)
+ cgtx.Constraints.Min = maxSZ
+ dims := w.widget(cgtx)
+ call := macro.Stop()
+ if w := dims.Size.X; w > maxSZ.X {
+ maxSZ.X = w
+ }
+ if h := dims.Size.Y; h > maxSZ.Y {
+ maxSZ.Y = h
+ }
+ children[i].call = call
+ children[i].dims = dims
+ }
+
+ maxSZ = gtx.Constraints.Constrain(maxSZ)
+ var baseline int
+ for _, ch := range children {
+ sz := ch.dims.Size
+ var p image.Point
+ switch s.Alignment {
+ case N, S, Center:
+ p.X = (maxSZ.X - sz.X) / 2
+ case NE, SE, E:
+ p.X = maxSZ.X - sz.X
+ }
+ switch s.Alignment {
+ case W, Center, E:
+ p.Y = (maxSZ.Y - sz.Y) / 2
+ case SW, S, SE:
+ p.Y = maxSZ.Y - sz.Y
+ }
+ stack := op.Save(gtx.Ops)
+ op.Offset(FPt(p)).Add(gtx.Ops)
+ ch.call.Add(gtx.Ops)
+ stack.Load()
+ if baseline == 0 {
+ if b := ch.dims.Baseline; b != 0 {
+ baseline = b + maxSZ.Y - sz.Y - p.Y
+ }
+ }
+ }
+ return Dimensions{
+ Size: maxSZ,
+ Baseline: baseline,
+ }
+}
diff --git a/gio/op/clip/clip.go b/gio/op/clip/clip.go
new file mode 100644
index 0000000..360d89f
--- /dev/null
+++ b/gio/op/clip/clip.go
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package clip
+
+import (
+ "encoding/binary"
+ "image"
+ "math"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/internal/ops"
+ "realy.lol/gio/internal/scene"
+ "realy.lol/gio/internal/stroke"
+ "realy.lol/gio/op"
+)
+
+// Op represents a clip area. Op intersects the current clip area with
+// itself.
+type Op struct {
+ bounds image.Rectangle
+ path PathSpec
+
+ outline bool
+ stroke StrokeStyle
+ dashes DashSpec
+}
+
+func (p Op) Add(o *op.Ops) {
+ str := p.stroke
+ dashes := p.dashes
+ path := p.path
+ outline := p.outline
+ approx := str.Width > 0 && !(dashes == DashSpec{} && str.Miter == 0 && str.Join == RoundJoin && str.Cap == RoundCap)
+ if approx {
+ // If the stroke is not natively supported by the compute renderer, construct a filled path
+ // that approximates it.
+ path = p.approximateStroke(o)
+ dashes = DashSpec{}
+ str = StrokeStyle{}
+ outline = true
+ }
+
+ if path.hasSegments {
+ data := o.Write(opconst.TypePathLen)
+ data[0] = byte(opconst.TypePath)
+ path.spec.Add(o)
+ }
+
+ if str.Width > 0 {
+ data := o.Write(opconst.TypeStrokeLen)
+ data[0] = byte(opconst.TypeStroke)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[1:], math.Float32bits(str.Width))
+ }
+
+ data := o.Write(opconst.TypeClipLen)
+ data[0] = byte(opconst.TypeClip)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[1:], uint32(p.bounds.Min.X))
+ bo.PutUint32(data[5:], uint32(p.bounds.Min.Y))
+ bo.PutUint32(data[9:], uint32(p.bounds.Max.X))
+ bo.PutUint32(data[13:], uint32(p.bounds.Max.Y))
+ if outline {
+ data[17] = byte(1)
+ }
+}
+
+func (p Op) approximateStroke(o *op.Ops) PathSpec {
+ if !p.path.hasSegments {
+ return PathSpec{}
+ }
+
+ var r ops.Reader
+ // Add path op for us to decode. Use a macro to omit it from later decodes.
+ ignore := op.Record(o)
+ r.ResetAt(o, ops.NewPC(o))
+ p.path.spec.Add(o)
+ ignore.Stop()
+ encOp, ok := r.Decode()
+ if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux {
+ panic("corrupt path data")
+ }
+ pathData := encOp.Data[opconst.TypeAuxLen:]
+
+ // Decode dashes in a similar way.
+ var dashes stroke.DashOp
+ if p.dashes.phase != 0 || p.dashes.size > 0 {
+ ignore := op.Record(o)
+ r.ResetAt(o, ops.NewPC(o))
+ p.dashes.spec.Add(o)
+ ignore.Stop()
+ encOp, ok := r.Decode()
+ if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux {
+ panic("corrupt dash data")
+ }
+ dashes.Dashes = make([]float32, p.dashes.size)
+ dashData := encOp.Data[opconst.TypeAuxLen:]
+ bo := binary.LittleEndian
+ for i := range dashes.Dashes {
+ dashes.Dashes[i] = math.Float32frombits(bo.Uint32(dashData[i*4:]))
+ }
+ dashes.Phase = p.dashes.phase
+ }
+
+ // Approximate and output path data.
+ var outline Path
+ outline.Begin(o)
+ ss := stroke.StrokeStyle{
+ Width: p.stroke.Width,
+ Miter: p.stroke.Miter,
+ Cap: stroke.StrokeCap(p.stroke.Cap),
+ Join: stroke.StrokeJoin(p.stroke.Join),
+ }
+ quads := stroke.StrokePathCommands(ss, dashes, pathData)
+ pen := f32.Pt(0, 0)
+ for _, quad := range quads {
+ q := quad.Quad
+ if q.From != pen {
+ pen = q.From
+ outline.MoveTo(pen)
+ }
+ outline.contour = int(quad.Contour)
+ outline.QuadTo(q.Ctrl, q.To)
+ }
+ return outline.End()
+}
+
+type PathSpec struct {
+ spec op.CallOp
+ // open is true if any path contour is not closed. A closed contour starts
+ // and ends in the same point.
+ open bool
+ // hasSegments tracks whether there are any segments in the path.
+ hasSegments bool
+}
+
+// Path constructs a Op clip path described by lines and
+// BĆ©zier curves, where drawing outside the Path is discarded.
+// The inside-ness of a pixel is determines by the non-zero winding rule,
+// similar to the SVG rule of the same name.
+//
+// Path generates no garbage and can be used for dynamic paths; path
+// data is stored directly in the Ops list supplied to Begin.
+type Path struct {
+ ops *op.Ops
+ open bool
+ contour int
+ pen f32.Point
+ macro op.MacroOp
+ start f32.Point
+ hasSegments bool
+}
+
+// Pos returns the current pen position.
+func (p *Path) Pos() f32.Point { return p.pen }
+
+// Begin the path, storing the path data and final Op into ops.
+func (p *Path) Begin(ops *op.Ops) {
+ p.ops = ops
+ p.macro = op.Record(ops)
+ // Write the TypeAux opcode
+ data := ops.Write(opconst.TypeAuxLen)
+ data[0] = byte(opconst.TypeAux)
+}
+
+// End returns a PathSpec ready to use in clipping operations.
+func (p *Path) End() PathSpec {
+ c := p.macro.Stop()
+ return PathSpec{
+ spec: c,
+ open: p.open || p.pen != p.start,
+ hasSegments: p.hasSegments,
+ }
+}
+
+// Move moves the pen by the amount specified by delta.
+func (p *Path) Move(delta f32.Point) {
+ to := delta.Add(p.pen)
+ p.MoveTo(to)
+}
+
+// MoveTo moves the pen to the specified absolute coordinate.
+func (p *Path) MoveTo(to f32.Point) {
+ p.open = p.open || p.pen != p.start
+ p.end()
+ p.pen = to
+ p.start = to
+}
+
+// end completes the current contour.
+func (p *Path) end() {
+ p.contour++
+}
+
+// Line moves the pen by the amount specified by delta, recording a line.
+func (p *Path) Line(delta f32.Point) {
+ to := delta.Add(p.pen)
+ p.LineTo(to)
+}
+
+// LineTo moves the pen to the absolute point specified, recording a line.
+func (p *Path) LineTo(to f32.Point) {
+ data := p.ops.Write(scene.CommandSize + 4)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[0:], uint32(p.contour))
+ ops.EncodeCommand(data[4:], scene.Line(p.pen, to))
+ p.pen = to
+ p.hasSegments = true
+}
+
+// Quad records a quadratic BĆ©zier from the pen to end
+// with the control point ctrl.
+func (p *Path) Quad(ctrl, to f32.Point) {
+ ctrl = ctrl.Add(p.pen)
+ to = to.Add(p.pen)
+ p.QuadTo(ctrl, to)
+}
+
+// QuadTo records a quadratic BĆ©zier from the pen to end
+// with the control point ctrl, with absolute coordinates.
+func (p *Path) QuadTo(ctrl, to f32.Point) {
+ data := p.ops.Write(scene.CommandSize + 4)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[0:], uint32(p.contour))
+ ops.EncodeCommand(data[4:], scene.Quad(p.pen, ctrl, to))
+ p.pen = to
+ p.hasSegments = true
+}
+
+// Arc adds an elliptical arc to the path. The implied ellipse is defined
+// by its focus points f1 and f2.
+// The arc starts in the current point and ends angle radians along the ellipse boundary.
+// The sign of angle determines the direction; positive being counter-clockwise,
+// negative clockwise.
+func (p *Path) Arc(f1, f2 f32.Point, angle float32) {
+ f1 = f1.Add(p.pen)
+ f2 = f2.Add(p.pen)
+ const segments = 16
+ m := stroke.ArcTransform(p.pen, f1, f2, angle, segments)
+
+ for i := 0; i < segments; i++ {
+ p0 := p.pen
+ p1 := m.Transform(p0)
+ p2 := m.Transform(p1)
+ ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
+ p.QuadTo(ctl, p2)
+ }
+}
+
+// Cube records a cubic BĆ©zier from the pen through
+// two control points ending in to.
+func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) {
+ p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to))
+}
+
+// CubeTo records a cubic BĆ©zier from the pen through
+// two control points ending in to, with absolute coordinates.
+func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) {
+ if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen {
+ return
+ }
+ data := p.ops.Write(scene.CommandSize + 4)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[0:], uint32(p.contour))
+ ops.EncodeCommand(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to))
+ p.pen = to
+ p.hasSegments = true
+}
+
+// Close closes the path contour.
+func (p *Path) Close() {
+ if p.pen != p.start {
+ p.LineTo(p.start)
+ }
+ p.end()
+}
+
+// Outline represents the area inside of a path, according to the
+// non-zero winding rule.
+type Outline struct {
+ Path PathSpec
+}
+
+// Op returns a clip operation representing the outline.
+func (o Outline) Op() Op {
+ if o.Path.open {
+ panic("not all path contours are closed")
+ }
+ return Op{
+ path: o.Path,
+ outline: true,
+ }
+}
diff --git a/gio/op/clip/clip_test.go b/gio/op/clip/clip_test.go
new file mode 100644
index 0000000..7962c6d
--- /dev/null
+++ b/gio/op/clip/clip_test.go
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package clip
+
+import (
+ "testing"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/op"
+)
+
+func TestOpenPathOutlinePanic(t *testing.T) {
+ defer func() {
+ if err := recover(); err == nil {
+ t.Error("Outline of an open path didn't panic")
+ }
+ }()
+ var p Path
+ p.Begin(new(op.Ops))
+ p.Line(f32.Pt(10, 10))
+ Outline{Path: p.End()}.Op()
+}
diff --git a/gio/op/clip/doc.go b/gio/op/clip/doc.go
new file mode 100644
index 0000000..6ba5546
--- /dev/null
+++ b/gio/op/clip/doc.go
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package clip provides operations for clipping paint operations.
+Drawing outside the current clip area is ignored.
+
+The current clip is initially the infinite set. An Op sets the clip
+to the intersection of the current clip and the clip area it
+represents. If you need to reset the current clip to its value
+before applying an Op, use op.StackOp.
+
+General clipping areas are constructed with Path. Simpler special
+cases such as rectangular clip areas also exist as convenient
+constructors.
+*/
+package clip
diff --git a/gio/op/clip/shapes.go b/gio/op/clip/shapes.go
new file mode 100644
index 0000000..9ea84e3
--- /dev/null
+++ b/gio/op/clip/shapes.go
@@ -0,0 +1,167 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package clip
+
+import (
+ "image"
+ "math"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/op"
+)
+
+// Rect represents the clip area of a pixel-aligned rectangle.
+type Rect image.Rectangle
+
+// Op returns the op for the rectangle.
+func (r Rect) Op() Op {
+ return Op{
+ bounds: image.Rectangle(r),
+ outline: true,
+ }
+}
+
+// Add the clip operation.
+func (r Rect) Add(ops *op.Ops) {
+ r.Op().Add(ops)
+}
+
+// UniformRRect returns an RRect with all corner radii set to the
+// provided radius.
+func UniformRRect(rect f32.Rectangle, radius float32) RRect {
+ return RRect{
+ Rect: rect,
+ SE: radius,
+ SW: radius,
+ NE: radius,
+ NW: radius,
+ }
+}
+
+// RRect represents the clip area of a rectangle with rounded
+// corners.
+//
+// Specify a square with corner radii equal to half the square size to
+// construct a circular clip area.
+type RRect struct {
+ Rect f32.Rectangle
+ // The corner radii.
+ SE, SW, NW, NE float32
+}
+
+// Op returns the op for the rounded rectangle.
+func (rr RRect) Op(ops *op.Ops) Op {
+ if rr.SE == 0 && rr.SW == 0 && rr.NW == 0 && rr.NE == 0 {
+ r := image.Rectangle{
+ Min: image.Point{X: int(rr.Rect.Min.X), Y: int(rr.Rect.Min.Y)},
+ Max: image.Point{X: int(rr.Rect.Max.X), Y: int(rr.Rect.Max.Y)},
+ }
+ // Only use Rect if rr is pixel-aligned, as Rect is guaranteed to be.
+ if fPt(r.Min) == rr.Rect.Min && fPt(r.Max) == rr.Rect.Max {
+ return Rect(r).Op()
+ }
+ }
+ return Outline{Path: rr.Path(ops)}.Op()
+}
+
+// Add the rectangle clip.
+func (rr RRect) Add(ops *op.Ops) {
+ rr.Op(ops).Add(ops)
+}
+
+// Path returns the PathSpec for the rounded rectangle.
+func (rr RRect) Path(ops *op.Ops) PathSpec {
+ var p Path
+ p.Begin(ops)
+
+ // https://pomax.github.io/bezierinfo/#circles_cubic.
+ const q = 4 * (math.Sqrt2 - 1) / 3
+ const iq = 1 - q
+
+ se, sw, nw, ne := rr.SE, rr.SW, rr.NW, rr.NE
+ w, n, e, s := rr.Rect.Min.X, rr.Rect.Min.Y, rr.Rect.Max.X, rr.Rect.Max.Y
+
+ p.MoveTo(f32.Point{X: w + nw, Y: n})
+ p.LineTo(f32.Point{X: e - ne, Y: n}) // N
+ p.CubeTo( // NE
+ f32.Point{X: e - ne*iq, Y: n},
+ f32.Point{X: e, Y: n + ne*iq},
+ f32.Point{X: e, Y: n + ne})
+ p.LineTo(f32.Point{X: e, Y: s - se}) // E
+ p.CubeTo( // SE
+ f32.Point{X: e, Y: s - se*iq},
+ f32.Point{X: e - se*iq, Y: s},
+ f32.Point{X: e - se, Y: s})
+ p.LineTo(f32.Point{X: w + sw, Y: s}) // S
+ p.CubeTo( // SW
+ f32.Point{X: w + sw*iq, Y: s},
+ f32.Point{X: w, Y: s - sw*iq},
+ f32.Point{X: w, Y: s - sw})
+ p.LineTo(f32.Point{X: w, Y: n + nw}) // W
+ p.CubeTo( // NW
+ f32.Point{X: w, Y: n + nw*iq},
+ f32.Point{X: w + nw*iq, Y: n},
+ f32.Point{X: w + nw, Y: n})
+
+ return p.End()
+}
+
+// Circle represents the clip area of a circle.
+type Circle struct {
+ Center f32.Point
+ Radius float32
+}
+
+// Op returns the op for the circle.
+func (c Circle) Op(ops *op.Ops) Op {
+ return Outline{Path: c.Path(ops)}.Op()
+}
+
+// Add the circle clip.
+func (c Circle) Add(ops *op.Ops) {
+ c.Op(ops).Add(ops)
+}
+
+// Path returns the PathSpec for the circle.
+func (c Circle) Path(ops *op.Ops) PathSpec {
+ var p Path
+ p.Begin(ops)
+
+ center := c.Center
+ r := c.Radius
+
+ // https://pomax.github.io/bezierinfo/#circles_cubic.
+ const q = 4 * (math.Sqrt2 - 1) / 3
+
+ curve := r * q
+ top := f32.Point{X: center.X, Y: center.Y - r}
+
+ p.MoveTo(top)
+ p.CubeTo(
+ f32.Point{X: center.X + curve, Y: center.Y - r},
+ f32.Point{X: center.X + r, Y: center.Y - curve},
+ f32.Point{X: center.X + r, Y: center.Y},
+ )
+ p.CubeTo(
+ f32.Point{X: center.X + r, Y: center.Y + curve},
+ f32.Point{X: center.X + curve, Y: center.Y + r},
+ f32.Point{X: center.X, Y: center.Y + r},
+ )
+ p.CubeTo(
+ f32.Point{X: center.X - curve, Y: center.Y + r},
+ f32.Point{X: center.X - r, Y: center.Y + curve},
+ f32.Point{X: center.X - r, Y: center.Y},
+ )
+ p.CubeTo(
+ f32.Point{X: center.X - r, Y: center.Y - curve},
+ f32.Point{X: center.X - curve, Y: center.Y - r},
+ top,
+ )
+ return p.End()
+}
+
+func fPt(p image.Point) f32.Point {
+ return f32.Point{
+ X: float32(p.X), Y: float32(p.Y),
+ }
+}
diff --git a/gio/op/clip/stroke.go b/gio/op/clip/stroke.go
new file mode 100644
index 0000000..8610eab
--- /dev/null
+++ b/gio/op/clip/stroke.go
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package clip
+
+import (
+ "encoding/binary"
+ "math"
+
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/op"
+)
+
+// Stroke represents a stroked path.
+type Stroke struct {
+ Path PathSpec
+ Style StrokeStyle
+
+ // Dashes specify the dashes of the stroke.
+ // The empty value denotes no dashes.
+ Dashes DashSpec
+}
+
+// Op returns a clip operation representing the stroke.
+func (s Stroke) Op() Op {
+ return Op{
+ path: s.Path,
+ stroke: s.Style,
+ dashes: s.Dashes,
+ }
+}
+
+// StrokeStyle describes how a path should be stroked.
+type StrokeStyle struct {
+ Width float32 // Width of the stroked path.
+
+ // Miter is the limit to apply to a miter joint.
+ // The zero Miter disables the miter joint; setting Miter to +ā
+ // unconditionally enables the miter joint.
+ Miter float32
+ Cap StrokeCap // Cap describes the head or tail of a stroked path.
+ Join StrokeJoin // Join describes how stroked paths are collated.
+}
+
+// StrokeCap describes the head or tail of a stroked path.
+type StrokeCap uint8
+
+const (
+ // RoundCap caps stroked paths with a round cap, joining the right-hand and
+ // left-hand sides of a stroked path with a half disc of diameter the
+ // stroked path's width.
+ RoundCap StrokeCap = iota
+
+ // FlatCap caps stroked paths with a flat cap, joining the right-hand
+ // and left-hand sides of a stroked path with a straight line.
+ FlatCap
+
+ // SquareCap caps stroked paths with a square cap, joining the right-hand
+ // and left-hand sides of a stroked path with a half square of length
+ // the stroked path's width.
+ SquareCap
+)
+
+// StrokeJoin describes how stroked paths are collated.
+type StrokeJoin uint8
+
+const (
+ // RoundJoin joins path segments with a round segment.
+ RoundJoin StrokeJoin = iota
+
+ // BevelJoin joins path segments with sharp bevels.
+ BevelJoin
+)
+
+// Dash records dashes' lengths and phase for a stroked path.
+type Dash struct {
+ ops *op.Ops
+ macro op.MacroOp
+ phase float32
+ size uint8 // size of the pattern
+}
+
+func (d *Dash) Begin(ops *op.Ops) {
+ d.ops = ops
+ d.macro = op.Record(ops)
+ // Write the TypeAux opcode
+ data := ops.Write(opconst.TypeAuxLen)
+ data[0] = byte(opconst.TypeAux)
+}
+
+func (d *Dash) Phase(v float32) {
+ d.phase = v
+}
+
+func (d *Dash) Dash(length float32) {
+ if d.size == math.MaxUint8 {
+ panic("clip: dash pattern too large")
+ }
+ data := d.ops.Write(4)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[0:], math.Float32bits(length))
+ d.size++
+}
+
+func (d *Dash) End() DashSpec {
+ c := d.macro.Stop()
+ return DashSpec{
+ spec: c,
+ phase: d.phase,
+ size: d.size,
+ }
+}
+
+// DashSpec describes a dashed pattern.
+type DashSpec struct {
+ spec op.CallOp
+ phase float32
+ size uint8 // size of the pattern
+}
diff --git a/gio/op/op.go b/gio/op/op.go
new file mode 100644
index 0000000..f29aa0b
--- /dev/null
+++ b/gio/op/op.go
@@ -0,0 +1,369 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+
+Package op implements operations for updating a user interface.
+
+Gio programs use operations, or ops, for describing their user
+interfaces. There are operations for drawing, defining input
+handlers, changing window properties as well as operations for
+controlling the execution of other operations.
+
+Ops represents a list of operations. The most important use
+for an Ops list is to describe a complete user interface update
+to a ui/app.Window's Update method.
+
+Drawing a colored square:
+
+ import "realy.lol/gio/unit"
+ import "realy.lol/gio/app"
+ import "realy.lol/gio/op/paint"
+
+ var w app.Window
+ var e system.FrameEvent
+ ops := new(op.Ops)
+ ...
+ ops.Reset()
+ paint.ColorOp{Color: ...}.Add(ops)
+ paint.PaintOp{Rect: ...}.Add(ops)
+ e.Frame(ops)
+
+State
+
+An Ops list can be viewed as a very simple virtual machine: it has an implicit
+mutable state stack and execution flow can be controlled with macros.
+
+The Save function saves the current state for later restoring:
+
+ ops := new(op.Ops)
+ // Save the current state, in particular the transform.
+ state := op.Save(ops)
+ // Apply a transform to subsequent operations.
+ op.Offset(...).Add(ops)
+ ...
+ // Restore the previous transform.
+ state.Load()
+
+You can also use this one-line to save the current state and restore it at the
+end of a function :
+
+ defer op.Save(ops).Load()
+
+The MacroOp records a list of operations to be executed later:
+
+ ops := new(op.Ops)
+ macro := op.Record(ops)
+ // Record operations by adding them.
+ op.InvalidateOp{}.Add(ops)
+ ...
+ // End recording.
+ call := macro.Stop()
+
+ // replay the recorded operations:
+ call.Add(ops)
+
+*/
+package op
+
+import (
+ "encoding/binary"
+ "math"
+ "time"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/opconst"
+)
+
+// Ops holds a list of operations. Operations are stored in
+// serialized form to avoid garbage during construction of
+// the ops list.
+type Ops struct {
+ // version is incremented at each Reset.
+ version int
+ // data contains the serialized operations.
+ data []byte
+ // refs hold external references for operations.
+ refs []interface{}
+ // nextStateID is the id allocated for the next
+ // StateOp.
+ nextStateID int
+
+ macroStack stack
+}
+
+// StateOp represents a saved operation snapshop to be restored
+// later.
+type StateOp struct {
+ id int
+ macroID int
+ ops *Ops
+}
+
+// MacroOp records a list of operations for later use.
+type MacroOp struct {
+ ops *Ops
+ id stackID
+ pc pc
+}
+
+// CallOp invokes the operations recorded by Record.
+type CallOp struct {
+ // Ops is the list of operations to invoke.
+ ops *Ops
+ pc pc
+}
+
+// InvalidateOp requests a redraw at the given time. Use
+// the zero value to request an immediate redraw.
+type InvalidateOp struct {
+ At time.Time
+}
+
+// TransformOp applies a transform to the current transform. The zero value
+// for TransformOp represents the identity transform.
+type TransformOp struct {
+ t f32.Affine2D
+}
+
+// stack tracks the integer identities of MacroOp
+// operations to ensure correct pairing of Record/End.
+type stack struct {
+ currentID int
+ nextID int
+}
+
+type stackID struct {
+ id int
+ prev int
+}
+
+type pc struct {
+ data int
+ refs int
+}
+
+// Defer executes c after all other operations have completed,
+// including previously deferred operations.
+// Defer saves the current transformation and restores it prior
+// to execution. All other operation state is reset.
+//
+// Note that deferred operations are executed in first-in-first-out
+// order, unlike the Go facility of the same name.
+func Defer(o *Ops, c CallOp) {
+ if c.ops == nil {
+ return
+ }
+ state := Save(o)
+ // Wrap c in a macro that loads the saved state before execution.
+ m := Record(o)
+ load(o, opconst.InitialStateID, opconst.AllState)
+ load(o, state.id, opconst.TransformState)
+ c.Add(o)
+ c = m.Stop()
+ // A Defer is recorded as a TypeDefer followed by the
+ // wrapped macro.
+ data := o.Write(opconst.TypeDeferLen)
+ data[0] = byte(opconst.TypeDefer)
+ c.Add(o)
+}
+
+// Save the current operations state.
+func Save(o *Ops) StateOp {
+ o.nextStateID++
+ s := StateOp{
+ ops: o,
+ id: o.nextStateID,
+ macroID: o.macroStack.currentID,
+ }
+ save(o, s.id)
+ return s
+}
+
+// save records a save of the operations state to
+// id.
+func save(o *Ops, id int) {
+ bo := binary.LittleEndian
+ data := o.Write(opconst.TypeSaveLen)
+ data[0] = byte(opconst.TypeSave)
+ bo.PutUint32(data[1:], uint32(id))
+}
+
+// Load a previously saved operations state.
+func (s StateOp) Load() {
+ if s.ops.macroStack.currentID != s.macroID {
+ panic("load in a different macro than save")
+ }
+ if s.id == 0 {
+ panic("zero-value op")
+ }
+ load(s.ops, s.id, opconst.AllState)
+}
+
+// load a previously saved operations state given
+// its ID. Only state included in mask is affected.
+func load(o *Ops, id int, m opconst.StateMask) {
+ bo := binary.LittleEndian
+ data := o.Write(opconst.TypeLoadLen)
+ data[0] = byte(opconst.TypeLoad)
+ data[1] = byte(m)
+ bo.PutUint32(data[2:], uint32(id))
+}
+
+// Reset the Ops, preparing it for re-use. Reset invalidates
+// any recorded macros.
+func (o *Ops) Reset() {
+ o.macroStack = stack{}
+ // Leave references to the GC.
+ for i := range o.refs {
+ o.refs[i] = nil
+ }
+ o.data = o.data[:0]
+ o.refs = o.refs[:0]
+ o.nextStateID = 0
+ o.version++
+}
+
+// Data is for internal use only.
+func (o *Ops) Data() []byte {
+ return o.data
+}
+
+// Refs is for internal use only.
+func (o *Ops) Refs() []interface{} {
+ return o.refs
+}
+
+// Version is for internal use only.
+func (o *Ops) Version() int {
+ return o.version
+}
+
+// Write is for internal use only.
+func (o *Ops) Write(n int) []byte {
+ o.data = append(o.data, make([]byte, n)...)
+ return o.data[len(o.data)-n:]
+}
+
+// Write1 is for internal use only.
+func (o *Ops) Write1(n int, ref1 interface{}) []byte {
+ o.data = append(o.data, make([]byte, n)...)
+ o.refs = append(o.refs, ref1)
+ return o.data[len(o.data)-n:]
+}
+
+// Write2 is for internal use only.
+func (o *Ops) Write2(n int, ref1, ref2 interface{}) []byte {
+ o.data = append(o.data, make([]byte, n)...)
+ o.refs = append(o.refs, ref1, ref2)
+ return o.data[len(o.data)-n:]
+}
+
+func (o *Ops) pc() pc {
+ return pc{data: len(o.data), refs: len(o.refs)}
+}
+
+// Record a macro of operations.
+func Record(o *Ops) MacroOp {
+ m := MacroOp{
+ ops: o,
+ id: o.macroStack.push(),
+ pc: o.pc(),
+ }
+ // Reserve room for a macro definition. Updated in Stop.
+ m.ops.Write(opconst.TypeMacroLen)
+ m.fill()
+ return m
+}
+
+// Stop ends a previously started recording and returns an
+// operation for replaying it.
+func (m MacroOp) Stop() CallOp {
+ m.ops.macroStack.pop(m.id)
+ m.fill()
+ return CallOp{
+ ops: m.ops,
+ pc: m.pc,
+ }
+}
+
+func (m MacroOp) fill() {
+ pc := m.ops.pc()
+ // Fill out the macro definition reserved in Record.
+ data := m.ops.data[m.pc.data:]
+ data = data[:opconst.TypeMacroLen]
+ data[0] = byte(opconst.TypeMacro)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[1:], uint32(pc.data))
+ bo.PutUint32(data[5:], uint32(pc.refs))
+}
+
+// Add the recorded list of operations. Add
+// panics if the Ops containing the recording
+// has been reset.
+func (c CallOp) Add(o *Ops) {
+ if c.ops == nil {
+ return
+ }
+ data := o.Write1(opconst.TypeCallLen, c.ops)
+ data[0] = byte(opconst.TypeCall)
+ bo := binary.LittleEndian
+ bo.PutUint32(data[1:], uint32(c.pc.data))
+ bo.PutUint32(data[5:], uint32(c.pc.refs))
+}
+
+func (r InvalidateOp) Add(o *Ops) {
+ data := o.Write(opconst.TypeRedrawLen)
+ data[0] = byte(opconst.TypeInvalidate)
+ bo := binary.LittleEndian
+ // UnixNano cannot represent the zero time.
+ if t := r.At; !t.IsZero() {
+ nanos := t.UnixNano()
+ if nanos > 0 {
+ bo.PutUint64(data[1:], uint64(nanos))
+ }
+ }
+}
+
+// Offset creates a TransformOp with the offset o.
+func Offset(o f32.Point) TransformOp {
+ return TransformOp{t: f32.Affine2D{}.Offset(o)}
+}
+
+// Affine creates a TransformOp representing the transformation a.
+func Affine(a f32.Affine2D) TransformOp {
+ return TransformOp{t: a}
+}
+
+func (t TransformOp) Add(o *Ops) {
+ data := o.Write(opconst.TypeTransformLen)
+ data[0] = byte(opconst.TypeTransform)
+ bo := binary.LittleEndian
+ a, b, c, d, e, f := t.t.Elems()
+ bo.PutUint32(data[1:], math.Float32bits(a))
+ bo.PutUint32(data[1+4*1:], math.Float32bits(b))
+ bo.PutUint32(data[1+4*2:], math.Float32bits(c))
+ bo.PutUint32(data[1+4*3:], math.Float32bits(d))
+ bo.PutUint32(data[1+4*4:], math.Float32bits(e))
+ bo.PutUint32(data[1+4*5:], math.Float32bits(f))
+}
+
+func (s *stack) push() stackID {
+ s.nextID++
+ sid := stackID{
+ id: s.nextID,
+ prev: s.currentID,
+ }
+ s.currentID = s.nextID
+ return sid
+}
+
+func (s *stack) check(sid stackID) {
+ if s.currentID != sid.id {
+ panic("unbalanced operation")
+ }
+}
+
+func (s *stack) pop(sid stackID) {
+ s.check(sid)
+ s.currentID = sid.prev
+}
diff --git a/gio/op/paint/doc.go b/gio/op/paint/doc.go
new file mode 100644
index 0000000..79054ab
--- /dev/null
+++ b/gio/op/paint/doc.go
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+Package paint provides drawing operations for 2D graphics.
+
+The PaintOp operation fills the current clip with the current brush,
+taking the current transformation into account.
+
+The current brush is set by either a ColorOp for a constant color, or
+ImageOp for an image, or LinearGradientOp for gradients.
+
+All color.NRGBA values are in the sRGB color space.
+*/
+package paint
diff --git a/gio/op/paint/paint.go b/gio/op/paint/paint.go
new file mode 100644
index 0000000..e53a763
--- /dev/null
+++ b/gio/op/paint/paint.go
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package paint
+
+import (
+ "encoding/binary"
+ "image"
+ "image/color"
+ "image/draw"
+ "math"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/opconst"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+)
+
+// ImageOp sets the brush to an image.
+//
+// Note: the ImageOp may keep a reference to the backing image.
+// See NewImageOp for details.
+type ImageOp struct {
+ uniform bool
+ color color.NRGBA
+ src *image.RGBA
+
+ // handle is a key to uniquely identify this ImageOp
+ // in a map of cached textures.
+ handle interface{}
+}
+
+// ColorOp sets the brush to a constant color.
+type ColorOp struct {
+ Color color.NRGBA
+}
+
+// LinearGradientOp sets the brush to a gradient starting at stop1 with color1 and
+// ending at stop2 with color2.
+type LinearGradientOp struct {
+ Stop1 f32.Point
+ Color1 color.NRGBA
+ Stop2 f32.Point
+ Color2 color.NRGBA
+}
+
+// PaintOp fills fills the current clip area with the current brush.
+type PaintOp struct {
+}
+
+// NewImageOp creates an ImageOp backed by src. See
+// realy.lol/gio/io/system.FrameEvent for a description of when data
+// referenced by operations is safe to re-use.
+//
+// NewImageOp assumes the backing image is immutable, and may cache a
+// copy of its contents in a GPU-friendly way. Create new ImageOps to
+// ensure that changes to an image is reflected in the display of
+// it.
+func NewImageOp(src image.Image) ImageOp {
+ switch src := src.(type) {
+ case *image.Uniform:
+ col := color.NRGBAModel.Convert(src.C).(color.NRGBA)
+ return ImageOp{
+ uniform: true,
+ color: col,
+ }
+ case *image.RGBA:
+ bounds := src.Bounds()
+ if bounds.Min == (image.Point{}) && src.Stride == bounds.Dx()*4 {
+ return ImageOp{
+ src: src,
+ handle: new(int),
+ }
+ }
+ }
+
+ sz := src.Bounds().Size()
+ // Copy the image into a GPU friendly format.
+ dst := image.NewRGBA(image.Rectangle{
+ Max: sz,
+ })
+ draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src)
+ return ImageOp{
+ src: dst,
+ handle: new(int),
+ }
+}
+
+func (i ImageOp) Size() image.Point {
+ if i.src == nil {
+ return image.Point{}
+ }
+ return i.src.Bounds().Size()
+}
+
+func (i ImageOp) Add(o *op.Ops) {
+ if i.uniform {
+ ColorOp{
+ Color: i.color,
+ }.Add(o)
+ return
+ }
+ data := o.Write2(opconst.TypeImageLen, i.src, i.handle)
+ data[0] = byte(opconst.TypeImage)
+}
+
+func (c ColorOp) Add(o *op.Ops) {
+ data := o.Write(opconst.TypeColorLen)
+ data[0] = byte(opconst.TypeColor)
+ data[1] = c.Color.R
+ data[2] = c.Color.G
+ data[3] = c.Color.B
+ data[4] = c.Color.A
+}
+
+func (c LinearGradientOp) Add(o *op.Ops) {
+ data := o.Write(opconst.TypeLinearGradientLen)
+ data[0] = byte(opconst.TypeLinearGradient)
+
+ bo := binary.LittleEndian
+ bo.PutUint32(data[1:], math.Float32bits(c.Stop1.X))
+ bo.PutUint32(data[5:], math.Float32bits(c.Stop1.Y))
+ bo.PutUint32(data[9:], math.Float32bits(c.Stop2.X))
+ bo.PutUint32(data[13:], math.Float32bits(c.Stop2.Y))
+
+ data[17+0] = c.Color1.R
+ data[17+1] = c.Color1.G
+ data[17+2] = c.Color1.B
+ data[17+3] = c.Color1.A
+ data[21+0] = c.Color2.R
+ data[21+1] = c.Color2.G
+ data[21+2] = c.Color2.B
+ data[21+3] = c.Color2.A
+}
+
+func (d PaintOp) Add(o *op.Ops) {
+ data := o.Write(opconst.TypePaintLen)
+ data[0] = byte(opconst.TypePaint)
+}
+
+// FillShape fills the clip shape with a color.
+func FillShape(ops *op.Ops, c color.NRGBA, shape clip.Op) {
+ defer op.Save(ops).Load()
+ shape.Add(ops)
+ Fill(ops, c)
+}
+
+// Fill paints an infinitely large plane with the provided color. It
+// is intended to be used with a clip.Op already in place to limit
+// the painted area. Use FillShape unless you need to paint several
+// times within the same clip.Op.
+func Fill(ops *op.Ops, c color.NRGBA) {
+ defer op.Save(ops).Load()
+ ColorOp{Color: c}.Add(ops)
+ PaintOp{}.Add(ops)
+}
diff --git a/gio/text/lru.go b/gio/text/lru.go
new file mode 100644
index 0000000..4f1c033
--- /dev/null
+++ b/gio/text/lru.go
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package text
+
+import (
+ "golang.org/x/image/math/fixed"
+
+ "realy.lol/gio/op"
+)
+
+type layoutCache struct {
+ m map[layoutKey]*layoutElem
+ head, tail *layoutElem
+}
+
+type pathCache struct {
+ m map[pathKey]*path
+ head, tail *path
+}
+
+type layoutElem struct {
+ next, prev *layoutElem
+ key layoutKey
+ layout []Line
+}
+
+type path struct {
+ next, prev *path
+ key pathKey
+ val op.CallOp
+}
+
+type layoutKey struct {
+ ppem fixed.Int26_6
+ maxWidth int
+ str string
+}
+
+type pathKey struct {
+ ppem fixed.Int26_6
+ str string
+}
+
+const maxSize = 1000
+
+func (l *layoutCache) Get(k layoutKey) ([]Line, bool) {
+ if lt, ok := l.m[k]; ok {
+ l.remove(lt)
+ l.insert(lt)
+ return lt.layout, true
+ }
+ return nil, false
+}
+
+func (l *layoutCache) Put(k layoutKey, lt []Line) {
+ if l.m == nil {
+ l.m = make(map[layoutKey]*layoutElem)
+ l.head = new(layoutElem)
+ l.tail = new(layoutElem)
+ l.head.prev = l.tail
+ l.tail.next = l.head
+ }
+ val := &layoutElem{key: k, layout: lt}
+ l.m[k] = val
+ l.insert(val)
+ if len(l.m) > maxSize {
+ oldest := l.tail.next
+ l.remove(oldest)
+ delete(l.m, oldest.key)
+ }
+}
+
+func (l *layoutCache) remove(lt *layoutElem) {
+ lt.next.prev = lt.prev
+ lt.prev.next = lt.next
+}
+
+func (l *layoutCache) insert(lt *layoutElem) {
+ lt.next = l.head
+ lt.prev = l.head.prev
+ lt.prev.next = lt
+ lt.next.prev = lt
+}
+
+func (c *pathCache) Get(k pathKey) (op.CallOp, bool) {
+ if v, ok := c.m[k]; ok {
+ c.remove(v)
+ c.insert(v)
+ return v.val, true
+ }
+ return op.CallOp{}, false
+}
+
+func (c *pathCache) Put(k pathKey, v op.CallOp) {
+ if c.m == nil {
+ c.m = make(map[pathKey]*path)
+ c.head = new(path)
+ c.tail = new(path)
+ c.head.prev = c.tail
+ c.tail.next = c.head
+ }
+ val := &path{key: k, val: v}
+ c.m[k] = val
+ c.insert(val)
+ if len(c.m) > maxSize {
+ oldest := c.tail.next
+ c.remove(oldest)
+ delete(c.m, oldest.key)
+ }
+}
+
+func (c *pathCache) remove(v *path) {
+ v.next.prev = v.prev
+ v.prev.next = v.next
+}
+
+func (c *pathCache) insert(v *path) {
+ v.next = c.head
+ v.prev = c.head.prev
+ v.prev.next = v
+ v.next.prev = v
+}
diff --git a/gio/text/lru_test.go b/gio/text/lru_test.go
new file mode 100644
index 0000000..fb8d8d1
--- /dev/null
+++ b/gio/text/lru_test.go
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package text
+
+import (
+ "strconv"
+ "testing"
+
+ "realy.lol/gio/op"
+)
+
+func TestLayoutLRU(t *testing.T) {
+ c := new(layoutCache)
+ put := func(i int) {
+ c.Put(layoutKey{str: strconv.Itoa(i)}, nil)
+ }
+ get := func(i int) bool {
+ _, ok := c.Get(layoutKey{str: strconv.Itoa(i)})
+ return ok
+ }
+ testLRU(t, put, get)
+}
+
+func TestPathLRU(t *testing.T) {
+ c := new(pathCache)
+ put := func(i int) {
+ c.Put(pathKey{str: strconv.Itoa(i)}, op.CallOp{})
+ }
+ get := func(i int) bool {
+ _, ok := c.Get(pathKey{str: strconv.Itoa(i)})
+ return ok
+ }
+ testLRU(t, put, get)
+}
+
+func testLRU(t *testing.T, put func(i int), get func(i int) bool) {
+ for i := 0; i < maxSize; i++ {
+ put(i)
+ }
+ for i := 0; i < maxSize; i++ {
+ if !get(i) {
+ t.Fatalf("key %d was evicted", i)
+ }
+ }
+ put(maxSize)
+ for i := 1; i < maxSize+1; i++ {
+ if !get(i) {
+ t.Fatalf("key %d was evicted", i)
+ }
+ }
+ if i := 0; get(i) {
+ t.Fatalf("key %d was not evicted", i)
+ }
+}
diff --git a/gio/text/shaper.go b/gio/text/shaper.go
new file mode 100644
index 0000000..88f1fbf
--- /dev/null
+++ b/gio/text/shaper.go
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package text
+
+import (
+ "io"
+ "strings"
+
+ "golang.org/x/image/math/fixed"
+
+ "realy.lol/gio/op"
+)
+
+// Shaper implements layout and shaping of text.
+type Shaper interface {
+ // Layout a text according to a set of options.
+ Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line,
+ error)
+ // LayoutString is Layout for strings.
+ LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line
+ // Shape a line of text and return a clipping operation for its outline.
+ Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp
+}
+
+// A FontFace is a Font and a matching Face.
+type FontFace struct {
+ Font Font
+ Face Face
+}
+
+// Cache implements cached layout and shaping of text from a set of
+// registered fonts.
+//
+// If a font matches no registered shape, Cache falls back to the
+// first registered face.
+//
+// The LayoutString and ShapeString results are cached and re-used if
+// possible.
+type Cache struct {
+ def Typeface
+ faces map[Font]*faceCache
+}
+
+type faceCache struct {
+ face Face
+ layoutCache layoutCache
+ pathCache pathCache
+}
+
+func (c *Cache) lookup(font Font) *faceCache {
+ f := c.faceForStyle(font)
+ if f == nil {
+ font.Typeface = c.def
+ f = c.faceForStyle(font)
+ }
+ return f
+}
+
+func (c *Cache) faceForStyle(font Font) *faceCache {
+ tf := c.faces[font]
+ if tf == nil {
+ font := font
+ font.Weight = Normal
+ tf = c.faces[font]
+ }
+ if tf == nil {
+ font := font
+ font.Style = Regular
+ tf = c.faces[font]
+ }
+ if tf == nil {
+ font := font
+ font.Style = Regular
+ font.Weight = Normal
+ tf = c.faces[font]
+ }
+ return tf
+}
+
+func NewCache(collection []FontFace) *Cache {
+ c := &Cache{
+ faces: make(map[Font]*faceCache),
+ }
+ for i, ff := range collection {
+ if i == 0 {
+ c.def = ff.Font.Typeface
+ }
+ c.faces[ff.Font] = &faceCache{face: ff.Face}
+ }
+ return c
+}
+
+// Layout implements the Shaper interface.
+func (s *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int,
+ txt io.Reader) ([]Line, error) {
+ cache := s.lookup(font)
+ return cache.face.Layout(size, maxWidth, txt)
+}
+
+// LayoutString is a caching implementation of the Shaper interface.
+func (s *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int,
+ str string) []Line {
+ cache := s.lookup(font)
+ return cache.layout(size, maxWidth, str)
+}
+
+// Shape is a caching implementation of the Shaper interface. Shape assumes that the layout
+// argument is unchanged from a call to Layout or LayoutString.
+func (s *Cache) Shape(font Font, size fixed.Int26_6, layout Layout) op.CallOp {
+ cache := s.lookup(font)
+ return cache.shape(size, layout)
+}
+
+func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int,
+ str string) []Line {
+ if f == nil {
+ return nil
+ }
+ lk := layoutKey{
+ ppem: ppem,
+ maxWidth: maxWidth,
+ str: str,
+ }
+ if l, ok := f.layoutCache.Get(lk); ok {
+ return l
+ }
+ l, _ := f.face.Layout(ppem, maxWidth, strings.NewReader(str))
+ f.layoutCache.Put(lk, l)
+ return l
+}
+
+func (f *faceCache) shape(ppem fixed.Int26_6, layout Layout) op.CallOp {
+ if f == nil {
+ return op.CallOp{}
+ }
+ pk := pathKey{
+ ppem: ppem,
+ str: layout.Text,
+ }
+ if clip, ok := f.pathCache.Get(pk); ok {
+ return clip
+ }
+ clip := f.face.Shape(ppem, layout)
+ f.pathCache.Put(pk, clip)
+ return clip
+}
diff --git a/gio/text/text.go b/gio/text/text.go
new file mode 100644
index 0000000..b50cc8a
--- /dev/null
+++ b/gio/text/text.go
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package text
+
+import (
+ "io"
+
+ "golang.org/x/image/math/fixed"
+
+ "realy.lol/gio/op"
+)
+
+// A Line contains the measurements of a line of text.
+type Line struct {
+ Layout Layout
+ // Width is the width of the line.
+ Width fixed.Int26_6
+ // Ascent is the height above the baseline.
+ Ascent fixed.Int26_6
+ // Descent is the height below the baseline, including
+ // the line gap.
+ Descent fixed.Int26_6
+ // Bounds is the visible bounds of the line.
+ Bounds fixed.Rectangle26_6
+}
+
+type Layout struct {
+ Text string
+ Advances []fixed.Int26_6
+}
+
+// Style is the font style.
+type Style int
+
+// Weight is a font weight, in CSS units subtracted 400 so the zero value
+// is normal text weight.
+type Weight int
+
+// Font specify a particular typeface variant, style and weight.
+type Font struct {
+ Typeface Typeface
+ Variant Variant
+ Style Style
+ // Weight is the text weight. If zero, Normal is used instead.
+ Weight Weight
+}
+
+// Face implements text layout and shaping for a particular font. All
+// methods must be safe for concurrent use.
+type Face interface {
+ Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error)
+ Shape(ppem fixed.Int26_6, str Layout) op.CallOp
+}
+
+// Typeface identifies a particular typeface design. The empty
+// string denotes the default typeface.
+type Typeface string
+
+// Variant denotes a typeface variant such as "Mono" or "Smallcaps".
+type Variant string
+
+type Alignment uint8
+
+const (
+ Start Alignment = iota
+ End
+ Middle
+)
+
+const (
+ Regular Style = iota
+ Italic
+)
+
+const (
+ Normal Weight = 400 - 400
+ Medium Weight = 500 - 400
+ Bold Weight = 600 - 400
+)
+
+func (a Alignment) String() string {
+ switch a {
+ case Start:
+ return "Start"
+ case End:
+ return "End"
+ case Middle:
+ return "Middle"
+ default:
+ panic("unreachable")
+ }
+}
diff --git a/gio/unit/unit.go b/gio/unit/unit.go
new file mode 100644
index 0000000..fd2245c
--- /dev/null
+++ b/gio/unit/unit.go
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+/*
+
+Package unit implements device independent units and values.
+
+A Value is a value with a Unit attached.
+
+Device independent pixel, or dp, is the unit for sizes independent of
+the underlying display device.
+
+Scaled pixels, or sp, is the unit for text sizes. An sp is like dp with
+text scaling applied.
+
+Finally, pixels, or px, is the unit for display dependent pixels. Their
+size vary between platforms and displays.
+
+To maintain a constant visual size across platforms and displays, always
+use dps or sps to define user interfaces. Only use pixels for derived
+values.
+
+*/
+package unit
+
+import (
+ "fmt"
+ "math"
+)
+
+// Value is a value with a unit.
+type Value struct {
+ V float32
+ U Unit
+}
+
+// Unit represents a unit for a Value.
+type Unit uint8
+
+// Metric converts Values to device-dependent pixels, px. The zero
+// value represents a 1-to-1 scale from dp, sp to pixels.
+type Metric struct {
+ // PxPerDp is the device-dependent pixels per dp.
+ PxPerDp float32
+ // PxPerSp is the device-dependent pixels per sp.
+ PxPerSp float32
+}
+
+const (
+ // UnitPx represent device pixels in the resolution of
+ // the underlying display.
+ UnitPx Unit = iota
+ // UnitDp represents device independent pixels. 1 dp will
+ // have the same apparent size across platforms and
+ // display resolutions.
+ UnitDp
+ // UnitSp is like UnitDp but for font sizes.
+ UnitSp
+)
+
+// Px returns the Value for v device pixels.
+func Px(v float32) Value {
+ return Value{V: v, U: UnitPx}
+}
+
+// Dp returns the Value for v device independent
+// pixels.
+func Dp(v float32) Value {
+ return Value{V: v, U: UnitDp}
+}
+
+// Sp returns the Value for v scaled dps.
+func Sp(v float32) Value {
+ return Value{V: v, U: UnitSp}
+}
+
+// Scale returns the value scaled by s.
+func (v Value) Scale(s float32) Value {
+ v.V *= s
+ return v
+}
+
+func (v Value) String() string {
+ return fmt.Sprintf("%g%s", v.V, v.U)
+}
+
+func (u Unit) String() string {
+ switch u {
+ case UnitPx:
+ return "px"
+ case UnitDp:
+ return "dp"
+ case UnitSp:
+ return "sp"
+ default:
+ panic("unknown unit")
+ }
+}
+
+// Add a list of Values.
+func Add(c Metric, values ...Value) Value {
+ var sum Value
+ for _, v := range values {
+ sum, v = compatible(c, sum, v)
+ sum.V += v.V
+ }
+ return sum
+}
+
+// Max returns the maximum of a list of Values.
+func Max(c Metric, values ...Value) Value {
+ var max Value
+ for _, v := range values {
+ max, v = compatible(c, max, v)
+ if v.V > max.V {
+ max.V = v.V
+ }
+ }
+ return max
+}
+
+func (c Metric) Px(v Value) int {
+ var r float32
+ switch v.U {
+ case UnitPx:
+ r = v.V
+ case UnitDp:
+ s := c.PxPerDp
+ if s == 0 {
+ s = 1
+ }
+ r = s * v.V
+ case UnitSp:
+ s := c.PxPerSp
+ if s == 0 {
+ s = 1
+ }
+ r = s * v.V
+ default:
+ panic("unknown unit")
+ }
+ return int(math.Round(float64(r)))
+}
+
+func compatible(c Metric, v1, v2 Value) (Value, Value) {
+ if v1.U == v2.U {
+ return v1, v2
+ }
+ if v1.V == 0 {
+ v1.U = v2.U
+ return v1, v2
+ }
+ if v2.V == 0 {
+ v2.U = v1.U
+ return v1, v2
+ }
+ return Px(float32(c.Px(v1))), Px(float32(c.Px(v2)))
+}
diff --git a/gio/widget/bool.go b/gio/widget/bool.go
new file mode 100644
index 0000000..feb4a8a
--- /dev/null
+++ b/gio/widget/bool.go
@@ -0,0 +1,44 @@
+package widget
+
+import (
+ "realy.lol/gio/layout"
+)
+
+type Bool struct {
+ Value bool
+
+ clk Clickable
+
+ changed bool
+}
+
+// Changed reports whether Value has changed since the last
+// call to Changed.
+func (b *Bool) Changed() bool {
+ changed := b.changed
+ b.changed = false
+ return changed
+}
+
+// Hovered returns whether pointer is over the element.
+func (b *Bool) Hovered() bool {
+ return b.clk.Hovered()
+}
+
+// Pressed returns whether pointer is pressing the element.
+func (b *Bool) Pressed() bool {
+ return b.clk.Pressed()
+}
+
+func (b *Bool) History() []Press {
+ return b.clk.History()
+}
+
+func (b *Bool) Layout(gtx layout.Context) layout.Dimensions {
+ dims := b.clk.Layout(gtx)
+ for b.clk.Clicked() {
+ b.Value = !b.Value
+ b.changed = true
+ }
+ return dims
+}
diff --git a/gio/widget/border.go b/gio/widget/border.go
new file mode 100644
index 0000000..e4bed6a
--- /dev/null
+++ b/gio/widget/border.go
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image/color"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+)
+
+// Border lays out a widget and draws a border inside it.
+type Border struct {
+ Color color.NRGBA
+ CornerRadius unit.Value
+ Width unit.Value
+}
+
+func (b Border) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
+ dims := w(gtx)
+ sz := layout.FPt(dims.Size)
+
+ rr := float32(gtx.Px(b.CornerRadius))
+ width := float32(gtx.Px(b.Width))
+ sz.X -= width
+ sz.Y -= width
+
+ r := f32.Rectangle{Max: sz}
+ r = r.Add(f32.Point{X: width * 0.5, Y: width * 0.5})
+
+ paint.FillShape(gtx.Ops,
+ b.Color,
+ clip.Stroke{
+ Path: clip.UniformRRect(r, rr).Path(gtx.Ops),
+ Style: clip.StrokeStyle{Width: width},
+ }.Op(),
+ )
+
+ return dims
+}
diff --git a/gio/widget/buffer.go b/gio/widget/buffer.go
new file mode 100644
index 0000000..e658d56
--- /dev/null
+++ b/gio/widget/buffer.go
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "io"
+ "strings"
+ "unicode/utf8"
+)
+
+// editBuffer implements a gap buffer for text editing.
+type editBuffer struct {
+ // pos is the byte position for Read and ReadRune.
+ pos int
+
+ // The gap start and end in bytes.
+ gapstart, gapend int
+ text []byte
+
+ // changed tracks whether the buffer content
+ // has changed since the last call to Changed.
+ changed bool
+}
+
+const minSpace = 5
+
+func (e *editBuffer) Changed() bool {
+ c := e.changed
+ e.changed = false
+ return c
+}
+
+func (e *editBuffer) deleteRunes(caret, runes int) int {
+ e.moveGap(caret, 0)
+ for ; runes < 0 && e.gapstart > 0; runes++ {
+ _, s := utf8.DecodeLastRune(e.text[:e.gapstart])
+ e.gapstart -= s
+ caret -= s
+ e.changed = e.changed || s > 0
+ }
+ for ; runes > 0 && e.gapend < len(e.text); runes-- {
+ _, s := utf8.DecodeRune(e.text[e.gapend:])
+ e.gapend += s
+ e.changed = e.changed || s > 0
+ }
+ return caret
+}
+
+// moveGap moves the gap to the caret position. After returning,
+// the gap is guaranteed to be at least space bytes long.
+func (e *editBuffer) moveGap(caret, space int) {
+ if e.gapLen() < space {
+ if space < minSpace {
+ space = minSpace
+ }
+ txt := make([]byte, e.len()+space)
+ // Expand to capacity.
+ txt = txt[:cap(txt)]
+ gaplen := len(txt) - e.len()
+ if caret > e.gapstart {
+ copy(txt, e.text[:e.gapstart])
+ copy(txt[caret+gaplen:], e.text[caret:])
+ copy(txt[e.gapstart:], e.text[e.gapend:caret+e.gapLen()])
+ } else {
+ copy(txt, e.text[:caret])
+ copy(txt[e.gapstart+gaplen:], e.text[e.gapend:])
+ copy(txt[caret+gaplen:], e.text[caret:e.gapstart])
+ }
+ e.text = txt
+ e.gapstart = caret
+ e.gapend = e.gapstart + gaplen
+ } else {
+ if caret > e.gapstart {
+ copy(e.text[e.gapstart:], e.text[e.gapend:caret+e.gapLen()])
+ } else {
+ copy(e.text[caret+e.gapLen():], e.text[caret:e.gapstart])
+ }
+ l := e.gapLen()
+ e.gapstart = caret
+ e.gapend = e.gapstart + l
+ }
+}
+
+func (e *editBuffer) len() int {
+ return len(e.text) - e.gapLen()
+}
+
+func (e *editBuffer) gapLen() int {
+ return e.gapend - e.gapstart
+}
+
+func (e *editBuffer) Reset() {
+ e.Seek(0, io.SeekStart)
+}
+
+// Seek implements io.Seeker
+func (e *editBuffer) Seek(offset int64, whence int) (ret int64, err error) {
+ switch whence {
+ case io.SeekStart:
+ e.pos = int(offset)
+ case io.SeekCurrent:
+ e.pos += int(offset)
+ case io.SeekEnd:
+ e.pos = e.len() - int(offset)
+ }
+ if e.pos < 0 {
+ e.pos = 0
+ } else if e.pos > e.len() {
+ e.pos = e.len()
+ }
+ return int64(e.pos), nil
+}
+
+func (e *editBuffer) Read(p []byte) (int, error) {
+ if e.pos == e.len() {
+ return 0, io.EOF
+ }
+ var total int
+ if e.pos < e.gapstart {
+ n := copy(p, e.text[e.pos:e.gapstart])
+ p = p[n:]
+ total += n
+ e.pos += n
+ }
+ if e.pos >= e.gapstart {
+ n := copy(p, e.text[e.pos+e.gapLen():])
+ total += n
+ e.pos += n
+ }
+ if e.pos > e.len() {
+ panic("hey!")
+ }
+ return total, nil
+}
+
+func (e *editBuffer) ReadRune() (rune, int, error) {
+ if e.pos == e.len() {
+ return 0, 0, io.EOF
+ }
+ r, s := e.runeAt(e.pos)
+ e.pos += s
+ return r, s, nil
+}
+
+func (e *editBuffer) String() string {
+ var b strings.Builder
+ b.Grow(e.len())
+ b.Write(e.text[:e.gapstart])
+ b.Write(e.text[e.gapend:])
+ return b.String()
+}
+
+func (e *editBuffer) prepend(caret int, s string) {
+ e.moveGap(caret, len(s))
+ copy(e.text[caret:], s)
+ e.gapstart += len(s)
+ e.changed = e.changed || len(s) > 0
+}
+
+func (e *editBuffer) runeBefore(idx int) (rune, int) {
+ if idx > e.gapstart {
+ idx += e.gapLen()
+ }
+ return utf8.DecodeLastRune(e.text[:idx])
+}
+
+func (e *editBuffer) runeAt(idx int) (rune, int) {
+ if idx >= e.gapstart {
+ idx += e.gapLen()
+ }
+ return utf8.DecodeRune(e.text[idx:])
+}
diff --git a/gio/widget/button.go b/gio/widget/button.go
new file mode 100644
index 0000000..2c23c5d
--- /dev/null
+++ b/gio/widget/button.go
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+ "time"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/gesture"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+)
+
+// Clickable represents a clickable area.
+type Clickable struct {
+ click gesture.Click
+ clicks []Click
+ // prevClicks is the index into clicks that marks the clicks
+ // from the most recent Layout call. prevClicks is used to keep
+ // clicks bounded.
+ prevClicks int
+ history []Press
+}
+
+// Click represents a click.
+type Click struct {
+ Modifiers key.Modifiers
+ NumClicks int
+}
+
+// Press represents a past pointer press.
+type Press struct {
+ // Position of the press.
+ Position f32.Point
+ // Start is when the press began.
+ Start time.Time
+ // End is when the press was ended by a release or cancel.
+ // A zero End means it hasn't ended yet.
+ End time.Time
+ // Cancelled is true for cancelled presses.
+ Cancelled bool
+}
+
+// Click executes a simple programmatic click
+func (b *Clickable) Click() {
+ b.clicks = append(b.clicks, Click{
+ Modifiers: 0,
+ NumClicks: 1,
+ })
+}
+
+// Clicked reports whether there are pending clicks as would be
+// reported by Clicks. If so, Clicked removes the earliest click.
+func (b *Clickable) Clicked() bool {
+ if len(b.clicks) == 0 {
+ return false
+ }
+ n := copy(b.clicks, b.clicks[1:])
+ b.clicks = b.clicks[:n]
+ if b.prevClicks > 0 {
+ b.prevClicks--
+ }
+ return true
+}
+
+// Hovered returns whether pointer is over the element.
+func (b *Clickable) Hovered() bool {
+ return b.click.Hovered()
+}
+
+// Pressed returns whether pointer is pressing the element.
+func (b *Clickable) Pressed() bool {
+ return b.click.Pressed()
+}
+
+// Clicks returns and clear the clicks since the last call to Clicks.
+func (b *Clickable) Clicks() []Click {
+ clicks := b.clicks
+ b.clicks = nil
+ b.prevClicks = 0
+ return clicks
+}
+
+// History is the past pointer presses useful for drawing markers.
+// History is retained for a short duration (about a second).
+func (b *Clickable) History() []Press {
+ return b.history
+}
+
+// Layout and update the button state
+func (b *Clickable) Layout(gtx layout.Context) layout.Dimensions {
+ b.update(gtx)
+ stack := op.Save(gtx.Ops)
+ pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
+ b.click.Add(gtx.Ops)
+ stack.Load()
+ for len(b.history) > 0 {
+ c := b.history[0]
+ if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second {
+ break
+ }
+ n := copy(b.history, b.history[1:])
+ b.history = b.history[:n]
+ }
+ return layout.Dimensions{Size: gtx.Constraints.Min}
+}
+
+// update the button state by processing events.
+func (b *Clickable) update(gtx layout.Context) {
+ // Flush clicks from before the last update.
+ n := copy(b.clicks, b.clicks[b.prevClicks:])
+ b.clicks = b.clicks[:n]
+ b.prevClicks = n
+
+ for _, e := range b.click.Events(gtx) {
+ switch e.Type {
+ case gesture.TypeClick:
+ b.clicks = append(b.clicks, Click{
+ Modifiers: e.Modifiers,
+ NumClicks: e.NumClicks,
+ })
+ if l := len(b.history); l > 0 {
+ b.history[l-1].End = gtx.Now
+ }
+ case gesture.TypeCancel:
+ for i := range b.history {
+ b.history[i].Cancelled = true
+ if b.history[i].End.IsZero() {
+ b.history[i].End = gtx.Now
+ }
+ }
+ case gesture.TypePress:
+ b.history = append(b.history, Press{
+ Position: e.Position,
+ Start: gtx.Now,
+ })
+ }
+ }
+}
diff --git a/gio/widget/doc.go b/gio/widget/doc.go
new file mode 100644
index 0000000..df4e55f
--- /dev/null
+++ b/gio/widget/doc.go
@@ -0,0 +1,6 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package widget implements state tracking and event handling of
+// common user interface controls. To draw widgets, use a theme
+// packages such as package realy.lol/gio/widget/material.
+package widget
diff --git a/gio/widget/editor.go b/gio/widget/editor.go
new file mode 100644
index 0000000..e44f54f
--- /dev/null
+++ b/gio/widget/editor.go
@@ -0,0 +1,1328 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "bufio"
+ "bytes"
+ "image"
+ "io"
+ "math"
+ "runtime"
+ "sort"
+ "strings"
+ "time"
+ "unicode"
+ "unicode/utf8"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/gesture"
+ "realy.lol/gio/io/clipboard"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+
+ "golang.org/x/image/math/fixed"
+)
+
+// Editor implements an editable and scrollable text area.
+type Editor struct {
+ Alignment text.Alignment
+ // SingleLine force the text to stay on a single line.
+ // SingleLine also sets the scrolling direction to
+ // horizontal.
+ SingleLine bool
+ // Submit enabled translation of carriage return keys to SubmitEvents.
+ // If not enabled, carriage returns are inserted as newlines in the text.
+ Submit bool
+ // Mask replaces the visual display of each rune in the contents with the given rune.
+ // Newline characters are not masked. When non-zero, the unmasked contents
+ // are accessed by Len, Text, and SetText.
+ Mask rune
+
+ eventKey int
+ font text.Font
+ shaper text.Shaper
+ textSize fixed.Int26_6
+ blinkStart time.Time
+ focused bool
+ rr editBuffer
+ maskReader maskReader
+ lastMask rune
+ maxWidth int
+ viewSize image.Point
+ valid bool
+ lines []text.Line
+ shapes []line
+ dims layout.Dimensions
+ requestFocus bool
+
+ caret struct {
+ on bool
+ scroll bool
+ // start is the current caret position, and also the start position of
+ // selected text. end is the end positon of selected text. If start.ofs
+ // == end.ofs, then there's no selection. Note that it's possible (and
+ // common) that the caret (start) is after the end, e.g. after
+ // Shift-DownArrow.
+ start combinedPos
+ end combinedPos
+ }
+
+ dragging bool
+ dragger gesture.Drag
+ scroller gesture.Scroll
+ scrollOff image.Point
+
+ clicker gesture.Click
+
+ // events is the list of events not yet processed.
+ events []EditorEvent
+ // prevEvents is the number of events from the previous frame.
+ prevEvents int
+}
+
+type maskReader struct {
+ // rr is the underlying reader.
+ rr io.RuneReader
+ maskBuf [utf8.UTFMax]byte
+ // mask is the utf-8 encoded mask rune.
+ mask []byte
+ // overflow contains excess mask bytes left over after the last Read call.
+ overflow []byte
+}
+
+// combinedPos is a point in the editor.
+type combinedPos struct {
+ // editorBuffer offset. The other three fields are based off of this one.
+ ofs int
+
+ // lineCol.Y = line (offset into Editor.lines), and X = col (offset into
+ // Editor.lines[Y])
+ lineCol screenPos
+
+ // Pixel coordinates
+ x fixed.Int26_6
+ y int
+
+ // xoff is the offset to the current position when moving between lines.
+ xoff fixed.Int26_6
+}
+
+type selectionAction int
+
+const (
+ selectionExtend selectionAction = iota
+ selectionClear
+)
+
+func (m *maskReader) Reset(r io.RuneReader, mr rune) {
+ m.rr = r
+ n := utf8.EncodeRune(m.maskBuf[:], mr)
+ m.mask = m.maskBuf[:n]
+}
+
+// Read reads from the underlying reader and replaces every
+// rune with the mask rune.
+func (m *maskReader) Read(b []byte) (n int, err error) {
+ for len(b) > 0 {
+ var replacement []byte
+ if len(m.overflow) > 0 {
+ replacement = m.overflow
+ } else {
+ var r rune
+ r, _, err = m.rr.ReadRune()
+ if err != nil {
+ break
+ }
+ if r == '\n' {
+ replacement = []byte{'\n'}
+ } else {
+ replacement = m.mask
+ }
+ }
+ nn := copy(b, replacement)
+ m.overflow = replacement[nn:]
+ n += nn
+ b = b[nn:]
+ }
+ return n, err
+}
+
+type EditorEvent interface {
+ isEditorEvent()
+}
+
+// A ChangeEvent is generated for every user change to the text.
+type ChangeEvent struct{}
+
+// A SubmitEvent is generated when Submit is set
+// and a carriage return key is pressed.
+type SubmitEvent struct {
+ Text string
+}
+
+// A SelectEvent is generated when the user selects some text, or changes the
+// selection (e.g. with a shift-click), including if they remove the
+// selection. The selected text is not part of the event, on the theory that
+// it could be a relatively expensive operation (for a large editor), most
+// applications won't actually care about it, and those that do can call
+// Editor.SelectedText() (which can be empty).
+type SelectEvent struct{}
+
+type line struct {
+ offset image.Point
+ clip op.CallOp
+ selected bool
+ selectionYOffs int
+ selectionSize image.Point
+}
+
+const (
+ blinksPerSecond = 1
+ maxBlinkDuration = 10 * time.Second
+)
+
+// Events returns available editor events.
+func (e *Editor) Events() []EditorEvent {
+ events := e.events
+ e.events = nil
+ e.prevEvents = 0
+ return events
+}
+
+func (e *Editor) processEvents(gtx layout.Context) {
+ // Flush events from before the previous Layout.
+ n := copy(e.events, e.events[e.prevEvents:])
+ e.events = e.events[:n]
+ e.prevEvents = n
+
+ if e.shaper == nil {
+ // Can't process events without a shaper.
+ return
+ }
+ oldStart, oldLen := min(e.caret.start.ofs,
+ e.caret.end.ofs), e.SelectionLen()
+ e.processPointer(gtx)
+ e.processKey(gtx)
+ // Queue a SelectEvent if the selection changed, including if it went away.
+ if newStart, newLen := min(e.caret.start.ofs,
+ e.caret.end.ofs), e.SelectionLen(); oldStart != newStart || oldLen != newLen {
+ e.events = append(e.events, SelectEvent{})
+ }
+}
+
+func (e *Editor) makeValid(positions ...*combinedPos) {
+ if e.valid {
+ return
+ }
+ e.lines, e.dims = e.layoutText(e.shaper)
+ e.makeValidCaret(positions...)
+ e.valid = true
+}
+
+func (e *Editor) processPointer(gtx layout.Context) {
+ sbounds := e.scrollBounds()
+ var smin, smax int
+ var axis gesture.Axis
+ if e.SingleLine {
+ axis = gesture.Horizontal
+ smin, smax = sbounds.Min.X, sbounds.Max.X
+ } else {
+ axis = gesture.Vertical
+ smin, smax = sbounds.Min.Y, sbounds.Max.Y
+ }
+ sdist := e.scroller.Scroll(gtx.Metric, gtx, gtx.Now, axis)
+ var soff int
+ if e.SingleLine {
+ e.scrollRel(sdist, 0)
+ soff = e.scrollOff.X
+ } else {
+ e.scrollRel(0, sdist)
+ soff = e.scrollOff.Y
+ }
+ for _, evt := range e.clickDragEvents(gtx) {
+ switch evt := evt.(type) {
+ case gesture.ClickEvent:
+ switch {
+ case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse,
+ evt.Type == gesture.TypeClick:
+ prevCaretPos := e.caret.start
+ e.blinkStart = gtx.Now
+ e.moveCoord(image.Point{
+ X: int(math.Round(float64(evt.Position.X))),
+ Y: int(math.Round(float64(evt.Position.Y))),
+ })
+ e.requestFocus = true
+ if e.scroller.State() != gesture.StateFlinging {
+ e.caret.scroll = true
+ }
+
+ if evt.Modifiers == key.ModShift {
+ // If they clicked closer to the end, then change the end to
+ // where the caret used to be (effectively swapping start & end).
+ if abs(e.caret.end.ofs-e.caret.start.ofs) < abs(e.caret.start.ofs-prevCaretPos.ofs) {
+ e.caret.end = prevCaretPos
+ }
+ } else {
+ e.ClearSelection()
+ }
+ e.dragging = true
+
+ // Process a double-click.
+ if evt.NumClicks == 2 {
+ e.moveWord(-1, selectionClear)
+ e.moveWord(1, selectionExtend)
+ e.dragging = false
+ }
+ }
+ case pointer.Event:
+ release := false
+ switch {
+ case evt.Type == pointer.Release && evt.Source == pointer.Mouse:
+ release = true
+ fallthrough
+ case evt.Type == pointer.Drag && evt.Source == pointer.Mouse:
+ if e.dragging {
+ e.blinkStart = gtx.Now
+ e.moveCoord(image.Point{
+ X: int(math.Round(float64(evt.Position.X))),
+ Y: int(math.Round(float64(evt.Position.Y))),
+ })
+ e.caret.scroll = true
+
+ if release {
+ e.dragging = false
+ }
+ }
+ }
+ }
+ }
+
+ if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) {
+ e.scroller.Stop()
+ }
+}
+
+func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event {
+ var combinedEvents []event.Event
+ for _, evt := range e.clicker.Events(gtx) {
+ combinedEvents = append(combinedEvents, evt)
+ }
+ for _, evt := range e.dragger.Events(gtx.Metric, gtx, gesture.Both) {
+ combinedEvents = append(combinedEvents, evt)
+ }
+ return combinedEvents
+}
+
+func (e *Editor) processKey(gtx layout.Context) {
+ if e.rr.Changed() {
+ e.events = append(e.events, ChangeEvent{})
+ }
+ for _, ke := range gtx.Events(&e.eventKey) {
+ e.blinkStart = gtx.Now
+ switch ke := ke.(type) {
+ case key.FocusEvent:
+ e.focused = ke.Focus
+ case key.Event:
+ if !e.focused || ke.State != key.Press {
+ break
+ }
+ if e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) {
+ if !ke.Modifiers.Contain(key.ModShift) {
+ e.events = append(e.events, SubmitEvent{
+ Text: e.Text(),
+ })
+ continue
+ }
+ }
+ if e.command(gtx, ke) {
+ e.caret.scroll = true
+ e.scroller.Stop()
+ }
+ case key.EditEvent:
+ e.caret.scroll = true
+ e.scroller.Stop()
+ e.append(ke.Text)
+ // Complete a paste event, initiated by Shortcut-V in Editor.command().
+ case clipboard.Event:
+ e.caret.scroll = true
+ e.scroller.Stop()
+ e.append(ke.Text)
+ }
+ if e.rr.Changed() {
+ e.events = append(e.events, ChangeEvent{})
+ }
+ }
+}
+
+func (e *Editor) moveLines(distance int, selAct selectionAction) {
+ e.caret.start = e.movePosToLine(e.caret.start,
+ e.caret.start.x+e.caret.start.xoff, e.caret.start.lineCol.Y+distance)
+ e.updateSelection(selAct)
+}
+
+func (e *Editor) command(gtx layout.Context, k key.Event) bool {
+ modSkip := key.ModCtrl
+ if runtime.GOOS == "darwin" {
+ modSkip = key.ModAlt
+ }
+ moveByWord := k.Modifiers.Contain(modSkip)
+ selAct := selectionClear
+ if k.Modifiers.Contain(key.ModShift) {
+ selAct = selectionExtend
+ }
+ switch k.Name {
+ case key.NameReturn, key.NameEnter:
+ e.append("\n")
+ case key.NameDeleteBackward:
+ if moveByWord {
+ e.deleteWord(-1)
+ } else {
+ e.Delete(-1)
+ }
+ case key.NameDeleteForward:
+ if moveByWord {
+ e.deleteWord(1)
+ } else {
+ e.Delete(1)
+ }
+ case key.NameUpArrow:
+ e.moveLines(-1, selAct)
+ case key.NameDownArrow:
+ e.moveLines(+1, selAct)
+ case key.NameLeftArrow:
+ if moveByWord {
+ e.moveWord(-1, selAct)
+ } else {
+ if selAct == selectionClear {
+ e.ClearSelection()
+ }
+ e.MoveCaret(-1, -1*int(selAct))
+ }
+ case key.NameRightArrow:
+ if moveByWord {
+ e.moveWord(1, selAct)
+ } else {
+ if selAct == selectionClear {
+ e.ClearSelection()
+ }
+ e.MoveCaret(1, int(selAct))
+ }
+ case key.NamePageUp:
+ e.movePages(-1, selAct)
+ case key.NamePageDown:
+ e.movePages(+1, selAct)
+ case key.NameHome:
+ e.moveStart(selAct)
+ case key.NameEnd:
+ e.moveEnd(selAct)
+ // Initiate a paste operation, by requesting the clipboard contents; other
+ // half is in Editor.processKey() under clipboard.Event.
+ case "V":
+ if k.Modifiers != key.ModShortcut {
+ return false
+ }
+ clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops)
+ // Copy or Cut selection -- ignored if nothing selected.
+ case "C", "X":
+ if k.Modifiers != key.ModShortcut {
+ return false
+ }
+ if text := e.SelectedText(); text != "" {
+ clipboard.WriteOp{Text: text}.Add(gtx.Ops)
+ if k.Name == "X" {
+ e.Delete(1)
+ }
+ }
+ // Select all
+ case "A":
+ if k.Modifiers != key.ModShortcut {
+ return false
+ }
+ e.caret.end, e.caret.start = e.offsetToScreenPos2(0, e.Len())
+ default:
+ return false
+ }
+ return true
+}
+
+// Focus requests the input focus for the Editor.
+func (e *Editor) Focus() {
+ e.requestFocus = true
+}
+
+// Focused returns whether the editor is focused or not.
+func (e *Editor) Focused() bool {
+ return e.focused
+}
+
+// Layout lays out the editor.
+func (e *Editor) Layout(gtx layout.Context, sh text.Shaper, font text.Font,
+ size unit.Value) layout.Dimensions {
+ textSize := fixed.I(gtx.Px(size))
+ if e.font != font || e.textSize != textSize {
+ e.invalidate()
+ e.font = font
+ e.textSize = textSize
+ }
+ maxWidth := gtx.Constraints.Max.X
+ if e.SingleLine {
+ maxWidth = inf
+ }
+ if maxWidth != e.maxWidth {
+ e.maxWidth = maxWidth
+ e.invalidate()
+ }
+ if sh != e.shaper {
+ e.shaper = sh
+ e.invalidate()
+ }
+ if e.Mask != e.lastMask {
+ e.lastMask = e.Mask
+ e.invalidate()
+ }
+
+ e.makeValid()
+ e.processEvents(gtx)
+ e.makeValid()
+
+ if viewSize := gtx.Constraints.Constrain(e.dims.Size); viewSize != e.viewSize {
+ e.viewSize = viewSize
+ e.invalidate()
+ }
+ e.makeValid()
+
+ return e.layout(gtx)
+}
+
+func (e *Editor) layout(gtx layout.Context) layout.Dimensions {
+ // Adjust scrolling for new viewport and layout.
+ e.scrollRel(0, 0)
+
+ if e.caret.scroll {
+ e.caret.scroll = false
+ e.scrollToCaret()
+ }
+
+ off := image.Point{
+ X: -e.scrollOff.X,
+ Y: -e.scrollOff.Y,
+ }
+ clip := textPadding(e.lines)
+ clip.Max = clip.Max.Add(e.viewSize)
+ startSel, endSel := sortPoints(e.caret.start.lineCol, e.caret.end.lineCol)
+ it := segmentIterator{
+ startSel: startSel,
+ endSel: endSel,
+ Lines: e.lines,
+ Clip: clip,
+ Alignment: e.Alignment,
+ Width: e.viewSize.X,
+ Offset: off,
+ }
+ e.shapes = e.shapes[:0]
+ for {
+ layout, off, selected, yOffs, size, ok := it.Next()
+ if !ok {
+ break
+ }
+ path := e.shaper.Shape(e.font, e.textSize, layout)
+ e.shapes = append(e.shapes, line{off, path, selected, yOffs, size})
+ }
+
+ key.InputOp{Tag: &e.eventKey}.Add(gtx.Ops)
+ if e.requestFocus {
+ key.FocusOp{Tag: &e.eventKey}.Add(gtx.Ops)
+ key.SoftKeyboardOp{Show: true}.Add(gtx.Ops)
+ }
+ e.requestFocus = false
+ pointerPadding := gtx.Px(unit.Dp(4))
+ r := image.Rectangle{Max: e.viewSize}
+ r.Min.X -= pointerPadding
+ r.Min.Y -= pointerPadding
+ r.Max.X += pointerPadding
+ r.Max.X += pointerPadding
+ pointer.Rect(r).Add(gtx.Ops)
+ pointer.CursorNameOp{Name: pointer.CursorText}.Add(gtx.Ops)
+
+ var scrollRange image.Rectangle
+ if e.SingleLine {
+ scrollRange.Min.X = -e.scrollOff.X
+ scrollRange.Max.X = max(0, e.dims.Size.X-(e.scrollOff.X+e.viewSize.X))
+ } else {
+ scrollRange.Min.Y = -e.scrollOff.Y
+ scrollRange.Max.Y = max(0, e.dims.Size.Y-(e.scrollOff.Y+e.viewSize.Y))
+ }
+ e.scroller.Add(gtx.Ops, scrollRange)
+
+ e.clicker.Add(gtx.Ops)
+ e.dragger.Add(gtx.Ops)
+ e.caret.on = false
+ if e.focused {
+ now := gtx.Now
+ dt := now.Sub(e.blinkStart)
+ blinking := dt < maxBlinkDuration
+ const timePerBlink = time.Second / blinksPerSecond
+ nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2))
+ if blinking {
+ redraw := op.InvalidateOp{At: nextBlink}
+ redraw.Add(gtx.Ops)
+ }
+ e.caret.on = e.focused && (!blinking || dt%timePerBlink < timePerBlink/2)
+ }
+
+ return layout.Dimensions{Size: e.viewSize, Baseline: e.dims.Baseline}
+}
+
+// PaintSelection paints the contrasting background for selected text.
+func (e *Editor) PaintSelection(gtx layout.Context) {
+ cl := textPadding(e.lines)
+ cl.Max = cl.Max.Add(e.viewSize)
+ clip.Rect(cl).Add(gtx.Ops)
+ for _, shape := range e.shapes {
+ if !shape.selected {
+ continue
+ }
+ stack := op.Save(gtx.Ops)
+ offset := shape.offset
+ offset.Y += shape.selectionYOffs
+ op.Offset(layout.FPt(offset)).Add(gtx.Ops)
+ clip.Rect(image.Rectangle{Max: shape.selectionSize}).Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ stack.Load()
+ }
+}
+
+func (e *Editor) PaintText(gtx layout.Context) {
+ cl := textPadding(e.lines)
+ cl.Max = cl.Max.Add(e.viewSize)
+ clip.Rect(cl).Add(gtx.Ops)
+ for _, shape := range e.shapes {
+ stack := op.Save(gtx.Ops)
+ op.Offset(layout.FPt(shape.offset)).Add(gtx.Ops)
+ shape.clip.Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ stack.Load()
+ }
+}
+
+func (e *Editor) PaintCaret(gtx layout.Context) {
+ if !e.caret.on {
+ return
+ }
+ e.makeValid()
+ carWidth := fixed.I(gtx.Px(unit.Dp(1)))
+ carX := e.caret.start.x
+ carY := e.caret.start.y
+
+ defer op.Save(gtx.Ops).Load()
+ carX -= carWidth / 2
+ carAsc, carDesc := -e.lines[e.caret.start.lineCol.Y].Bounds.Min.Y, e.lines[e.caret.start.lineCol.Y].Bounds.Max.Y
+ carRect := image.Rectangle{
+ Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()},
+ Max: image.Point{X: carX.Ceil() + carWidth.Ceil(),
+ Y: carY + carDesc.Ceil()},
+ }
+ carRect = carRect.Add(image.Point{
+ X: -e.scrollOff.X,
+ Y: -e.scrollOff.Y,
+ })
+ cl := textPadding(e.lines)
+ // Account for caret width to each side.
+ whalf := (carWidth / 2).Ceil()
+ if cl.Max.X < whalf {
+ cl.Max.X = whalf
+ }
+ if cl.Min.X > -whalf {
+ cl.Min.X = -whalf
+ }
+ cl.Max = cl.Max.Add(e.viewSize)
+ carRect = cl.Intersect(carRect)
+ if !carRect.Empty() {
+ st := op.Save(gtx.Ops)
+ clip.Rect(carRect).Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ st.Load()
+ }
+}
+
+// Len is the length of the editor contents.
+func (e *Editor) Len() int {
+ return e.rr.len()
+}
+
+// Text returns the contents of the editor.
+func (e *Editor) Text() string {
+ return e.rr.String()
+}
+
+// SetText replaces the contents of the editor, clearing any selection first.
+func (e *Editor) SetText(s string) {
+ e.rr = editBuffer{}
+ e.caret.start = combinedPos{}
+ e.caret.end = combinedPos{}
+ e.prepend(s)
+}
+
+func (e *Editor) scrollBounds() image.Rectangle {
+ var b image.Rectangle
+ if e.SingleLine {
+ if len(e.lines) > 0 {
+ b.Min.X = align(e.Alignment, e.lines[0].Width, e.viewSize.X).Floor()
+ if b.Min.X > 0 {
+ b.Min.X = 0
+ }
+ }
+ b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X
+ } else {
+ b.Max.Y = e.dims.Size.Y - e.viewSize.Y
+ }
+ return b
+}
+
+func (e *Editor) scrollRel(dx, dy int) {
+ e.scrollAbs(e.scrollOff.X+dx, e.scrollOff.Y+dy)
+}
+
+func (e *Editor) scrollAbs(x, y int) {
+ e.scrollOff.X = x
+ e.scrollOff.Y = y
+ b := e.scrollBounds()
+ if e.scrollOff.X > b.Max.X {
+ e.scrollOff.X = b.Max.X
+ }
+ if e.scrollOff.X < b.Min.X {
+ e.scrollOff.X = b.Min.X
+ }
+ if e.scrollOff.Y > b.Max.Y {
+ e.scrollOff.Y = b.Max.Y
+ }
+ if e.scrollOff.Y < b.Min.Y {
+ e.scrollOff.Y = b.Min.Y
+ }
+}
+
+func (e *Editor) moveCoord(pos image.Point) {
+ var (
+ prevDesc fixed.Int26_6
+ carLine int
+ y int
+ )
+ for _, l := range e.lines {
+ y += (prevDesc + l.Ascent).Ceil()
+ prevDesc = l.Descent
+ if y+prevDesc.Ceil() >= pos.Y+e.scrollOff.Y {
+ break
+ }
+ carLine++
+ }
+ x := fixed.I(pos.X + e.scrollOff.X)
+ e.caret.start = e.movePosToLine(e.caret.start, x, carLine)
+ e.caret.start.xoff = 0
+}
+
+func (e *Editor) layoutText(s text.Shaper) ([]text.Line, layout.Dimensions) {
+ e.rr.Reset()
+ var r io.Reader = &e.rr
+ if e.Mask != 0 {
+ e.maskReader.Reset(&e.rr, e.Mask)
+ r = &e.maskReader
+ }
+ var lines []text.Line
+ if s != nil {
+ lines, _ = s.Layout(e.font, e.textSize, e.maxWidth, r)
+ } else {
+ lines, _ = nullLayout(r)
+ }
+ dims := linesDimens(lines)
+ for i := 0; i < len(lines)-1; i++ {
+ // To avoid layout flickering while editing, assume a soft newline takes
+ // up all available space.
+ if layout := lines[i].Layout; len(layout.Text) > 0 {
+ r := layout.Text[len(layout.Text)-1]
+ if r != '\n' {
+ dims.Size.X = e.maxWidth
+ break
+ }
+ }
+ }
+ return lines, dims
+}
+
+// CaretPos returns the line & column numbers of the caret.
+func (e *Editor) CaretPos() (line, col int) {
+ e.makeValid()
+ return e.caret.start.lineCol.Y, e.caret.start.lineCol.X
+}
+
+// CaretCoords returns the coordinates of the caret, relative to the
+// editor itself.
+func (e *Editor) CaretCoords() f32.Point {
+ e.makeValid()
+ return f32.Pt(float32(e.caret.start.x)/64, float32(e.caret.start.y))
+}
+
+// offsetToScreenPos2 is a utility function to shortcut the common case of
+// wanting the positions of exactly two offsets.
+func (e *Editor) offsetToScreenPos2(o1, o2 int) (combinedPos, combinedPos) {
+ cp1, iter := e.offsetToScreenPos(o1)
+ return cp1, iter(o2)
+}
+
+// offsetToScreenPos takes an offset into the editor text (e.g.
+// e.caret.end.ofs) and returns a combinedPos that corresponds to its current
+// screen position, as well as an iterator that lets you get the combinedPos
+// of a later offset. The offsets given to offsetToScreenPos and to the
+// returned iterator must be sorted, lowest first, and they must be valid (0
+// <= offset <= e.Len()).
+//
+// This function is written this way to take advantage of previous work done
+// for offsets after the first. Otherwise you have to start from the top each
+// time.
+func (e *Editor) offsetToScreenPos(offset int) (combinedPos,
+ func(int) combinedPos) {
+ var col, line, idx int
+ var x fixed.Int26_6
+
+ l := e.lines[line]
+ y := l.Ascent.Ceil()
+ prevDesc := l.Descent
+
+ iter := func(offset int) combinedPos {
+ LOOP:
+ for {
+ for ; col < len(l.Layout.Advances); col++ {
+ if idx >= offset {
+ break LOOP
+ }
+
+ x += l.Layout.Advances[col]
+ _, s := e.rr.runeAt(idx)
+ idx += s
+ }
+ if lastLine := line == len(e.lines)-1; lastLine || idx > offset {
+ break LOOP
+ }
+
+ line++
+ x = 0
+ col = 0
+ l = e.lines[line]
+ y += (prevDesc + l.Ascent).Ceil()
+ prevDesc = l.Descent
+ }
+ return combinedPos{
+ lineCol: screenPos{Y: line, X: col},
+ x: x + align(e.Alignment, e.lines[line].Width, e.viewSize.X),
+ y: y,
+ ofs: offset,
+ }
+ }
+ return iter(offset), iter
+}
+
+func (e *Editor) invalidate() {
+ e.valid = false
+}
+
+// Delete runes from the caret position. The sign of runes specifies the
+// direction to delete: positive is forward, negative is backward.
+//
+// If there is a selection, it is deleted and counts as a single rune.
+func (e *Editor) Delete(runes int) {
+ if runes == 0 {
+ return
+ }
+
+ if l := e.caret.end.ofs - e.caret.start.ofs; l != 0 {
+ e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, l)
+ runes -= sign(runes)
+ }
+
+ e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs, runes)
+ e.caret.start.xoff = 0
+ e.ClearSelection()
+ e.invalidate()
+}
+
+// Insert inserts text at the caret, moving the caret forward. If there is a
+// selection, Insert overwrites it.
+func (e *Editor) Insert(s string) {
+ e.append(s)
+ e.caret.scroll = true
+}
+
+// append inserts s at the cursor, leaving the caret is at the end of s. If
+// there is a selection, append overwrites it.
+// xxx|yyy + append zzz => xxxzzz|yyy
+func (e *Editor) append(s string) {
+ e.prepend(s)
+ e.caret.start.ofs += len(s)
+ e.caret.end.ofs = e.caret.start.ofs
+}
+
+// prepend inserts s after the cursor; the caret does not change. If there is
+// a selection, prepend overwrites it.
+// xxx|yyy + prepend zzz => xxx|zzzyyy
+func (e *Editor) prepend(s string) {
+ if e.SingleLine {
+ s = strings.ReplaceAll(s, "\n", " ")
+ }
+ e.caret.start.ofs = e.rr.deleteRunes(e.caret.start.ofs,
+ e.caret.end.ofs-e.caret.start.ofs) // Delete any selection first.
+ e.rr.prepend(e.caret.start.ofs, s)
+ e.caret.start.xoff = 0
+ e.invalidate()
+}
+
+func (e *Editor) movePages(pages int, selAct selectionAction) {
+ e.makeValid()
+ y := e.caret.start.y + pages*e.viewSize.Y
+ var (
+ prevDesc fixed.Int26_6
+ carLine2 int
+ )
+ y2 := e.lines[0].Ascent.Ceil()
+ for i := 1; i < len(e.lines); i++ {
+ if y2 >= y {
+ break
+ }
+ l := e.lines[i]
+ h := (prevDesc + l.Ascent).Ceil()
+ prevDesc = l.Descent
+ if y2+h-y >= y-y2 {
+ break
+ }
+ y2 += h
+ carLine2++
+ }
+ e.caret.start = e.movePosToLine(e.caret.start,
+ e.caret.start.x+e.caret.start.xoff, carLine2)
+ e.updateSelection(selAct)
+}
+
+func (e *Editor) movePosToLine(pos combinedPos, x fixed.Int26_6,
+ line int) combinedPos {
+ e.makeValid(&pos)
+ if line < 0 {
+ line = 0
+ }
+ if line >= len(e.lines) {
+ line = len(e.lines) - 1
+ }
+
+ prevDesc := e.lines[line].Descent
+ for pos.lineCol.Y < line {
+ pos = e.movePosToEnd(pos)
+ l := e.lines[pos.lineCol.Y]
+ _, s := e.rr.runeAt(pos.ofs)
+ pos.ofs += s
+ pos.y += (prevDesc + l.Ascent).Ceil()
+ pos.lineCol.X = 0
+ prevDesc = l.Descent
+ pos.lineCol.Y++
+ }
+ for pos.lineCol.Y > line {
+ pos = e.movePosToStart(pos)
+ l := e.lines[pos.lineCol.Y]
+ _, s := e.rr.runeBefore(pos.ofs)
+ pos.ofs -= s
+ pos.y -= (prevDesc + l.Ascent).Ceil()
+ prevDesc = l.Descent
+ pos.lineCol.Y--
+ l = e.lines[pos.lineCol.Y]
+ pos.lineCol.X = len(l.Layout.Advances) - 1
+ }
+
+ pos = e.movePosToStart(pos)
+ l := e.lines[line]
+ pos.x = align(e.Alignment, l.Width, e.viewSize.X)
+ // Only move past the end of the last line
+ end := 0
+ if line < len(e.lines)-1 {
+ end = 1
+ }
+ // Move to rune closest to x.
+ for i := 0; i < len(l.Layout.Advances)-end; i++ {
+ adv := l.Layout.Advances[i]
+ if pos.x >= x {
+ break
+ }
+ if pos.x+adv-x >= x-pos.x {
+ break
+ }
+ pos.x += adv
+ _, s := e.rr.runeAt(pos.ofs)
+ pos.ofs += s
+ pos.lineCol.X++
+ }
+ pos.xoff = x - pos.x
+ return pos
+}
+
+// MoveCaret moves the caret (aka selection start) and the selection end
+// relative to their current positions. Positive distances moves forward,
+// negative distances moves backward. Distances are in runes.
+func (e *Editor) MoveCaret(startDelta, endDelta int) {
+ e.makeValid()
+ keepSame := e.caret.start.ofs == e.caret.end.ofs && startDelta == endDelta
+ e.caret.start = e.movePos(e.caret.start, startDelta)
+ e.caret.start.xoff = 0
+ // If they were in the same place, and we're moving them the same distance,
+ // just assign the new position, instead of recalculating it.
+ if keepSame {
+ e.caret.end = e.caret.start
+ } else {
+ e.caret.end = e.movePos(e.caret.end, endDelta)
+ e.caret.end.xoff = 0
+ }
+}
+
+func (e *Editor) movePos(pos combinedPos, distance int) combinedPos {
+ for ; distance < 0 && pos.ofs > 0; distance++ {
+ if pos.lineCol.X == 0 {
+ // Move to end of previous line.
+ pos = e.movePosToLine(pos, fixed.I(e.maxWidth), pos.lineCol.Y-1)
+ continue
+ }
+ l := e.lines[pos.lineCol.Y].Layout
+ _, s := e.rr.runeBefore(pos.ofs)
+ pos.ofs -= s
+ pos.lineCol.X--
+ pos.x -= l.Advances[pos.lineCol.X]
+ }
+ for ; distance > 0 && pos.ofs < e.rr.len(); distance-- {
+ l := e.lines[pos.lineCol.Y].Layout
+ // Only move past the end of the last line
+ end := 0
+ if pos.lineCol.Y < len(e.lines)-1 {
+ end = 1
+ }
+ if pos.lineCol.X >= len(l.Advances)-end {
+ // Move to start of next line.
+ pos = e.movePosToLine(pos, 0, pos.lineCol.Y+1)
+ continue
+ }
+ pos.x += l.Advances[pos.lineCol.X]
+ _, s := e.rr.runeAt(pos.ofs)
+ pos.ofs += s
+ pos.lineCol.X++
+ }
+ return pos
+}
+
+func (e *Editor) moveStart(selAct selectionAction) {
+ e.caret.start = e.movePosToStart(e.caret.start)
+ e.updateSelection(selAct)
+}
+
+func (e *Editor) movePosToStart(pos combinedPos) combinedPos {
+ e.makeValid(&pos)
+ layout := e.lines[pos.lineCol.Y].Layout
+ for i := pos.lineCol.X - 1; i >= 0; i-- {
+ _, s := e.rr.runeBefore(pos.ofs)
+ pos.ofs -= s
+ pos.x -= layout.Advances[i]
+ }
+ pos.lineCol.X = 0
+ pos.xoff = -pos.x
+ return pos
+}
+
+func (e *Editor) moveEnd(selAct selectionAction) {
+ e.caret.start = e.movePosToEnd(e.caret.start)
+ e.updateSelection(selAct)
+}
+
+func (e *Editor) movePosToEnd(pos combinedPos) combinedPos {
+ e.makeValid(&pos)
+ l := e.lines[pos.lineCol.Y]
+ // Only move past the end of the last line
+ end := 0
+ if pos.lineCol.Y < len(e.lines)-1 {
+ end = 1
+ }
+ layout := l.Layout
+ for i := pos.lineCol.X; i < len(layout.Advances)-end; i++ {
+ adv := layout.Advances[i]
+ _, s := e.rr.runeAt(pos.ofs)
+ pos.ofs += s
+ pos.x += adv
+ pos.lineCol.X++
+ }
+ a := align(e.Alignment, l.Width, e.viewSize.X)
+ pos.xoff = l.Width + a - pos.x
+ return pos
+}
+
+// moveWord moves the caret to the next word in the specified direction.
+// Positive is forward, negative is backward.
+// Absolute values greater than one will skip that many words.
+func (e *Editor) moveWord(distance int, selAct selectionAction) {
+ e.makeValid()
+ // split the distance information into constituent parts to be
+ // used independently.
+ words, direction := distance, 1
+ if distance < 0 {
+ words, direction = distance*-1, -1
+ }
+ // atEnd if caret is at either side of the buffer.
+ atEnd := func() bool {
+ return e.caret.start.ofs == 0 || e.caret.start.ofs == e.rr.len()
+ }
+ // next returns the appropriate rune given the direction.
+ next := func() (r rune) {
+ if direction < 0 {
+ r, _ = e.rr.runeBefore(e.caret.start.ofs)
+ } else {
+ r, _ = e.rr.runeAt(e.caret.start.ofs)
+ }
+ return r
+ }
+ for ii := 0; ii < words; ii++ {
+ for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() {
+ e.MoveCaret(direction, 0)
+ }
+ e.MoveCaret(direction, 0)
+ for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() {
+ e.MoveCaret(direction, 0)
+ }
+ }
+ e.updateSelection(selAct)
+}
+
+// deleteWord deletes the next word(s) in the specified direction.
+// Unlike moveWord, deleteWord treats whitespace as a word itself.
+// Positive is forward, negative is backward.
+// Absolute values greater than one will delete that many words.
+// The selection counts as a single word.
+func (e *Editor) deleteWord(distance int) {
+ if distance == 0 {
+ return
+ }
+
+ e.makeValid()
+
+ if e.caret.start.ofs != e.caret.end.ofs {
+ e.Delete(1)
+ distance -= sign(distance)
+ }
+ if distance == 0 {
+ return
+ }
+
+ // split the distance information into constituent parts to be
+ // used independently.
+ words, direction := distance, 1
+ if distance < 0 {
+ words, direction = distance*-1, -1
+ }
+ // atEnd if offset is at or beyond either side of the buffer.
+ atEnd := func(offset int) bool {
+ idx := e.caret.start.ofs + offset*direction
+ return idx <= 0 || idx >= e.rr.len()
+ }
+ // next returns the appropriate rune given the direction and offset.
+ next := func(offset int) (r rune) {
+ idx := e.caret.start.ofs + offset*direction
+ if idx < 0 {
+ idx = 0
+ } else if idx > e.rr.len() {
+ idx = e.rr.len()
+ }
+ if direction < 0 {
+ r, _ = e.rr.runeBefore(idx)
+ } else {
+ r, _ = e.rr.runeAt(idx)
+ }
+ return r
+ }
+ var runes = 1
+ for ii := 0; ii < words; ii++ {
+ if r := next(runes); unicode.IsSpace(r) {
+ for r := next(runes); unicode.IsSpace(r) && !atEnd(runes); r = next(runes) {
+ runes += 1
+ }
+ } else {
+ for r := next(runes); !unicode.IsSpace(r) && !atEnd(runes); r = next(runes) {
+ runes += 1
+ }
+ }
+ }
+ e.Delete(runes * direction)
+}
+
+func (e *Editor) scrollToCaret() {
+ e.makeValid()
+ l := e.lines[e.caret.start.lineCol.Y]
+ if e.SingleLine {
+ var dist int
+ if d := e.caret.start.x.Floor() - e.scrollOff.X; d < 0 {
+ dist = d
+ } else if d := e.caret.start.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 {
+ dist = d
+ }
+ e.scrollRel(dist, 0)
+ } else {
+ miny := e.caret.start.y - l.Ascent.Ceil()
+ maxy := e.caret.start.y + l.Descent.Ceil()
+ var dist int
+ if d := miny - e.scrollOff.Y; d < 0 {
+ dist = d
+ } else if d := maxy - (e.scrollOff.Y + e.viewSize.Y); d > 0 {
+ dist = d
+ }
+ e.scrollRel(0, dist)
+ }
+}
+
+// NumLines returns the number of lines in the editor.
+func (e *Editor) NumLines() int {
+ e.makeValid()
+ return len(e.lines)
+}
+
+// SelectionLen returns the length of the selection, in bytes; it is
+// equivalent to len(e.SelectedText()).
+func (e *Editor) SelectionLen() int {
+ return abs(e.caret.start.ofs - e.caret.end.ofs)
+}
+
+// Selection returns the start and end of the selection, as offsets into the
+// editor text. start can be > end.
+func (e *Editor) Selection() (start, end int) {
+ return e.caret.start.ofs, e.caret.end.ofs
+}
+
+// SetCaret moves the caret to start, and sets the selection end to end. start
+// and end are in bytes, and represent offsets into the editor text. start and
+// end must be at a rune boundary.
+func (e *Editor) SetCaret(start, end int) {
+ e.makeValid()
+ // Constrain start and end to [0, e.Len()].
+ l := e.Len()
+ start = max(min(start, l), 0)
+ end = max(min(end, l), 0)
+ e.caret.start.ofs, e.caret.end.ofs = start, end
+ e.makeValidCaret()
+ e.caret.scroll = true
+ e.scroller.Stop()
+}
+
+func (e *Editor) makeValidCaret(positions ...*combinedPos) {
+ // Jump through some hoops to order the offsets given to offsetToScreenPos,
+ // but still be able to update them correctly with the results thereof.
+ positions = append(positions, &e.caret.start, &e.caret.end)
+ sort.Slice(positions, func(i, j int) bool {
+ return positions[i].ofs < positions[j].ofs
+ })
+ var iter func(offset int) combinedPos
+ *positions[0], iter = e.offsetToScreenPos(positions[0].ofs)
+ for _, cp := range positions[1:] {
+ *cp = iter(cp.ofs)
+ }
+}
+
+// SelectedText returns the currently selected text (if any) from the editor.
+func (e *Editor) SelectedText() string {
+ l := e.SelectionLen()
+ if l == 0 {
+ return ""
+ }
+ buf := make([]byte, l)
+ e.rr.Seek(int64(min(e.caret.start.ofs, e.caret.end.ofs)), io.SeekStart)
+ _, err := e.rr.Read(buf)
+ if err != nil {
+ // The only error that rr.Read can return is EOF, which just means no
+ // selection, but we've already made sure that shouldn't happen.
+ panic("impossible error because end is before e.rr.Len()")
+ }
+ return string(buf)
+}
+
+func (e *Editor) updateSelection(selAct selectionAction) {
+ if selAct == selectionClear {
+ e.ClearSelection()
+ }
+}
+
+// ClearSelection clears the selection, by setting the selection end equal to
+// the selection start.
+func (e *Editor) ClearSelection() {
+ e.caret.end = e.caret.start
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+func abs(n int) int {
+ if n < 0 {
+ return -n
+ }
+ return n
+}
+
+func sign(n int) int {
+ switch {
+ case n < 0:
+ return -1
+ case n > 0:
+ return 1
+ default:
+ return 0
+ }
+}
+
+// sortPoints returns a and b sorted such that a2 <= b2.
+func sortPoints(a, b screenPos) (a2, b2 screenPos) {
+ if b.Less(a) {
+ return b, a
+ }
+ return a, b
+}
+
+func nullLayout(r io.Reader) ([]text.Line, error) {
+ rr := bufio.NewReader(r)
+ var rerr error
+ var n int
+ var buf bytes.Buffer
+ for {
+ r, s, err := rr.ReadRune()
+ n += s
+ buf.WriteRune(r)
+ if err != nil {
+ rerr = err
+ break
+ }
+ }
+ return []text.Line{
+ {
+ Layout: text.Layout{
+ Text: buf.String(),
+ Advances: make([]fixed.Int26_6, n),
+ },
+ },
+ }, rerr
+}
+
+func (s ChangeEvent) isEditorEvent() {}
+func (s SubmitEvent) isEditorEvent() {}
+func (s SelectEvent) isEditorEvent() {}
diff --git a/gio/widget/editor_test.go b/gio/widget/editor_test.go
new file mode 100644
index 0000000..6b37b50
--- /dev/null
+++ b/gio/widget/editor_test.go
@@ -0,0 +1,540 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "fmt"
+ "image"
+ "math/rand"
+ "reflect"
+ "strings"
+ "testing"
+ "testing/quick"
+ "unicode"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/font/gofont"
+ "realy.lol/gio/io/event"
+ "realy.lol/gio/io/key"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+ "golang.org/x/image/math/fixed"
+)
+
+func TestEditor(t *testing.T) {
+ e := new(Editor)
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Exact(image.Pt(100, 100)),
+ }
+ cache := text.NewCache(gofont.Collection())
+ fontSize := unit.Px(10)
+ font := text.Font{}
+
+ e.SetCaret(0, 0) // shouldn't panic
+ assertCaret(t, e, 0, 0, 0)
+ e.SetText("Ʀbc\naĆøĆ„ā¢")
+ e.Layout(gtx, cache, font, fontSize)
+ assertCaret(t, e, 0, 0, 0)
+ e.moveEnd(selectionClear)
+ assertCaret(t, e, 0, 3, len("Ʀbc"))
+ e.MoveCaret(+1, +1)
+ assertCaret(t, e, 1, 0, len("Ʀbc\n"))
+ e.MoveCaret(-1, -1)
+ assertCaret(t, e, 0, 3, len("Ʀbc"))
+ e.moveLines(+1, +1)
+ assertCaret(t, e, 1, 3, len("Ʀbc\naĆøĆ„"))
+ e.moveEnd(selectionClear)
+ assertCaret(t, e, 1, 4, len("Ʀbc\naĆøĆ„ā¢"))
+ e.MoveCaret(+1, +1)
+ assertCaret(t, e, 1, 4, len("Ʀbc\naĆøĆ„ā¢"))
+
+ e.SetCaret(0, 0)
+ assertCaret(t, e, 0, 0, 0)
+ e.SetCaret(len("Ʀ"), len("Ʀ"))
+ assertCaret(t, e, 0, 1, 2)
+ e.SetCaret(len("Ʀbc\naĆøĆ„ā¢"), len("Ʀbc\naĆøĆ„ā¢"))
+ assertCaret(t, e, 1, 4, len("Ʀbc\naĆøĆ„ā¢"))
+
+ // Ensure that password masking does not affect caret behavior
+ e.MoveCaret(-3, -3)
+ assertCaret(t, e, 1, 1, len("Ʀbc\na"))
+ e.Mask = '*'
+ e.Layout(gtx, cache, font, fontSize)
+ assertCaret(t, e, 1, 1, len("Ʀbc\na"))
+ e.MoveCaret(-3, -3)
+ assertCaret(t, e, 0, 2, len("Ʀb"))
+ e.Mask = '\U0001F92B'
+ e.Layout(gtx, cache, font, fontSize)
+ e.moveEnd(selectionClear)
+ assertCaret(t, e, 0, 3, len("Ʀbc"))
+
+ // When a password mask is applied, it should replace all visible glyphs
+ for i, line := range e.lines {
+ for j, r := range line.Layout.Text {
+ if r != e.Mask && !unicode.IsSpace(r) {
+ t.Errorf("glyph at (%d, %d) is unmasked rune %d", i, j, r)
+ }
+ }
+ }
+}
+
+func TestEditorDimensions(t *testing.T) {
+ e := new(Editor)
+ tq := &testQueue{
+ events: []event.Event{
+ key.EditEvent{Text: "A"},
+ },
+ }
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Constraints{Max: image.Pt(100, 100)},
+ Queue: tq,
+ }
+ cache := text.NewCache(gofont.Collection())
+ fontSize := unit.Px(10)
+ font := text.Font{}
+ dims := e.Layout(gtx, cache, font, fontSize)
+ if dims.Size.X == 0 {
+ t.Errorf("EditEvent was not reflected in Editor width")
+ }
+}
+
+// assertCaret asserts that the editor caret is at a particular line
+// and column, and that the byte position matches as well.
+func assertCaret(t *testing.T, e *Editor, line, col, bytes int) {
+ t.Helper()
+ gotLine, gotCol := e.CaretPos()
+ if gotLine != line || gotCol != col {
+ t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line,
+ col)
+ }
+ if bytes != e.caret.start.ofs {
+ t.Errorf("caret at buffer position %d, expected %d", e.caret.start.ofs,
+ bytes)
+ }
+}
+
+type editMutation int
+
+const (
+ setText editMutation = iota
+ moveRune
+ moveLine
+ movePage
+ moveStart
+ moveEnd
+ moveCoord
+ moveWord
+ deleteWord
+ moveLast // Mark end; never generated.
+)
+
+func TestEditorCaretConsistency(t *testing.T) {
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Exact(image.Pt(100, 100)),
+ }
+ cache := text.NewCache(gofont.Collection())
+ fontSize := unit.Px(10)
+ font := text.Font{}
+ for _, a := range []text.Alignment{text.Start, text.Middle, text.End} {
+ e := &Editor{
+ Alignment: a,
+ }
+ e.Layout(gtx, cache, font, fontSize)
+
+ consistent := func() error {
+ t.Helper()
+ gotLine, gotCol := e.CaretPos()
+ gotCoords := e.CaretCoords()
+ want, _ := e.offsetToScreenPos(e.caret.start.ofs)
+ wantCoords := f32.Pt(float32(want.x)/64, float32(want.y))
+ if want.lineCol.Y == gotLine && want.lineCol.X == gotCol && gotCoords == wantCoords {
+ return nil
+ }
+ return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s",
+ gotLine, gotCol, gotCoords, want.lineCol.Y, want.lineCol.X,
+ wantCoords)
+ }
+ if err := consistent(); err != nil {
+ t.Errorf("initial editor inconsistency (alignment %s): %v", a, err)
+ }
+
+ move := func(mutation editMutation, str string, distance int8,
+ x, y uint16) bool {
+ switch mutation {
+ case setText:
+ e.SetText(str)
+ e.Layout(gtx, cache, font, fontSize)
+ case moveRune:
+ e.MoveCaret(int(distance), int(distance))
+ case moveLine:
+ e.moveLines(int(distance), selectionClear)
+ case movePage:
+ e.movePages(int(distance), selectionClear)
+ case moveStart:
+ e.moveStart(selectionClear)
+ case moveEnd:
+ e.moveEnd(selectionClear)
+ case moveCoord:
+ e.moveCoord(image.Pt(int(x), int(y)))
+ case moveWord:
+ e.moveWord(int(distance), selectionClear)
+ case deleteWord:
+ e.deleteWord(int(distance))
+ default:
+ return false
+ }
+ if err := consistent(); err != nil {
+ t.Error(err)
+ return false
+ }
+ return true
+ }
+ if err := quick.Check(move, nil); err != nil {
+ t.Errorf("editor inconsistency (alignment %s): %v", a, err)
+ }
+ }
+}
+
+func TestEditorMoveWord(t *testing.T) {
+ type Test struct {
+ Text string
+ Start int
+ Skip int
+ Want int
+ }
+ tests := []Test{
+ {"", 0, 0, 0},
+ {"", 0, -1, 0},
+ {"", 0, 1, 0},
+ {"hello", 0, -1, 0},
+ {"hello", 0, 1, 5},
+ {"hello world", 3, 1, 5},
+ {"hello world", 3, -1, 0},
+ {"hello world", 8, -1, 6},
+ {"hello world", 8, 1, 11},
+ {"hello world", 3, 1, 5},
+ {"hello world", 3, 2, 14},
+ {"hello world", 8, 1, 14},
+ {"hello world", 8, -1, 0},
+ {"hello brave new world", 0, 3, 15},
+ }
+ setup := func(t string) *Editor {
+ e := new(Editor)
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Exact(image.Pt(100, 100)),
+ }
+ cache := text.NewCache(gofont.Collection())
+ fontSize := unit.Px(10)
+ font := text.Font{}
+ e.SetText(t)
+ e.Layout(gtx, cache, font, fontSize)
+ return e
+ }
+ for ii, tt := range tests {
+ e := setup(tt.Text)
+ e.MoveCaret(tt.Start, tt.Start)
+ e.moveWord(tt.Skip, selectionClear)
+ if e.caret.start.ofs != tt.Want {
+ t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii,
+ e.caret.start.ofs, tt.Want)
+ }
+ }
+}
+
+func TestEditorDeleteWord(t *testing.T) {
+ type Test struct {
+ Text string
+ Start int
+ Selection int
+ Delete int
+
+ Want int
+ Result string
+ }
+ tests := []Test{
+ // No text selected
+ {"", 0, 0, 0, 0, ""},
+ {"", 0, 0, -1, 0, ""},
+ {"", 0, 0, 1, 0, ""},
+ {"", 0, 0, -2, 0, ""},
+ {"", 0, 0, 2, 0, ""},
+ {"hello", 0, 0, -1, 0, "hello"},
+ {"hello", 0, 0, 1, 0, ""},
+
+ // Document (imho) incorrect behavior w.r.t. deleting spaces following
+ // words.
+ {"hello world", 0, 0, 1, 0,
+ " world"}, // Should be "world", if you ask me.
+ {"hello world", 0, 0, 2, 0, "world"}, // Should be "".
+ {"hello ", 0, 0, 1, 0, " "}, // Should be "".
+ {"hello world", 11, 0, -1, 6, "hello "}, // Should be "hello".
+ {"hello world", 11, 0, -2, 5, "hello"}, // Should be "".
+ {"hello ", 6, 0, -1, 0, ""}, // Correct result.
+
+ {"hello world", 3, 0, 1, 3, "hel world"},
+ {"hello world", 3, 0, -1, 0, "lo world"},
+ {"hello world", 8, 0, -1, 6, "hello rld"},
+ {"hello world", 8, 0, 1, 8, "hello wo"},
+ {"hello world", 3, 0, 1, 3, "hel world"},
+ {"hello world", 3, 0, 2, 3, "helworld"},
+ {"hello world", 8, 0, 1, 8, "hello "},
+ {"hello world", 8, 0, -1, 5, "hello world"},
+ {"hello brave new world", 0, 0, 3, 0, " new world"},
+ // Add selected text.
+ //
+ // Several permutations must be tested:
+ // - select from the left or right
+ // - Delete + or -
+ // - abs(Delete) == 1 or > 1
+ //
+ // "brave |" selected; caret at |
+ {"hello there brave new world", 12, 6, 1, 12,
+ "hello there new world"}, // #16
+ {"hello there brave new world", 12, 6, 2, 12,
+ "hello there world"}, // The two spaces after "there" are actually suboptimal, if you ask me. See also above cases.
+ {"hello there brave new world", 12, 6, -1, 12, "hello there new world"},
+ {"hello there brave new world", 12, 6, -2, 6, "hello new world"},
+ // "|brave " selected
+ {"hello there brave new world", 18, -6, 1, 12,
+ "hello there new world"}, // #20
+ {"hello there brave new world", 18, -6, 2, 12,
+ "hello there world"}, // ditto
+ {"hello there brave new world", 18, -6, -1, 12,
+ "hello there new world"},
+ {"hello there brave new world", 18, -6, -2, 6, "hello new world"},
+ // Random edge cases
+ {"hello there brave new world", 12, 6, 99, 12, "hello there "},
+ {"hello there brave new world", 18, -6, -99, 0, "new world"},
+ }
+ setup := func(t string) *Editor {
+ e := new(Editor)
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Exact(image.Pt(100, 100)),
+ }
+ cache := text.NewCache(gofont.Collection())
+ fontSize := unit.Px(10)
+ font := text.Font{}
+ e.SetText(t)
+ e.Layout(gtx, cache, font, fontSize)
+ return e
+ }
+ for ii, tt := range tests {
+ e := setup(tt.Text)
+ e.MoveCaret(tt.Start, tt.Start)
+ e.MoveCaret(0, tt.Selection)
+ e.deleteWord(tt.Delete)
+ if e.caret.start.ofs != tt.Want {
+ t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii,
+ e.caret.start.ofs, tt.Want)
+ }
+ if e.Text() != tt.Result {
+ t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii,
+ e.Text(), tt.Result)
+ }
+ }
+}
+
+func TestEditorNoLayout(t *testing.T) {
+ var e Editor
+ e.SetText("hi!\n")
+ e.MoveCaret(1, 1)
+}
+
+// Generate generates a value of itself, for testing/quick.
+func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
+ t := editMutation(rand.Intn(int(moveLast)))
+ return reflect.ValueOf(t)
+}
+
+// TestSelect tests the selection code. It lays out an editor with several
+// lines in it, selects some text, verifies the selection, resizes the editor
+// to make it much narrower (which makes the lines in the editor reflow), and
+// then verifies that the updated (col, line) positions of the selected text
+// are where we expect.
+func TestSelect(t *testing.T) {
+ e := new(Editor)
+ e.SetText(`a123456789a
+b123456789b
+c123456789c
+d123456789d
+e123456789e
+f123456789f
+g123456789g
+`)
+
+ gtx := layout.Context{Ops: new(op.Ops)}
+ cache := text.NewCache(gofont.Collection())
+ font := text.Font{}
+ fontSize := unit.Px(10)
+
+ selected := func(start, end int) string {
+ // Layout once with no events; populate e.lines.
+ gtx.Queue = nil
+ e.Layout(gtx, cache, font, fontSize)
+ _ = e.Events() // throw away any events from this layout
+
+ // Build the selection events
+ startPos, endPos := e.offsetToScreenPos2(sortInts(start, end))
+ tq := &testQueue{
+ events: []event.Event{
+ pointer.Event{
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Press,
+ Source: pointer.Mouse,
+ Position: f32.Pt(textWidth(e, startPos.lineCol.Y, 0,
+ startPos.lineCol.X), textHeight(e, startPos.lineCol.Y)),
+ },
+ pointer.Event{
+ Type: pointer.Release,
+ Source: pointer.Mouse,
+ Position: f32.Pt(textWidth(e, endPos.lineCol.Y, 0,
+ endPos.lineCol.X), textHeight(e, endPos.lineCol.Y)),
+ },
+ },
+ }
+ gtx.Queue = tq
+
+ e.Layout(gtx, cache, font, fontSize)
+ for _, evt := range e.Events() {
+ switch evt.(type) {
+ case SelectEvent:
+ return e.SelectedText()
+ }
+ }
+ return ""
+ }
+
+ type testCase struct {
+ // input text offsets
+ start, end int
+
+ // expected selected text
+ selection string
+ // expected line/col positions of selection after resize
+ startPos, endPos screenPos
+ }
+
+ for n, tst := range []testCase{
+ {0, 1, "a", screenPos{}, screenPos{Y: 0, X: 1}},
+ {0, 4, "a123", screenPos{}, screenPos{Y: 0, X: 4}},
+ {0, 11, "a123456789a", screenPos{}, screenPos{Y: 1, X: 5}},
+ {2, 6, "2345", screenPos{Y: 0, X: 2}, screenPos{Y: 1, X: 0}},
+ {41, 66, "56789d\ne123456789e\nf12345", screenPos{Y: 6, X: 5},
+ screenPos{Y: 11, X: 0}},
+ } {
+ // printLines(e)
+
+ gtx.Constraints = layout.Exact(image.Pt(100, 100))
+ if got := selected(tst.start, tst.end); got != tst.selection {
+ t.Errorf("Test %d pt1: Expected %q, got %q", n, tst.selection, got)
+ continue
+ }
+
+ // Constrain the editor to roughly 6 columns wide and redraw
+ gtx.Constraints = layout.Exact(image.Pt(36, 36))
+ // Keep existing selection
+ gtx.Queue = nil
+ e.Layout(gtx, cache, font, fontSize)
+
+ if e.caret.end.lineCol != tst.startPos || e.caret.start.lineCol != tst.endPos {
+ t.Errorf("Test %d pt2: Expected %#v, %#v; got %#v, %#v",
+ n,
+ e.caret.end.lineCol, e.caret.start.lineCol,
+ tst.startPos, tst.endPos)
+ continue
+ }
+
+ // printLines(e)
+ }
+}
+
+// Verify that an existing selection is dismissed when you press arrow keys.
+func TestSelectMove(t *testing.T) {
+ e := new(Editor)
+ e.SetText(`0123456789`)
+
+ gtx := layout.Context{Ops: new(op.Ops)}
+ cache := text.NewCache(gofont.Collection())
+ font := text.Font{}
+ fontSize := unit.Px(10)
+
+ // Layout once to populate e.lines and get focus.
+ gtx.Queue = newQueue(key.FocusEvent{Focus: true})
+ e.Layout(gtx, cache, font, fontSize)
+
+ testKey := func(keyName string) {
+ // Select 345
+ e.SetCaret(3, 6)
+ if expected, got := "345", e.SelectedText(); expected != got {
+ t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got)
+ }
+
+ // Press the key
+ gtx.Queue = newQueue(key.Event{State: key.Press, Name: keyName})
+ e.Layout(gtx, cache, font, fontSize)
+
+ if expected, got := "", e.SelectedText(); expected != got {
+ t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got)
+ }
+ }
+
+ testKey(key.NameLeftArrow)
+ testKey(key.NameRightArrow)
+ testKey(key.NameUpArrow)
+ testKey(key.NameDownArrow)
+}
+
+func textWidth(e *Editor, lineNum, colStart, colEnd int) float32 {
+ var w fixed.Int26_6
+ advances := e.lines[lineNum].Layout.Advances
+ if colEnd > len(advances) {
+ colEnd = len(advances)
+ }
+ for _, adv := range advances[colStart:colEnd] {
+ w += adv
+ }
+ return float32(w.Floor())
+}
+
+func textHeight(e *Editor, lineNum int) float32 {
+ var h fixed.Int26_6
+ for _, line := range e.lines[0:lineNum] {
+ h += line.Ascent + line.Descent
+ }
+ return float32(h.Floor() + 1)
+}
+
+type testQueue struct {
+ events []event.Event
+}
+
+func newQueue(e ...event.Event) *testQueue {
+ return &testQueue{events: e}
+}
+
+func (q *testQueue) Events(_ event.Tag) []event.Event {
+ return q.events
+}
+
+func printLines(e *Editor) {
+ for n, line := range e.lines {
+ text := strings.TrimSuffix(line.Layout.Text, "\n")
+ fmt.Printf("%d: %s\n", n, text)
+ }
+}
+
+// sortInts returns a and b sorted such that a2 <= b2.
+func sortInts(a, b int) (a2, b2 int) {
+ if b < a {
+ return b, a
+ }
+ return a, b
+}
diff --git a/gio/widget/enum.go b/gio/widget/enum.go
new file mode 100644
index 0000000..1ef721a
--- /dev/null
+++ b/gio/widget/enum.go
@@ -0,0 +1,77 @@
+package widget
+
+import (
+ "image"
+
+ "realy.lol/gio/gesture"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+)
+
+type Enum struct {
+ Value string
+ hovered string
+ hovering bool
+
+ changed bool
+
+ clicks []gesture.Click
+ values []string
+}
+
+func index(vs []string, t string) int {
+ for i, v := range vs {
+ if v == t {
+ return i
+ }
+ }
+ return -1
+}
+
+// Changed reports whether Value has changed by user interaction since the last
+// call to Changed.
+func (e *Enum) Changed() bool {
+ changed := e.changed
+ e.changed = false
+ return changed
+}
+
+// Hovered returns the key that is highlighted, or false if none are.
+func (e *Enum) Hovered() (string, bool) {
+ return e.hovered, e.hovering
+}
+
+// Layout adds the event handler for key.
+func (e *Enum) Layout(gtx layout.Context, key string) layout.Dimensions {
+ defer op.Save(gtx.Ops).Load()
+ pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
+
+ if index(e.values, key) == -1 {
+ e.values = append(e.values, key)
+ e.clicks = append(e.clicks, gesture.Click{})
+ e.clicks[len(e.clicks)-1].Add(gtx.Ops)
+ } else {
+ idx := index(e.values, key)
+ clk := &e.clicks[idx]
+ for _, ev := range clk.Events(gtx) {
+ switch ev.Type {
+ case gesture.TypeClick:
+ if new := e.values[idx]; new != e.Value {
+ e.Value = new
+ e.changed = true
+ }
+ }
+ }
+ if e.hovering && e.hovered == key {
+ e.hovering = false
+ }
+ if clk.Hovered() {
+ e.hovered = key
+ e.hovering = true
+ }
+ clk.Add(gtx.Ops)
+ }
+
+ return layout.Dimensions{Size: gtx.Constraints.Min}
+}
diff --git a/gio/widget/example_test.go b/gio/widget/example_test.go
new file mode 100644
index 0000000..f5e9cf5
--- /dev/null
+++ b/gio/widget/example_test.go
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget_test
+
+import (
+ "fmt"
+ "image"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/io/router"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/widget"
+)
+
+func ExampleClickable_passthrough() {
+ // When laying out clickable widgets on top of each other,
+ // pointer events can be passed down for the underlying
+ // widgets to pick them up.
+ var button1, button2 widget.Clickable
+ var r router.Router
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Exact(image.Pt(100, 100)),
+ Queue: &r,
+ }
+
+ // widget lays out two buttons on top of each other.
+ widget := func() {
+ // button2 completely covers button1, but PassOp allows pointer
+ // events to pass through to button1.
+ button1.Layout(gtx)
+ // PassOp is applied to the area defined by button1.
+ pointer.PassOp{Pass: true}.Add(gtx.Ops)
+ button2.Layout(gtx)
+ }
+
+ // The first layout and call to Frame declare the Clickable handlers
+ // to the input router, so the following pointer events are propagated.
+ widget()
+ r.Frame(gtx.Ops)
+ // Simulate one click on the buttons by sending a Press and Release event.
+ r.Queue(
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Press,
+ Position: f32.Pt(50, 50),
+ },
+ pointer.Event{
+ Source: pointer.Mouse,
+ Buttons: pointer.ButtonPrimary,
+ Type: pointer.Release,
+ Position: f32.Pt(50, 50),
+ },
+ )
+ // The second layout ensures that the click event is registered by the buttons.
+ widget()
+
+ if button1.Clicked() {
+ fmt.Println("button1 clicked!")
+ }
+ if button2.Clicked() {
+ fmt.Println("button2 clicked!")
+ }
+
+ // Output:
+ // button1 clicked!
+ // button2 clicked!
+}
diff --git a/gio/widget/fit.go b/gio/widget/fit.go
new file mode 100644
index 0000000..08adb74
--- /dev/null
+++ b/gio/widget/fit.go
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+)
+
+// Fit scales a widget to fit and clip to the constraints.
+type Fit uint8
+
+const (
+ // Unscaled does not alter the scale of a widget.
+ Unscaled Fit = iota
+ // Contain scales widget as large as possible without cropping
+ // and it preserves aspect-ratio.
+ Contain
+ // Cover scales the widget to cover the constraint area and
+ // preserves aspect-ratio.
+ Cover
+ // ScaleDown scales the widget smaller without cropping,
+ // when it exceeds the constraint area.
+ // It preserves aspect-ratio.
+ ScaleDown
+ // Fill stretches the widget to the constraints and does not
+ // preserve aspect-ratio.
+ Fill
+)
+
+// scale adds clip and scale operations to fit dims to the constraints.
+// It positions the widget to the appropriate position.
+// It returns dimensions modified accordingly.
+func (fit Fit) scale(gtx layout.Context, pos layout.Direction,
+ dims layout.Dimensions) layout.Dimensions {
+ widgetSize := dims.Size
+
+ if fit == Unscaled || dims.Size.X == 0 || dims.Size.Y == 0 {
+ dims.Size = gtx.Constraints.Constrain(dims.Size)
+ clip.Rect{Max: dims.Size}.Add(gtx.Ops)
+
+ offset := pos.Position(widgetSize, dims.Size)
+ op.Offset(layout.FPt(offset)).Add(gtx.Ops)
+ dims.Baseline += offset.Y
+ return dims
+ }
+
+ scale := f32.Point{
+ X: float32(gtx.Constraints.Max.X) / float32(dims.Size.X),
+ Y: float32(gtx.Constraints.Max.Y) / float32(dims.Size.Y),
+ }
+
+ switch fit {
+ case Contain:
+ if scale.Y < scale.X {
+ scale.X = scale.Y
+ } else {
+ scale.Y = scale.X
+ }
+ case Cover:
+ if scale.Y > scale.X {
+ scale.X = scale.Y
+ } else {
+ scale.Y = scale.X
+ }
+ case ScaleDown:
+ if scale.Y < scale.X {
+ scale.X = scale.Y
+ } else {
+ scale.Y = scale.X
+ }
+
+ // The widget would need to be scaled up, no change needed.
+ if scale.X >= 1 {
+ dims.Size = gtx.Constraints.Constrain(dims.Size)
+ clip.Rect{Max: dims.Size}.Add(gtx.Ops)
+
+ offset := pos.Position(widgetSize, dims.Size)
+ op.Offset(layout.FPt(offset)).Add(gtx.Ops)
+ dims.Baseline += offset.Y
+ return dims
+ }
+ case Fill:
+ }
+
+ var scaledSize image.Point
+ scaledSize.X = int(float32(widgetSize.X) * scale.X)
+ scaledSize.Y = int(float32(widgetSize.Y) * scale.Y)
+ dims.Size = gtx.Constraints.Constrain(scaledSize)
+ dims.Baseline = int(float32(dims.Baseline) * scale.Y)
+
+ clip.Rect{Max: dims.Size}.Add(gtx.Ops)
+
+ offset := pos.Position(scaledSize, dims.Size)
+ op.Affine(f32.Affine2D{}.
+ Scale(f32.Point{}, scale).
+ Offset(layout.FPt(offset)),
+ ).Add(gtx.Ops)
+
+ dims.Baseline += offset.Y
+
+ return dims
+}
diff --git a/gio/widget/fit_test.go b/gio/widget/fit_test.go
new file mode 100644
index 0000000..925ad34
--- /dev/null
+++ b/gio/widget/fit_test.go
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "bytes"
+ "encoding/binary"
+ "image"
+ "math"
+ "testing"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+)
+
+func TestFit(t *testing.T) {
+ type test struct {
+ Dims image.Point
+ Scale f32.Point
+ Result image.Point
+ }
+
+ fittests := [...][]test{
+ Unscaled: {
+ {
+ Dims: image.Point{0, 0},
+ Scale: f32.Point{X: 1, Y: 1},
+ Result: image.Point{X: 0, Y: 0},
+ }, {
+ Dims: image.Point{50, 25},
+ Scale: f32.Point{X: 1, Y: 1},
+ Result: image.Point{X: 50, Y: 25},
+ }, {
+ Dims: image.Point{50, 200},
+ Scale: f32.Point{X: 1, Y: 1},
+ Result: image.Point{X: 50, Y: 100},
+ }},
+ Contain: {
+ {
+ Dims: image.Point{50, 25},
+ Scale: f32.Point{X: 2, Y: 2},
+ Result: image.Point{X: 100, Y: 50},
+ }, {
+ Dims: image.Point{50, 200},
+ Scale: f32.Point{X: 0.5, Y: 0.5},
+ Result: image.Point{X: 25, Y: 100},
+ }},
+ Cover: {
+ {
+ Dims: image.Point{50, 25},
+ Scale: f32.Point{X: 4, Y: 4},
+ Result: image.Point{X: 100, Y: 100},
+ }, {
+ Dims: image.Point{50, 200},
+ Scale: f32.Point{X: 2, Y: 2},
+ Result: image.Point{X: 100, Y: 100},
+ }},
+ ScaleDown: {
+ {
+ Dims: image.Point{50, 25},
+ Scale: f32.Point{X: 1, Y: 1},
+ Result: image.Point{X: 50, Y: 25},
+ }, {
+ Dims: image.Point{50, 200},
+ Scale: f32.Point{X: 0.5, Y: 0.5},
+ Result: image.Point{X: 25, Y: 100},
+ }},
+ Fill: {
+ {
+ Dims: image.Point{50, 25},
+ Scale: f32.Point{X: 2, Y: 4},
+ Result: image.Point{X: 100, Y: 100},
+ }, {
+ Dims: image.Point{50, 200},
+ Scale: f32.Point{X: 2, Y: 0.5},
+ Result: image.Point{X: 100, Y: 100},
+ }},
+ }
+
+ for fit, tests := range fittests {
+ fit := Fit(fit)
+ for i, test := range tests {
+ ops := new(op.Ops)
+ gtx := layout.Context{
+ Ops: ops,
+ Constraints: layout.Constraints{
+ Max: image.Point{X: 100, Y: 100},
+ },
+ }
+
+ result := fit.scale(gtx, layout.NW,
+ layout.Dimensions{Size: test.Dims})
+
+ if test.Scale.X != 1 || test.Scale.Y != 1 {
+ opsdata := gtx.Ops.Data()
+ scaleX := float32Bytes(test.Scale.X)
+ scaleY := float32Bytes(test.Scale.Y)
+ if !bytes.Contains(opsdata, scaleX) {
+ t.Errorf("did not find scale.X:%v (%x) in ops: %x",
+ test.Scale.X, scaleX, opsdata)
+ }
+ if !bytes.Contains(opsdata, scaleY) {
+ t.Errorf("did not find scale.Y:%v (%x) in ops: %x",
+ test.Scale.Y, scaleY, opsdata)
+ }
+ }
+
+ if result.Size != test.Result {
+ t.Errorf("fit %v, #%v: expected %#v, got %#v", fit, i,
+ test.Result, result.Size)
+ }
+ }
+ }
+}
+
+func float32Bytes(v float32) []byte {
+ var dst [4]byte
+ binary.LittleEndian.PutUint32(dst[:], math.Float32bits(v))
+ return dst[:]
+}
diff --git a/gio/widget/float.go b/gio/widget/float.go
new file mode 100644
index 0000000..e26e296
--- /dev/null
+++ b/gio/widget/float.go
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+
+ "realy.lol/gio/gesture"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+)
+
+// Float is for selecting a value in a range.
+type Float struct {
+ Value float32
+ Axis layout.Axis
+
+ drag gesture.Drag
+ pos float32 // position normalized to [0, 1]
+ length float32
+ changed bool
+}
+
+// Dragging returns whether the value is being interacted with.
+func (f *Float) Dragging() bool { return f.drag.Dragging() }
+
+// Layout updates the value according to drag events along the f's main axis.
+//
+// The range of f is set by the minimum constraints main axis value.
+func (f *Float) Layout(gtx layout.Context, pointerMargin int,
+ min, max float32) layout.Dimensions {
+ size := gtx.Constraints.Min
+ f.length = float32(f.Axis.Convert(size).X)
+
+ var de *pointer.Event
+ for _, e := range f.drag.Events(gtx.Metric, gtx, gesture.Axis(f.Axis)) {
+ if e.Type == pointer.Press || e.Type == pointer.Drag {
+ de = &e
+ }
+ }
+
+ value := f.Value
+ if de != nil {
+ xy := de.Position.X
+ if f.Axis == layout.Vertical {
+ xy = de.Position.Y
+ }
+ f.pos = xy / f.length
+ value = min + (max-min)*f.pos
+ } else if min != max {
+ f.pos = (value - min) / (max - min)
+ }
+ // Unconditionally call setValue in case min, max, or value changed.
+ f.setValue(value, min, max)
+
+ if f.pos < 0 {
+ f.pos = 0
+ } else if f.pos > 1 {
+ f.pos = 1
+ }
+
+ defer op.Save(gtx.Ops).Load()
+ margin := f.Axis.Convert(image.Pt(pointerMargin, 0))
+ rect := image.Rectangle{
+ Min: margin.Mul(-1),
+ Max: size.Add(margin),
+ }
+ pointer.Rect(rect).Add(gtx.Ops)
+ f.drag.Add(gtx.Ops)
+
+ return layout.Dimensions{Size: size}
+}
+
+func (f *Float) setValue(value, min, max float32) {
+ if min > max {
+ min, max = max, min
+ }
+ if value < min {
+ value = min
+ } else if value > max {
+ value = max
+ }
+ if f.Value != value {
+ f.Value = value
+ f.changed = true
+ }
+}
+
+// Pos reports the selected position.
+func (f *Float) Pos() float32 {
+ return f.pos * f.length
+}
+
+// Changed reports whether the value has changed since
+// the last call to Changed.
+func (f *Float) Changed() bool {
+ changed := f.changed
+ f.changed = false
+ return changed
+}
diff --git a/gio/widget/icon.go b/gio/widget/icon.go
new file mode 100644
index 0000000..6f37d48
--- /dev/null
+++ b/gio/widget/icon.go
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+ "image/color"
+ "image/draw"
+
+ "golang.org/x/exp/shiny/iconvg"
+
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+)
+
+type Icon struct {
+ Color color.NRGBA
+ src []byte
+ // Cached values.
+ op paint.ImageOp
+ imgSize int
+ imgColor color.NRGBA
+}
+
+// NewIcon returns a new Icon from IconVG data.
+func NewIcon(data []byte) (*Icon, error) {
+ _, err := iconvg.DecodeMetadata(data)
+ if err != nil {
+ return nil, err
+ }
+ return &Icon{src: data, Color: color.NRGBA{A: 0xff}}, nil
+}
+
+func (ic *Icon) Layout(gtx layout.Context, sz unit.Value) layout.Dimensions {
+ ico := ic.image(gtx.Px(sz))
+ ico.Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ return layout.Dimensions{
+ Size: ico.Size(),
+ }
+}
+
+func (ic *Icon) image(sz int) paint.ImageOp {
+ if sz == ic.imgSize && ic.Color == ic.imgColor {
+ return ic.op
+ }
+ m, _ := iconvg.DecodeMetadata(ic.src)
+ dx, dy := m.ViewBox.AspectRatio()
+ img := image.NewRGBA(image.Rectangle{Max: image.Point{X: sz,
+ Y: int(float32(sz) * dy / dx)}})
+ var ico iconvg.Rasterizer
+ ico.SetDstImage(img, img.Bounds(), draw.Src)
+ m.Palette[0] = f32color.NRGBAToLinearRGBA(ic.Color)
+ iconvg.Decode(&ico, ic.src, &iconvg.DecodeOptions{
+ Palette: &m.Palette,
+ })
+ ic.op = paint.NewImageOp(img)
+ ic.imgSize = sz
+ ic.imgColor = ic.Color
+ return ic.op
+}
diff --git a/gio/widget/icon_test.go b/gio/widget/icon_test.go
new file mode 100644
index 0000000..1a3e8d9
--- /dev/null
+++ b/gio/widget/icon_test.go
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+ "image/color"
+ "testing"
+
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/unit"
+ "golang.org/x/exp/shiny/materialdesign/icons"
+)
+
+func TestIcon_Alpha(t *testing.T) {
+ icon, err := NewIcon(icons.ToggleCheckBox)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ icon.Color = color.NRGBA{B: 0xff, A: 0x40}
+
+ gtx := layout.Context{
+ Ops: new(op.Ops),
+ Constraints: layout.Exact(image.Pt(100, 100)),
+ }
+
+ _ = icon.Layout(gtx, unit.Sp(18))
+}
diff --git a/gio/widget/image.go b/gio/widget/image.go
new file mode 100644
index 0000000..0e0351f
--- /dev/null
+++ b/gio/widget/image.go
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+)
+
+// Image is a widget that displays an image.
+type Image struct {
+ // Src is the image to display.
+ Src paint.ImageOp
+ // Fit specifies how to scale the image to the constraints.
+ // By default it does not do any scaling.
+ Fit Fit
+ // Position specifies where to position the image within
+ // the constraints.
+ Position layout.Direction
+ // Scale is the ratio of image pixels to
+ // dps. If Scale is zero Image falls back to
+ // a scale that match a standard 72 DPI.
+ Scale float32
+}
+
+const defaultScale = float32(160.0 / 72.0)
+
+func (im Image) Layout(gtx layout.Context) layout.Dimensions {
+ defer op.Save(gtx.Ops).Load()
+
+ scale := im.Scale
+ if scale == 0 {
+ scale = defaultScale
+ }
+
+ size := im.Src.Size()
+ wf, hf := float32(size.X), float32(size.Y)
+ w, h := gtx.Px(unit.Dp(wf*scale)), gtx.Px(unit.Dp(hf*scale))
+
+ dims := im.Fit.scale(gtx, im.Position,
+ layout.Dimensions{Size: image.Pt(w, h)})
+
+ pixelScale := scale * gtx.Metric.PxPerDp
+ op.Affine(f32.Affine2D{}.Scale(f32.Point{},
+ f32.Pt(pixelScale, pixelScale))).Add(gtx.Ops)
+
+ im.Src.Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+
+ return dims
+}
diff --git a/gio/widget/image_test.go b/gio/widget/image_test.go
new file mode 100644
index 0000000..774dfc7
--- /dev/null
+++ b/gio/widget/image_test.go
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "image"
+ "testing"
+
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/paint"
+)
+
+func TestImageScale(t *testing.T) {
+ var ops op.Ops
+ gtx := layout.Context{
+ Ops: &ops,
+ Constraints: layout.Constraints{
+ Max: image.Pt(50, 50),
+ },
+ }
+ imgSize := image.Pt(10, 10)
+ img := image.NewNRGBA(image.Rectangle{Max: imgSize})
+ imgOp := paint.NewImageOp(img)
+
+ // Ensure the default scales correctly.
+ dims := Image{Src: imgOp}.Layout(gtx)
+ expectedSize := imgSize
+ expectedSize.X = int(float32(expectedSize.X) * defaultScale)
+ expectedSize.Y = int(float32(expectedSize.Y) * defaultScale)
+ if dims.Size != expectedSize {
+ t.Fatalf("non-scaled image is wrong size, expected %v, got %v",
+ expectedSize, dims.Size)
+ }
+
+ // Ensure scaling the image via the Scale field works.
+ currentScale := float32(0.5)
+ dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx)
+ expectedSize = imgSize
+ expectedSize.X = int(float32(expectedSize.X) * currentScale)
+ expectedSize.Y = int(float32(expectedSize.Y) * currentScale)
+ if dims.Size != expectedSize {
+ t.Fatalf(".5 scale image is wrong size, expected %v, got %v",
+ expectedSize, dims.Size)
+ }
+
+ // Ensure the image responds to changes in DPI.
+ currentScale = float32(1)
+ gtx.Metric.PxPerDp = 2
+ dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx)
+ expectedSize = imgSize
+ expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp)
+ expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp)
+ if dims.Size != expectedSize {
+ t.Fatalf("HiDPI non-scaled image is wrong size, expected %v, got %v",
+ expectedSize, dims.Size)
+ }
+
+ // Ensure scaling the image responds to changes in DPI.
+ currentScale = float32(.5)
+ gtx.Metric.PxPerDp = 2
+ dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx)
+ expectedSize = imgSize
+ expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp)
+ expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp)
+ if dims.Size != expectedSize {
+ t.Fatalf("HiDPI .5 scale image is wrong size, expected %v, got %v",
+ expectedSize, dims.Size)
+ }
+}
diff --git a/gio/widget/label.go b/gio/widget/label.go
new file mode 100644
index 0000000..acf6b50
--- /dev/null
+++ b/gio/widget/label.go
@@ -0,0 +1,252 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package widget
+
+import (
+ "fmt"
+ "image"
+ "unicode/utf8"
+
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+
+ "golang.org/x/image/math/fixed"
+)
+
+// Label is a widget for laying out and drawing text.
+type Label struct {
+ // Alignment specify the text alignment.
+ Alignment text.Alignment
+ // MaxLines limits the number of lines. Zero means no limit.
+ MaxLines int
+}
+
+// screenPos describes a character position (in text line and column numbers,
+// not pixels): Y = line number, X = rune column.
+type screenPos image.Point
+
+type segmentIterator struct {
+ Lines []text.Line
+ Clip image.Rectangle
+ Alignment text.Alignment
+ Width int
+ Offset image.Point
+ startSel screenPos
+ endSel screenPos
+
+ pos screenPos // current position
+ line text.Line // current line
+ layout text.Layout // current line's Layout
+
+ // pixel positions
+ off fixed.Point26_6
+ y, prevDesc fixed.Int26_6
+}
+
+const inf = 1e6
+
+func (l *segmentIterator) Next() (text.Layout, image.Point, bool, int,
+ image.Point, bool) {
+ for l.pos.Y < len(l.Lines) {
+ if l.pos.X == 0 {
+ l.line = l.Lines[l.pos.Y]
+
+ // Calculate X & Y pixel coordinates of left edge of line. We need y
+ // for the next line, so it's in l, but we only need x here, so it's
+ // not.
+ x := align(l.Alignment, l.line.Width, l.Width) + fixed.I(l.Offset.X)
+ l.y += l.prevDesc + l.line.Ascent
+ l.prevDesc = l.line.Descent
+ // Align baseline and line start to the pixel grid.
+ l.off = fixed.Point26_6{X: fixed.I(x.Floor()),
+ Y: fixed.I(l.y.Ceil())}
+ l.y = l.off.Y
+ l.off.Y += fixed.I(l.Offset.Y)
+ if (l.off.Y + l.line.Bounds.Min.Y).Floor() > l.Clip.Max.Y {
+ break
+ }
+
+ if (l.off.Y + l.line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y {
+ // This line is outside/before the clip area; go on to the next line.
+ l.pos.Y++
+ continue
+ }
+
+ // Copy the line's Layout, since we slice it up later.
+ l.layout = l.line.Layout
+
+ // Find the left edge of the text visible in the l.Clip clipping
+ // area.
+ for len(l.layout.Advances) > 0 {
+ _, n := utf8.DecodeRuneInString(l.layout.Text)
+ adv := l.layout.Advances[0]
+ if (l.off.X + adv + l.line.Bounds.Max.X - l.line.Width).Ceil() >= l.Clip.Min.X {
+ break
+ }
+ l.off.X += adv
+ l.layout.Text = l.layout.Text[n:]
+ l.layout.Advances = l.layout.Advances[1:]
+ l.pos.X++
+ }
+ }
+
+ selected := l.inSelection()
+ endx := l.off.X
+ rune := 0
+ nextLine := true
+ retLayout := l.layout
+ for n := range l.layout.Text {
+ selChanged := selected != l.inSelection()
+ beyondClipEdge := (endx + l.line.Bounds.Min.X).Floor() > l.Clip.Max.X
+ if selChanged || beyondClipEdge {
+ retLayout.Advances = l.layout.Advances[:rune]
+ retLayout.Text = l.layout.Text[:n]
+ if selChanged {
+ // Save the rest of the line
+ l.layout.Advances = l.layout.Advances[rune:]
+ l.layout.Text = l.layout.Text[n:]
+ nextLine = false
+ }
+ break
+ }
+ endx += l.layout.Advances[rune]
+ rune++
+ l.pos.X++
+ }
+ offFloor := image.Point{X: l.off.X.Floor(), Y: l.off.Y.Floor()}
+
+ // Calculate the width & height if the returned text.
+ //
+ // If there's a better way to do this, I'm all ears.
+ var d fixed.Int26_6
+ for _, adv := range retLayout.Advances {
+ d += adv
+ }
+ size := image.Point{
+ X: d.Ceil(),
+ Y: (l.line.Ascent + l.line.Descent).Ceil(),
+ }
+
+ if nextLine {
+ l.pos.Y++
+ l.pos.X = 0
+ } else {
+ l.off.X = endx
+ }
+
+ return retLayout, offFloor, selected, l.prevDesc.Ceil() - size.Y, size, true
+ }
+ return text.Layout{}, image.Point{}, false, 0, image.Point{}, false
+}
+
+func (l *segmentIterator) inSelection() bool {
+ return l.startSel.LessOrEqual(l.pos) &&
+ l.pos.Less(l.endSel)
+}
+
+func (p1 screenPos) LessOrEqual(p2 screenPos) bool {
+ return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X <= p2.X)
+}
+
+func (p1 screenPos) Less(p2 screenPos) bool {
+ return p1.Y < p2.Y || (p1.Y == p2.Y && p1.X < p2.X)
+}
+
+func (l Label) Layout(gtx layout.Context, s text.Shaper, font text.Font,
+ size unit.Value, txt string) layout.Dimensions {
+ cs := gtx.Constraints
+ textSize := fixed.I(gtx.Px(size))
+ lines := s.LayoutString(font, textSize, cs.Max.X, txt)
+ if max := l.MaxLines; max > 0 && len(lines) > max {
+ lines = lines[:max]
+ }
+ dims := linesDimens(lines)
+ dims.Size = cs.Constrain(dims.Size)
+ cl := textPadding(lines)
+ cl.Max = cl.Max.Add(dims.Size)
+ it := segmentIterator{
+ Lines: lines,
+ Clip: cl,
+ Alignment: l.Alignment,
+ Width: dims.Size.X,
+ }
+ for {
+ l, off, _, _, _, ok := it.Next()
+ if !ok {
+ break
+ }
+ stack := op.Save(gtx.Ops)
+ op.Offset(layout.FPt(off)).Add(gtx.Ops)
+ s.Shape(font, textSize, l).Add(gtx.Ops)
+ clip.Rect(cl.Sub(off)).Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ stack.Load()
+ }
+ return dims
+}
+
+func textPadding(lines []text.Line) (padding image.Rectangle) {
+ if len(lines) == 0 {
+ return
+ }
+ first := lines[0]
+ if d := first.Ascent + first.Bounds.Min.Y; d < 0 {
+ padding.Min.Y = d.Ceil()
+ }
+ last := lines[len(lines)-1]
+ if d := last.Bounds.Max.Y - last.Descent; d > 0 {
+ padding.Max.Y = d.Ceil()
+ }
+ if d := first.Bounds.Min.X; d < 0 {
+ padding.Min.X = d.Ceil()
+ }
+ if d := first.Bounds.Max.X - first.Width; d > 0 {
+ padding.Max.X = d.Ceil()
+ }
+ return
+}
+
+func linesDimens(lines []text.Line) layout.Dimensions {
+ var width fixed.Int26_6
+ var h int
+ var baseline int
+ if len(lines) > 0 {
+ baseline = lines[0].Ascent.Ceil()
+ var prevDesc fixed.Int26_6
+ for _, l := range lines {
+ h += (prevDesc + l.Ascent).Ceil()
+ prevDesc = l.Descent
+ if l.Width > width {
+ width = l.Width
+ }
+ }
+ h += lines[len(lines)-1].Descent.Ceil()
+ }
+ w := width.Ceil()
+ return layout.Dimensions{
+ Size: image.Point{
+ X: w,
+ Y: h,
+ },
+ Baseline: h - baseline,
+ }
+}
+
+func align(align text.Alignment, width fixed.Int26_6,
+ maxWidth int) fixed.Int26_6 {
+ mw := fixed.I(maxWidth)
+ switch align {
+ case text.Middle:
+ return fixed.I(((mw - width) / 2).Floor())
+ case text.End:
+ return fixed.I((mw - width).Floor())
+ case text.Start:
+ return 0
+ default:
+ panic(fmt.Errorf("unknown alignment %v", align))
+ }
+}
diff --git a/gio/widget/material/button.go b/gio/widget/material/button.go
new file mode 100644
index 0000000..78bfcf2
--- /dev/null
+++ b/gio/widget/material/button.go
@@ -0,0 +1,291 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image"
+ "image/color"
+ "math"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type ButtonStyle struct {
+ Text string
+ // Color is the text color.
+ Color color.NRGBA
+ Font text.Font
+ TextSize unit.Value
+ Background color.NRGBA
+ CornerRadius unit.Value
+ Inset layout.Inset
+ Button *widget.Clickable
+ shaper text.Shaper
+}
+
+type ButtonLayoutStyle struct {
+ Background color.NRGBA
+ CornerRadius unit.Value
+ Button *widget.Clickable
+}
+
+type IconButtonStyle struct {
+ Background color.NRGBA
+ // Color is the icon color.
+ Color color.NRGBA
+ Icon *widget.Icon
+ // Size is the icon size.
+ Size unit.Value
+ Inset layout.Inset
+ Button *widget.Clickable
+}
+
+func Button(th *Theme, button *widget.Clickable, txt string) ButtonStyle {
+ return ButtonStyle{
+ Text: txt,
+ Color: th.Palette.ContrastFg,
+ CornerRadius: unit.Dp(4),
+ Background: th.Palette.ContrastBg,
+ TextSize: th.TextSize.Scale(14.0 / 16.0),
+ Inset: layout.Inset{
+ Top: unit.Dp(10), Bottom: unit.Dp(10),
+ Left: unit.Dp(12), Right: unit.Dp(12),
+ },
+ Button: button,
+ shaper: th.Shaper,
+ }
+}
+
+func ButtonLayout(th *Theme, button *widget.Clickable) ButtonLayoutStyle {
+ return ButtonLayoutStyle{
+ Button: button,
+ Background: th.Palette.ContrastBg,
+ CornerRadius: unit.Dp(4),
+ }
+}
+
+func IconButton(th *Theme, button *widget.Clickable,
+ icon *widget.Icon) IconButtonStyle {
+ return IconButtonStyle{
+ Background: th.Palette.ContrastBg,
+ Color: th.Palette.ContrastFg,
+ Icon: icon,
+ Size: unit.Dp(24),
+ Inset: layout.UniformInset(unit.Dp(12)),
+ Button: button,
+ }
+}
+
+// Clickable lays out a rectangular clickable widget without further
+// decoration.
+func Clickable(gtx layout.Context, button *widget.Clickable,
+ w layout.Widget) layout.Dimensions {
+ return layout.Stack{}.Layout(gtx,
+ layout.Expanded(button.Layout),
+ layout.Expanded(func(gtx layout.Context) layout.Dimensions {
+ clip.Rect{Max: gtx.Constraints.Min}.Add(gtx.Ops)
+ for _, c := range button.History() {
+ drawInk(gtx, c)
+ }
+ return layout.Dimensions{Size: gtx.Constraints.Min}
+ }),
+ layout.Stacked(w),
+ )
+}
+
+func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
+ return ButtonLayoutStyle{
+ Background: b.Background,
+ CornerRadius: b.CornerRadius,
+ Button: b.Button,
+ }.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
+ paint.ColorOp{Color: b.Color}.Add(gtx.Ops)
+ return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper,
+ b.Font, b.TextSize, b.Text)
+ })
+ })
+}
+
+func (b ButtonLayoutStyle) Layout(gtx layout.Context,
+ w layout.Widget) layout.Dimensions {
+ min := gtx.Constraints.Min
+ return layout.Stack{Alignment: layout.Center}.Layout(gtx,
+ layout.Expanded(func(gtx layout.Context) layout.Dimensions {
+ rr := float32(gtx.Px(b.CornerRadius))
+ clip.UniformRRect(f32.Rectangle{Max: f32.Point{
+ X: float32(gtx.Constraints.Min.X),
+ Y: float32(gtx.Constraints.Min.Y),
+ }}, rr).Add(gtx.Ops)
+ background := b.Background
+ switch {
+ case gtx.Queue == nil:
+ background = f32color.Disabled(b.Background)
+ case b.Button.Hovered():
+ background = f32color.Hovered(b.Background)
+ }
+ paint.Fill(gtx.Ops, background)
+ for _, c := range b.Button.History() {
+ drawInk(gtx, c)
+ }
+ return layout.Dimensions{Size: gtx.Constraints.Min}
+ }),
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ gtx.Constraints.Min = min
+ return layout.Center.Layout(gtx, w)
+ }),
+ layout.Expanded(b.Button.Layout),
+ )
+}
+
+func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
+ return layout.Stack{Alignment: layout.Center}.Layout(gtx,
+ layout.Expanded(func(gtx layout.Context) layout.Dimensions {
+ sizex, sizey := gtx.Constraints.Min.X, gtx.Constraints.Min.Y
+ sizexf, sizeyf := float32(sizex), float32(sizey)
+ rr := (sizexf + sizeyf) * .25
+ clip.UniformRRect(f32.Rectangle{
+ Max: f32.Point{X: sizexf, Y: sizeyf},
+ }, rr).Add(gtx.Ops)
+ background := b.Background
+ switch {
+ case gtx.Queue == nil:
+ background = f32color.Disabled(b.Background)
+ case b.Button.Hovered():
+ background = f32color.Hovered(b.Background)
+ }
+ paint.Fill(gtx.Ops, background)
+ for _, c := range b.Button.History() {
+ drawInk(gtx, c)
+ }
+ return layout.Dimensions{Size: gtx.Constraints.Min}
+ }),
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ return b.Inset.Layout(gtx,
+ func(gtx layout.Context) layout.Dimensions {
+ size := gtx.Px(b.Size)
+ if b.Icon != nil {
+ b.Icon.Color = b.Color
+ b.Icon.Layout(gtx, unit.Px(float32(size)))
+ }
+ return layout.Dimensions{
+ Size: image.Point{X: size, Y: size},
+ }
+ })
+ }),
+ layout.Expanded(func(gtx layout.Context) layout.Dimensions {
+ pointer.Ellipse(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
+ return b.Button.Layout(gtx)
+ }),
+ )
+}
+
+func drawInk(gtx layout.Context, c widget.Press) {
+ // duration is the number of seconds for the
+ // completed animation: expand while fading in, then
+ // out.
+ const (
+ expandDuration = float32(0.5)
+ fadeDuration = float32(0.9)
+ )
+
+ now := gtx.Now
+
+ t := float32(now.Sub(c.Start).Seconds())
+
+ end := c.End
+ if end.IsZero() {
+ // If the press hasn't ended, don't fade-out.
+ end = now
+ }
+
+ endt := float32(end.Sub(c.Start).Seconds())
+
+ // Compute the fade-in/out position in [0;1].
+ var alphat float32
+ {
+ var haste float32
+ if c.Cancelled {
+ // If the press was cancelled before the inkwell
+ // was fully faded in, fast forward the animation
+ // to match the fade-out.
+ if h := 0.5 - endt/fadeDuration; h > 0 {
+ haste = h
+ }
+ }
+ // Fade in.
+ half1 := t/fadeDuration + haste
+ if half1 > 0.5 {
+ half1 = 0.5
+ }
+
+ // Fade out.
+ half2 := float32(now.Sub(end).Seconds())
+ half2 /= fadeDuration
+ half2 += haste
+ if half2 > 0.5 {
+ // Too old.
+ return
+ }
+
+ alphat = half1 + half2
+ }
+
+ // Compute the expand position in [0;1].
+ sizet := t
+ if c.Cancelled {
+ // Freeze expansion of cancelled presses.
+ sizet = endt
+ }
+ sizet /= expandDuration
+
+ // Animate only ended presses, and presses that are fading in.
+ if !c.End.IsZero() || sizet <= 1.0 {
+ op.InvalidateOp{}.Add(gtx.Ops)
+ }
+
+ if sizet > 1.0 {
+ sizet = 1.0
+ }
+
+ if alphat > .5 {
+ // Start fadeout after half the animation.
+ alphat = 1.0 - alphat
+ }
+ // Twice the speed to attain fully faded in at 0.5.
+ t2 := alphat * 2
+ // BeziƩr ease-in curve.
+ alphaBezier := t2 * t2 * (3.0 - 2.0*t2)
+ sizeBezier := sizet * sizet * (3.0 - 2.0*sizet)
+ size := float32(gtx.Constraints.Min.X)
+ if h := float32(gtx.Constraints.Min.Y); h > size {
+ size = h
+ }
+ // Cover the entire constraints min rectangle.
+ size *= 2 * float32(math.Sqrt(2))
+ // Apply curve values to size and color.
+ size *= sizeBezier
+ alpha := 0.7 * alphaBezier
+ const col = 0.8
+ ba, bc := byte(alpha*0xff), byte(col*0xff)
+ defer op.Save(gtx.Ops).Load()
+ rgba := f32color.MulAlpha(color.NRGBA{A: 0xff, R: bc, G: bc, B: bc}, ba)
+ ink := paint.ColorOp{Color: rgba}
+ ink.Add(gtx.Ops)
+ rr := size * .5
+ op.Offset(c.Position.Add(f32.Point{
+ X: -rr,
+ Y: -rr,
+ })).Add(gtx.Ops)
+ clip.UniformRRect(f32.Rectangle{Max: f32.Pt(size, size)}, rr).Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+}
diff --git a/gio/widget/material/checkable.go b/gio/widget/material/checkable.go
new file mode 100644
index 0000000..e895b81
--- /dev/null
+++ b/gio/widget/material/checkable.go
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image"
+ "image/color"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type checkable struct {
+ Label string
+ Color color.NRGBA
+ Font text.Font
+ TextSize unit.Value
+ IconColor color.NRGBA
+ Size unit.Value
+ shaper text.Shaper
+ checkedStateIcon *widget.Icon
+ uncheckedStateIcon *widget.Icon
+}
+
+func (c *checkable) layout(gtx layout.Context,
+ checked, hovered bool) layout.Dimensions {
+ var icon *widget.Icon
+ if checked {
+ icon = c.checkedStateIcon
+ } else {
+ icon = c.uncheckedStateIcon
+ }
+
+ dims := layout.Flex{Alignment: layout.Middle}.Layout(gtx,
+ layout.Rigid(func(gtx layout.Context) layout.Dimensions {
+ return layout.Stack{Alignment: layout.Center}.Layout(gtx,
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ size := gtx.Px(c.Size) * 4 / 3
+ dims := layout.Dimensions{
+ Size: image.Point{X: size, Y: size},
+ }
+ if !hovered {
+ return dims
+ }
+
+ background := f32color.MulAlpha(c.IconColor, 70)
+
+ radius := float32(size) / 2
+ paint.FillShape(gtx.Ops, background,
+ clip.Circle{
+ Center: f32.Point{X: radius, Y: radius},
+ Radius: radius,
+ }.Op(gtx.Ops))
+
+ return dims
+ }),
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ return layout.UniformInset(unit.Dp(2)).Layout(gtx,
+ func(gtx layout.Context) layout.Dimensions {
+ size := gtx.Px(c.Size)
+ icon.Color = c.IconColor
+ if gtx.Queue == nil {
+ icon.Color = f32color.Disabled(icon.Color)
+ }
+ icon.Layout(gtx, unit.Px(float32(size)))
+ return layout.Dimensions{
+ Size: image.Point{X: size, Y: size},
+ }
+ })
+ }),
+ )
+ }),
+
+ layout.Rigid(func(gtx layout.Context) layout.Dimensions {
+ return layout.UniformInset(unit.Dp(2)).Layout(gtx,
+ func(gtx layout.Context) layout.Dimensions {
+ paint.ColorOp{Color: c.Color}.Add(gtx.Ops)
+ return widget.Label{}.Layout(gtx, c.shaper, c.Font,
+ c.TextSize, c.Label)
+ })
+ }),
+ )
+ pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
+ return dims
+}
diff --git a/gio/widget/material/checkbox.go b/gio/widget/material/checkbox.go
new file mode 100644
index 0000000..2483cfe
--- /dev/null
+++ b/gio/widget/material/checkbox.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "realy.lol/gio/layout"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type CheckBoxStyle struct {
+ checkable
+ CheckBox *widget.Bool
+}
+
+func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle {
+ return CheckBoxStyle{
+ CheckBox: checkBox,
+ checkable: checkable{
+ Label: label,
+ Color: th.Palette.Fg,
+ IconColor: th.Palette.ContrastBg,
+ TextSize: th.TextSize.Scale(14.0 / 16.0),
+ Size: unit.Dp(26),
+ shaper: th.Shaper,
+ checkedStateIcon: th.Icon.CheckBoxChecked,
+ uncheckedStateIcon: th.Icon.CheckBoxUnchecked,
+ },
+ }
+}
+
+// Layout updates the checkBox and displays it.
+func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions {
+ dims := c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered())
+ gtx.Constraints.Min = dims.Size
+ c.CheckBox.Layout(gtx)
+ return dims
+}
diff --git a/gio/widget/material/doc.go b/gio/widget/material/doc.go
new file mode 100644
index 0000000..715f5a0
--- /dev/null
+++ b/gio/widget/material/doc.go
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+// Package material implements the Material design.
+//
+// To maximize reusability and visual flexibility, user interface controls are
+// split into two parts: the stateful widget and the stateless drawing of it.
+//
+// For example, widget.Clickable encapsulates the state and event
+// handling of all clickable areas, while the Theme is responsible to
+// draw a specific area, for example a button.
+//
+// This snippet defines a button that prints a message when clicked:
+//
+// var gtx layout.Context
+// button := new(widget.Clickable)
+//
+// for button.Clicked(gtx) {
+// fmt.Println("Clicked!")
+// }
+//
+// Use a Theme to draw the button:
+//
+// theme := material.NewTheme(...)
+//
+// material.Button(theme, "Click me!").Layout(gtx, button)
+//
+// Customization
+//
+// Quite often, a program needs to customize the theme-provided defaults. Several
+// options are available, depending on the nature of the change.
+//
+// Mandatory parameters: Some parameters are not part of the widget state but
+// have no obvious default. In the program above, the button text is a
+// parameter to the Theme.Button method.
+//
+// Theme-global parameters: For changing the look of all widgets drawn with a
+// particular theme, adjust the `Theme` fields:
+//
+// theme.Color.Primary = color.NRGBA{...}
+//
+// Widget-local parameters: For changing the look of a particular widget,
+// adjust the widget specific theme object:
+//
+// btn := material.Button(theme, "Click me!")
+// btn.Font.Style = text.Italic
+// btn.Layout(gtx, button)
+//
+// Widget variants: A widget can have several distinct representations even
+// though the underlying state is the same. A widget.Clickable can be drawn as a
+// round icon button:
+//
+// icon := material.NewIcon(...)
+//
+// material.IconButton(theme, icon).Layout(gtx, button)
+//
+// Specialized widgets: Theme both define a generic Label method
+// that takes a text size, and specialized methods for standard text
+// sizes such as Theme.H1 and Theme.Body2.
+package material
diff --git a/gio/widget/material/editor.go b/gio/widget/material/editor.go
new file mode 100644
index 0000000..93d02cf
--- /dev/null
+++ b/gio/widget/material/editor.go
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image/color"
+
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type EditorStyle struct {
+ Font text.Font
+ TextSize unit.Value
+ // Color is the text color.
+ Color color.NRGBA
+ // Hint contains the text displayed when the editor is empty.
+ Hint string
+ // HintColor is the color of hint text.
+ HintColor color.NRGBA
+ // SelectionColor is the color of the background for selected text.
+ SelectionColor color.NRGBA
+ Editor *widget.Editor
+
+ shaper text.Shaper
+}
+
+func Editor(th *Theme, editor *widget.Editor, hint string) EditorStyle {
+ return EditorStyle{
+ Editor: editor,
+ TextSize: th.TextSize,
+ Color: th.Palette.Fg,
+ shaper: th.Shaper,
+ Hint: hint,
+ HintColor: f32color.MulAlpha(th.Palette.Fg, 0xbb),
+ SelectionColor: f32color.MulAlpha(th.Palette.ContrastBg, 0x60),
+ }
+}
+
+func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions {
+ defer op.Save(gtx.Ops).Load()
+ macro := op.Record(gtx.Ops)
+ paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops)
+ var maxlines int
+ if e.Editor.SingleLine {
+ maxlines = 1
+ }
+ tl := widget.Label{Alignment: e.Editor.Alignment, MaxLines: maxlines}
+ dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint)
+ call := macro.Stop()
+ if w := dims.Size.X; gtx.Constraints.Min.X < w {
+ gtx.Constraints.Min.X = w
+ }
+ if h := dims.Size.Y; gtx.Constraints.Min.Y < h {
+ gtx.Constraints.Min.Y = h
+ }
+ dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize)
+ disabled := gtx.Queue == nil
+ if e.Editor.Len() > 0 {
+ paint.ColorOp{Color: blendDisabledColor(disabled,
+ e.SelectionColor)}.Add(gtx.Ops)
+ e.Editor.PaintSelection(gtx)
+ paint.ColorOp{Color: blendDisabledColor(disabled, e.Color)}.Add(gtx.Ops)
+ e.Editor.PaintText(gtx)
+ } else {
+ call.Add(gtx.Ops)
+ }
+ if !disabled {
+ paint.ColorOp{Color: e.Color}.Add(gtx.Ops)
+ e.Editor.PaintCaret(gtx)
+ }
+ return dims
+}
+
+func blendDisabledColor(disabled bool, c color.NRGBA) color.NRGBA {
+ if disabled {
+ return f32color.Disabled(c)
+ }
+ return c
+}
diff --git a/gio/widget/material/label.go b/gio/widget/material/label.go
new file mode 100644
index 0000000..80c4b02
--- /dev/null
+++ b/gio/widget/material/label.go
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image/color"
+
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type LabelStyle struct {
+ // Face defines the text style.
+ Font text.Font
+ // Color is the text color.
+ Color color.NRGBA
+ // Alignment specify the text alignment.
+ Alignment text.Alignment
+ // MaxLines limits the number of lines. Zero means no limit.
+ MaxLines int
+ Text string
+ TextSize unit.Value
+
+ shaper text.Shaper
+}
+
+func H1(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(96.0/16.0), txt)
+}
+
+func H2(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(60.0/16.0), txt)
+}
+
+func H3(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(48.0/16.0), txt)
+}
+
+func H4(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(34.0/16.0), txt)
+}
+
+func H5(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(24.0/16.0), txt)
+}
+
+func H6(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(20.0/16.0), txt)
+}
+
+func Body1(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize, txt)
+}
+
+func Body2(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(14.0/16.0), txt)
+}
+
+func Caption(th *Theme, txt string) LabelStyle {
+ return Label(th, th.TextSize.Scale(12.0/16.0), txt)
+}
+
+func Label(th *Theme, size unit.Value, txt string) LabelStyle {
+ return LabelStyle{
+ Text: txt,
+ Color: th.Palette.Fg,
+ TextSize: size,
+ shaper: th.Shaper,
+ }
+}
+
+func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
+ paint.ColorOp{Color: l.Color}.Add(gtx.Ops)
+ tl := widget.Label{Alignment: l.Alignment, MaxLines: l.MaxLines}
+ return tl.Layout(gtx, l.shaper, l.Font, l.TextSize, l.Text)
+}
diff --git a/gio/widget/material/loader.go b/gio/widget/material/loader.go
new file mode 100644
index 0000000..77afede
--- /dev/null
+++ b/gio/widget/material/loader.go
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image"
+ "image/color"
+ "math"
+ "time"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+)
+
+type LoaderStyle struct {
+ Color color.NRGBA
+}
+
+func Loader(th *Theme) LoaderStyle {
+ return LoaderStyle{
+ Color: th.Palette.ContrastBg,
+ }
+}
+
+func (l LoaderStyle) Layout(gtx layout.Context) layout.Dimensions {
+ diam := gtx.Constraints.Min.X
+ if minY := gtx.Constraints.Min.Y; minY > diam {
+ diam = minY
+ }
+ if diam == 0 {
+ diam = gtx.Px(unit.Dp(24))
+ }
+ sz := gtx.Constraints.Constrain(image.Pt(diam, diam))
+ radius := float64(sz.X) * .5
+ defer op.Save(gtx.Ops).Load()
+ op.Offset(f32.Pt(float32(radius), float32(radius))).Add(gtx.Ops)
+
+ dt := (time.Duration(gtx.Now.UnixNano()) % (time.Second)).Seconds()
+ startAngle := dt * math.Pi * 2
+ endAngle := startAngle + math.Pi*1.5
+
+ clipLoader(gtx.Ops, startAngle, endAngle, radius)
+ paint.ColorOp{
+ Color: l.Color,
+ }.Add(gtx.Ops)
+ op.Offset(f32.Pt(-float32(radius), -float32(radius))).Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ op.InvalidateOp{}.Add(gtx.Ops)
+ return layout.Dimensions{
+ Size: sz,
+ }
+}
+
+func clipLoader(ops *op.Ops, startAngle, endAngle, radius float64) {
+ const thickness = .25
+
+ var (
+ width = float32(radius * thickness)
+ delta = float32(endAngle - startAngle)
+
+ vy, vx = math.Sincos(startAngle)
+
+ pen = f32.Pt(float32(vx), float32(vy)).Mul(float32(radius))
+ center = f32.Pt(0, 0).Sub(pen)
+
+ p clip.Path
+ )
+
+ p.Begin(ops)
+ p.Move(pen)
+ p.Arc(center, center, delta)
+ clip.Stroke{
+ Path: p.End(),
+ Style: clip.StrokeStyle{
+ Width: width,
+ Cap: clip.FlatCap,
+ },
+ }.Op().Add(ops)
+}
diff --git a/gio/widget/material/progressbar.go b/gio/widget/material/progressbar.go
new file mode 100644
index 0000000..98ae4cf
--- /dev/null
+++ b/gio/widget/material/progressbar.go
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image"
+ "image/color"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+)
+
+type ProgressBarStyle struct {
+ Color color.NRGBA
+ TrackColor color.NRGBA
+ Progress float32
+}
+
+func ProgressBar(th *Theme, progress float32) ProgressBarStyle {
+ return ProgressBarStyle{
+ Progress: progress,
+ Color: th.Palette.ContrastBg,
+ TrackColor: f32color.MulAlpha(th.Palette.Fg, 0x88),
+ }
+}
+
+func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions {
+ shader := func(width float32, color color.NRGBA) layout.Dimensions {
+ maxHeight := unit.Dp(4)
+ rr := float32(gtx.Px(unit.Dp(2)))
+
+ d := image.Point{X: int(width), Y: gtx.Px(maxHeight)}
+
+ height := float32(gtx.Px(maxHeight))
+ clip.UniformRRect(f32.Rectangle{Max: f32.Pt(width, height)},
+ rr).Add(gtx.Ops)
+ paint.ColorOp{Color: color}.Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+
+ return layout.Dimensions{Size: d}
+ }
+
+ progressBarWidth := float32(gtx.Constraints.Max.X)
+ return layout.Stack{Alignment: layout.W}.Layout(gtx,
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ return shader(progressBarWidth, p.TrackColor)
+ }),
+ layout.Stacked(func(gtx layout.Context) layout.Dimensions {
+ fillWidth := progressBarWidth * clamp1(p.Progress)
+ fillColor := p.Color
+ if gtx.Queue == nil {
+ fillColor = f32color.Disabled(fillColor)
+ }
+ return shader(fillWidth, fillColor)
+ }),
+ )
+}
+
+// clamp1 limits v to range [0..1].
+func clamp1(v float32) float32 {
+ if v >= 1 {
+ return 1
+ } else if v <= 0 {
+ return 0
+ } else {
+ return v
+ }
+}
diff --git a/gio/widget/material/radiobutton.go b/gio/widget/material/radiobutton.go
new file mode 100644
index 0000000..79dd763
--- /dev/null
+++ b/gio/widget/material/radiobutton.go
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "realy.lol/gio/layout"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type RadioButtonStyle struct {
+ checkable
+ Key string
+ Group *widget.Enum
+}
+
+// RadioButton returns a RadioButton with a label. The key specifies
+// the value for the Enum.
+func RadioButton(th *Theme, group *widget.Enum,
+ key, label string) RadioButtonStyle {
+ return RadioButtonStyle{
+ Group: group,
+ checkable: checkable{
+ Label: label,
+
+ Color: th.Palette.Fg,
+ IconColor: th.Palette.ContrastBg,
+ TextSize: th.TextSize.Scale(14.0 / 16.0),
+ Size: unit.Dp(26),
+ shaper: th.Shaper,
+ checkedStateIcon: th.Icon.RadioChecked,
+ uncheckedStateIcon: th.Icon.RadioUnchecked,
+ },
+ Key: key,
+ }
+}
+
+// Layout updates enum and displays the radio button.
+func (r RadioButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
+ hovered, hovering := r.Group.Hovered()
+ dims := r.layout(gtx, r.Group.Value == r.Key, hovering && hovered == r.Key)
+ gtx.Constraints.Min = dims.Size
+ r.Group.Layout(gtx, r.Key)
+ return dims
+}
diff --git a/gio/widget/material/slider.go b/gio/widget/material/slider.go
new file mode 100644
index 0000000..e038d75
--- /dev/null
+++ b/gio/widget/material/slider.go
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image"
+ "image/color"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+// Slider is for selecting a value in a range.
+func Slider(th *Theme, float *widget.Float, min, max float32) SliderStyle {
+ return SliderStyle{
+ Min: min,
+ Max: max,
+ Color: th.Palette.ContrastBg,
+ Float: float,
+ FingerSize: th.FingerSize,
+ }
+}
+
+type SliderStyle struct {
+ Min, Max float32
+ Color color.NRGBA
+ Float *widget.Float
+
+ FingerSize unit.Value
+}
+
+func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions {
+ thumbRadius := gtx.Px(unit.Dp(6))
+ trackWidth := gtx.Px(unit.Dp(2))
+
+ axis := s.Float.Axis
+ // Keep a minimum length so that the track is always visible.
+ minLength := thumbRadius + 3*thumbRadius + thumbRadius
+ // Try to expand to finger size, but only if the constraints
+ // allow for it.
+ touchSizePx := min(gtx.Px(s.FingerSize),
+ axis.Convert(gtx.Constraints.Max).Y)
+ sizeMain := max(axis.Convert(gtx.Constraints.Min).X, minLength)
+ sizeCross := max(2*thumbRadius, touchSizePx)
+ size := axis.Convert(image.Pt(sizeMain, sizeCross))
+
+ st := op.Save(gtx.Ops)
+ o := axis.Convert(image.Pt(thumbRadius, 0))
+ op.Offset(layout.FPt(o)).Add(gtx.Ops)
+ gtx.Constraints.Min = axis.Convert(image.Pt(sizeMain-2*thumbRadius,
+ sizeCross))
+ s.Float.Layout(gtx, thumbRadius, s.Min, s.Max)
+ gtx.Constraints.Min = gtx.Constraints.Min.Add(axis.Convert(image.Pt(0,
+ sizeCross)))
+ thumbPos := thumbRadius + int(s.Float.Pos())
+ st.Load()
+
+ color := s.Color
+ if gtx.Queue == nil {
+ color = f32color.Disabled(color)
+ }
+
+ // Draw track before thumb.
+ st = op.Save(gtx.Ops)
+ track := image.Rectangle{
+ Min: axis.Convert(image.Pt(thumbRadius, sizeCross/2-trackWidth/2)),
+ Max: axis.Convert(image.Pt(thumbPos, sizeCross/2+trackWidth/2)),
+ }
+ clip.Rect(track).Add(gtx.Ops)
+ paint.Fill(gtx.Ops, color)
+ st.Load()
+
+ // Draw track after thumb.
+ st = op.Save(gtx.Ops)
+ track = image.Rectangle{
+ Min: axis.Convert(image.Pt(thumbPos, axis.Convert(track.Min).Y)),
+ Max: axis.Convert(image.Pt(sizeMain-thumbRadius,
+ axis.Convert(track.Max).Y)),
+ }
+ clip.Rect(track).Add(gtx.Ops)
+ paint.Fill(gtx.Ops, f32color.MulAlpha(color, 96))
+ st.Load()
+
+ // Draw thumb.
+ pt := axis.Convert(image.Pt(thumbPos, sizeCross/2))
+ paint.FillShape(gtx.Ops, color,
+ clip.Circle{
+ Center: f32.Point{X: float32(pt.X), Y: float32(pt.Y)},
+ Radius: float32(thumbRadius),
+ }.Op(gtx.Ops))
+
+ return layout.Dimensions{Size: size}
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
diff --git a/gio/widget/material/switch.go b/gio/widget/material/switch.go
new file mode 100644
index 0000000..14a4134
--- /dev/null
+++ b/gio/widget/material/switch.go
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image"
+ "image/color"
+
+ "realy.lol/gio/f32"
+ "realy.lol/gio/internal/f32color"
+ "realy.lol/gio/io/pointer"
+ "realy.lol/gio/layout"
+ "realy.lol/gio/op"
+ "realy.lol/gio/op/clip"
+ "realy.lol/gio/op/paint"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+type SwitchStyle struct {
+ Color struct {
+ Enabled color.NRGBA
+ Disabled color.NRGBA
+ Track color.NRGBA
+ }
+ Switch *widget.Bool
+}
+
+// Switch is for selecting a boolean value.
+func Switch(th *Theme, swtch *widget.Bool) SwitchStyle {
+ sw := SwitchStyle{
+ Switch: swtch,
+ }
+ sw.Color.Enabled = th.Palette.ContrastBg
+ sw.Color.Disabled = th.Palette.Bg
+ sw.Color.Track = f32color.MulAlpha(th.Palette.Fg, 0x88)
+ return sw
+}
+
+// Layout updates the switch and displays it.
+func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions {
+ trackWidth := gtx.Px(unit.Dp(36))
+ trackHeight := gtx.Px(unit.Dp(16))
+ thumbSize := gtx.Px(unit.Dp(20))
+ trackOff := float32(thumbSize-trackHeight) * .5
+
+ // Draw track.
+ stack := op.Save(gtx.Ops)
+ trackCorner := float32(trackHeight) / 2
+ trackRect := f32.Rectangle{Max: f32.Point{
+ X: float32(trackWidth),
+ Y: float32(trackHeight),
+ }}
+ col := s.Color.Disabled
+ if s.Switch.Value {
+ col = s.Color.Enabled
+ }
+ if gtx.Queue == nil {
+ col = f32color.Disabled(col)
+ }
+ trackColor := s.Color.Track
+ op.Offset(f32.Point{Y: trackOff}).Add(gtx.Ops)
+ clip.UniformRRect(trackRect, trackCorner).Add(gtx.Ops)
+ paint.ColorOp{Color: trackColor}.Add(gtx.Ops)
+ paint.PaintOp{}.Add(gtx.Ops)
+ stack.Load()
+
+ // Draw thumb ink.
+ stack = op.Save(gtx.Ops)
+ inkSize := gtx.Px(unit.Dp(44))
+ rr := float32(inkSize) * .5
+ inkOff := f32.Point{
+ X: float32(trackWidth)*.5 - rr,
+ Y: -rr + float32(trackHeight)*.5 + trackOff,
+ }
+ op.Offset(inkOff).Add(gtx.Ops)
+ gtx.Constraints.Min = image.Pt(inkSize, inkSize)
+ clip.UniformRRect(f32.Rectangle{Max: layout.FPt(gtx.Constraints.Min)},
+ rr).Add(gtx.Ops)
+ for _, p := range s.Switch.History() {
+ drawInk(gtx, p)
+ }
+ stack.Load()
+
+ // Compute thumb offset and color.
+ stack = op.Save(gtx.Ops)
+ if s.Switch.Value {
+ off := trackWidth - thumbSize
+ op.Offset(f32.Point{X: float32(off)}).Add(gtx.Ops)
+ }
+
+ thumbRadius := float32(thumbSize) / 2
+
+ // Draw hover.
+ if s.Switch.Hovered() {
+ r := 1.7 * thumbRadius
+ background := f32color.MulAlpha(s.Color.Enabled, 70)
+ paint.FillShape(gtx.Ops, background,
+ clip.Circle{
+ Center: f32.Point{X: thumbRadius, Y: thumbRadius},
+ Radius: r,
+ }.Op(gtx.Ops))
+ }
+
+ // Draw thumb shadow, a translucent disc slightly larger than the
+ // thumb itself.
+ // Center shadow horizontally and slightly adjust its Y.
+ paint.FillShape(gtx.Ops, argb(0x55000000),
+ clip.Circle{
+ Center: f32.Point{X: thumbRadius, Y: thumbRadius + .25},
+ Radius: thumbRadius + 1,
+ }.Op(gtx.Ops))
+
+ // Draw thumb.
+ paint.FillShape(gtx.Ops, col,
+ clip.Circle{
+ Center: f32.Point{X: thumbRadius, Y: thumbRadius},
+ Radius: thumbRadius,
+ }.Op(gtx.Ops))
+
+ // Set up click area.
+ stack = op.Save(gtx.Ops)
+ clickSize := gtx.Px(unit.Dp(40))
+ clickOff := f32.Point{
+ X: (float32(trackWidth) - float32(clickSize)) * .5,
+ Y: (float32(trackHeight)-float32(clickSize))*.5 + trackOff,
+ }
+ op.Offset(clickOff).Add(gtx.Ops)
+ sz := image.Pt(clickSize, clickSize)
+ pointer.Ellipse(image.Rectangle{Max: sz}).Add(gtx.Ops)
+ gtx.Constraints.Min = sz
+ s.Switch.Layout(gtx)
+ stack.Load()
+
+ dims := image.Point{X: trackWidth, Y: thumbSize}
+ return layout.Dimensions{Size: dims}
+}
diff --git a/gio/widget/material/theme.go b/gio/widget/material/theme.go
new file mode 100644
index 0000000..e19f7df
--- /dev/null
+++ b/gio/widget/material/theme.go
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+package material
+
+import (
+ "image/color"
+
+ "golang.org/x/exp/shiny/materialdesign/icons"
+
+ "realy.lol/gio/text"
+ "realy.lol/gio/unit"
+ "realy.lol/gio/widget"
+)
+
+// Palette contains the minimal set of colors that a widget may need to
+// draw itself.
+type Palette struct {
+ // Bg is the background color atop which content is currently being
+ // drawn.
+ Bg color.NRGBA
+
+ // Fg is a color suitable for drawing on top of Bg.
+ Fg color.NRGBA
+
+ // ContrastBg is a color used to draw attention to active,
+ // important, interactive widgets such as buttons.
+ ContrastBg color.NRGBA
+
+ // ContrastFg is a color suitable for content drawn on top of
+ // ContrastBg.
+ ContrastFg color.NRGBA
+}
+
+type Theme struct {
+ Shaper text.Shaper
+ Palette
+ TextSize unit.Value
+ Icon struct {
+ CheckBoxChecked *widget.Icon
+ CheckBoxUnchecked *widget.Icon
+ RadioChecked *widget.Icon
+ RadioUnchecked *widget.Icon
+ }
+
+ // FingerSize is the minimum touch target size.
+ FingerSize unit.Value
+}
+
+func NewTheme(fontCollection []text.FontFace) *Theme {
+ t := &Theme{
+ Shaper: text.NewCache(fontCollection),
+ }
+ t.Palette = Palette{
+ Fg: rgb(0x000000),
+ Bg: rgb(0xffffff),
+ ContrastBg: rgb(0x3f51b5),
+ ContrastFg: rgb(0xffffff),
+ }
+ t.TextSize = unit.Sp(16)
+
+ t.Icon.CheckBoxChecked = mustIcon(widget.NewIcon(icons.ToggleCheckBox))
+ t.Icon.CheckBoxUnchecked = mustIcon(widget.NewIcon(icons.ToggleCheckBoxOutlineBlank))
+ t.Icon.RadioChecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonChecked))
+ t.Icon.RadioUnchecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonUnchecked))
+
+ // 38dp is on the lower end of possible finger size.
+ t.FingerSize = unit.Dp(38)
+
+ return t
+}
+
+func (t Theme) WithPalette(p Palette) Theme {
+ t.Palette = p
+ return t
+}
+
+func mustIcon(ic *widget.Icon, err error) *widget.Icon {
+ if err != nil {
+ panic(err)
+ }
+ return ic
+}
+
+func rgb(c uint32) color.NRGBA {
+ return argb(0xff000000 | c)
+}
+
+func argb(c uint32) color.NRGBA {
+ return color.NRGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8),
+ B: uint8(c)}
+}
diff --git a/realy/version b/realy/version
index db62a50..40d22ac 100644
--- a/realy/version
+++ b/realy/version
@@ -1 +1 @@
-v24.12.23
\ No newline at end of file
+v24.12.24
\ No newline at end of file