We're going to add a GUI to our plugin. Start by defining the size of the GUI and creating some stubs we'll fill out later in the tutorial.

// GUI size.
#define GUI_WIDTH (300)
#define GUI_HEIGHT (200)

static void PluginPaint(MyPlugin *plugin, uint32_t *bits) {
    // TODO.
}

static void PluginProcessMouseDrag(MyPlugin *plugin, int32_t x, int32_t y) {
    // TODO.
}

static void PluginProcessMousePress(MyPlugin *plugin, int32_t x, int32_t y) {
    // TODO.
}

static void PluginProcessMouseRelease(MyPlugin *plugin) {
    // TODO.
}

Let's also include a platform-specific file where we can put our GUI code. I'm going for a unity build here, but you could easily compile this as a separate translation unit.

#if defined(_WIN32)
#include "gui_w32.cpp"
#elif defined(__linux__)
#include "gui_x11.cpp"
#elif defined(__APPLE__)
#include "gui_mac.cpp"
#endif

We'll be implementing two new extensions, the GUI extension and the POSIX FD support extension. The latter will only be used on Linux, but it doesn't hurt to have it around on other platforms, AFAIK.

To our MyPlugin structure, add two fields, one for storing the GUI data, and another for storing the host's implementation of the POSIX FD support extension.

struct MyPlugin {
    clap_plugin_t plugin;
    const clap_host_t *host;
    float sampleRate;
    Array<Voice> voices;
    float parameters[P_COUNT], mainParameters[P_COUNT];
    bool changed[P_COUNT], mainChanged[P_COUNT];
    Mutex syncParameters;
    struct GUI *gui; // New!
    const clap_host_posix_fd_support_t *hostPOSIXFDSupport; // New!
};

In pluginClass.init, query the host for its implementation of the POSIX FD support extension. If this returns a non-null pointer, we'll be able to call into the function pointers in the structure. We'll talk about this when we get to the X11/Linux GUI specifics.

        MyPlugin *plugin = (MyPlugin *) _plugin->plugin_data;

        plugin->hostPOSIXFDSupport = (const clap_host_posix_fd_support_t *) plugin->host->get_extension(plugin->host, CLAP_EXT_POSIX_FD_SUPPORT);

        // ... as before

Next, in pluginClass.get_extension return the pointers to the extensions we're about to implement.

    .get_extension = [] (const clap_plugin *plugin, const char *id) -> const void * {
        if (0 == strcmp(id, CLAP_EXT_NOTE_PORTS      )) return &extensionNotePorts;
        if (0 == strcmp(id, CLAP_EXT_AUDIO_PORTS     )) return &extensionAudioPorts;
        if (0 == strcmp(id, CLAP_EXT_PARAMS          )) return &extensionParams;
        if (0 == strcmp(id, CLAP_EXT_STATE           )) return &extensionState;
        if (0 == strcmp(id, CLAP_EXT_GUI             )) return &extensionGUI; // New!
        if (0 == strcmp(id, CLAP_EXT_POSIX_FD_SUPPORT)) return &extensionPOSIXFDSupport; // New!
        return nullptr;
    },

First, let's do the POSIX fd suport extension since it's only small. Again, we'll discuss what this is for when we're covering the X11/Linux GUI code specifics.

static const clap_plugin_posix_fd_support_t extensionPOSIXFDSupport = {
    .on_fd = [] (const clap_plugin_t *_plugin, int fd, clap_posix_fd_flags_t flags) {
        MyPlugin *plugin = (MyPlugin *) _plugin->plugin_data;
        // We'll define GUIOnPOSIXFD in our platform specific code file.
        GUIOnPOSIXFD(plugin);
    },
};

And now for the GUI extension. This is another fairly large extension, but we only need to implement most of the functions in the simplest possible way. This code will call out to various functions from the platform specific code we're going to write next.

static const clap_plugin_gui_t extensionGUI = {
    .is_api_supported = [] (const clap_plugin_t *plugin, const char *api, bool isFloating) -> bool {
        // We'll define GUI_API in our platform specific code file.
        return 0 == strcmp(api, GUI_API) && !isFloating;
    },

    .get_preferred_api = [] (const clap_plugin_t *plugin, const char **api, bool *isFloating) -> bool {
        *api = GUI_API;
        *isFloating = false;
        return true;
    },

    .create = [] (const clap_plugin_t *_plugin, const char *api, bool isFloating) -> bool {
        if (!extensionGUI.is_api_supported(_plugin, api, isFloating)) return false;
        // We'll define GUICreate in our platform specific code file.
        GUICreate((MyPlugin *) _plugin->plugin_data);
        return true;
    },

    .destroy = [] (const clap_plugin_t *_plugin) {
        // We'll define GUIDestroy in our platform specific code file.
        GUIDestroy((MyPlugin *) _plugin->plugin_data);
    },

    .set_scale = [] (const clap_plugin_t *plugin, double scale) -> bool {
        return false;
    },

    .get_size = [] (const clap_plugin_t *plugin, uint32_t *width, uint32_t *height) -> bool {
        *width = GUI_WIDTH;
        *height = GUI_HEIGHT;
        return true;
    },

    .can_resize = [] (const clap_plugin_t *plugin) -> bool {
        return false;
    },

    .get_resize_hints = [] (const clap_plugin_t *plugin, clap_gui_resize_hints_t *hints) -> bool {
        return false;
    },

    .adjust_size = [] (const clap_plugin_t *plugin, uint32_t *width, uint32_t *height) -> bool {
        return extensionGUI.get_size(plugin, width, height);
    },

    .set_size = [] (const clap_plugin_t *plugin, uint32_t width, uint32_t height) -> bool {
        return true;
    },

    .set_parent = [] (const clap_plugin_t *_plugin, const clap_window_t *window) -> bool {
        assert(0 == strcmp(window->api, GUI_API));
        // We'll define GUISetParent in our platform specific code file.
        GUISetParent((MyPlugin *) _plugin->plugin_data, window);
        return true;
    },

    .set_transient = [] (const clap_plugin_t *plugin, const clap_window_t *window) -> bool {
        return false;
    },

    .suggest_title = [] (const clap_plugin_t *plugin, const char *title) {
    },

    .show = [] (const clap_plugin_t *_plugin) -> bool {
        // We'll define GUISetVisible in our platform specific code file.
        GUISetVisible((MyPlugin *) _plugin->plugin_data, true);
        return true;
    },

    .hide = [] (const clap_plugin_t *_plugin) -> bool {
        GUISetVisible((MyPlugin *) _plugin->plugin_data, false);
        return true;
    },
};

Okay, we're ready for the platform specific code!

X11/Linux

To your compile command, append -lX11.

We now write gui_x11.cpp. We begin by defining the GUI_API constant, including some header files and defining the GUI structure that's part of MyPlugin.

#define GUI_API CLAP_WINDOW_API_X11

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xatom.h>

struct GUI {
    Display *display;
    Window window;
    XImage *image;
    uint32_t *bits;
};

To create the GUI, there are a few steps. First, we allocate a GUI structure. Then, we open an X11 display and a window in it. Next, we set the _XEMBED_INFO atom so that the window we embed correctly in the host. Then, we set the window's size and select the input events the window receives. Next, the bitmap to paint on is allocated. Finally, we register the file descriptor that the X11 display gets events from with the host, via the POSIX FD support extension. The host will call extensionPOSIXFDSupport.on_fd when events are ready, which we're redirecting to GUIOnPOSIXFD.

static void GUICreate(MyPlugin *plugin) {
    // Allocate the GUI pointer.
    assert(!plugin->gui);
    plugin->gui = (GUI *) calloc(1, sizeof(GUI));

    // Open the display and window.
    plugin->gui->display = XOpenDisplay(NULL);
    XSetWindowAttributes attributes = {};
    plugin->gui->window = XCreateWindow(plugin->gui->display, DefaultRootWindow(plugin->gui->display), 0, 0, GUI_WIDTH, GUI_HEIGHT, 0, 0, 
            InputOutput, CopyFromParent, CWOverrideRedirect, &attributes);
    XStoreName(plugin->gui->display, plugin->gui->window, pluginDescriptor.name);

    // Set embed information for the window.
    Atom embedInfoAtom = XInternAtom(plugin->gui->display, "_XEMBED_INFO", 0);
    uint32_t embedInfoData[2] = { 0 /* version */, 0 /* not mapped */ };
    XChangeProperty(plugin->gui->display, plugin->gui->window, embedInfoAtom, embedInfoAtom, 32, PropModeReplace, (uint8_t *) embedInfoData, 2);

    // Set the size of the window.
    XSizeHints sizeHints = {};
    sizeHints.flags = PMinSize | PMaxSize; 
    sizeHints.min_width = sizeHints.max_width = GUI_WIDTH;
    sizeHints.min_height = sizeHints.max_height = GUI_HEIGHT;
    XSetWMNormalHints(plugin->gui->display, plugin->gui->window, &sizeHints);

    // Select the events the window will receive.
    XSelectInput(plugin->gui->display, plugin->gui->window, SubstructureNotifyMask | ExposureMask | PointerMotionMask 
            | ButtonPressMask | ButtonReleaseMask | KeyPressMask | KeyReleaseMask | StructureNotifyMask
            | EnterWindowMask | LeaveWindowMask | ButtonMotionMask | KeymapStateMask | FocusChangeMask | PropertyChangeMask);

    // Create the bitmap to paint on.
    plugin->gui->image = XCreateImage(plugin->gui->display, DefaultVisual(plugin->gui->display, 0), 24, ZPixmap, 0, NULL, 10, 10, 32, 0);
    plugin->gui->bits = (uint32_t *) calloc(1, GUI_WIDTH * GUI_HEIGHT * 4);
    plugin->gui->image->width = GUI_WIDTH;
    plugin->gui->image->height = GUI_HEIGHT;
    plugin->gui->image->bytes_per_line = GUI_WIDTH * 4;
    plugin->gui->image->data = (char *) plugin->gui->bits;

    // Register the file descriptor we'll receive events from.
    if (plugin->hostPOSIXFDSupport && plugin->hostPOSIXFDSupport->register_fd) {
        plugin->hostPOSIXFDSupport->register_fd(plugin->host, ConnectionNumber(plugin->gui->display), CLAP_POSIX_FD_READ);
    }
}

We implement GUIOnPOSIXFD to process as many events as are available, and merge adjacent mouse motion events.

static void GUIOnPOSIXFD(MyPlugin *plugin) {
    XFlush(plugin->gui->display);

    if (XPending(plugin->gui->display)) {
        XEvent event;
        XNextEvent(plugin->gui->display, &event);

        while (XPending(plugin->gui->display)) {
            XEvent event0;
            XNextEvent(plugin->gui->display, &event0);

            if (event.type == MotionNotify && event0.type == MotionNotify) {
                // Merge adjacent mouse motion events.
            } else {
                GUIX11ProcessEvent(plugin, &event);
                XFlush(plugin->gui->display);
            }

            event = event0;
        }

        GUIX11ProcessEvent(plugin, &event);
        XFlush(plugin->gui->display);
        GUIPaint(plugin, true);
    }

    XFlush(plugin->gui->display);
}

Next, we add GUIPaint and GUIX11ProcessEvent. The former sends our bitmap to the display; the latter send events to the main plugin file.

static void GUIPaint(MyPlugin *plugin, bool internal) {
    if (internal) PluginPaint(plugin, plugin->gui->bits); // Repaint if necessary.
    XPutImage(plugin->gui->display, plugin->gui->window, DefaultGC(plugin->gui->display, 0), plugin->gui->image, 0, 0, 0, 0, GUI_WIDTH, GUI_HEIGHT);
}

static void GUIX11ProcessEvent(MyPlugin *plugin, XEvent *event) {
    if (event->type == Expose) {
        if (event->xexpose.window == plugin->gui->window) {
            GUIPaint(plugin, false /* don't repaint our bitmap buffer */);
        }
    } else if (event->type == MotionNotify) {
        if (event->xmotion.window == plugin->gui->window) {
            PluginProcessMouseDrag(plugin, event->xmotion.x, event->xmotion.y);
        }
    } else if (event->type == ButtonPress) {
        if (event->xbutton.window == plugin->gui->window && event->xbutton.button == 1) {
            PluginProcessMousePress(plugin, event->xbutton.x, event->xbutton.y);
        }
    } else if (event->type == ButtonRelease) {
        if (event->xbutton.window == plugin->gui->window && event->xbutton.button == 1) {
            PluginProcessMouseRelease(plugin);
        }
    }
}

There are three remaining functions we need to implement: GUIDestroy, GUISetParent and GUISetVisible. These are all fairly simple.

static void GUIDestroy(MyPlugin *plugin) {
    assert(plugin->gui);

    // Unregister the connection's file descriptor with the host.
    if (plugin->hostPOSIXFDSupport && plugin->hostPOSIXFDSupport->unregister_fd) {
        plugin->hostPOSIXFDSupport->unregister_fd(plugin->host, ConnectionNumber(plugin->gui->display));
    }

    // Destroy the bitmap, window and display.
    free(plugin->gui->bits);
    plugin->gui->image->data = NULL;
    XDestroyImage(plugin->gui->image);
    XDestroyWindow(plugin->gui->display, plugin->gui->window);
    XCloseDisplay(plugin->gui->display);

    // Free the GUI structure.
    free(plugin->gui);
    plugin->gui = nullptr;
}

static void GUISetParent(MyPlugin *plugin, const clap_window_t *window) {
    XReparentWindow(plugin->gui->display, plugin->gui->window, (Window) window->x11, 0, 0);
    XFlush(plugin->gui->display);
}

static void GUISetVisible(MyPlugin *plugin, bool visible) {
    if (visible) XMapRaised(plugin->gui->display, plugin->gui->window);
    else XUnmapWindow(plugin->gui->display, plugin->gui->window);
    XFlush(plugin->gui->display);
}

And our plugin's GUI is ready! It's just a black screen for now -- we'll first this in the next (and last) part of the tutorial.

Windows

To your compile command, append user32.dll gdi32.dll.

We now write gui_w32.cpp. We begin by defining the GUI_API constant, including some header files and defining the GUI structure that's part of MyPlugin.

#define GUI_API CLAP_WINDOW_API_WIN32

#include <windowsx.h>

struct GUI {
    HWND window;
    uint32_t *bits;
};

static int globalOpenGUICount = 0;

We define the functions/macros to set the parent of the window, change the visibility of the window and repaint the window. We ignore "POSIX FD" events -- those are only for Linux.

#define GUISetParent(plugin, parent) SetParent((plugin)->gui->window, (HWND) (parent)->win32)
#define GUISetVisible(plugin, visible) ShowWindow((plugin)->gui->window, (visible) ? SW_SHOW : SW_HIDE)
static void GUIOnPOSIXFD(MyPlugin *) {}

static void GUIPaint(MyPlugin *plugin, bool internal) {
    if (internal) PluginPaint(plugin, plugin->gui->bits);
    RedrawWindow(plugin->gui->window, 0, 0, RDW_INVALIDATE);
}

Creation and deletion of the GUI is a standard affair. We register and unregister the window class as necessary.

static void GUICreate(MyPlugin *plugin) {
    // Allocate the structure to store GUI specific data.
    assert(!plugin->gui);
    plugin->gui = (GUI *) calloc(1, sizeof(GUI));

    if (globalOpenGUICount++ == 0) {
        // Register the window class.
        WNDCLASS windowClass = {};
        windowClass.lpfnWndProc = GUIWindowProcedure;
        windowClass.cbWndExtra = sizeof(MyPlugin *);
        windowClass.lpszClassName = pluginDescriptor.id;
        windowClass.hCursor = LoadCursor(NULL, IDC_ARROW);
        windowClass.style = CS_DBLCLKS;
        RegisterClass(&windowClass);
    }

    // Create the window and allocate the bitmap.
    plugin->gui->window = CreateWindow(pluginDescriptor.id, pluginDescriptor.name, WS_CHILDWINDOW | WS_CLIPSIBLINGS, 
            CW_USEDEFAULT, 0, GUI_WIDTH, GUI_HEIGHT, GetDesktopWindow(), NULL, NULL, NULL);
    plugin->gui->bits = (uint32_t *) calloc(1, GUI_WIDTH * GUI_HEIGHT * 4);
    SetWindowLongPtr(plugin->gui->window, 0, (LONG_PTR) plugin);
    GUIPaint(plugin, true);
}

static void GUIDestroy(MyPlugin *plugin) {
    // Deallocate the window, the bitmap and the GUI structure.
    assert(plugin->gui);
    DestroyWindow(plugin->gui->window);
    free(plugin->gui->bits);
    free(plugin->gui);
    plugin->gui = nullptr;

    if (--globalOpenGUICount == 0) {
        // Unregister the window class.
        UnregisterClass(pluginDescriptor.id, NULL);
    }
}

And our plugin's GUI is ready! It's just a black screen for now -- we'll first this in the next (and last) part of the tutorial.

macOS

We're going to split the code into two files, gui_mac.cpp (C++) and gui_mac.m (Objective C). The former will mostly just be glue code connecting plugin.cpp with gui_mac.m.

We change the compile command to the following:

gcc -c gui_mac.m -g
g++ -shared -g -Wall -Wextra -Wno-unused-parameter -o ~/Library/Audio/Plug-Ins/CLAP/HelloCLAP.clap plugin.cpp gui_mac.o --std=gnu++17 -framework Cocoa

Starting with gui_mac.cpp, we declare the functions that will be implemented in gui_mac.m, as well as providing definitions for GUI_API and the GUI structure.

#define GUI_API CLAP_WINDOW_API_COCOA

struct GUI {
    void *mainView;
    uint32_t *bits;
};

extern "C" void *MacInitialise(struct MyPlugin *plugin, uint32_t *bits, uint32_t width, uint32_t height);
extern "C" void  MacDestroy   (void *mainView);
extern "C" void  MacSetParent (void *mainView, void *parentView);
extern "C" void  MacSetVisible(void *mainView, bool show);
extern "C" void  MacPaint     (void *mainView);

Next, we implement MacInputEvent, which is responsible for forwarding input events to plugin.cpp.

extern "C" void MacInputEvent(struct MyPlugin *plugin, int32_t cursorX, int32_t cursorY, int8_t button) {
    if (button == -1) PluginProcessMouseRelease(plugin);
    if (button ==  0) PluginProcessMouseDrag   (plugin, cursorX, GUI_HEIGHT - 1 - cursorY);
    if (button ==  1) PluginProcessMousePress  (plugin, cursorX, GUI_HEIGHT - 1 - cursorY);
    GUIPaint(plugin, true);
}

Then we define the functions the GUI platform layer must provide. Most of these defer the main implementation to gui_mac.m.

static void GUIPaint(MyPlugin *plugin, bool internal) {
    if (internal) PluginPaint(plugin, plugin->gui->bits);
    MacPaint(plugin->gui->mainView);
}

static void GUICreate(MyPlugin *plugin) {
    assert(!plugin->gui);
    plugin->gui = (GUI *) calloc(1, sizeof(GUI));
    plugin->gui->bits = (uint32_t *) calloc(1, GUI_WIDTH * GUI_HEIGHT * 4);
    PluginPaint(plugin, plugin->gui->bits);
    plugin->gui->mainView = MacInitialise(plugin, plugin->gui->bits, GUI_WIDTH, GUI_HEIGHT);
}

static void GUIDestroy(MyPlugin *plugin) {
    assert(plugin->gui);
    MacDestroy(plugin->gui->mainView);
    free(plugin->gui->bits);
    free(plugin->gui);
    plugin->gui = nullptr;
}

static void GUISetParent(MyPlugin *plugin, const clap_window_t *parent) { MacSetParent(plugin->gui->mainView, parent->cocoa); }
static void GUISetVisible(MyPlugin *plugin, bool visible) { MacSetVisible(plugin->gui->mainView, visible); }
static void GUIOnPOSIXFD(MyPlugin *) {}

Now onto gui_mac.m. We include the necessary headers, declare the MacInputEvent callback and define the MainView interface.

#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>

struct MyPlugin;
void MacInputEvent(struct MyPlugin *plugin, int32_t cursorX, int32_t cursorY, int8_t button);

@interface MainView : NSView
@property (nonatomic) struct MyPlugin *plugin;
@property (nonatomic) uint32_t *bits;
@property (nonatomic) uint32_t width;
@property (nonatomic) uint32_t height;
@property (nonatomic) BOOL hasSuperView;
@end

The implementation of the MainView consists of copying our bitmap to the screen when requested, and forwarding input events to gui_mac.cpp via MacInputEvent.

@implementation MainView
- (void)drawRect:(NSRect)dirtyRect {
    const unsigned char *data = (const unsigned char *) _bits;
    NSDrawBitmap(self.bounds, _width, _height, 8 /* bits per channel */, 4 /* channels per pixel */, 32 /* bits per pixel */,
            4 * _width /* bytes per row */, NO /* planar */, NO /* has alpha */, 
            NSDeviceRGBColorSpace /* color space */, &data /* data */);
}

- (BOOL)acceptsFirstMouse:(NSEvent *)event {
    // Don't ignore activation clicks on the plugin window.
    return YES;
}

- (void)mouseDown:(NSEvent *)event {
    NSPoint cursor = [self convertPoint:[event locationInWindow] fromView:nil];
    MacInputEvent(_plugin, cursor.x, cursor.y, 1);
}

- (void)mouseUp:(NSEvent *)event {
    NSPoint cursor = [self convertPoint:[event locationInWindow] fromView:nil];
    MacInputEvent(_plugin, cursor.x, cursor.y, -1);
}

- (void)mouseDragged:(NSEvent *)event {
    NSPoint cursor = [self convertPoint:[event locationInWindow] fromView:nil];
    MacInputEvent(_plugin, cursor.x, cursor.y, 0);
}
@end

To initialise and destroy the GUI, we allocate and release an instance of MainView respectively.

void *MacInitialise(struct MyPlugin *plugin, uint32_t *bits, uint32_t width, uint32_t height) {
    NSRect frame;
    frame.origin.x = 0;
    frame.origin.y = 0;
    frame.size.width = width;
    frame.size.height = height;
    MainView *mainView = [[MainView alloc] initWithFrame:frame];
    mainView.plugin = plugin;
    mainView.bits = bits;
    mainView.width = width;
    mainView.height = height;
    return mainView;
}

void MacDestroy(void *mainView) {
    [((MainView *) mainView) release];
}

All that remains is to implement the remaining functions by forwarding them to the correct Cocoa calls.

void MacSetParent(void *_mainView, void *_parentView) {
    MainView *mainView = (MainView *) _mainView;
    NSView *parentView = (NSView *) _parentView;
    if (mainView.hasSuperView) [mainView removeFromSuperview];
    [parentView addSubview:mainView];
    mainView.hasSuperView = true;
}

void MacSetVisible(void *_mainView, bool show) {
    MainView *mainView = (MainView *) _mainView;
    [mainView setHidden:(show ? NO : YES)];
}

void MacPaint(void *_mainView) {
    MainView *mainView = (MainView *) _mainView;
    [mainView setNeedsDisplayInRect:mainView.bounds];
}

And our plugin's GUI is ready! It's just a black screen for now -- we'll first this in the next (and last) part of the tutorial.


clap-tutorial-part-3-plugin.cpp

clap-tutorial-gui_x11.cpp

clap-tutorial-gui_w32.cpp

clap-tutorial-gui_mac.cpp

clap-tutorial-gui_mac.m

Part 1: Basics.

Part 2: Parameters.

Part 3: You are here!

Part 4: Gestures.