Creating a View

A view is a drawable region that receives events. You may think of it as a window, though it may be embedded and not represent a top-level system window. 1

Creating a visible view is a multi-step process. When a new view is created with puglNewView(), it does not yet represent a “real” system view:

PuglView* view = puglNewView(world);

Configuring the Frame

Before display, the necessary frame and window attributes should be set. These allow the window system (or plugin host) to arrange the view properly. For example:

const double defaultWidth  = 1920.0;
const double defaultHeight = 1080.0;

puglSetWindowTitle(view, "My Window");
puglSetDefaultSize(view, defaultWidth, defaultHeight);
puglSetMinSize(view, defaultWidth / 4.0, defaultHeight / 4.0);
puglSetAspectRatio(view, 1, 1, 16, 9);

There are also several hints for basic attributes that can be set:

puglSetViewHint(view, PUGL_RESIZABLE, PUGL_TRUE);
puglSetViewHint(view, PUGL_IGNORE_KEY_REPEAT, PUGL_TRUE);

Embedding

To embed the view in another window, you will need to somehow get the native view handle for the parent, then set it with puglSetParentWindow(). If the parent is a Pugl view, the native handle can be accessed with puglGetNativeWindow(). For example:

puglSetParentWindow(view, puglGetNativeWindow(parent));

Setting an Event Handler

In order to actually do anything, a view must process events from the system. Pugl dispatches all events to a single event handling function, which is set with puglSetEventFunc():

puglSetEventFunc(view, onEvent);

See Handling Events for details on writing the event handler itself.

Setting View Data

Since the event handler is called with only a view pointer and an event, there needs to be some way to access application data associated with the view. Similar to setting application data, this is done by setting an opaque handle on the view with puglSetHandle(), for example:

puglSetHandle(view, myViewData);

The handle can be later retrieved, likely in the event handler, with puglGetHandle():

MyViewData* data = (MyViewData*)puglGetHandle(view);

All non-constant data should be accessed via this handle, to avoid problems associated with static mutable data.

If data is also associated with the world, it can be retrieved via the view using puglGetWorld():

PuglWorld* world = puglGetWorld(view);
MyApp*     app   = (MyApp*)puglGetWorldHandle(world);

Setting a Backend

Before being realized, the view must have a backend set with puglSetBackend().

The backend manages the graphics API that will be used for drawing. Pugl includes backends and supporting API for Cairo, OpenGL, and Vulkan.

Using Cairo

Cairo-specific API is declared in the cairo.h header:

#include <pugl/cairo.h>

The Cairo backend is provided by puglCairoBackend():

puglSetBackend(view, puglCairoBackend());

No additional configuration is required for Cairo. To draw when handling an expose event, the Cairo context can be accessed with puglGetContext():

cairo_t* cr = (cairo_t*)puglGetContext(view);

Using OpenGL

OpenGL-specific API is declared in the gl.h header:

#include <pugl/gl.h>

The OpenGL backend is provided by puglGlBackend():

puglSetBackend(view, puglGlBackend());

Some hints must also be set so that the context can be set up correctly. For example, to use OpenGL 3.3 Core Profile:

puglSetViewHint(view, PUGL_USE_COMPAT_PROFILE, PUGL_FALSE);
puglSetViewHint(view, PUGL_CONTEXT_VERSION_MAJOR, 3);
puglSetViewHint(view, PUGL_CONTEXT_VERSION_MINOR, 3);

If you need to perform some setup using the OpenGL API, there are two ways to do so.

The OpenGL context is active when PUGL_CREATE and PUGL_DESTROY events are dispatched, so things like creating and destroying shaders and textures can be done then.

Alternatively, if it is cumbersome to set up and tear down OpenGL in the event handler, puglEnterContext() and puglLeaveContext() can be used to manually activate the OpenGL context during application setup. Note, however, that unlike many other APIs, these functions must not be used for drawing. It is only valid to use the OpenGL API for configuration in a manually entered context, rendering will not work. For example:

puglEnterContext(view);
setupOpenGL(myApp);
puglLeaveContext(view);

while (!myApp->quit) {
  puglUpdate(world, 0.0);
}

puglEnterContext(view);
teardownOpenGL(myApp);
puglLeaveContext(view);

Using Vulkan

Vulkan-specific API is declared in the vulkan.h header. This header includes Vulkan headers, so if you are dynamically loading Vulkan at runtime, you should define VK_NO_PROTOTYPES before including it.

#define VK_NO_PROTOTYPES

#include <pugl/vulkan.h>

The Vulkan backend is provided by puglVulkanBackend():

puglSetBackend(view, puglVulkanBackend());

Unlike OpenGL, almost all Vulkan configuration is done using the Vulkan API directly. Pugl only provides a portable mechanism to load the Vulkan library and get the functions used to load the rest of the Vulkan API.

Loading Vulkan

For maximum compatibility, it is best to not link to Vulkan at compile-time, but instead load the Vulkan API at run-time. To do so, first create a PuglVulkanLoader:

PuglVulkanLoader* loader = puglNewVulkanLoader(world);

The loader manages the dynamically loaded Vulkan library, so it must be kept alive for as long as the application is using Vulkan. You can get the function used to load Vulkan functions with puglGetInstanceProcAddrFunc():

PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr =
  puglGetInstanceProcAddrFunc(loader);

This vkGetInstanceProcAddr function can be used to load the rest of the Vulkan API. For example, you can use it to get the vkCreateInstance function, then use that to create your Vulkan instance. In practice, you will want to use some loader or wrapper API since there are many Vulkan functions.

For advanced situations, there is also puglGetDeviceProcAddrFunc() which retrieves the vkGetDeviceProcAddr function instead.

The Vulkan loader is provided for convenience, so that applications to not need to write platform-specific code to load Vulkan. Its use it not mandatory and Pugl can be used with Vulkan loaded by some other method.

Linking with Vulkan

If you do want to link to the Vulkan library at compile time, note that the Pugl Vulkan backend does not depend on it, so you will have to do so explicitly.

Creating a Surface

The details of using Vulkan are far beyond the scope of this documentation, but Pugl provides a portable function, puglCreateSurface(), to get the Vulkan surface for a view. Assuming you have somehow created your VkInstance, you can get the surface for a view using puglCreateSurface():

VkSurfaceKHR* surface = NULL;
puglCreateSurface(puglGetDeviceProcAddrFunc(loader),
                  view,
                  vulkanInstance,
                  NULL,
                  &surface);

Showing the View

Once the view is configured, it can be “realized” with puglRealize(). This creates a “real” system view, for example:

PuglStatus status = puglRealize(view);
if (status) {
  fprintf(stderr, "Error realizing view (%s)\n", puglStrerror(status));
}

Note that realizing a view can fail for many reasons, so the return code should always be checked. This is generally the case for any function that interacts with the window system. Most functions also return a PuglStatus, but these checks are omitted for brevity in the rest of this documentation.

A realized view is not initially visible, but can be shown with puglShow():

puglShow(view);

To create an initially visible view, it is also possible to simply call puglShow() right away. The view will be automatically realized if necessary.

Footnotes

1

MacOS has a strong distinction between views, which may be nested, and windows, which may not. On Windows and X11, everything is a nestable window, but top-level windows are configured differently.