FLTK
The Wayland backend in a developer's eye

This chapter describes how the Wayland backend of FLTK works from a developer's viewpoint.

Introduction to Wayland

Wayland usage involves communication via a socket between a client application and another process called the Wayland compositor which creates, moves, resizes and draws windows on the display. Diverse Wayland compositors exist. They can follow rather diverse logics. For example, FreeBSD offers Sway which is a tiling compositor where the display is always entirely filled with whatever windows are mapped at any given time. Compositors follow either the client-side decoration (CSD) rule where client apps draw window titlebars, or the server-side decoration (SSD) rule where the compositor draws titlebars. FLTK supports both CSD and SSD compositors. It bundles a library called libdecor charged of determining whether a CSD or a SSD compositor is active, and of drawing titlebars in the first case.

Wayland is divided in various protocols that a given compositor may or may not support, although they all support the core protocol. The core protocol allows a client app to discover what protocols its compositor supports. Protocols can be stable, which means they have a defined API that will not change but can be expanded, or unstable. For example, mapping a window on a display is not done by the core protocol but by the xdg shell protocol which is stable. Unstable protocols are named beginning with letter 'z'. For example, the protocol FLTK uses to support CJK input methods is called zwp_text_input_v3 and is, unfortunately, unstable.

Wayland differs noticeably from X11 in that the position of a window in the display is completely hidden to the client app. Besides toplevel and sub-windows, Wayland allows to create popup windows positioned relatively to a previously mapped other window. This allows FLTK to create menus and tooltips, but it seriously complicates the algorithm to pilot menus, because the previous algorithm conceived for other platforms assumes the position of a window in the display to be known to the client app, which is wrong under Wayland.

Wayland makes intensive use of the 'listener' mechanism. A listener is a small array of pointers to FLTK-defined callback functions associated to a Wayland-defined object; Wayland calls these functions when defined events occur and transmits relevant information to the client app as parameters of these calls. Each listener is first associated to its corresponding Wayland object by a call to a specific Wayland function of the form wl_XXX_add_listener().

Wayland uses a trick of its own to handle lists of linked records. It defines the opaque type struct wl_list and the macro wl_list_for_each(arg1, arg2, arg3) where:

  • arg1 is a pointer variable of the type of elements of the linked list;
  • arg2 is the address of a variable of type struct wl_list identifying the targetted list;
  • arg3 is the name of the member variable of these elements used to link them together.

For example, wl_list_for_each() can be used as follows to scan the linked list of all displays of the system (see output):


  Fl_Wayland_Screen_Driver::output *output;
  Fl_Wayland_Screen_Driver *scr_driver = (Fl_Wayland_Screen_Driver*)Fl::screen_driver();
  wl_list_for_each(output, &(scr_driver->outputs), link) {
… work with output, a member of the linked list of all displays in the system …
  }

Other macros (wl_list_init(), wl_list_insert(), wl_list_for_each_safe(), wl_list_remove()) can be used to edit linked lists.

Overall, the FLTK library interacts with Wayland calling C functions present in the libwayland-client.so, libwayland-cursor.so and libxkbcommon.so shared libraries, being called by these libraries via the 'listener' mechanism, and listening via Fl::add_fd() to data sent by the compositor to the client via the socket. The names of these functions begin with wl_. The core protocol defines also a number of opaque structures whose names begin with wl_. FLTK defines a few structures holding Wayland-related data. The names of FLTK-defined structures don't begin with wl_. For example, struct wld_window (see wld_window) is used to store all Wayland-specific data associated to a mapped Fl_Window.

Building libfltk as a Wayland client

Classes Fl_Wayland_Window_Driver, Fl_Wayland_Screen_Driver, Fl_Wayland_Graphics_Driver, Fl_Wayland_Copy_Surface_Driver, Fl_Wayland_Image_Surface_Driver and Fl_Wayland_Gl_Window_Driver contain all the Wayland-specific code of the FLTK library. This code is located at src/drivers/Wayland/ in the FLTK source tree. Furthermore, class Fl_Unix_System_Driver is used by both the Wayland and the X11 FLTK platforms, so that a specially important element of the FLTK library, the event loop, is nearly completely identical in X11 and in Wayland.

The public Wayland C API is obtained with

#include <wayland-client.h>
#include <wayland-cursor.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-compose.h>
#include <linux/input.h>

as necessary.

File README.Wayland.txt details what software packages are needed on Debian-based, Fedora and FreeBSD systems for FLTK to use Wayland. Wayland protocols are packaged as XML files accompanied by a utility program, wayland-scanner, able to generate a header file and a necessary glue C source file from a given XML file. For example, for FLTK to use the xdg shell protocol, these commands are run by CMake to generate a header file that FLTK code will include and a .c file that will be compiled into libfltk :

set(PROTOCOLS /usr/share/wayland-protocols)
wayland-scanner private-code ${PROTOCOLS}/stable/xdg-shell/xdg-shell.xml xdg-shell-protocol.c
wayland-scanner client-header ${PROTOCOLS}/stable/xdg-shell/xdg-shell.xml xdg-shell-client-protocol.h

Similar operations are performed for FLTK to use protocols xdg decoration unstable v1 and text input unstable v3.

The hybrid Wayland/X11 platform

The Wayland platform of FLTK is normally a two-legged hybrid able to use either Wayland or X11 and to choose between these possibilities at run-time, without any change to the client application. The Wayland/X11 hybrid is essentially a version of the FLTK library containing both all Wayland-specific and all X11-specific code, plus a dedicated source file, fl_wayland_platform_init.cxx, that determines, at startup time, whether the app will run as a Wayland or as an X11 client. Function attempt_wayland() therein performs this choice as follows :

  • if the app defines a global bool variable called fl_disable_wayland and this variable is true, the X11 leg is chosen;
  • if environment variable FLTK_BACKEND is defined to string "wayland", the Wayland leg is chosen;
  • if environment variable FLTK_BACKEND is defined to string "x11", the X11 leg is chosen;
  • otherwise, a connection to a Wayland compositor is attempted; if it's successful, the Wayland leg is chosen; if it's not, the X11 leg is chosen.

The first condition listed above is meant to facilitate transition to FLTK 1.4 of source code written for FLTK 1.3 and containing X11-specific code : it's enough to put

FL_EXPORT bool fl_disable_wayland = true;

anywhere in the source code, for the app to run with 1.4, using the x11 leg of the hybrid platform, without any other change in the source code nor to the application's environment.

In special situations, such as with embedded systems equipped with the Wayland software but lacking the X11 library, it's possible to build the FLTK library such as it contains only the Wayland backend. This is achieved building FLTK with cmake -DOPTION_WAYLAND_ONLY=on or with configure –disable-x11.

The rest of this chapter describes what happens when the Wayland leg has been chosen.

Opening a Wayland connection

Function Fl_Wayland_Screen_Driver::open_display_platform() establishes the Wayland connection calling wl_display_connect(NULL) which returns a struct wl_display object. Function wl_display_get_fd() returns the file descriptor of the Wayland socket. Then, function wl_registry_add_listener() associates a callback function, registry_handle_global(), that will be called a number of times to indicate each time a feature of the running Wayland compositor. This allows a client app to become aware of what protocols (and at which version) are supported by the compositor. This also allows the client app to discover system features such as displays and keyboards. Finally, Fl::add_fd() makes FLTK listen to the Wayland socket and associates function fd_callback() from file Fl_Wayland_Screen_Driver.cxx with it. This function essentially calls wl_display_dispatch() which asks the Wayland client library to process requests arrived in the socket.

The event loop is run by function Fl_Unix_System_Driver::wait() which is used by both the Wayland and X11 FLTK backends. Among various tasks, this function waits for data arriving on the file descriptors FLTK is listening. Overall, the event loop of the Wayland backend is nearly exactly the same as that used by the X11 backend. The Wayland backend differs only in the callback function called to handle data read from the Wayland connection socket, which is Wayland-specific.

Wayland surfaces

Wayland defines objects called surfaces of type struct wl_surface. A Wayland surface has a rectangular area which may be displayed on zero or more displays, present buffers, receive user input, and define a local coordinate system. Buffers allow the client app to draw to surfaces. FLTK makes no use of local coordinate systems. FLTK creates a surface with function wl_compositor_create_surface() each time an Fl_Window is show()'n. Function wl_surface_add_listener() associates the surface with a listener which allows to associate each surface with the display where it is mapped. FLTK recognizes 4 distinct kinds of surfaces named DECORATED, SUBWINDOW, UNFRAMED and POPUP. DECORATED are toplevel windows with a titlebar. UNFRAMED have no titlebar. POPUP correspond to menus and tooltips, SUBWINDOW to an Fl_Window embedded in another Fl_Window. Function Fl_Wayland_Window_Driver::makeWindow() creates all these surfaces. Except for SUBWINDOW's, each surface is associated to a 'configure' function that Wayland calls one or more times when the window is going to be mapped on the display. The 'configure' function of DECORATED surfaces is handle_configure(). Wayland calls it twice when mapping a DECORATED surface. It calls it also during operations such as resizing, minimizing. With the help of a few calls to libdecor functions, FLTK obtains in this function all needed information about the size of the mapped window. The 'configure' functions of UNFRAMED and POPUP surfaces are xdg_surface_configure() and xdg_toplevel_configure(). They transmit effective window size information to FLTK. Caution: there are some small differences between how and when the various Wayland compositors call these 'configure' functions.

Fl_Wayland_Graphics_Driver and Fl_Cairo_Graphics_Driver

Wayland uses an Fl_Wayland_Graphics_Driver object for all its on-screen drawing operations. This object is created by function Fl_Graphics_Driver::newMainGraphicsDriver() called by Fl_Display_Device::display_device() when the library opens the display. New Fl_Wayland_Graphics_Driver objects are also created for each Fl_Image_Surface and each Fl_Copy_Surface used, and deleted when these objects are deleted.

Class Fl_Wayland_Graphics_Driver derives from class Fl_Cairo_Graphics_Driver which implements all the FLTK drawing API for a Cairo surface. Function Fl_Wayland_Graphics_Driver::cairo_init() creates the Cairo surface used by each Fl_Wayland_Graphics_Driver object by calling cairo_image_surface_create_for_data() for the window's or offscreen's draw_buffer (see below).

Class Fl_Cairo_Graphics_Driver is also used by the X11 leg of the hybrid Wayland-X11 platform because this leg draws to the display with an Fl_Display_Cairo_Graphics_Driver object which derives from class Fl_Cairo_Graphics_Driver. Finally, Fl_Cairo_Graphics_Driver is also used, in the form of an object from its derived class Fl_PostScript_Graphics_Driver, when the hybrid Wayland-X11 platform draws PostScript, or when the classic X11 platform uses Pango and draws PostScript. This happens when classes Fl_PostScript_File_Device and Fl_Printer are used.

Wayland buffers

Wayland uses buffers, objects of type struct wl_buffer, to draw to surfaces. In principle, one or more buffers can be associated to a surface, and functions wl_surface_attach() and wl_surface_commit() are called to first attach one such buffer to the surface and then inform the compositor to map this buffer on the display. Wayland buffers can use various memory layouts. FLTK uses WL_SHM_FORMAT_ARGB8888, which is the same layout as what Cairo calls CAIRO_FORMAT_ARGB32.

FLTK calls function Fl_Wayland_Window_Driver::make_current() before drawing to any Fl_Window. Member buffer of this Fl_Window's struct wld_window (see wld_window) is NULL when the window has just been created or resized. In that case, FLTK calls member functions create_shm_buffer() and cairo_init() of Fl_Wayland_Graphics_Driver to create

  • a Wayland buffer;
  • a Cairo image surface. Each of these two objects bundles a byte array of the same size and the same memory layout destined to contain the Fl_Window's graphics. The Cairo surface object is where FLTK draws. The Wayland buffer is what Wayland maps on the display. FLTK copies the Cairo surface's byte array to the Wayland buffer's byte array before beginning the mapping operation.

A Wayland buffer is a section of a larger memory structure shared between the client app and the compositor. The shared memory structure is initially sized at 10 MB and increased by steps of 10 MB when necessary. FLTK uses a function of the libdecor library, os_create_anonymous_file(), to create an adequate file and mmap's this file.

FLTK associates to each surface a struct fl_wld_buffer (see fl_wld_buffer) containing a pointer to the byte array of the Cairo image surface (member draw_buffer), a pointer to the Wayland buffer (member wl_buffer), and other information. A pointer to this struct fl_wld_buffer is memorized as member buffer of the Fl_Window's wld_window. All drawing operations to the Fl_Window then modify the content of the Cairo image surface. Function Fl_Wayland_Window_Driver::flush() is in charge of sending FLTK graphics data to the display. That is done by calling function Fl_Wayland_Graphics_Driver::buffer_commit() which copies the byte array of the Cairo surface to the Wayland buffer's starting memory address, and calls functions wl_surface_attach() and wl_surface_commit(). Before calling Fl_Window::flush(), FLTK has computed a damaged region. Fl_Wayland_Window_Driver::flush() also calls function wl_surface_damage_buffer() with that information to inform the compositor of what parts of the surface need its attention.

An important detail here is that FLTK uses Wayland's synchronization mechanism to make sure the surface's wl_buffer is not changed until the surface is fully mapped on the display. This 3-step mechanism first calls function wl_surface_frame() to obtain a struct wl_callback object and stores it as member cb of the surface's fl_wld_buffer. Then it calls wl_callback_add_listener() to associate this object to the FLTK-defined, callback function surface_frame_done() that Wayland calls at the end of the mapping operation. Finally surface_frame_done() destroys the wl_callback object by function wl_callback_destroy() and sets member cb to NULL. This procedure ensures that FLTK never changes the surface's Wayland buffer while it's being used by the compositor because Fl_Wayland_Window_Driver::flush() checks that cb is NULL before calling Fl_Wayland_Graphics_Driver::buffer_commit(). If it's not NULL, FLTK calls function wl_callback_destroy() which instructs the compositor to abort the mapping operation and to get ready for processing of a new byte buffer.

FLTK supports progressive drawing when an app calls function Fl_Window::make_current() at any time and then calls the FLTK drawing API. This is made possible in function Fl_Wayland_Window_Driver::make_current() with

// to support progressive drawing
if ( (!Fl_Wayland_Window_Driver::in_flush) && window->buffer && (!window->buffer->cb) &&
!wait_for_expose_value ) {
Fl_Wayland_Graphics_Driver::buffer_commit(window);
}

Thus, buffer_commit() runs only when cb is NULL. If an app rapidly performs calls to Fl_Window::make_current() and to drawing functions, FLTK will copy draw_buffer to the Wayland buffer and instruct Wayland to map it to the display when cb is NULL which means that the compositor is ready to start performing a mapping operation, and will only modify draw_buffer when cb is not NULL, letting the compositor complete its ongoing mapping task. For example, FLTK's mandelbrot test app can be seen to progressively fill its window from top to bottom by blocks of lines, each block appearing when the compositor is ready to map a new buffer. When the compositor is not ready, the app does not block but continues computing and drawing in memory but not on display more lines of the desired Mandelbrot graph.

Displays and HighDPI support

Wayland uses the concept of seat of type struct wl_seat which encompasses displays, a keyboard, a mouse, and a trackball. It might be possible for an app to deal with several seats, but that has not been tested with FLTK yet. Each seat may contain one or more displays, which Wayland calls outputs, of type struct wl_output.

As written above, function registry_handle_global() discovers at start-up time available seats and outputs and initializes data for each. This function also associates a 'listener' to each display by calling function wl_output_add_listener(). This 'listener' is an array of callback function pointers among which one (output_mode) runs when the display is resized and another (output_scale) when the Wayland scale factor (see below) is changed. FLTK defines type struct output (see output) inside class Fl_Wayland_Screen_Driver which contains display size and scaling information. One such record is created for each display. FLTK uses 2 distinct scaling parameters under Wayland:

  • int wld_scale;. This member variable of struct output typically equals 1 for standard, and 2 for HighDPI displays. Its value is set by the Wayland compositor for each display with the effect that 1 Wayland graphics unit represents a block of nxn pixels when the value is n. Another effect is that a drawing buffer for a surface of size WxH units contains W * n * H * n * 4 bytes. This is enough to make FLTK apps HighDPI-aware because the Wayland compositor automatically initializes parameter wld_scale to the value adequate for each display's DPI. Under the gnome desktop, this parameter is visible in the "Settings" app, "Displays" section, "Scale" parameter which is 200% on HighDPI displays.
  • float gui_scale;. This other member variable is where FLTK's own GUI scaling mechanism with ctrl/+/-/0/ keystrokes and with environment variable FLTK_SCALING_FACTOR operates: when FLTK is scaled at 150%, gui_scale is assigned value 1.5. Function Fl_Wayland_Screen_Driver::scale(int n, float f) assigns value f to the gui_scale member variable of display # n. This variable is used by function Fl_Wayland_Window_Driver::make_current() when it calls Fl_Wayland_Graphics_Driver::set_buffer() that scales the graphics driver by this factor with cairo_scale().

Overall, an FLTK object, say an Fl_Window, of size WxH FLTK units occupies W * wld_scale * gui_scale x H * wld_scale * gui_scale pixels on the display.

Event handling

TBD

Mouse and trackpad handling

FLTK receives information about mouse and pointer events via a 'listener' made up of 5 pointers to functions which Wayland calls when these events occur:

static struct wl_pointer_listener pointer_listener = {
pointer_enter, // called when pointer enters a window
pointer_leave, // called when pointer leaves a window
pointer_motion, // called when pointer moves inside a window
pointer_button, // called when the state of mouse buttons changes.
pointer_axis // called when the trackpad is moved vertically or horizontally
};

These functions receive from Wayland enough information in their parameters to generate corresponding FLTK events, that is, calls to Fl::handle(int event_type, Fl_Window *).

pointer_listener is installed by a call to function wl_pointer_add_listener() made by function seat_capabilities() which is itself another 'listener' made up of 2 function pointers

static struct wl_seat_listener seat_listener = {
seat_capabilities,
seat_name
};

installed by a call to function wl_seat_add_listener() made by function registry_handle_global() when it receives a "wl_seat" interface.

Wayland cursors

Wayland defines types struct wl_cursor and struct wl_cursor_theme to hold cursor-related data. FLTK stores in member variable default_cursor of the seat record, a pointer to the currently used cursor. Function Fl_Wayland_Window_Driver::set_cursor(Fl_Cursor) calls wl_cursor_theme_get_cursor() to set the current cursor shape to one of the standard shapes from the Fl_Cursor enumeration. This Wayland function selects a cursor shape based on the current 'cursor theme' and a cursor name. Cursor names are the files of directory /usr/share/icons/XXXX/cursors/ where XXXX is the name of the 'cursor theme'. For example, what FLTK calls FL_CURSOR_INSERT corresponds to file xterm therein. The full correspondance between Fl_Cursor values and Wayland cursor names is found in function Fl_Wayland_Window_Driver::set_cursor(Fl_Cursor).

FLTK uses function init_cursors() from file Fl_Wayland_Screen_Driver.cxx to identify the app's 'cursor theme' using function libdecor_get_cursor_settings() of library libdecor, and to store it in member variable cursor_theme of the seat record. Function init_cursors() is itself called by a 'listener' installed when function registry_handle_global() receives a "wl_seat" interface, at program startup. It is also called when the value of the Wayland scaling factor changes.

Function Fl_Wayland_Window_Driver::set_cursor(const Fl_RGB_Image *rgb, int hotx, int hoty) is used to create a custom cursor shape. This operation is relatively complex, specially because it uses a non-public structure, struct cursor_image, defined in file wayland-cursor.c of the Wayland project source code.

Text input

TBD

EGL

TBD

FLTK-defined, Wayland-specific types

  • struct wld_window is defined in Fl_Wayland_Window_Driver.H. One such record is created for each shown()'n Fl_Window by Fl_Wayland_Window_Driver::makeWindow(). Function fl_wl_xid(Fl_Window*) returns a pointer to the struct wld_window of its argument.
    struct wld_window {
      struct wl_list outputs; // linked list of outputs where this surface is mapped
      struct wl_surface *wl_surface; // the window's surface
      struct fl_wld_buffer *buffer; // see fl_wld_buffer
      struct xdg_surface *xdg_surface;
      union {
        struct libdecor_frame *frame; // for DECORATED windows
        struct wl_subsurface *subsurface; // for SUBWINDOW windows
        struct xdg_popup *xdg_popup; // for POPUP windows
        struct xdg_toplevel *xdg_toplevel; // for UNFRAMED windows
      };
      Fl_Window *fl_win;
      enum Fl_Wayland_Window_Driver::kind kind; // DECORATED or POPUP or SUBWINDOW or UNFRAMED
      int configured_width; // used when negotiating window size with the compositor
      int configured_height;
      int floating_width; // helps restoring size after un-maximizing
      int floating_height;
      int scale; // the Wayland scale factor for HighDPI displays (1 or 2, possibly 3)
      int state; // indicates whether window is fullscreen, maximized. Used otherwise for POPUPs
    }
    

  • struct fl_wld_buffer is defined in Fl_Wayland_Graphics_Driver.H. One such record is created for each shown()'n or resized Fl_Window by Fl_Wayland_Graphics_Driver::create_shm_buffer().
    struct fl_wld_buffer {
      struct wl_buffer *wl_buffer; // the Wayland buffer
      void *data; // address of the beginning of the Wayland buffer's byte array
      size_t data_size; // of wl_buffer and draw_buffer
      int stride; // bytes per line
      int width;
      unsigned char *draw_buffer; // address of the beginning of the Cairo image surface's byte array
      struct wl_callback *cb; // non-NULL while Wayland buffer is being committed
      bool draw_buffer_needs_commit; // true when draw_buffer has been modfied but not yet committed
      cairo_t *cairo_; // used when drawing to the Cairo image surface
    };
    

  • struct output is defined inside class Fl_Wayland_Screen_Driver. One such record is created for each display of the system by function registry_handle_global() when it receives a "wl_output" interface. These records are kept in a linked list of them all, and an identifier of this linked list is stored in member outputs of the unique Fl_Wayland_Screen_Driver object FLTK uses. Thus,
    Fl_Wayland_Screen_Driver *scr_driver = (Fl_Wayland_Screen_Driver*)Fl::screen_driver();
    struct wl_list list_of_all_displays = scr_driver->outputs;
    static Fl_Screen_Driver * screen_driver()
    Returns a pointer to the unique Fl_Screen_Driver object of the platform.
    Definition: Fl.cxx:95
    gives access, the Wayland way, to the linked list of displays in the system.
    struct output { // one record for each display
      uint32_t id; // an identifier of the display
      short x_org;
      short y_org;
      short width; // in pixels
      short height; // in pixels
      float dpi;
      struct wl_output *wl_output;
      int wld_scale; // Wayland scale factor
      float gui_scale; // FLTK scale factor
      struct wl_list link; // links these records together
    };
    

  • struct seat is defined in file Fl_Wayland_Screen_Driver.H. One such record is created for each seat (e.g., a collection of displays, a keyboard and a mouse) of the system by function registry_handle_global() when it receives a "wl_seat" or wl_data_device_manager_interface.name interface.
    struct seat {
      struct wl_seat *wl_seat;
      struct wl_pointer *wl_pointer;
      struct wl_keyboard *wl_keyboard;
      uint32_t keyboard_enter_serial;
      struct wl_surface *keyboard_surface;
      struct wl_list link;
      struct wl_list pointer_outputs;
      struct wl_cursor_theme *cursor_theme;
      struct wl_cursor *default_cursor;
      struct wl_surface *cursor_surface;
      struct wl_surface *pointer_focus;
      int pointer_scale;
      uint32_t serial;
      uint32_t pointer_enter_serial;
      struct wl_data_device_manager *data_device_manager;
      struct wl_data_device *data_device;
      struct wl_data_source *data_source;
      struct xkb_state *xkb_state;
      struct xkb_context *xkb_context;
      struct xkb_keymap *xkb_keymap;
      struct xkb_compose_state *xkb_compose_state;
      char *name;
      struct zwp_text_input_v3 *text_input;
    };
    

Documentation resources

The Wayland Protocol Extensive introduction to Wayland programming written by the Wayland author, unfortunately unachieved.

Wayland Explorer Documentation of all Wayland protocols, both stable and unstable. A language-independent syntax is used which makes function names usable in the C language not always obvious. Also some useful functions seem undocumented here for an unclear reason.

Wayland Protocol Specification Documentation for all functions of the Wayland core protocol.

Wayland clipboard and drag & drop Detailed explanation of how clipboard and drag-and-drop work under Wayland.

Wayland and input methods Blog article introducing to the issue of text input methods under Wayland.

Input Method Hub Entry page for input method support giving newcomers a first understanding of what input methods are and how they are implemented in Wayland.